像素领域介绍:
4邻域是指中心的像素与它邻近的上下左右一共有4个像素,那么称这4个像素为中心像素的4邻域。
8邻域是以中心像素周围的8个像素分别是上下左右和对角线上的4个像素。
连通域的定义(分割)分为两种:以4邻域为相邻判定条件的连通域分割和8邻域为判定条件的连通域分割。连通域指某个区域内所有像素是相邻的,如果一个像素不能够通过这个区域中的像素到达另一个像素,那么这两个像素就不再同一连通域内。
连通域的划分通常采用两遍法,在进行连通域分析的时候,我们往往先对图像进行二值化处理,确定连通域的判定标准是采用4邻域还是8邻域,然后先对图像进行遍历得到结果,然后再对此结果进行遍历得到最终结果。
在进行遍历时遇到非0(非黑)的像素值,就对此像素进行编号,例如上图,进行第一次遍历时,遇到黄色的方格,标记为1,之后凡是与此放个相邻的像素,都将其设置为1,第一行遍历完后,赋值了1个1,遍历第二行,最左侧有个黄色像素,此像素的上方和左方的像素若有标记的数值,那么就将这个黄色像素标记为上方与左方数值较小的数,这里可以看到上方为空,左方是边缘,因此这个像素没有办法借助上方和左方标记,我们就需要对其进行独立标记,由于1已经用过,我们将其标记为2。
然后看下一个黄色的像素,这个像素左边的像素被标记为2,而上方是一个没有被标记的像素,因此这个像素也被标记为2。
然后继续看下一个像素,这个像素的左方和上方都被标记了,而两者标记数值不一致,因此我们根据规则选择两者之间较小数值,即1。
到达第三行时,遇见黄色像素,因为此像素上方的像素为1,因此也要记为1。
来到第四行,第四行第一个像素,左侧和上方都没有被标记,因此需要进行独立标记,1和2都用过了,我们对此像素标记3 。
下一个像素左侧为3上方为1,标记为1。
最后一行分别被标记为3和1。
这样完成了第一次遍历,第二次遍历,将两个邻近的结果进行统一,比如上图第二行,标记为2和标记1的像素相连,因此我们将其中的2全部置为1,下方像素同理。
这样便完成了两遍法实现图像邻域分割,图像分割的结果通过示例中也可以看出是一个与原图像具有相同尺寸的图像 。
int cv::connectedComponents(InputArray image,
OutputArray labels,
connectivity =
int 8,
int Itype = CV_325
)
·image:待标记不同连通域的图像单通道,数据类型必须为CV_8U。上面说过图像通常会进行二值化处理,若不进行二值化,则会将1~255之间的任何数都独立的看成不同区域,也就是即使出现2,3,4相邻的区域,也会判断为3个不同的连通域,因此,我们最好是使用0~255的数据,这样的数据是有限的,若我们使用0~1之间的小数,分割形式是无限的,那么分割的结果就会出现每一个单独的像素都是一个单独的连通域。我们要先对图像进行二值化,因为图像可能会受到光照影响,即使在真实物理世界中,是同一个像素值的物体,通过相机的采集,也会出现不同像素值的情况。
·labels:标记不同连通域后的输出图像,与输入图像具有相同尺寸。
·connectivity:标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域,默认参数为8.
·ltype:输出图像的数据类型,目前支持CV_32S和CV_16U两种数据类型,默认参数为CV_32S。
void cv::connectedComponentsWithStats(InputArray images,
OutputArray labels,
OutputArray stats,
OutputArray centroids,
connectiviity =
int 8,
int ltype = cv_32s
)
·image:待标记不同连通域的图像单通道,数据类型必须为CV_8U。
·labels:标记不同连通域后的输出图像,与输入图像具有相同的尺寸。
·stats:不同连通域的统计信息矩阵,矩阵的数据类型为CV_32S。矩阵中第i行是标签为i的连通域的统计特性。
·centroids:每个连通域的质心坐标,数据类型为CV_64F。
·connectivity:标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域,默认参数为8。
·ltype:输出图像的数据类型,目前支持CV_32S和CV_16U两种数据类型,默认参数为CV_32S。
上图参数是此函数可以获得的信息。
#include
#include
#include
using namespace cv; //opencv的命名空间
using namespace std;
//主函数
int main()
{
//对图像进行距离变换
Mat img = imread("E:/opencv/opencv-4.6.0-vc14_vc15/opencv/hua.jpg");
if (img.empty())
{
cout << "请确认图像文件名是否正确" << endl;
return -1;
}
Mat hua, huaBW;
//将图像转成二值化的图像,用于统计连通域。
cvtColor(img, hua, COLOR_BGR2GRAY);
threshold(hua, huaBW, 25, 255, THRESH_BINARY); //二值化
//生成随机颜色,用于区分不同连通域。
RNG rng(10086); //RNG用来生成随机数,这里用了10086进行初始化。
Mat out;
int number = connectedComponents(huaBW, out, 8, CV_16U); //统计图像中连通域的个数
vector colors; //vector是一个能够存放任意类型的动态数组,Vec3b可以看成vector,即一个uchar类型,长度为3的vector向量(简单地说,就是一个uchar类型的数组,长度为3).
for (int i = 0; i < number; i++);
{
//使用均匀分布的随机数确定颜色;rng.uniform(),可以生成指定范围的均匀分布的随机数
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors.push_back(vec3);
}
//以不同的颜色标记出不同的连通域
Mat result = Mat::zeros(hua.size(), img.type());
int w = result.cols;
int h = result.rows;
for (int row = 0; row < h; row++)
{
for (int col = 0; col < w; col++)
{
int label = out.at(row, col);
if (label == 0) //背景的黑色不改变
{
continue;
}
result.at(row, col) = colors[label];
}
}
//显示结果
imshow("原图", img);
imshow("标记后的图像", result);
cout <<"接下来统计连通域信息"<< endl;
waitKey(0);
Mat stats, centroids;
number = connectedComponentsWithStats(huaBW, out, stats, centroids, 8, CV_16U);
vector colors_new;
for (int i = 0; i < number; i++)
{
//使用均匀分布的随机数确定颜色
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors_new.push_back(vec3);
}
//以不同的颜色标记出不同的连通域
for (int i = 1; i < number; i++)
{
//中心位置
int center_x = centroids.at(i, 0);
int center_y = centroids.at(i, 1);
//矩形边框
int x = stats.at(i, CC_STAT_LEFT);
int y = stats.at(i, CC_STAT_TOP);
int w = stats.at(i, CC_STAT_WIDTH);
int h = stats.at(i, CC_STAT_HEIGHT);
int area = stats.at(i, CC_STAT_AREA);
//中心位置绘制
circle(img, Point(center_x, center_y), 2, Scalar(0, 255, 0), 2, 8, 0);
//外接矩形
Rect rect(x, y, w, h);
rectangle(img, rect, colors_new[i], 1, 8, 0);
putText(img, format("%d", i), Point(center_x, center_y),FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255), 1);
cout << "number" << i << ",aera:" << area << endl;
}
//显示结果
imshow("标记后的图像", img);
waitKey(0);//等待函数用于显示图像,按下键盘任意键后退出
return 0;
}
原图: