QT opencv实现高拍仪场景识别物体轮廓的案例详解
前言
opencv库功能非常丰富,本篇仅分享在高拍仪场景下(黑色背景板),实现识别物体轮廓描绘的功能,关键模块会以参数差异+图片方式进行讲解,方便深入理解。
环境安装
window opencv安装及引入qtcreator
https://blog.csdn.net/liangyuna8787/article/details/149693144?spm=1001.2014.3001.5502
参考之前分享的博文,包含了添加库以及打包的方法。
案例详细讲解
重要模块一览
| 函数名称 | 作用 | 详解 | 用途 | 应用场景 |
|---|---|---|---|---|
| imread() | 读取图像 | 从指定路径加载图像文件 | 读取磁盘中的图像到内存 | 图像处理流程的初始化阶段 |
| cvtColor() | 颜色空间 | 将图像从一种颜色空间转换为另一种 | BGR到灰度图转换 | 预处理阶段,简化图像信息 |
| GaussianBlur() | 图像滤波 | 应用高斯模糊降低图像噪声 | 平滑图像,消除细节干扰 | 边缘检测前的预处理 |
| Canny() | 边缘检测 | 检测图像中的边缘轮廓 | 提取图像边缘特征 | 轮廓检测、物体识别 |
| morphologyEx() | 形态学操作 | 执行形态学闭运算操作 | 连接断开的边缘 | 边缘增强和连接 |
| dilate() | 形态学膨胀 | 扩大图像中的亮区区域 | 增强边缘连续性 | 轮廓修复和增强 |
| findContours() | 轮廓检测 | 查找图像中的所有轮廓 | 提取图像中的连通区域 | 物体检测、形状分析 |
| contourArea() | 几何计算 | 计算轮廓的面积 | 评估轮廓大小 | 轮廓筛选和分类 |
| minAreaRect() | 几何分析 | 计算轮廓的最小外接矩形 | 获取物体的旋转矩形边界 | 文档检测、物体定位 |
| boundingRect() | 边界计算 | 计算轮廓的垂直边界矩形 | 获取轮廓的包围盒 | 区域分析和裁剪 |
| ▇▇▇▇▇▇▇▇▇▇▇▇ | ▇▇▇▇▇▇▇▇▇ | ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ | ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ | ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ |
| line() | 图形绘制 | 在图像上绘制直线段 | 标记检测结果 | 结果可视化和标注 |
| circle() | 图形绘制 | 在图像上绘制圆形标记 | 突出显示关键点 | 特征点标记 |
| putText() | 文本绘制 | 在图像上添加文字标注 | 显示测量结果和标识 | 结果说明和注释 |
| imshow() | 图像显示 | 在窗口中显示图像 | 实时查看处理结果 | 调试和演示 |
| drawContours() | 轮廓绘制 | 图像上绘制检测到的轮廓 | 可视化轮廓结果 | 调试和结果展示 |
以上加粗的地方用于记忆该函数的特点
模块一:读取图像imread
std::string imagePath = "myimage.jpg";// 读取图片cv::Mat image = cv::imread(imagePath);if (image.empty()) {std::cout << "no can read: " << imagePath << std::endl;return -1;}
入参是cv::String类型,也是std::string类型,可见这是一个C++的库,不是QT专属库。
模块二:颜色空间cvtColor
cv::Mat gray;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
第一个参数:输入图像
第二个参数:输出图像
第三个参数常用的值:
- COLOR_BGR2GRAY:BGR 转灰度图
- COLOR_BGR2HSV:BGR 转 HSV 空间
- COLOR_BGR2RGB:BGR 转 RGB 空间
- COLOR_RGB2BGR:RGB 转 BGR 空间
OpenCV 默认彩色图像为 BGR,COLOR_BGR2GRAY即为转灰度图,转灰度图可以简化数据及减少计算量,减少了后续处理(如降噪、边缘检测等)的计算复杂度,从而提升算法运行速度,是非常常用的处理方式;
对颜色敏感的场景时,使用COLOR_BGR2HSV,比如只识别指定颜色(绿色)的银行卡;
调用其他明确需要RGB的库时,需要COLOR_BGR2RGB进行转换;

原图-灰度图对比图

原图-HSV图对比图
本篇不针对指定的银行卡,所有物体通用,使用的是灰度图。
模块三:高斯滤波GaussianBlur
cv::Mat blurred;
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
第一个参数:输入图像
第二个参数:输出图像
第三个参数:高斯核的大小,需要是奇数,值越小,越清晰,去噪越少,值越大越模糊,去噪越多
第三四个参数:高斯函数的标准差,配合高斯核使用
原理是通过高斯核(权重矩阵)对图像进行加权平均,从而平滑图像、降噪或作为预处理步骤。
cv::Size(3, 3)轻度模糊,基本保留所有的轮廓,如下图,右边轮廓图中对比原图强光的位置有零零散散的噪点;

cv::Size(5, 5)中度模糊,去掉比较明显的干扰,如下图,零零散散的强光噪点基本没有了;

cv::Size(7, 7)及以上是强模糊,清除更多的噪点,比如中间的凹槽轮廓与卡片衔接,影响后续的物体轮廓描边,此时希望去掉更多的噪点,如下图,靠近卡片的凹槽轮廓噪点被抹除了。

提出都是先使用cv::Size(5, 5)来调试整体效果,需要保留更多细节时就降为3,需要祛除更多噪点就提升为7、9,并且调节标准差进行调试。
另外右边的轮廓,是调用cv::Canny边缘检测之后,生成的边缘效果,这两个函数也是经常搭配使用的。
模块四:边缘检测Canny
效果图以及在模块三中进行展示,以下介绍参数。
cv::Mat edges;double cannyThreshold1 = 30;//50double cannyThreshold2 = 90;//150cv::Canny(blurred, edges, cannyThreshold1, cannyThreshold2);
第一个参数:输入图像
第二个参数:输出图像
第三个参数:低阈值(threshold1),用于连接边缘。如果设置过高,可能会漏掉弱边缘;如果设置过低,可能会引入过多的噪声
第四个参数:高阈值(threshold2):用于检测强边缘。如果设置过高,可能会漏掉一些重要的边缘;如果设置过低,可能会检测到过多的噪声
threshold1 与 threshold2,一般建议 threshold2 ≈ 2~3 × threshold1。
模块五:形态学操作morphologyEx
//形态学操作连接断开的边缘cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(7, 7));cv::morphologyEx(edges, edges, cv::MORPH_CLOSE, kernel);
先通过cv::getStructuringElement生成形态学操作所需的结构元素,
第一个参数:输入图像
第二个参数:输出图像
第三个参数:形态学操作类型,决定了具体的处理效果
第四个参数:结构元素,用于定义形态学操作的邻域形状和大小
如下图,可以看到轮廓的线条明显连续,但是目标卡片的图像还是比较模糊

模块六:形态学膨胀dilate
cv::dilate(edges, edges, kernel); // 添加膨胀操作
结果模块五的形态学初步处理之后,目标卡片的轮廓还是比较模糊,那么就对整体轮廓进行加膨胀粗操作,效果如下图,可以看到卡片的轮廓以及很明显了。

模块七:轮廓检测findContours
//查找轮廓std::vector<std::vector<cv::Point>> contours;cv::findContours(edges, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
第一个参数:输入单通道的二值图像
第二个参数:输出轮廓的坐标点集合,及连续的坐标点代表一个轮廓
第三个参数:轮廓检索模式,如 CV_RETR_EXTERNAL(仅最外层轮廓)、CV_RETR_TREE(完整嵌套层级)等
第四个参数:轮廓近似方法,如 CV_CHAIN_APPROX_SIMPLE(压缩水平/垂直/对角线段)
经过前面6个模块的处理,目标物体的轮廓已经很清晰了,此时可以调用轮廓检测函数,获取被预处理之后的图片的所有轮廓集合(轮廓不仅仅一个,噪点处理不会的图片甚至会检测出几百个轮廓)。因为是描边,获取最外层轮廓( CV_RETR_EXTERNAL)即可。
针对符合的轮廓集合contours,可以用cv::drawContours函数把轮廓坐标描绘出来
void showimg(const string& name, const cv::Mat& curImage) {cv::Mat resizedImage;double scale = 0.2;//把图片压缩到20%cv::resize(curImage, resizedImage, cv::Size(), scale, scale);cv::imshow(name, resizedImage);
}std::vector<std::vector<cv::Point>> contours;cv::findContours(edges, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);// 用绿色绘制所有轮廓cv::drawContours(result, contours, -1, cv::Scalar(0, 255, 0), 2);showimg("result", result);

模块八:获取轮廓像素面积contourArea
// 找到面积最大的轮廓auto maxContour = *std::max_element(contours.begin(), contours.end(),[](const std::vector<cv::Point>& a, const std::vector<cv::Point>& b) {return cv::contourArea(a) < cv::contourArea(b);});
正常来说最大的轮廓区域面积就是我们需要识别的目标物体,但是需要注意的是,这个轮廓像素面积≠几何平面面积!即它不是以几何学方式得出的结果,在噪点不干扰的情况下,像素面积最大以及几何面积最大都能指向同一区域。项目允许容错的话,用contourArea即可。
(后续我会分享一篇自己构建矩形的博文)
模块九:获取最小外接矩形minAreaRect
// 获取最小外接矩形cv::RotatedRect rotatedRect = cv::minAreaRect(maxContour);cv::Point2f vertices[4];rotatedRect.points(vertices);// 转换为整数点坐标std::vector<cv::Point> rectPoints;for (int i = 0; i < 4; i++) {rectPoints.push_back(cv::Point(static_cast<int>(vertices[i].x),static_cast<int>(vertices[i].y)));}
用于寻找能够包围给定点集的最小面积矩形,该矩形可以是旋转的,通过 RotatedRect 的 points() 方法可以获取旋转矩形的四个顶点坐标,顶点坐标是浮点型,需要转换成整型。
最终效果图

绿色显示所有的轮廓,红色是最小面积矩形的(刚好把物体框进去),黄色点是最小面积矩形的四个顶点,代码偏向于绘图,辅助调试用的。
此时如果需要衔接裁剪功能,只需要把最小面积矩形的rectPoints坐标点去处理即可。
// 用绿色绘制所有轮廓cv::drawContours(result, contours, -1, cv::Scalar(0, 255, 0), 2);if (!rectPoints.empty() && rectPoints.size() == 4) {// 用红色绘制矩形轮廓for (int i = 0; i < 4; i++) {cv::line(result, rectPoints[i], rectPoints[(i+1)%4], cv::Scalar(0, 0, 255), 3);}// 在4个点上画黄色圆标记for (int i = 0; i < 4; i++) {cv::circle(result, rectPoints[i], 8, cv::Scalar(0, 255, 255), -1);// 标记顶点编号cv::putText(result, std::to_string(i+1),cv::Point(rectPoints[i].x + 10, rectPoints[i].y - 10),cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 255, 255), 2);}// 计算并显示面积double area = cv::contourArea(rectPoints);std::string label = cv::format("Area: %.0f", area);cv::putText(result, label, cv::Point(20, 40),cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 255), 2);std::cout << "检测到纸张轮廓,面积: " << area << std::endl;for (int i = 0; i < 4; i++) {std::cout << "顶点" << i+1 << ": (" << rectPoints[i].x << ", " << rectPoints[i].y << ")" << std::endl;}} else {std::cout << "未检测到明显的纸张轮廓" << std::endl;}// 显示结果showimg("result", result);
篇尾
三、四、五、九模块需要反复调试才能得到很好的效果,需要本篇源码的请留言。
