数字图像处理【11】OpenCV-Canny边缘提取到FindContours轮廓发现

本章主要介绍图像处理中一个比较基础的操作:Canny边缘发现、轮廓发现 和 绘制轮廓。概念不难,主要是结合OpenCV 4.5+的API相关操作,为往下 "基于距离变换的分水岭图像分割" 做知识储备。

Canny边缘检测

在讲述轮廓之前,要花点时间学学边缘检测提取的一个著名算法——Canny边缘提取算法。该算法检测出边相对于其他边缘检测算法的效果显著不同就是,Canny 检测出的边是比较细且清晰。该算法相比之前学习的Sobel和Laplace而言,它是一个应用方法,是真正的做到“提取”边缘这个操作;而Sobel和Laplace只是提留在图像像素的集合中。

Canny 算法的边缘检测到提取,主要有如下几个步骤:

1、灰度化cvColor与高斯滤波GaussianBlur

将图像变为灰度图像,减少通道,高斯滤波的作用是平滑图像,减少噪声,不让Canny算法检测时,误认为是边缘,所以在一开始就使用这个高斯滤波来减少比较突出的地方,简单的来说就是过滤掉图像上不合适的地方。

2、计算图像的梯度和梯度方向Sobel/Scharr

图像的边缘是灰度值急剧变化的位置。比如在灰度图像中它只有明暗的变化,当某个地方的强度变化比较的剧烈,那么它就会形成一个边缘。明暗变化较大的地方,梯度变化也会很大。

3、非极大值抑制

这一步是要做什么的呢?经过上面的操作,我们只是对图像进行了一个增强,而并不是找到真正的边缘。而且经过1-2步骤发现的边缘是一个范围信号,但是边缘只能是一条或者一簇,所以我需要对非边缘的个体进行压制除掉?怎么做呢,就是在边缘的梯度方向上,不是给定的最大值的话,那就去掉不要。(如下图,左边是Sobel求梯度方向,右边是常用的抑制范围选取)

但是这个最大的阈值该如何设置?过高将许多原本是线的位置未设置,过低就会细碎边,我们希望效果是找到清晰、连续的边缘线。

4、双阈值筛选边缘连接

经过非极大值抑制后图像检测出边还是有许多灰色而且不算清晰。所以接下来设置双阈值,规定上下阈值,所谓双阈值就是有两个阈值分别是低阈值和高阈值。如果像素点灰度值是大于最大阈值就直接将其更新为 255 。如果像素点的灰度值小于最小阈值就将其灰度值更新为 0。如果像素点灰度值是处于最大阈值到最小阈值之间,就看其 8 邻域中是否有大于最大阈值的值,如果有也就将其归为 255,也可以取 8 领域的平均值。

Canny边缘检测算法基本上就是经过以上的步骤。OpenCV有对应优化的Canny方法,一起看看如何使用,往下在学发现轮廓的时候就要用到。

CV_EXPORTS_W void Canny(
    InputArray image,  // 8-bit输入图像
    OutputArray edges, // 输出的边缘图像,一般都是二值图像,背景是黑色
    double threshold1, // 低阈值,常取高阈值的1/2或者1/3 
    double threshold2, // 高阈值
    int apertureSize = 3,   // Sobel算子的size,取值3代表是3x3
    bool L2gradient = false // 选择true用L2=sqrt{(dI/dx)^2 + (dI/dy)^2}求梯度方向,
                            // 默认false用L1=|dI/dx|+|dI/dy|计算
);

轮廓(contour)

  • 轮廓发现是基于图像边缘提取的基础上,寻找对象轮廓的方法。所以边缘提取的阈值选定会影响最终轮廓发现的结果
  • API介绍:findContours发现轮廓 / drawContours绘制轮廓

有时候,轮廓和边缘的概念是非常相似的,在单一物体对象上表面的轮廓就相当于其边缘特征。但是多个物体对象叠加之后,轮廓和边缘就不再能这样相提并论了。(如上图示)

轮廓是在边缘的基础上,构成一张轮廓的拓扑图,然后利用不同的拓扑算法去寻找和构建轮廓。所以边缘提取的阈值选定会影响最终轮廓发现的结果。

说完边缘与轮廓的关系与区别之后。那么在OpenCV中,轮廓的发现绘制与绘制要如何实现呢?

  1. 输入图像转为灰度图像cvtColor
  2. 使用Canny进行边缘提取,转化二值图像
  3. 使用findContours发现轮廓
  4. 使用drawContours绘制轮廓

这里先介绍cv::findCountours 和 cv::drawContours这两个api

CV_EXPORTS_W void findContours(
    InputArray image,             // 输入图像,二值图,一般就是Canny的输出,8-bit
    OutputArrayOfArrays contours, // 全部发现的轮廓对象,就是一个二维数组,图结构,往下细说
    OutputArray hierarchy, // 轮廓图的拓扑结构,可选输出,最终的轮廓发现就是基于这个拓扑结构实现
    int mode,              // 寻找轮廓的模式,一般返回RETR_TREE树模式
    int method,            // 轮廓发现的方法,一般使用CHAIN_APPROX_SIMPLE简单方式
    Point offset = Point() // 轮廓像素偏移,默认(0,0)没偏移
);

CV_EXPORTS_W void drawContours( 
    InputOutputArray image, // 绘制的目标图像
    InputArrayOfArrays contours, // 全部轮廓对象,就是findContours的第二个输出参数
    int contourIdx,              // 轮廓索引号,contours的第一维索引
    const Scalar& color,         // 绘制颜色
    int thickness = 1,           // 绘制线宽
    int lineType = LINE_8,       // 绘制线类型
    InputArray hierarchy = noArray(), // 拓扑结构图,findContours的第三个可选输出参数
    int maxLevel = INT_MAX, // 最大层数,0只绘制当前的,1包含内部轮廓,2所有轮廓
    Point offset = Point()  // 轮廓偏移
);

接着来一段案例代码,讲讲findContours的第二、第三参数如何理解。

int main()
{
    //读取测试图片
    src = imread("F:\\other\\learncv\\bottle.png");
    namedWindow(titleStr + "src", WINDOW_AUTOSIZE);
    imshow(titleStr + "src", src);
    //rgb转gray
    cvtColor(src, gray, COLOR_BGR2GRAY);
    namedWindow(titleStr + "circles", WINDOW_AUTOSIZE);
    namedWindow(titleStr + "contours", WINDOW_AUTOSIZE);
    createTrackbar("边缘检测阈值", titleStr + "src", &threshold_value, threshold_max, Callback_Contours);

    waitKey(0);
    return 0;
}

void Callback_Contours(int pos, void* userdata) {
    Mat canny_img;
    Canny(gray, canny_img, threshold_value, threshold_value * 2.0, 3, false);

    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(canny_img, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));

    Mat dst1 = Mat::zeros(gray.size(), CV_8UC3);
    Mat dst2 = Mat::zeros(gray.size(), CV_8UC3);

    for (size_t i = 0; i < contours.size(); i++) {
        drawContours(dst1, contours, i, colorWhite, 1, LINE_8, hierarchy, 0, Point(0, 0));

        vector<Point> contourPoints = contours[i];
        for (size_t j = 0; j < contourPoints.size(); j++) {
            circle(dst2, contourPoints[j], 1, colorWhite);
        }
    }
    imshow(titleStr + "contours", dst1);
    imshow(titleStr + "circles", dst2);
}

以上代码运行的效果,Canny边缘阈值在100~200之间。 其中我把Canny的第二个输出参数contours也以点的方式绘制出来,对应的是图最左边,中间部分是drawContours绘制的轮廓,右边是原图。放大可以清楚观察到 “荷花” 二字上方,荷花瓣的位置,明显看出点的方式是断断续续的中间留有很大一部分的空白,而绘制轮廓后是能把它们连接成一条线。这是因为drawContours会根据contours二维数据的第一维去判断这些是不是属于同一线段。Debug调试就可以知道contours[i]的每一层长度都是不一样的。

至于第三个参数 hierarchy 轮廓拓扑关系,此参数输出的内容 与 第四个参数 mode 寻找轮廓的模式,有莫大的关系,详细看看查阅以下这个同学的详细分析。

(十二) findContours函数的hierarchy详解_findcontours hierarchy_恒友成的博客-CSDN博客获取对象的轮廓,一般最好先对图像进行灰度化再进行阈值处理,然后用来检测轮廓。_findcontours hierarchyhttps://blog.csdn.net/lx_ros/article/details/126258801

Ok,That’s All.

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/131602095