• OpenCV4(C++)—— 图像连通域的详细分析


    前言

    图像连通域,其实就是图像分割的一种方法。它通过检测像素之间的连接关系和相似性来划分图像中的区域,以便进行后续处理。图像邻域和图像邻域分析就不介绍了,网上很多。本文会通过代码和结果图来直观展示。


    一、connectedComponents函数

      OpenCV4提供了提取图像中不同连通域的 connectedComponents() 函数,并为部分参数增加了默认值。使用时直接填入输入图像和输出图像两个参数即可。
    在这里插入图片描述

      举例:找出上面图像中所有水果,并用矩形框标出

    代码如下(示例):

    #include 
    #include  
    using namespace std;
    
    int main()
    {
    	cv::Mat image = cv::imread("C:/Users/Opencv/temp/300.png", cv::IMREAD_GRAYSCALE);
    	cv::Mat binImg;
    	cv::threshold(image, binImg, 235, 255, cv::THRESH_BINARY_INV);  // 二值化处理
    
    	// //统计图像中连通域的个数
    	cv::Mat labels;
    	int numComponents = cv::connectedComponents(binImg, labels);
    
        // 绘制每个连通域的边界框
        cv::Mat output = image.clone();
        for (int i = 1; i < numComponents; ++i) {
            // 提取当前连通域的mask
            cv::Mat mask = (labels == i);
    
            // 查找轮廓
            std::vector<std::vector<cv::Point>> contours;
            cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
    
            // 绘制边界框
            cv::Rect boundingRect = cv::boundingRect(contours[0]);
            cv::rectangle(output, boundingRect, cv::Scalar(0, 255, 0), 2);
        }
    	cv::imshow("原图", image);
    	cv::imshow("二值图", binImg);
    	cv::imshow("标记", output);
    
    	cv::waitKey(0);
    	cv::destroyAllWindows();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    分析:
    (1)根据图像邻域分析原理,进行连通域统计之前,要先把输入图像转换为二值图像。

    (2)将二值图传入connectedComponents函数中,连通域结果记录在labels中,返回值是连通域的个数。可视化labels,白色区域就是函数判定的连通域,对这些连通域的所有像素会进行标号处理,从“1”开始。从图1中可以看到很许多较小的连通域,将其放大(如图2),可以看到标号“17”的连通域只有4个像素,标号“18”的只有2个像素。

    在这里插入图片描述

    图 1

    在这里插入图片描述

    图 2

    (3)示例代码中 cv::Mat mask = (labels == i); 分析:当 i=1 时,labels图中只会把标号“1”的连通域提取出来,其他连通域都置为0,所以通过循环可依次找出不同标号的连通域。下面图3为标号“1”的连通域,可以看到其他白色区域都没有了。然后再对它进行查找轮廓,再添加矩形框,结果如图4。
    在这里插入图片描述

    图 3

    在这里插入图片描述

    图 4

    下面图5为循环到标号“7”的连通域,详细像素值如图6,矩形框结果如图7

    在这里插入图片描述

    图 5

    在这里插入图片描述

    图 6

    在这里插入图片描述

    图 7

    (4)示例代码的最终结果如图8。而原本只想要检测出三个水果,可以通过判定连通域大小,将较大的连通域删除,只保留较大的。示例代码修改如下,结果如图9所示

    在这里插入图片描述

    图 8
    int main()
    {
    	cv::Mat image = cv::imread("C:/Users/Opencv/temp/300.png", cv::IMREAD_GRAYSCALE);
    	cv::Mat binImg;
    	cv::threshold(image, binImg, 235, 255, cv::THRESH_BINARY_INV);  // 二值化处理
    
    	// //统计图像中连通域的个数
    	cv::Mat labels;
    	int numComponents = cv::connectedComponents(binImg, labels);
    
        // 绘制每个连通域的边界框
        cv::Mat output = image.clone();
        for (int i = 1; i < numComponents; ++i) {
            // 提取当前连通域的mask
            cv::Mat mask = (labels == i);
    
            // 查找轮廓
            std::vector<std::vector<cv::Point>> contours;
            cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);  
    
            // 计算边界框面积
            cv::Rect boundingRect = cv::boundingRect(contours[0]);
            int area = boundingRect.width * boundingRect.height;
            if (area < 200)  // 面积小于200的不要
                continue;
    
            // 绘制边界框
            cv::rectangle(output, boundingRect, cv::Scalar(0, 255, 0), 2);
        }
    
    	cv::imshow("原图", image);
    	cv::imshow("二值图", binImg);
    	cv::imshow("标记", output);
    
    	cv::waitKey(0);
    	cv::destroyAllWindows();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在这里插入图片描述

    图 9

    二、connectedComponentsWithStats函数

      connectedComponents()函数只能通过标签将图像中的不同连通域区分开,无法得到更多的统计信息。上面的示例代码中,额外采用了findContours函数获得轮廓,然后使用boundingRect函数来计算其最小外接矩形。

      因为得到目标的矩形区域是非常常用的一个步骤,所以OpenCV4提供了connectedComponentsWithStats ()函数用于标记出图像中不同连通域的同时统计连通域的位置、面积的信息。

      相比于connectedComponents()函数,该函数主要多了两个参数:stats和centroids

    int cv::connectedComponentsWithStats(
        InputArray image,                  // 输入图像,应为二值化图像
        OutputArray labels,                // 输出标签图像,每个像素点对应一个连通域标签
        OutputArray stats,                 // 输出统计信息,包括连通域的外接矩形、面积等
        OutputArray centroids,             // 输出连通域的质心坐标
        int connectivity = 8,              // 连通性,可选择 4 或 8 连通
        int ltype = CV_32S                   // 标签图像的数据类型,默认为 CV_32S
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上述示例代码可以修改为:

    int main()
    {
    	cv::Mat image = cv::imread("C:/Users/Opencv/temp/300.png", cv::IMREAD_GRAYSCALE);
    	cv::Mat binImg;
    	cv::threshold(image, binImg, 235, 255, cv::THRESH_BINARY_INV);  // 二值化处理
    
    	// 连通组件标记及统计信息计算
    	cv::Mat labels, stats, centroids;
    	int numComponents = cv::connectedComponentsWithStats(binImg, labels, stats, centroids);
    
    	// 绘制每个连通组件的外接矩形和质心
    	cv::Mat output = image.clone();
    	cv::cvtColor(output, output, cv::COLOR_GRAY2BGR); // 将灰度图转为伪彩色图以便绘制
    	// cv::Mat output = cv::imread("C:/Users/jutze/ljw_C++/Opencv/temp/300.png"); // 或者直接用原图的彩色图
    
    	for (int i = 1; i < numComponents; ++i) {
    		// 根据面积过滤连通组件
    		int area = stats.at<int>(i, cv::CC_STAT_AREA);
    		if (area > 200)
    		{
    			// 绘制外接矩形
    			cv::Rect boundingRect(
    				stats.at<int>(i, cv::CC_STAT_LEFT),
    				stats.at<int>(i, cv::CC_STAT_TOP),
    				stats.at<int>(i, cv::CC_STAT_WIDTH),
    				stats.at<int>(i, cv::CC_STAT_HEIGHT));
    			cv::rectangle(output, boundingRect, cv::Scalar(0, 255, 0), 2);
    
    			// 绘制质心
    			cv::Point centroid(centroids.at<double>(i, 0), centroids.at<double>(i, 1));
    			cv::circle(output, centroid, 5, cv::Scalar(255, 0, 0), -1);
    		}
    	}
    	cv::imshow("原图", image);
    	cv::imshow("二值图", binImg);
    	cv::imshow("标记", output);
    
    	cv::waitKey(0);
    	cv::destroyAllWindows();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    在这里插入图片描述
    注:示例代码中,有一行代码是将灰度图转为伪彩色图,我们知道灰度图是无法转回彩色图的,所以这步称为伪彩色。即将单通道转为三通道,但数值一样,如[247]转为[247,247,247]。三个通道数值一样,所呈现的视觉效果跟灰度图是相同的,如下图
    在这里插入图片描述
    可以看到,output已经是3×UINT8的三通道类型了,但看起来还是跟灰度图一样。再放大看看数值,三个通道一样

    在这里插入图片描述
    因为原图是彩色图,示例代码中将其转为灰度图,我们也可以直接用原图来进行绘制,结果如下

    在这里插入图片描述

  • 相关阅读:
    学习突围2 - 关于高效学习的方法
    Unity ab包加载文本 puerts 自定义loader
    一文解析 Pinia 和 Vuex,带你全面理解这两个 Vue 状态管理模式
    多线程与高并发(三)—— 源码解析 AQS 原理
    054、Python 函数的概念以及定义
    品质主管的面试题目
    六零导航页SQL注入漏洞复现(CVE-2023-45951)
    电源模块选型指导
    辛弃疾,笔墨剑影的一生
    【Hack The Box】linux练习-- Ophiuchi
  • 原文地址:https://blog.csdn.net/qq_43199575/article/details/133810085