二值图像分析:连通组件寻找算法
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
}
运行结果: