前言

        opencv库功能非常丰富,本篇仅分享在高拍仪场景下(黑色背景板),实现识别物体轮廓描绘的功能,关键模块会以参数差异+图片方式进行讲解,方便深入理解。

环境安装

window opencv安装及引入qtcreatorhttps://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;//50
    double cannyThreshold2 = 90;//150
    cv::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);

篇尾

        三、四、五、九模块需要反复调试才能得到很好的效果,需要本篇源码的请留言。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐