零基础玩转 C++ OpenCV(Ubuntu 24.04实战指南)

零基础玩转 C++ OpenCV(Ubuntu 24.04实战指南)

前言:为什么是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:更新系统包列表

首先,确保我们的软件包列表是最新的:

sudoapt update 

步骤2:安装C++编译器和构建工具

我们需要g++(GNU C++编译器)来将我们写的C++代码翻译成机器能懂的语言,以及make这个自动化构建工具。

sudoaptinstall build-essential 
在这里插入图片描述

build-essential是一个元包,它会自动安装gcc, g++, make, libc6-dev等一整套C/C++开发必需的工具链。

步骤3:安装CMake

CMake是一个跨平台的构建系统生成器。OpenCV项目结构复杂,直接使用g++命令行编译会非常繁琐。CMake能帮我们自动生成合适的Makefile,极大地简化编译过程。

sudoaptinstall cmake 
在这里插入图片描述

步骤4:安装OpenCV及其开发文件

万幸的是,Ubuntu 24.04的官方仓库已经包含了预编译好的OpenCV库。我们可以直接通过apt安装,省去了从源码编译的漫长等待(通常需要1-2小时)。

# 安装OpenCV的核心库sudoaptinstall libopencv-dev # 为了确保所有功能模块都可用,也可以安装以下包sudoaptinstall python3-opencv # 虽然我们主攻C++,但这个包有时会包含一些额外的依赖
在这里插入图片描述

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插件后,体验极佳。
    • 安装:sudo apt install code
  • 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++编译这个文件:

g++ hello.cpp -o hello 

编译成功后,运行程序:

./hello 
在这里插入图片描述

你应该能在终端看到Hello, World!的输出。

编写代码:
创建一个名为hello.cpp的文件:

// hello.cpp#include<iostream>// 包含输入输出流库intmain(){ std::cout <<"Hello, World!"<< std::endl;return0;// 程序成功结束}
在这里插入图片描述

这段代码非常简单:

创建项目目录:

mkdir ~/opencv_tutorial cd ~/opencv_tutorial 

恭喜!你已经成功完成了C++开发的第一步。这个看似简单的流程(写代码 -> 编译 -> 运行)是我们未来所有工作的基础。


第二部分:初识OpenCV——图像的加载、显示与保存

现在,我们的工具箱已经准备好了。是时候请出今天的主角——OpenCV了!

2.1 核心概念:Mat类——图像的容器

在OpenCV中,cv::Mat 类是绝对的核心。你可以把它想象成一个超级智能的“相框”。这个相框不仅能装下一张图片的所有像素数据,还能记录这张图片的尺寸(宽高)、颜色通道数(灰度图1通道,彩色图3通道BGR)、数据类型(每个像素占多少字节)等元信息。

  • 像素 (Pixel): 图像的最小单位。对于一张彩色图像,每个像素通常由三个数值表示,分别对应蓝色(B)、绿色(G)、红色® 的强度。注意,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>// 包含OpenCV所有模块的头文件#include<iostream>// 为了方便,我们可以使用命名空间,避免每次都要写cv::usingnamespace cv;usingnamespace std;intmain(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;cvtColor(image, gray_image, COLOR_BGR2GRAY);// BGR转灰度// 6. 显示灰度图namedWindow("Gray Image", WINDOW_AUTOSIZE);imshow("Gray Image", gray_image);// 7. 保存灰度图到磁盘imwrite("output_gray.jpg", gray_image);// 8. 等待用户按键waitKey(0);// 0表示无限等待,直到有按键按下// 9. 销毁所有窗口destroyAllWindows();return0;}

代码详解:

  • #include <opencv2/opencv.hpp>:这是包含OpenCV所有功能的万能头文件。对于大型项目,为了编译速度,可以只包含需要的特定模块头文件(如<opencv2/imgproc.hpp>),但对于学习和小项目,用这个最方便。
  • imread: 从文件加载图像。第二个参数指定了加载模式。
  • image.empty(): 检查Mat对象是否为空,这是判断图像加载是否成功的关键。
  • namedWindowimshow: 用于创建窗口并显示图像。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编程的第一步!


第三部分:深入像素——图像的基本操作与处理

现在我们已经能和图像“见面”了,接下来我们要学会“触摸”它,也就是对像素进行操作。

3.1 访问和修改像素值

访问像素是图像处理的基础。OpenCV提供了多种方式,最常用且高效的是使用at<T>()方法。

案例:创建一个自定义图像并修改像素

#include<opencv2/opencv.hpp>usingnamespace cv;usingnamespace std;intmain(){// 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();return0;}

运行效果:

在这里插入图片描述
  • 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++的算术运算符,使得对图像进行加、减、乘、除等操作变得异常简单。

案例:图像混合(Alpha Blending)

图像混合是将两张图像按一定比例叠加在一起,常用于制作水印、淡入淡出效果等。

#include<opencv2/opencv.hpp>usingnamespace cv;usingnamespace std;intmain(){ 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(src1, alpha, src2, beta, gamma, dst)addWeighted(img1, alpha, img2, beta, gamma, blended);imshow("Blended Image", blended);waitKey(0);destroyAllWindows();return0;}

运行效果:

在这里插入图片描述
  • addWeighted函数实现了公式:dst = src1*alpha + src2*beta + gamma
  • resize函数用于调整图像尺寸,确保两张图可以进行运算。

3.3 图像的几何变换

改变图像的形状、大小或视角是常见的操作。

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));

第四部分:图像的“化妆术”——滤波与形态学操作

如果说前面的操作是“骨架”,那么滤波和形态学就是图像的“血肉”和“皮肤”,它们能极大地改善图像质量,为后续的高级分析(如目标检测)打下坚实基础。

4.1 图像滤波(平滑/模糊)

图像在获取过程中常常会受到噪声干扰。滤波的目的就是去除噪声,或者提取图像的某些特征(如边缘)。

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);// 第二个参数是邻域直径,第三、四个是颜色空间和坐标空间的标准差

4.2 形态学操作 (Morphological Operations)

形态学操作主要针对二值图像(只有黑白两色),通过结构元素(Structuring Element)来探测和修改图像的形状。最常见的两种操作是腐蚀 (Erosion)膨胀 (Dilation)

  • 腐蚀 (Erosion): 用结构元素扫描图像,只有当结构元素完全被前景(白色)覆盖时,中心点才保留为白色。效果是缩小前景物体,消除小的白色噪点。
  • 膨胀 (Dilation): 用结构元素扫描图像,只要结构元素与前景有交集,中心点就变为白色。效果是扩大前景物体,填充小的黑色空洞。

开运算 (Opening) = 先腐蚀后膨胀
主要用于去除小的白色噪点,分离粘连的物体。

闭运算 (Closing) = 先膨胀后腐蚀
主要用于填充物体内部的小孔洞,连接邻近的物体。

实战:清理二值图像

#include<opencv2/opencv.hpp>usingnamespace cv;usingnamespace std;intmain(){ 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();return0;}

运行效果:

在这里插入图片描述
  • morphologyEx是一个通用的形态学操作函数,通过第三个参数指定操作类型。

第五部分:寻找图像的“轮廓”——边缘、轮廓与直方图

现在,我们已经能让图像变得更“干净”了。下一步,我们要教会计算机“看”出图像中的物体边界和形状。

5.1 边缘检测 (Edge Detection)

边缘是图像中亮度发生急剧变化的地方,通常对应着物体的边界。Canny边缘检测算法是目前最优秀、应用最广泛的边缘检测算法之一。

Canny算法的步骤:

  1. 高斯滤波: 去除噪声。
  2. 计算梯度: 使用Sobel算子计算图像在水平和垂直方向的梯度强度和方向。
  3. 非极大值抑制 (NMS): 只保留梯度方向上的局部最大值,细化边缘。
  4. 双阈值检测: 设定高阈值和低阈值。高于高阈值的点被确定为边缘;低于低阈值的点被抛弃;介于两者之间的点,只有当它们与确定的边缘点相连时,才被保留。

实战:Canny边缘检测

#include<opencv2/opencv.hpp>usingnamespace cv;usingnamespace std;intmain(){ 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核大小3imshow("Original", src);imshow("Edges", edges);waitTime(0);destroyAllWindows();return0;}

运行效果:

在这里插入图片描述

5.2 轮廓检测 (Contour Detection)

边缘检测得到的是“线”,而轮廓检测得到的是“面”。轮廓是具有相同颜色或强度的连续点构成的曲线,用于形状分析、物体检测和识别。

实战:查找并绘制轮廓

#include<opencv2/opencv.hpp>usingnamespace cv;usingnamespace std;intmain(){ 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,0.5,Scalar(255,0,0),1);}imshow("Contours", contour_img);waitKey(0);destroyAllWindows();return0;}

运行效果:

在这里插入图片描述
  • findContours:在二值图像中查找轮廓。RETR_TREE表示检索所有轮廓并重构嵌套轮廓的完整层次结构。CHAIN_APPROX_SIMPLE表示压缩水平、垂直和对角线段,只保留端点。
  • drawContours:绘制轮廓。
  • contourAreaarcLength:计算轮廓的面积和周长。
  • boundingRect:计算包围轮廓的最小正立矩形。

5.3 图像直方图 (Histogram)

直方图是图像中像素强度分布的图形表示。X轴是像素强度值(0-255),Y轴是该强度值出现的频率。直方图能告诉我们图像的整体明暗、对比度等信息。

直方图均衡化 (Histogram Equalization) 是一种常用的图像增强技术,它通过重新分配像素强度值,使直方图变得平坦,从而增加图像的全局对比度。

实战:直方图均衡化

#include<opencv2/opencv.hpp>usingnamespace cv;usingnamespace std;intmain(){ 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();return0;}

运行效果:

在这里插入图片描述

对于彩色图像,不能直接对BGR通道进行均衡化,否则会改变颜色。正确的方法是先转换到HSV或YUV颜色空间,只对亮度(V或Y)通道进行均衡化,然后再转换回来。


第六部分:综合案例——构建一个简单的文档扫描仪

理论知识学得再多,不如动手做一个小项目。我们将综合运用前面学到的所有技能,构建一个能够自动检测文档边缘并进行透视矫正的简易扫描仪。

项目目标:

  1. 从一张包含文档的照片中,自动检测出文档的四条边缘。
  2. 对检测到的四边形进行透视变换,得到正面的、规整的文档图像。

实现思路:

  1. 预处理: 读取图像,转换为灰度图,进行高斯模糊去噪。
  2. 边缘检测: 使用Canny算法检测边缘。
  3. 轮廓查找: 在边缘图上查找所有轮廓。
  4. 筛选轮廓: 遍历所有轮廓,找到面积最大的、近似为四边形的轮廓(即我们的文档)。
  5. 透视变换: 获取该四边形的四个顶点,进行透视变换,得到矫正后的图像。

代码实现 (document_scanner.cpp):

#include<opencv2/opencv.hpp>#include<iostream>#include<vector>#include<algorithm>usingnamespace cv;usingnamespace std;// 辅助函数:计算点到直线的距离doubledistanceToLine(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;returnabs(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(constauto& 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)returntrue;if(a.x - center.x <0&& b.x - center.x >=0)returnfalse;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)returntrue;if(det >0)returnfalse;// 点在同一条线上,按距离排序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;}intmain(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;}// 1. 预处理 Mat gray, blurred, edges;cvtColor(src, gray, COLOR_BGR2GRAY);GaussianBlur(gray, blurred,Size(5,5),0);// 2. Canny边缘检测Canny(blurred, edges,75,200);// 3. 查找轮廓 vector<vector<Point>> contours;findContours(edges.clone(), contours, RETR_LIST, CHAIN_APPROX_SIMPLE);// 4. 筛选轮廓 vector<Point> doc_corners;double max_area =0;for(constauto& 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;// 如果角度偏离90度太多,则不是矩形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;}// 5. 排序角点 doc_corners =sortCorners(doc_corners);// 6. 透视变换 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));// 7. 显示结果// 在原图上绘制检测到的四边形 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);return0;}

编译与运行:

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等框架训练好的模型。

学习建议:

  1. 动手实践: 编程是手艺活,光看不练假把式。尝试修改文中的每一个例子,看看参数变化会带来什么效果。
  2. 阅读官方文档: OpenCV的官方文档(docs.opencv.org)是最好的学习资料,里面有详细的函数说明和示例代码。
  3. 探索更多: 在GitHub上搜索OpenCV项目,看看别人是如何使用它的。
  4. 不要害怕错误: 编译错误、运行时崩溃是学习过程中最宝贵的老师。学会阅读错误信息,利用搜索引擎(如Stack Overflow)解决问题。
Could not load content