C++ OpenCV 入门实战指南(Ubuntu 24.04)
提供基于 Ubuntu 24.04 系统的 C++ OpenCV 开发环境搭建指南,涵盖 g++、CMake 及库安装步骤。内容深入讲解 Mat 类核心概念,演示图像加载、显示、保存及灰度转换。进一步介绍像素访问、图像算术运算、几何变换、滤波平滑及形态学操作。通过边缘检测、轮廓查找与直方图分析,最终结合透视变换实现简易文档扫描仪项目。适合初学者系统学习计算机视觉基础与实战应用。

提供基于 Ubuntu 24.04 系统的 C++ OpenCV 开发环境搭建指南,涵盖 g++、CMake 及库安装步骤。内容深入讲解 Mat 类核心概念,演示图像加载、显示、保存及灰度转换。进一步介绍像素访问、图像算术运算、几何变换、滤波平滑及形态学操作。通过边缘检测、轮廓查找与直方图分析,最终结合透视变换实现简易文档扫描仪项目。适合初学者系统学习计算机视觉基础与实战应用。

在人工智能与计算机视觉(Computer Vision, CV)风起云涌的今天,你或许已经听说过 TensorFlow、PyTorch 这些大名鼎鼎的深度学习框架。它们无疑是 AI 皇冠上的明珠,但在这颗明珠之下,有一块不可或缺的基石——OpenCV(Open Source Computer Vision Library)。
OpenCV 是一个开源的、跨平台的计算机视觉和机器学习软件库。它包含了超过 2500 个优化过的函数,涵盖了从图像处理、特征检测、目标识别到 3D 重建、增强现实等几乎所有计算机视觉领域的核心算法。而C++,作为 OpenCV 的'母语',是其性能最强大、功能最完整的接口。虽然 Python 因其简洁性在快速原型开发中广受欢迎,但当你需要构建高性能、低延迟、资源受限的工业级应用(如自动驾驶、机器人、实时视频分析系统)时,C++ OpenCV 几乎是无可替代的选择。
选择 Ubuntu 24.04 作为本文的开发环境,是因为 Linux(尤其是 Ubuntu)是开发者和研究人员的首选操作系统,它拥有强大的包管理工具、丰富的开发库和社区支持,能让我们更专注于学习本身,而非被环境配置所困扰。
本文的目标很明确:让一个从未接触过 C++ 或 OpenCV 的读者,在阅读完本文后,能够自信地编写、编译并运行自己的第一个计算机视觉程序,并理解其背后的基本原理。 我们将遵循'理论 -> 实践 -> 案例'的螺旋式上升学习路径,确保每一步都扎实可靠。
在开始任何编程之旅前,我们必须先准备好工具。
Ubuntu 24.04 默认已经包含了许多开发工具,但我们仍需手动安装一些关键组件。
步骤 1:更新系统包列表
首先,确保我们的软件包列表是最新的:
sudo apt update
步骤 2:安装 C++ 编译器和构建工具
我们需要 g++(GNU C++ 编译器)来将我们写的 C++ 代码翻译成机器能懂的语言,以及 make 这个自动化构建工具。
sudo apt install build-essential

build-essential 是一个元包,它会自动安装 gcc, g++, make, libc6-dev 等一整套 C/C++ 开发必需的工具链。
步骤 3:安装 CMake
CMake 是一个跨平台的构建系统生成器。OpenCV 项目结构复杂,直接使用 g++ 命令行编译会非常繁琐。CMake 能帮我们自动生成合适的 Makefile,极大地简化编译过程。
sudo apt install cmake

步骤 4:安装 OpenCV 及其开发文件
万幸的是,Ubuntu 24.04 的官方仓库已经包含了预编译好的 OpenCV 库。我们可以直接通过 apt 安装,省去了从源码编译的漫长等待(通常需要 1-2 小时)。
# 安装 OpenCV 的核心库
sudo apt install libopencv-dev
# 为了确保所有功能模块都可用,也可以安装以下包
sudo apt install python3-opencv # 虽然我们主攻 C++,但这个包有时会包含一些额外的依赖

libopencv-dev 包含了 OpenCV 的头文件(.h 或 .hpp)和静态/动态链接库,这是我们开发 C++ 程序所必需的。
验证安装:
安装完成后,我们可以通过以下命令检查 OpenCV 的版本:
pkg-config --modversion opencv4

你应该能看到类似 4.6.0 的输出(具体版本号可能随时间更新)。如果提示找不到 opencv4,可以尝试 opencv。
一个好的编辑器能让你事半功倍。对于初学者,我推荐以下两个选择:
sudo apt install code本文将以通用的文本编辑器(如 VS Code 或系统自带的 gedit)配合终端命令行进行演示,这样能让你更清晰地理解底层的编译链接过程。
在接触 OpenCV 之前,让我们先用最经典的'Hello, World!'程序来熟悉 C++ 的编译流程。
#include <iostream>:告诉编译器我们需要使用标准输入输出库。int main():这是每个 C++ 程序的入口点。std::cout:用于向控制台输出内容。return 0;:表示程序正常退出。g++:调用 C++ 编译器。hello.cpp:要编译的源文件。-o hello:指定输出的可执行文件名为 hello。编译与运行:
在终端中,使用 g++ 编译这个文件:
g++ hello.cpp -o hello
编译成功后,运行程序:
./hello

你应该能在终端看到 Hello, World! 的输出。
编写代码:
创建一个名为 hello.cpp 的文件:
// hello.cpp
#include <iostream>
// 包含输入输出流库
int main() {
std::cout << "Hello, World!" << std::endl;
return 0; // 程序成功结束
}

这段代码非常简单:
创建项目目录:
mkdir ~/opencv_tutorial
cd ~/opencv_tutorial
恭喜!你已经成功完成了 C++ 开发的第一步。这个看似简单的流程(写代码 -> 编译 -> 运行)是我们未来所有工作的基础。
现在,我们的工具箱已经准备好了。是时候请出今天的主角——OpenCV 了!
在 OpenCV 中,cv::Mat 类是绝对的核心。你可以把它想象成一个超级智能的'相框'。这个相框不仅能装下一张图片的所有像素数据,还能记录这张图片的尺寸(宽高)、颜色通道数(灰度图 1 通道,彩色图 3 通道 BGR)、数据类型(每个像素占多少字节)等元信息。
CV_8UC3,其中:
8U 表示 8 位无符号整数(范围 0-255)。C3 表示 3 个通道。这里我们只需要知道一些概念即可,如需深入了解可查阅 官方文档:

让我们动手写一个程序,实现最基本的图像 I/O 操作。
步骤 1:编写代码 (load_display_save.cpp)
#include <opencv2/opencv.hpp> // 包含 OpenCV 所有模块的头文件
#include <iostream> // 为了方便,我们可以使用命名空间,避免每次都要写 cv::
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
// 1. 加载图像
// IMREAD_COLOR: 加载彩色图像,忽略透明度
// IMREAD_GRAYSCALE: 加载灰度图像
// IMREAD_UNCHANGED: 加载包括 alpha 通道的图像
Mat image = imread("input.jpg", IMREAD_COLOR);
// 2. 检查图像是否成功加载
if (image.empty()) {
cout << "Error: Could not load image!" << endl;
return -1; // 返回错误码
}
// 3. 打印图像的一些基本信息
cout << "Image size: " << image.size() << endl; // 输出尺寸,例如 [640 x 480]
cout << "Channels: " << image.channels() << endl; // 输出通道数,彩色图为 3
cout << "Data type: " << image.type() << endl; // 输出数据类型
// 4. 显示图像
namedWindow("Original Image", WINDOW_AUTOSIZE); // 创建一个窗口
imshow("Original Image", image); // 在窗口中显示图像
// 5. 将图像转换为灰度图
Mat gray_image;
(image, gray_image, COLOR_BGR2GRAY);
(, WINDOW_AUTOSIZE);
(, gray_image);
(, gray_image);
();
();
;
}
代码详解:
#include <opencv2/opencv.hpp>:这是包含 OpenCV 所有功能的万能头文件。对于大型项目,为了编译速度,可以只包含需要的特定模块头文件(如 <opencv2/imgproc.hpp>),但对于学习和小项目,用这个最方便。imread: 从文件加载图像。第二个参数指定了加载模式。image.empty(): 检查 Mat 对象是否为空,这是判断图像加载是否成功的关键。namedWindow 和 imshow: 用于创建窗口并显示图像。WINDOW_AUTOSIZE 会让窗口大小自动适应图像。cvtColor: 颜色空间转换函数。COLOR_BGR2GRAY 是将 BGR 图像转换为灰度图的标志。imwrite: 将 Mat 对象保存为图像文件。waitKey(0): 这是一个非常重要的函数。它会暂停程序执行,等待键盘事件。如果没有这一行,窗口会一闪而过,你根本看不到图像。0 表示永久等待。destroyAllWindows(): 清理资源,关闭所有 OpenCV 创建的窗口。步骤 2:准备测试图像
将一张名为 input.jpg 的图片放到项目目录 ~/opencv_tutorial 中。我们可以从网上下载任何一张 JPG 图片,并重命名为 input.jpg。
步骤 3:编译程序
这里我们不能直接用 g++ 了,因为需要告诉编译器 OpenCV 头文件在哪里,以及链接哪些库。这就是 pkg-config 发挥作用的时候了。
g++ load_display_save.cpp -o load_display_save `pkg-config --cflags --libs opencv4`
`pkg-config --cflags --libs opencv4`:这是一个命令替换。pkg-config 会查询 OpenCV 的安装信息,并返回编译所需的头文件路径(--cflags)和链接库信息(--libs)。反引号 `...` 会将这个命令的输出作为参数传递给 g++。步骤 4:运行程序
./load_display_save

你应该会看到两个窗口弹出,分别显示原始彩色图像和转换后的灰度图像。同时,项目目录下会多出一个 output_gray.jpg 文件。
恭喜!你已经成功迈出了 OpenCV 编程的第一步!
现在我们已经能和图像'见面'了,接下来我们要学会'触摸'它,也就是对像素进行操作。
访问像素是图像处理的基础。OpenCV 提供了多种方式,最常用且高效的是使用 at<T>() 方法。
案例:创建一个自定义图像并修改像素
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main() {
// 1. 创建一个空白图像 (黑色)
// 参数:高度,宽度,类型
Mat img(300, 300, CV_8UC3, Scalar(0, 0, 0));
// 300x300, 3 通道,黑色
// 2. 修改单个像素 (在 (150, 150) 位置画一个白点)
// 注意:OpenCV 中坐标是 (行,列),即 (y, x)
img.at<Vec3b>(150, 150) = Vec3b(255, 255, 255); // B=255, G=255, R=255
// 3. 画一个矩形区域 (左上角 (100,100), 右下角 (200,200))
for (int y = 100; y < 200; y++) {
for (int x = 100; x < 200; x++) {
img.at<Vec3b>(y, x) = Vec3b(0, 255, 0); // 绿色
}
}
// 4. 显示图像
imshow("Custom Image", img);
waitKey(0);
destroyAllWindows();
return ;
}
运行效果:

Vec3b: 这是一个 OpenCV 的模板类,代表一个包含 3 个 unsigned char(即 uchar)的向量。正好对应 BGR 三个通道的像素值。Scalar(0,0,0): 在创建 Mat 时,可以用 Scalar 来初始化所有像素值。(0,0) 在左上角。x 轴向右,y 轴向下。所以 img.at<Vec3b>(y, x) 中的 y 是行号(高度方向),x 是列号(宽度方向)。性能提示: 虽然 at<T>() 方法简单直观,但在需要遍历整个图像进行大量操作时,它的性能不是最优的,因为每次访问都会进行边界检查。对于高性能需求,可以使用 ptr<T>() 方法获取指向某一行的指针,然后进行指针运算。
OpenCV 重载了 C++ 的算术运算符,使得对图像进行加、减、乘、除等操作变得异常简单。
案例:图像混合(Alpha Blending)
图像混合是将两张图像按一定比例叠加在一起,常用于制作水印、淡入淡出效果等。
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main() {
Mat img1 = imread("input1.jpg");
Mat img2 = imread("input2.jpg");
if (img1.empty() || img2.empty()) {
cout << "Error loading images!" << endl;
return -1;
}
// 确保两张图像尺寸相同
resize(img2, img2, img1.size());
// 定义混合权重
double alpha = 0.7;
double beta = 1.0 - alpha;
double gamma = 0.0; // 亮度调节
Mat blended;
addWeighted(img1, alpha, img2, beta, gamma, blended);
imshow("Blended Image", blended);
waitKey(0);
destroyAllWindows();
return 0;
}
运行效果:

addWeighted 函数实现了公式:dst = src1*alpha + src2*beta + gamma。resize 函数用于调整图像尺寸,确保两张图可以进行运算。改变图像的形状、大小或视角是常见的操作。
1. 缩放 (Resizing)
Mat resized;
// 方法 1: 指定缩放因子
resize(original, resized, Size(), 0.5, 0.5, INTER_LINEAR); // 缩小一半
// 方法 2: 指定目标尺寸
resize(original, resized, Size(800, 600), 0, 0, INTER_CUBIC);
INTER_LINEAR: 双线性插值,速度快,质量一般。INTER_CUBIC: 三次样条插值,速度慢,质量高。2. 平移 (Translation)
平移是通过仿射变换矩阵实现的。
Mat translated;
// 定义平移矩阵 M = [1, 0, tx;
// 0, 1, ty]
Mat M = (Mat_<float>(2, 3) << 1, 0, 100, // 向右平移 100 像素
0, 1, 50); // 向下平移 50 像素
warpAffine(original, translated, M, original.size());
3. 旋转 (Rotation)
Mat rotated;
Point2f center(original.cols / 2.0, original.rows / 2.0); // 旋转中心
double angle = 45; // 旋转角度(逆时针为正)
double scale = 1.0; // 缩放因子
Mat R = getRotationMatrix2D(center, angle, scale);
warpAffine(original, rotated, R, original.size());
4. 仿射变换 (Affine Transformation)
仿射变换可以保持图像的'平行线'特性,包括平移、旋转、缩放、错切等。它需要三个点的对应关系来确定变换矩阵。
// 原始图像中的三个点
Point2f srcTri[3] = { Point2f(0, 0), Point2f(original.cols - 1, 0), Point2f(0, original.rows - 1) };
// 目标图像中的三个对应点
Point2f dstTri[3] = { Point2f(0, original.rows * 0.3), Point2f(original.cols * 0.8, original.rows * 0.2), Point2f(original.cols * 0.15, original.rows * 0.7) };
Mat warpMat = getAffineTransform(srcTri, dstTri);
Mat affine;
warpAffine(original, affine, warpMat, original.size());
5. 透视变换 (Perspective Transformation)
透视变换更为强大,可以模拟相机视角的变化,常用于文档矫正、AR 等领域。它需要四个点的对应关系。
// 原始四边形的四个顶点
Point2f srcQuad[] = { Point2f(0, 0), Point2f(original.cols, 0), Point2f(original.cols, original.rows), Point2f(0, original.rows) };
// 目标四边形的四个顶点
Point2f dstQuad[] = { Point2f(100, 100), Point2f(500, 50), Point2f(550, 400), Point2f(50, 350) };
Mat perspectiveMat = getPerspectiveTransform(srcQuad, dstQuad);
Mat perspective;
warpPerspective(original, perspective, perspectiveMat, Size(600, 500));
如果说前面的操作是'骨架',那么滤波和形态学就是图像的'血肉'和'皮肤',它们能极大地改善图像质量,为后续的高级分析(如目标检测)打下坚实基础。
图像在获取过程中常常会受到噪声干扰。滤波的目的就是去除噪声,或者提取图像的某些特征(如边缘)。
1. 均值滤波 (Averaging)
用一个滑动窗口(核)覆盖图像,将窗口内所有像素的平均值赋给中心像素。效果是图像变得模糊,噪声被抑制。
Mat blurred;
blur(original, blurred, Size(15, 15)); // 15x15 的核
2. 高斯滤波 (Gaussian Filtering)
与均值滤波类似,但窗口内的像素值会根据高斯分布进行加权平均。中心像素权重最大,离中心越远权重越小。高斯滤波在去噪的同时能更好地保留图像的边缘信息,是最常用的平滑滤波器。
Mat gaussian_blur;
GaussianBlur(original, gaussian_blur, Size(15, 15), 0, 0); // 最后两个参数是 X 和 Y 方向的标准差,设为 0 时会根据核大小自动计算
3. 中值滤波 (Median Filtering)
将窗口内所有像素值排序,取中值作为中心像素的新值。这种方法对椒盐噪声(Salt-and-Pepper Noise,即图像中随机出现的黑白点)有奇效,且能很好地保护边缘。
Mat median_blur;
medianBlur(original, median_blur, 15); // 核大小必须是大于 1 的奇数
4. 双边滤波 (Bilateral Filtering)
这是一种非常高级的滤波器,它在平滑图像的同时能完美地保留边缘。它结合了空间邻近度和像素值相似度两个因素。
Mat bilateral;
bilateralFilter(original, bilateral, 15, 80, 80); // 第二个参数是邻域直径,第三、四个是颜色空间和坐标空间的标准差
形态学操作主要针对二值图像(只有黑白两色),通过结构元素(Structuring Element)来探测和修改图像的形状。最常见的两种操作是腐蚀 (Erosion) 和膨胀 (Dilation)。
开运算 (Opening) = 先腐蚀后膨胀
主要用于去除小的白色噪点,分离粘连的物体。
闭运算 (Closing) = 先膨胀后腐蚀
主要用于填充物体内部的小孔洞,连接邻近的物体。
实战:清理二值图像
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main() {
Mat src = imread("noisy_binary.png", IMREAD_GRAYSCALE);
if (src.empty()) return -1;
// 创建一个椭圆形的结构元素
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(5, 5));
// 开运算:去除噪点
Mat opened;
morphologyEx(src, opened, MORPH_OPEN, kernel);
// 闭运算:填充孔洞
Mat closed;
morphologyEx(src, closed, MORPH_CLOSE, kernel);
imshow("Original", src);
imshow("Opened", opened);
imshow("Closed", closed);
waitKey(0);
destroyAllWindows();
return 0;
}
运行效果:

morphologyEx 是一个通用的形态学操作函数,通过第三个参数指定操作类型。现在,我们已经能让图像变得更'干净'了。下一步,我们要教会计算机'看'出图像中的物体边界和形状。
边缘是图像中亮度发生急剧变化的地方,通常对应着物体的边界。Canny 边缘检测算法是目前最优秀、应用最广泛的边缘检测算法之一。
Canny 算法的步骤:
实战:Canny 边缘检测
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main() {
Mat src = imread("building.jpg");
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
// 高斯模糊去噪
GaussianBlur(gray, gray, Size(5, 5), 0);
// Canny 边缘检测
Mat edges;
Canny(gray, edges, 50, 150, 3); // 低阈值 50, 高阈值 150, Sobel 核大小 3
imshow("Original", src);
imshow("Edges", edges);
waitKey(0); // 修复原稿笔误 waitTime -> waitKey
destroyAllWindows();
return 0;
}
运行效果:

边缘检测得到的是'线',而轮廓检测得到的是'面'。轮廓是具有相同颜色或强度的连续点构成的曲线,用于形状分析、物体检测和识别。
实战:查找并绘制轮廓
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main() {
Mat src = imread("shapes.png");
Mat gray, thresh;
cvtColor(src, gray, COLOR_BGR2GRAY);
threshold(gray, thresh, 127, 255, THRESH_BINARY); // 二值化
// 查找轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(thresh, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
// 在原图上绘制轮廓
Mat contour_img = src.clone();
drawContours(contour_img, contours, -1, Scalar(0, 255, 0), 2); // -1 表示绘制所有轮廓
// 计算并标记每个轮廓的面积和周长
for (size_t i = 0; i < contours.size(); i++) {
double area = contourArea(contours[i]);
double perimeter = arcLength(contours[i], true);
// 找到轮廓的边界矩形
Rect bounding_rect = boundingRect(contours[i]);
// 在图上标记信息
putText(contour_img, format("A:%.0f P:%.0f", area, perimeter), Point(bounding_rect.x, bounding_rect.y - 10), FONT_HERSHEY_SIMPLEX, , (, , ), );
}
(, contour_img);
();
();
;
}
运行效果:

findContours:在二值图像中查找轮廓。RETR_TREE 表示检索所有轮廓并重构嵌套轮廓的完整层次结构。CHAIN_APPROX_SIMPLE 表示压缩水平、垂直和对角线段,只保留端点。drawContours:绘制轮廓。contourArea 和 arcLength:计算轮廓的面积和周长。boundingRect:计算包围轮廓的最小正立矩形。直方图是图像中像素强度分布的图形表示。X 轴是像素强度值(0-255),Y 轴是该强度值出现的频率。直方图能告诉我们图像的整体明暗、对比度等信息。
直方图均衡化 (Histogram Equalization) 是一种常用的图像增强技术,它通过重新分配像素强度值,使直方图变得平坦,从而增加图像的全局对比度。
实战:直方图均衡化
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main() {
Mat src = imread("low_contrast.jpg", IMREAD_GRAYSCALE);
if (src.empty()) return -1;
// 直方图均衡化
Mat equalized;
equalizeHist(src, equalized);
imshow("Original", src);
imshow("Equalized", equalized);
waitKey(0);
destroyAllWindows();
return 0;
}
运行效果:

对于彩色图像,不能直接对 BGR 通道进行均衡化,否则会改变颜色。正确的方法是先转换到 HSV 或 YUV 颜色空间,只对亮度(V 或 Y)通道进行均衡化,然后再转换回来。
理论知识学得再多,不如动手做一个小项目。我们将综合运用前面学到的所有技能,构建一个能够自动检测文档边缘并进行透视矫正的简易扫描仪。
项目目标:
实现思路:
代码实现 (document_scanner.cpp):
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace cv;
using namespace std;
// 辅助函数:计算点到直线的距离
double distanceToLine(Point p, Point a, Point b) {
double A = b.y - a.y;
double B = a.x - b.x;
double C = b.x * a.y - a.x * b.y;
return abs(A * p.x + B * p.y + C) / sqrt(A * A + B * B);
}
// 辅助函数:对四边形的四个点进行排序,使其按左上、右上、右下、左下的顺序排列
vector<Point> sortCorners(const vector<Point>& corners) {
vector<Point> sorted(corners);
// 计算质心
Point center(0, 0);
for (const auto& p : corners) {
center.x += p.x;
center.y += p.y;
}
center.x /= 4;
center.y /= 4;
// 根据点相对于质心的位置进行排序
sort(sorted.begin(), sorted.end(), [center](const Point& a, const Point& b) {
(a.x - center.x >= && b.x - center.x < ) ;
(a.x - center.x < && b.x - center.x >= ) ;
(a.x - center.x == && b.x - center.x == ) {
(a.y - center.y >= || b.y - center.y >= ) a.y > b.y;
a.y < b.y;
}
det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y);
(det < ) ;
(det > ) ;
d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y);
d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y);
d1 > d2;
});
sorted;
}
{
(argc != ) {
cout << << argv[] << << endl;
;
}
Mat src = (argv[]);
(src.()) {
cout << << endl;
;
}
Mat gray, blurred, edges;
(src, gray, COLOR_BGR2GRAY);
(gray, blurred, (, ), );
(blurred, edges, , );
vector<vector<Point>> contours;
(edges.(), contours, RETR_LIST, CHAIN_APPROX_SIMPLE);
vector<Point> doc_corners;
max_area = ;
( & contour : contours) {
area = (contour);
(area < ) ;
vector<Point> approx;
epsilon = * (contour, );
(contour, approx, epsilon, );
(approx.() == ) {
is_rectangle = ;
( i = ; i < ; i++) {
Point p1 = approx[i];
Point p2 = approx[(i + ) % ];
Point p3 = approx[(i + ) % ];
Point v1 = p1 - p2;
Point v2 = p3 - p2;
dot = vx * vx + vy * vy;
mag1 = (vx * vx + vy * vy);
mag2 = (vx * vx + vy * vy);
angle = (dot / (mag1 * mag2)) * / CV_PI;
((angle - ) > ) {
is_rectangle = ;
;
}
}
(is_rectangle && area > max_area) {
max_area = area;
doc_corners = approx;
}
}
}
(doc_corners.()) {
cout << << endl;
(, src);
();
;
}
doc_corners = (doc_corners);
Point2f src_pts[] = { (Point2f)doc_corners[], (Point2f)doc_corners[], (Point2f)doc_corners[], (Point2f)doc_corners[] };
width = , height = ;
Point2f dst_pts[] = { (, ), (width - , ), (width - , height - ), (, height - ) };
Mat transform = (src_pts, dst_pts);
Mat warped;
(src, warped, transform, (width, height));
Mat result = src.();
( i = ; i < ; i++) {
(result, doc_corners[i], doc_corners[(i + ) % ], (, , ), );
}
(, result);
(, warped);
();
();
(, warped);
;
}
编译与运行:
g++ document_scanner.cpp -o scanner `pkg-config --cflags --libs opencv4`
./scanner test_photo.jpg
运行效果:

代码亮点解析:
approxPolyDP): 这是关键一步。它能将复杂的轮廓用更少的点(这里是 4 个)来近似表示,从而找到四边形。sortCorners 函数通过计算质心和叉积来完成这个排序任务。这个案例虽然简单,但它完整地展示了计算机视觉项目从数据输入、预处理、特征提取、目标识别到最终输出的典型流程。你已经亲手构建了一个有实用价值的小工具!
至此,我们已经共同走过了从零配置环境到完成一个综合性项目的完整旅程。我们学习了 C++ OpenCV 的核心概念——Mat,掌握了图像的 I/O、基本操作、滤波、形态学、边缘与轮廓检测,并最终将这些知识融会贯通,构建了一个文档扫描仪。
但这仅仅是冰山一角。OpenCV 的世界远比这广阔:
VideoCapture 类让你能实时处理来自摄像头的视频流。dnn 模块可以加载和运行 TensorFlow、PyTorch 等框架训练好的模型。学习建议:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online