1. 图像的连通域以及连通域分析
在该系列第六篇文章中,曾经介绍过连通的概念,下面再来回顾一下。
连通 :若 S 是图像中的一个像素子集,对于任意的 <math xmlns="http://www.w3.org/1998/Math/MathML"> p 、 q ∈ S p、q \in S </math>p、q ∈ S。如果存在一条由 S 中像素组成的从 p 到 q 的通路,则称 p 在像素集 S 中与 q 连通。
连通域是指具有相同属性的连通集合。例如,在一个二值图像中,具有相同像素值的区域构成一个连通域。
所以,连通 和连通域 是两个不同的概念,连通域 是连通的子集。连通域具有以下特性:
- 一个连通域中的所有像素都具有相同的性质。
- 一个连通域中的任意两个像素都可以通过连续的路径连接起来。
- 一个连通域可以是单连通的,也可以是多连通的。
连通域分析 是指在图像中查找 和标记 连通域的算法。这是一种常用的图像处理技术,可以用于目标检测、图像分割和形状识别等等。本文是基于二值图像进行连通域的分析。
2. 连通域分析的算法
连通域分析的算法可以分为以下几类:
-
基于标记的算法:该类算法首先将每个像素分配一个唯一的标记,然后使用某种策略将具有相同标记的像素连接起来。基于标记的算法包括两遍扫描法、种子填充法和快速连通域查找法。
-
基于邻域查找的算法:该类算法从图像中的一个起始像素开始,逐个检查其邻域像素,如果邻域像素具有相同像素值,则将其加入到当前连通域中。基于邻域查找的算法包括深度优先搜索(DFS)和广度优先搜索(BFS)。
-
基于分割的算法:该类算法将图像分割成多个区域,每个区域都具有相同的像素值。基于分割的算法包括图论方法和区域生长法。
其中,基于标记的算法是比较常用的连通域分析的算法。
2.1 两遍扫描法
两遍扫描算法的步骤如下
- 第一遍扫描
- 从左到右,从上到下遍历图像。
- 将每个有效像素赋予一个唯一的标记。
- 第二遍扫描
- 再次从左到右,从上到下遍历图像。
- 检查两个相邻像素是否具有相同的标记。
- 如果两个相邻像素具有相同的标记,则将它们连接起来。
2.2 种子填充法
种子填充算法的步骤如下:
-
初始化:将图像中的所有像素标记为未访问。
-
选择一个起始像素作为种子。
-
将种子像素标记为已访问。
-
检查种子像素的邻域像素。
- 如果邻域像素具有相同的像素值,则将其标记为已访问。
- 如果邻域像素具有不同的像素值,则忽略。
-
重复步骤 4,直到图像中的所有像素都被标记为已访问或直到没有未访问的邻域像素为止。
3. OpenCV 自带的连通域函数
OpenCV 提供了两个函数:connectedComponents()、connectedComponentsWithStats() 在二值图像中查找连通域。
下面的例子,在图中找到连通域并标记不同的颜色。
cpp
#include <iostream>
#include <map>
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
using namespace std;
using namespace cv;
void labelColor(Mat& labelImg, Mat& dst)
{
map<int, Scalar> colors;
int width = labelImg.cols;
int height = labelImg.rows;
dst = Mat::zeros(labelImg.size(), CV_8UC3);
uchar r = 255 * (rand()/(1.0 + RAND_MAX));
uchar g = 255 * (rand()/(1.0 + RAND_MAX));
uchar b = 255 * (rand()/(1.0 + RAND_MAX));
for (int i = 0; i < height; i++)
{
int* data_src = (int*)labelImg.ptr<int>(i);
uchar* data_dst = dst.ptr<uchar>(i);
for (int j = 0; j < width; j++)
{
int pixelValue = data_src[j];
if (pixelValue >= 1)
{
if (colors.count(pixelValue) == 0)
{
colors[pixelValue] = Scalar(b,g,r);
r = 255 * (rand()/(1.0 + RAND_MAX));
g = 255 * (rand()/(1.0 + RAND_MAX));
b = 255 * (rand()/(1.0 + RAND_MAX));
}
Scalar color = colors[pixelValue];
*data_dst++ = color[0];
*data_dst++ = color[1];
*data_dst++ = color[2];
}
else
{
data_dst++;
data_dst++;
data_dst++;
}
}
}
}
int main(int argc, char **argv) {
Mat src = imread(".../coins.jpg");
imshow("src", src);
Mat gray,thresh;
cvtColor(src, gray, cv::COLOR_BGR2GRAY); // 灰度化
imshow("gray", gray);
Mat gauss;
GaussianBlur(gray, gauss, Size(15, 15),0); //降噪
threshold(gauss, thresh,0,255,THRESH_BINARY | THRESH_OTSU );
imshow("thresh", thresh);
Mat labels, stats, centroids;
int num_labels = connectedComponents(thresh, labels,8,CV_32S);
cout << "num_labels = " << num_labels << endl;
Mat result;
labelColor(labels,result);
imshow("Connected Components", result);
waitKey(0);
return 0;
}
cpp
int connectedComponents(InputArray image, OutputArray labels,int connectivity, int ltype, int ccltype);
其各个参数的含义:
第一个参数 image:输入图像必须是二值图像1。 第二个参数 labels:输出图像,其中每个像素值表示其所在连通域的标签。标签的值从 0 开始,依次递增。 第三个参数 connectivity:标记连通域时使用的邻域种类。可选的是 4、8,默认值是 8。 第四个参数 ltype:输出标签的类型。可以设置为 CV_32S 或 CV_16U,默认值是 CV_32S 。 第五个参数 ccltype:标记连通域分析算法的类型。
参数类型 | 值 | 作用 |
---|---|---|
CCL_WU | 0 | 8 邻域和 4 邻域均用 SAUF 算法。 |
CCL_DEFAULT | -1 | 8 邻域用 BBDT 算法,4 邻域用 SAUF 算法。 |
CCL_GRANA | 1 | 8 邻域用 BBDT 算法,4 邻域用 SAUF 算法。 |
connectedComponentsWithStats() 函数除了返回每个连通域的标签之外,还返回每个连通域的其他信息,包括:
- 面积
- 外接矩形
- 中心坐标
cpp
int main(int argc, char **argv) {
Mat src = imread(".../coins.jpg");
imshow("src", src);
Mat gray,thresh;
cvtColor(src, gray, cv::COLOR_BGR2GRAY); // 灰度化
imshow("gray", gray);
Mat gauss;
GaussianBlur(gray, gauss, Size(15, 15),0); //降噪
threshold(gauss, thresh,0,255,THRESH_BINARY | THRESH_OTSU );
imshow("thresh", thresh);
Mat labels, stats, centroids;
int num_labels = connectedComponentsWithStats(thresh, labels, stats, centroids);
cout << "num_labels = " << num_labels << endl;
for (int i = 1; i < num_labels; i++)
{
int area = stats.at<int>(i, CC_STAT_AREA);
int left = stats.at<int>(i, CC_STAT_LEFT);
int top = stats.at<int>(i, CC_STAT_TOP);
int width = stats.at<int>(i, CC_STAT_WIDTH);
int height = stats.at<int>(i, CC_STAT_HEIGHT);
int cx = centroids.at<double>(i, 0);
int cy = centroids.at<double>(i, 1);
cout << "Object " << i << ": " << "Area=" << area << ", Left=" << left << ", Top=" << top << ", Width=" << width << ", Height=" << height << ", Centroid=(" << cx << ", " << cy << ")" << endl;
rectangle(src, Point(left, top), Point(left + width, top + height), Scalar(0,0,255), 2);
circle(src, Point(cx, cy), 2, Scalar(0,0,255), 2);
}
imshow("Connected Components", src);
waitKey(0);
return 0;
}
执行结果:
ini
num_labels = 8
Object 1: Area=17061, Left=344, Top=160, Width=144, Height=151, Centroid=(415, 234)
Object 2: Area=9655, Left=592, Top=411, Width=108, Height=114, Centroid=(645, 467)
Object 3: Area=9558, Left=776, Top=441, Width=108, Height=113, Centroid=(828, 497)
Object 4: Area=17874, Left=435, Top=517, Width=150, Height=152, Centroid=(509, 592)
Object 5: Area=17502, Left=660, Top=648, Width=148, Height=151, Centroid=(733, 723)
Object 6: Area=10420, Left=726, Top=864, Width=114, Height=117, Centroid=(782, 922)
Object 7: Area=15634, Left=628, Top=1075, Width=141, Height=143, Centroid=(698, 1146)
4. 总结
连通域分析是图像处理中常用的算法之一,用于在二值图像中找到具有相同像素值且相互连接的区域。它在图像处理中具有广泛的应用场景。