二值图像分析:连通组件寻找算法

1.连通组件标记算法介绍

连接组件标记算法(connected component labeling algorithm)是图像分析中最常用的算法之一,算法的实质是扫描二值图像的每个像素点,对于像素值相同的而且相互连通分为相同的组(group),最终得到图像中所有的像素连通组件。扫描的方式可以是从上到下,从左到右。对于一幅有N个像素的图像来说,最大连通组件个数为N/2。扫描是基于每个像素单位的,OpenCV中进行连通组件扫描调用的时候必须保证背景像素是黑色、前景像素是白色。

最常见的连通组件扫描有如下两类算法:

  • 1.一步扫描法:基于图的搜索算法,比较复杂,效率较低。
  • 2.两步扫描法:基于扫描与等价类合并算法。

2.两步扫描算法分析

2.1 算法第一步:扫描

两步扫描算法的第一步是扫描图像,假设二值图像由0像素值(背景)和255像素值(前景对象)组成的,对扫描到的前景(255高像素值)像素点标记类别。扫描顺序为从左往右,从上往下依次扫描。

如果扫描到背景(黑色)像素点就不做处理直接pass,如果扫描到的是255像素值点,则按如下规则来对其标记类别:

  • 1.如果该点上边或者左边的像素值均为0或者不存在(例如第一行第一个点),则将其标记为新的类。
  • 2.如果该点上边或者左边的像素值有一个也为255,则将其标记为与之相同像素值所属于的类,左上同时都是255则优先分配到上面点所属的类中。

以下面的二值图为例,完成遍历过程,遍历起始点为黄色点。

在这里插入图片描述

在第一行中,扫描到第一个点,该点左边上边均无像素点,所以该点为新的类,由于其是扫描到的第一个类,所以将其标记为类1。扫描到第二个点的时候,由于这是第一行,上面没有像素,所以只需要看左边即可。显然第二个点左边的点,即第一个也是255,与之相同,所以该点也标记为类1。第三个像素值为0,不管。到第4个点的时候,左边像素为0,上面又不存在,所以标记为新的类,即第2类,同理后面两个也是类2,最后一个为类3,第一行遍历完成后标记如下图:

在这里插入图片描述
在第一行中,由于上面没有像素点,所以其中每个点的类别判断只用看它左边。而在第二行中,由于上面有第一行的各个点,所以第二行中的各个点类别判断需要看它上面的点的类别。在第二行中,第一个点是前景点,像素值为255,它上面的点也是255且类别为1,所以该点类别标记为1。第二个像素点值也是255且上面的点为类1,所以该点也为类1。同理,第四、六、八个点依次为类2,2,3:

在这里插入图片描述

同理可完成第三、四、五、六行的扫描:

在这里插入图片描述

扫描到第七行的时候,第一个点上边为背景点,左边不存在,所以该点为新的类6,并且后面2个点也是类6。对于第4个点,它左上均为255值点,但是不属于同一个类,根据之前的规则,左上均为255优先分配到上面点所属的类,所以该点的类别是类4:

在这里插入图片描述
同理完成剩下的,最终标记结果如下:

在这里插入图片描述

2.1 算法第二步:合并等价类

所谓等价类,就是在上面扫描结果图中的类1、2和类4、6以及类3、7,这些类两两之间不属于同一类,但是实际上是连通的。合并等价类要做的就是将这些等价类合并,将类别大的统一到小类别中(2–>1,6–>4,7–>3)去:

在这里插入图片描述
OpenCV中提供的连通组件扫描的API有两个,一个是带统计信息的connectedComponentsWithStats(),一个不带统计信息connectedComponents()

3.不带统计信息的connectedComponents()

不带统计信息的函数原型:

int cv::connectedComponents(InputArray image,OutputArray labels, 
					int connectivity = 8, int ltype = CV_32S)

connectedComponents()函数生成简单的标记图,它的参数解释如下:

  • image:输入二值图像,黑色为背景,白色为前景。
  • labels:也是一个Mat图像,大小和输入图像一样,每个位置上的值为对应原图位置像素点所属的类别,其中背景的index=0
  • connectivity:连通域,可选4或者8,默认是8连通。
  • ltype = CV_32S:输出的labels类型,可选CV_16S或者CV_32S默认是CV_32S

以自己绘制的下面图像为例:

在这里插入图片描述

代码实践:

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace  cv;

int main()
{
    
    
    RNG rng;
    Mat srcImage = imread("/mnt/hgfs/winshare/images/binary.png");

    if(srcImage.empty())
    {
    
    
        cout<<"load image failed."<<endl;
        return -1;
    }

    Mat binaryImage;
    
    //原图看起来是二值图,但是实际上是3通道的,所以要转为单通道
    cvtColor(srcImage,binaryImage,COLOR_BGR2GRAY);

	//定义一个和原图大小一样的矩阵来保存每个点的类别标签
    Mat labels = Mat::zeros(binaryImage.size(),CV_32S);
	
	//返回值是扫描到的联通区域数量,包括背景区域,因此实际上前景区域的数量是labelNums-1
    int labelNums = connectedComponents(binaryImage,labels,8,CV_32S);
    cout<<"total nums = "<<labelNums<<endl;
	
	//为每一个类别准备一个颜色
    vector<Vec3b> colors(labelNums);
    //背景就设为黑色
    colors[0] = Vec3b(0,0,0);
    //前景每个区域准备一个颜色
    for(int i=0;i<labelNums;++i)
    {
    
    
        colors[i] = Vec3b(rng.uniform(0,256),rng.uniform(0,256),rng.uniform(0,256));
    }

	//创建一个图像,到时候把同一个连通区域的画上同样的颜色
    Mat dstImage = Mat::zeros(srcImage.size(),srcImage.type());
    
    int w = dstImage.cols,h = dstImage.rows;
    for(int row=0;row<h;++row)
    {
    
    
        for(int col=0;col<w;++col)
        {
    
    
        	//获取labels每个位置的值,该值就是原图中对应位置像素点所属的连通区域类标签
            int label = labels.at<int>(row,col);
            if(label == 0)	//label=0为背景区域,不用管
                continue;
            dstImage.at<Vec3b>(row,col) = colors[label];
        }
    }

    imshow("dst",dstImage);
    waitKey(0);
    return 0;
}

输出:

total nums = 4

运行结果图像:

在这里插入图片描述

4.带统计信息的connectedComponentsWithStats()

相比于之前不带统计信息的API,一个更加有用的API是带统计信息的函数connectedComponentsWithStats(),它的函数原型如下:

int cv::connectedComponentsWithStats(InputArray image,OutputArray labels,
			OutputArray stats,OutputArray centroids,
			int connectivity,int ltype,int ccltype )

connectedComponentsWithStats()也会生成标记图,同时返回每一个连通区域的重要信息,包括包围框、面积、质心(可选)等,这使得我们可以利用这些信息完成一些任务,它的参数解释如下:

  • image:输入二值图像,黑色为背景,白色为前景。
  • labels:也是一个Mat图像,大小和输入图像一样,每个位置上的值为对应原图位置像素点所属的类别,其中背景的index=0
  • stats:输出每个标签,包括背景标签的统计信息,
  • centroids:返回质心信息,如果不需要该信息,可以传递cv::noArray()给该参数。
  • connectivity:连通域,可选4或者8,默认是8连通。
  • ltype:输出的labels类型,可选CV_16S或者CV_32S默认是CV_32S
  • ccltype:连接组件算法类型。

相关的统计信息包括在输出stats的对象中,每个连通组件有如下的输出:

  • CC_STAT_LEFT:连通组件外接矩形左上角坐标的X位置信息,获取语法为 int x =stats.at<int>(label,CC_STAT_LEFT)

  • CC_STAT_TOP:连通组件外接左上角坐标的Y位置信息,获取语法为 int y =stats.at<int>(label,CC_STAT_TOP)

  • CC_STAT_WIDTH:连通组件外接矩形宽度,获取语法为 int w =stats.at<int>(label,CC_STAT_WIDTH)

  • CC_STAT_HEIGHT:连通组件外接矩形高度,获取语法为 int h =stats.at<int>(label,CC_STAT_HEIGHT)

  • CC_STAT_AREA:连通组件的面积大小,基于像素多少统计,获取语法为 int area =stats.at<int>(label,CC_STAT_AREA)

同样以上面的图像为例,代码实践:

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace  cv;

int main()
{
    
    
    Mat srcImage = imread("/mnt/hgfs/winshare/images/binary.png");

    if(srcImage.empty())
    {
    
    
        cout<<"load image failed."<<endl;
        return -1;
    }

    Mat binaryImage;
    //原图看起来是二值图,但是实际上是3通道的,所以要转为单通道
    cvtColor(srcImage,binaryImage,COLOR_BGR2GRAY);

    //定义一个和原图大小一样的矩阵来保存每个点的类别标签
    Mat labels = Mat::zeros(binaryImage.size(),CV_32S);
    
    //定义存放相关信息的矩阵
    Mat stats,centroids;
    
    //返回值是扫描到的联通区域数量,包括背景区域,因此实际上前景区域的数量是labelNums-1
    int labelNums = connectedComponentsWithStats(binaryImage,labels, stats, centroids,8, 4);

    for(int i=1;i<labelNums;++i)
    {
    
    
    	//对于每一个连通类获取质心坐标并绘制
        int cx = centroids.at<double>(i,0);
        int cy = centroids.at<double>(i,1);
        circle(srcImage,Point(cx,cy),2,Scalar(0,0,255),2,8,0);

		对于每一个连通类获取连通组件外接矩形信息和面积坐标并绘制
        int x =stats.at<int>(i,CC_STAT_LEFT);
        int y =stats.at<int>(i,CC_STAT_TOP);
        int w =stats.at<int>(i,CC_STAT_WIDTH);
        int h =stats.at<int>(i,CC_STAT_HEIGHT);
        int area =stats.at<int>(i,CC_STAT_AREA);
        Rect rect(x,y,w,h);
        rectangle(srcImage,rect,Scalar(0,255,0),2,8,0);
        putText(srcImage,format("label:%d",i),Point(x,y-10),FONT_HERSHEY_SIMPLEX,.5,Scalar(0,255,0),2);
        putText(srcImage,format("area:%d",area),Point(x+60,y-10),FONT_HERSHEY_SIMPLEX,.5,Scalar(0,255,0),2);
    }

    imwrite("/home/peco/Desktop/res.jpg",srcImage);
    waitKey(0);
    return 0;
#endif
}

运行结果:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/PecoHe/article/details/113887895
今日推荐