跳到主要内容C++ OpenCV 入门实战指南(Ubuntu 24.04) | 极客日志C++AI算法
C++ OpenCV 入门实战指南(Ubuntu 24.04)
提供基于 Ubuntu 24.04 系统的 C++ OpenCV 开发环境搭建指南,涵盖 g++、CMake 及库安装步骤。内容深入讲解 Mat 类核心概念,演示图像加载、显示、保存及灰度转换。进一步介绍像素访问、图像算术运算、几何变换、滤波平滑及形态学操作。通过边缘检测、轮廓查找与直方图分析,最终结合透视变换实现简易文档扫描仪项目。适合初学者系统学习计算机视觉基础与实战应用。
岁月神偷24K 浏览 前言:为什么是 C++ 和 OpenCV?
在人工智能与计算机视觉(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 的读者,在阅读完本文后,能够自信地编写、编译并运行自己的第一个计算机视觉程序,并理解其背后的基本原理。 我们将遵循'理论 -> 实践 -> 案例'的螺旋式上升学习路径,确保每一步都扎实可靠。
第一部分:筑基——搭建 C++ OpenCV 开发环境 (Ubuntu 24.04)
在开始任何编程之旅前,我们必须先准备好工具。
1.1 环境准备:安装必要的依赖
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 小时)。
sudo apt install libopencv-dev
sudo apt install python3-opencv
libopencv-dev 包含了 OpenCV 的头文件(.h 或 .hpp)和静态/动态链接库,这是我们开发 C++ 程序所必需的。
验证安装:
安装完成后,我们可以通过以下命令检查 OpenCV 的版本:
pkg-config --modversion opencv4
你应该能看到类似 4.6.0 的输出(具体版本号可能随时间更新)。如果提示找不到 opencv4,可以尝试 opencv。
1.2 选择你的'画布':代码编辑器
一个好的编辑器能让你事半功倍。对于初学者,我推荐以下两个选择:
- VS Code (Visual Studio Code): 微软出品的轻量级但功能强大的编辑器,拥有海量插件。安装 C++ 和 CMake 插件后,体验极佳。
- CLion: JetBrains 出品的专业 C++ IDE,功能全面,但需要付费(学生可免费申请)。如果你追求极致的开发体验,可以考虑。
本文将以通用的文本编辑器(如 VS Code 或系统自带的 gedit)配合终端命令行进行演示,这样能让你更清晰地理解底层的编译链接过程。
1.3 第一个 C++ 程序:Hello, World!
在接触 OpenCV 之前,让我们先用最经典的'Hello, World!'程序来熟悉 C++ 的编译流程。
#include <iostream>:告诉编译器我们需要使用标准输入输出库。
int main():这是每个 C++ 程序的入口点。
std::cout:用于向控制台输出内容。
return 0;:表示程序正常退出。
g++:调用 C++ 编译器。
hello.cpp:要编译的源文件。
-o hello:指定输出的可执行文件名为 hello。
编译与运行:
在终端中,使用 g++ 编译这个文件:
你应该能在终端看到 Hello, World! 的输出。
编写代码:
创建一个名为 hello.cpp 的文件:
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
mkdir ~/opencv_tutorial
cd ~/opencv_tutorial
恭喜!你已经成功完成了 C++ 开发的第一步。这个看似简单的流程(写代码 -> 编译 -> 运行)是我们未来所有工作的基础。
第二部分:初识 OpenCV——图像的加载、显示与保存
现在,我们的工具箱已经准备好了。是时候请出今天的主角——OpenCV 了!
2.1 核心概念:Mat 类——图像的容器
在 OpenCV 中,cv::Mat 类是绝对的核心。你可以把它想象成一个超级智能的'相框'。这个相框不仅能装下一张图片的所有像素数据,还能记录这张图片的尺寸(宽高)、颜色通道数(灰度图 1 通道,彩色图 3 通道 BGR)、数据类型(每个像素占多少字节)等元信息。
- 像素 (Pixel): 图像的最小单位。对于一张彩色图像,每个像素通常由三个数值表示,分别对应蓝色 (B)、绿色 (G)、红色 (R) 的强度。注意,OpenCV 默认使用BGR顺序,而不是我们更熟悉的 RGB。
- 通道 (Channel): 一个像素可以包含多个分量。灰度图只有 1 个通道(亮度),彩色图有 3 个通道(B, G, R)。
- 数据类型 (Data Type): 像素值的存储格式。最常见的是
CV_8UC3,其中:
8U 表示 8 位无符号整数(范围 0-255)。
C3 表示 3 个通道。
这里我们只需要知道一些概念即可,如需深入了解可查阅 官方文档:
2.2 实战:加载、显示和保存图像
让我们动手写一个程序,实现最基本的图像 I/O 操作。
步骤 1:编写代码 (load_display_save.cpp)
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
Mat image = imread("input.jpg", IMREAD_COLOR);
if (image.empty()) {
cout << "Error: Could not load image!" << endl;
return -1;
}
cout << "Image size: " << image.size() << endl;
cout << "Channels: " << image.channels() << endl;
cout << "Data type: " << image.type() << endl;
namedWindow("Original Image", WINDOW_AUTOSIZE);
imshow("Original Image", image);
Mat gray_image;
cvtColor(image, gray_image, COLOR_BGR2GRAY);
namedWindow("Gray Image", WINDOW_AUTOSIZE);
imshow("Gray Image", gray_image);
imwrite("output_gray.jpg", gray_image);
waitKey(0);
destroyAllWindows();
return 0;
}
#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 创建的窗口。
将一张名为 input.jpg 的图片放到项目目录 ~/opencv_tutorial 中。我们可以从网上下载任何一张 JPG 图片,并重命名为 input.jpg。
这里我们不能直接用 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++。
你应该会看到两个窗口弹出,分别显示原始彩色图像和转换后的灰度图像。同时,项目目录下会多出一个 output_gray.jpg 文件。
恭喜!你已经成功迈出了 OpenCV 编程的第一步!
第三部分:深入像素——图像的基本操作与处理
现在我们已经能和图像'见面'了,接下来我们要学会'触摸'它,也就是对像素进行操作。
3.1 访问和修改像素值
访问像素是图像处理的基础。OpenCV 提供了多种方式,最常用且高效的是使用 at<T>() 方法。
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main() {
Mat img(300, 300, CV_8UC3, Scalar(0, 0, 0));
img.at<Vec3b>(150, 150) = Vec3b(255, 255, 255);
for (int y = 100; y < 200; y++) {
for (int x = 100; x < 200; x++) {
img.at<Vec3b>(y, x) = Vec3b(0, 255, 0);
}
}
imshow("Custom Image", img);
waitKey(0);
destroyAllWindows();
return 0;
}
Vec3b: 这是一个 OpenCV 的模板类,代表一个包含 3 个 unsigned char(即 uchar)的向量。正好对应 BGR 三个通道的像素值。
Scalar(0,0,0): 在创建 Mat 时,可以用 Scalar 来初始化所有像素值。
- 坐标系统: 这里有一个非常重要的概念:在 OpenCV 中,图像的坐标原点
(0,0) 在左上角。x 轴向右,y 轴向下。所以 img.at<Vec3b>(y, x) 中的 y 是行号(高度方向),x 是列号(宽度方向)。
性能提示: 虽然 at<T>() 方法简单直观,但在需要遍历整个图像进行大量操作时,它的性能不是最优的,因为每次访问都会进行边界检查。对于高性能需求,可以使用 ptr<T>() 方法获取指向某一行的指针,然后进行指针运算。
3.2 基本图像算术运算
OpenCV 重载了 C++ 的算术运算符,使得对图像进行加、减、乘、除等操作变得异常简单。
图像混合是将两张图像按一定比例叠加在一起,常用于制作水印、淡入淡出效果等。
#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 函数用于调整图像尺寸,确保两张图可以进行运算。
3.3 图像的几何变换
Mat resized;
resize(original, resized, Size(), 0.5, 0.5, INTER_LINEAR);
resize(original, resized, Size(800, 600), 0, 0, INTER_CUBIC);
INTER_LINEAR: 双线性插值,速度快,质量一般。
INTER_CUBIC: 三次样条插值,速度慢,质量高。
Mat translated;
Mat M = (Mat_<float>(2, 3) << 1, 0, 100,
0, 1, 50);
warpAffine(original, translated, M, original.size());
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));
第四部分:图像的'化妆术'——滤波与形态学操作
如果说前面的操作是'骨架',那么滤波和形态学就是图像的'血肉'和'皮肤',它们能极大地改善图像质量,为后续的高级分析(如目标检测)打下坚实基础。
4.1 图像滤波(平滑/模糊)
图像在获取过程中常常会受到噪声干扰。滤波的目的就是去除噪声,或者提取图像的某些特征(如边缘)。
用一个滑动窗口(核)覆盖图像,将窗口内所有像素的平均值赋给中心像素。效果是图像变得模糊,噪声被抑制。
Mat blurred;
blur(original, blurred, Size(15, 15));
2. 高斯滤波 (Gaussian Filtering)
与均值滤波类似,但窗口内的像素值会根据高斯分布进行加权平均。中心像素权重最大,离中心越远权重越小。高斯滤波在去噪的同时能更好地保留图像的边缘信息,是最常用的平滑滤波器。
Mat gaussian_blur;
GaussianBlur(original, gaussian_blur, Size(15, 15), 0, 0);
3. 中值滤波 (Median Filtering)
将窗口内所有像素值排序,取中值作为中心像素的新值。这种方法对椒盐噪声(Salt-and-Pepper Noise,即图像中随机出现的黑白点)有奇效,且能很好地保护边缘。
Mat median_blur;
medianBlur(original, median_blur, 15);
4. 双边滤波 (Bilateral Filtering)
这是一种非常高级的滤波器,它在平滑图像的同时能完美地保留边缘。它结合了空间邻近度和像素值相似度两个因素。
Mat bilateral;
bilateralFilter(original, bilateral, 15, 80, 80);
4.2 形态学操作 (Morphological Operations)
形态学操作主要针对二值图像(只有黑白两色),通过结构元素(Structuring Element)来探测和修改图像的形状。最常见的两种操作是腐蚀 (Erosion) 和膨胀 (Dilation)。
- 腐蚀 (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 是一个通用的形态学操作函数,通过第三个参数指定操作类型。
第五部分:寻找图像的'轮廓'——边缘、轮廓与直方图
现在,我们已经能让图像变得更'干净'了。下一步,我们要教会计算机'看'出图像中的物体边界和形状。
5.1 边缘检测 (Edge Detection)
边缘是图像中亮度发生急剧变化的地方,通常对应着物体的边界。Canny 边缘检测算法是目前最优秀、应用最广泛的边缘检测算法之一。
- 高斯滤波: 去除噪声。
- 计算梯度: 使用 Sobel 算子计算图像在水平和垂直方向的梯度强度和方向。
- 非极大值抑制 (NMS): 只保留梯度方向上的局部最大值,细化边缘。
- 双阈值检测: 设定高阈值和低阈值。高于高阈值的点被确定为边缘;低于低阈值的点被抛弃;介于两者之间的点,只有当它们与确定的边缘点相连时,才被保留。
#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);
Mat edges;
Canny(gray, edges, 50, 150, 3);
imshow("Original", src);
imshow("Edges", edges);
waitKey(0);
destroyAllWindows();
return 0;
}
5.2 轮廓检测 (Contour Detection)
边缘检测得到的是'线',而轮廓检测得到的是'面'。轮廓是具有相同颜色或强度的连续点构成的曲线,用于形状分析、物体检测和识别。
#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);
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, 0.5, Scalar(255, 0, 0), 1);
}
imshow("Contours", contour_img);
waitKey(0);
destroyAllWindows();
return 0;
}
findContours:在二值图像中查找轮廓。RETR_TREE 表示检索所有轮廓并重构嵌套轮廓的完整层次结构。CHAIN_APPROX_SIMPLE 表示压缩水平、垂直和对角线段,只保留端点。
drawContours:绘制轮廓。
contourArea 和 arcLength:计算轮廓的面积和周长。
boundingRect:计算包围轮廓的最小正立矩形。
5.3 图像直方图 (Histogram)
直方图是图像中像素强度分布的图形表示。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)通道进行均衡化,然后再转换回来。
第六部分:综合案例——构建一个简单的文档扫描仪
理论知识学得再多,不如动手做一个小项目。我们将综合运用前面学到的所有技能,构建一个能够自动检测文档边缘并进行透视矫正的简易扫描仪。
- 从一张包含文档的照片中,自动检测出文档的四条边缘。
- 对检测到的四边形进行透视变换,得到正面的、规整的文档图像。
- 预处理: 读取图像,转换为灰度图,进行高斯模糊去噪。
- 边缘检测: 使用 Canny 算法检测边缘。
- 轮廓查找: 在边缘图上查找所有轮廓。
- 筛选轮廓: 遍历所有轮廓,找到面积最大的、近似为四边形的轮廓(即我们的文档)。
- 透视变换: 获取该四边形的四个顶点,进行透视变换,得到矫正后的图像。
代码实现 (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) {
if (a.x - center.x >= 0 && b.x - center.x < 0) return true;
if (a.x - center.x < 0 && b.x - center.x >= 0) return false;
if (a.x - center.x == 0 && b.x - center.x == 0) {
if (a.y - center.y >= 0 || b.y - center.y >= 0) return a.y > b.y;
return a.y < b.y;
}
int det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y);
if (det < 0) return true;
if (det > 0) return false;
double d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y);
double d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y);
return d1 > d2;
});
return sorted;
}
int main(int argc, char** argv) {
if (argc != 2) {
cout << "Usage: " << argv[0] << " <image_path>" << endl;
return -1;
}
Mat src = imread(argv[1]);
if (src.empty()) {
cout << "Could not open or find the image!" << endl;
return -1;
}
Mat gray, blurred, edges;
cvtColor(src, gray, COLOR_BGR2GRAY);
GaussianBlur(gray, blurred, Size(5, 5), 0);
Canny(blurred, edges, 75, 200);
vector<vector<Point>> contours;
findContours(edges.clone(), contours, RETR_LIST, CHAIN_APPROX_SIMPLE);
vector<Point> doc_corners;
double max_area = 0;
for (const auto& contour : contours) {
double area = contourArea(contour);
if (area < 1000) continue;
vector<Point> approx;
double epsilon = 0.02 * arcLength(contour, true);
approxPolyDP(contour, approx, epsilon, true);
if (approx.size() == 4) {
bool is_rectangle = true;
for (int i = 0; i < 4; i++) {
Point p1 = approx[i];
Point p2 = approx[(i + 1) % 4];
Point p3 = approx[(i + 2) % 4];
Point v1 = p1 - p2;
Point v2 = p3 - p2;
double dot = v1.x * v2.x + v1.y * v2.y;
double mag1 = sqrt(v1.x * v1.x + v1.y * v1.y);
double mag2 = sqrt(v2.x * v2.x + v2.y * v2.y);
double angle = acos(dot / (mag1 * mag2)) * 180 / CV_PI;
if (abs(angle - 90) > 30) {
is_rectangle = false;
break;
}
}
if (is_rectangle && area > max_area) {
max_area = area;
doc_corners = approx;
}
}
}
if (doc_corners.empty()) {
cout << "Could not find document corners." << endl;
imshow("Result", src);
waitKey(0);
return -1;
}
doc_corners = sortCorners(doc_corners);
Point2f src_pts[4] = { (Point2f)doc_corners[0], (Point2f)doc_corners[1], (Point2f)doc_corners[2], (Point2f)doc_corners[3] };
int width = 800, height = 1000;
Point2f dst_pts[4] = { Point2f(0, 0), Point2f(width - 1, 0), Point2f(width - 1, height - 1), Point2f(0, height - 1) };
Mat transform = getPerspectiveTransform(src_pts, dst_pts);
Mat warped;
warpPerspective(src, warped, transform, Size(width, height));
Mat result = src.clone();
for (int i = 0; i < 4; i++) {
line(result, doc_corners[i], doc_corners[(i + 1) % 4], Scalar(0, 255, 0), 3);
}
imshow("Original", result);
imshow("Scanned Document", warped);
waitKey(0);
destroyAllWindows();
imwrite("scanned_document.jpg", warped);
return 0;
}
g++ document_scanner.cpp -o scanner `pkg-config --cflags --libs opencv4`
./scanner test_photo.jpg
- 多边形逼近 (
approxPolyDP): 这是关键一步。它能将复杂的轮廓用更少的点(这里是 4 个)来近似表示,从而找到四边形。
- 矩形验证: 仅仅有 4 个点还不够,我们还需要验证这四个点构成的是否是接近矩形的形状。通过计算相邻边的夹角是否接近 90 度来实现。
- 角点排序: 透视变换要求源点和目标点的顺序严格对应(左上->右上->右下->左下)。
sortCorners 函数通过计算质心和叉积来完成这个排序任务。
这个案例虽然简单,但它完整地展示了计算机视觉项目从数据输入、预处理、特征提取、目标识别到最终输出的典型流程。你已经亲手构建了一个有实用价值的小工具!
结语:通往更广阔世界的起点
至此,我们已经共同走过了从零配置环境到完成一个综合性项目的完整旅程。我们学习了 C++ OpenCV 的核心概念——Mat,掌握了图像的 I/O、基本操作、滤波、形态学、边缘与轮廓检测,并最终将这些知识融会贯通,构建了一个文档扫描仪。
但这仅仅是冰山一角。OpenCV 的世界远比这广阔:
- 特征检测与匹配: SIFT, SURF, ORB 等算法可以用于图像拼接、3D 重建。
- 目标检测: Haar 级联分类器、HOG+SVM 可以用于人脸、行人检测。
- 摄像头操作:
VideoCapture 类让你能实时处理来自摄像头的视频流。
- 深度学习集成: OpenCV 的
dnn 模块可以加载和运行 TensorFlow、PyTorch 等框架训练好的模型。
- 动手实践: 编程是手艺活,光看不练假把式。尝试修改文中的每一个例子,看看参数变化会带来什么效果。
- 阅读官方文档: OpenCV 的官方文档(docs.opencv.org)是最好的学习资料,里面有详细的函数说明和示例代码。
- 探索更多: 在 GitHub 上搜索 OpenCV 项目,看看别人是如何使用它的。
- 不要害怕错误: 编译错误、运行时崩溃是学习过程中最宝贵的老师。学会阅读错误信息,利用搜索引擎(如 Stack Overflow)解决问题。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- 随机西班牙地址生成器
随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online