一、连通域
需要了解连通域和两遍扫描法的可以看连通域分析之两遍扫描法(Two-Pass)这篇博文。我在里面已经介绍过了连通域的两种定义,这里就不再介绍。在此文中仍以4-邻域为例。
二、种子填充(区域填充)
1、遍历二值化后待测连通域的图像,当当前访问的像素不为零,将该像素入栈(栈中保存像素点的坐标);
2、访问栈顶元素对应的像素,退栈;
3、访问2中得到的栈顶像素的四个邻域像素点,若像素值不为零,则将对应像素入栈;
4、继续访问栈顶元素,直至栈为空;
5、继续遍历图像,直至完成遍历。
以上就是种子填充法的一个基本步骤,在访问像素的同时,我们可以根据需求做出操作。
三、算法代码
int myBoundaryFill(Mat &src, Mat &dst)
{
if (src.empty() || src.type() != CV_8UC1)
return 0;
stack<Point2i> pts;
int label = 1;//标注初始为1
int row = src.rows;
int col = src.cols;
dst.release();
dst = cv::Mat::zeros(row, col, CV_32S);
for (int i = 0;i < row ;i++)
{
for (int j = 0;j < col ;j++)
{
int curPixel = (int)src.at<uchar>(i,j);
int curLabel = (int)dst.at<int>(i, j);
if (curPixel != 0 && curLabel == 0)
{
dst.at<int>(i,j)= label;
pts.push(Point2i(j, i));
while (!pts.empty())
{
Point2i curPt = pts.top();
int x = curPt.x;
int y = curPt.y;
pts.pop();
if (y > 0)
{//upPixel
if (src.at<uchar>(y-1, x) != 0 && dst.at<int>(y-1, x) == 0)
{
dst.at<int>(y - 1, x) = label;
pts.push(Point2i(x, y-1));
}
}
if (y < row - 1)
{//downPixel
if ((int)src.at<uchar>(y + 1, x) != 0 && dst.at<int>(y + 1, x) == 0)
{
dst.at<int>(y + 1, x) = label;
pts.push(Point2i(x, y+1));
}
}
if (x > 0)
{//leftPixel
if ((int)src.at<uchar>(y, x-1) != 0 && dst.at<int>(y, x-1) == 0)
{
dst.at<int>(y, x-1) = label;
pts.push(Point2i(x-1, y));
}
}
if (x < col - 1)
{//rightPixel
if (src.at<uchar>(y, x + 1) != 0 && dst.at<int>(y, x + 1) == 0)
{
dst.at<int>(y, x + 1) = label;
pts.push(Point2i(x+1, y));
}
}
}
label++;//自增
}
}
}
return label-1;//最后得到的连通域数量
}
四、算法解析
1、不遗漏像素点的访问
for (int i = 0;i < row ;i++)
{
for (int j = 0;j < col ;j++)
{
.....
}
}
遍历图像行和宽都从0开始,可以确保每个像素点都被访问;
2、避免入过栈的像素再次入栈
int curPixel = (int)src.at<uchar>(i,j);
int curLabel = (int)dst.at<int>(i, j);
if (curPixel != 0 && curLabel == 0)
curPixel表示当前访问的像素点,curLabel表示该像素点在输出图像dst中对应的标注。输出图像dst是一个像素初始值全为零的图像。当输出图像dst中的某个像素值不为0,说明已经访问过其对应于输入图像中的像素点,即该像素点已经被标注了,无需再次标注。
3、越界访问控制
由于是从(0,0)开始访问的,包含了边界的像素点,但边界像素点并不是四个邻域像素都存在。所以在访问栈顶像素四个邻域像素之前在相应的边界进行一次边界判定,使用if (y > 0)、if (y < row - 1)、if (x > 0)和if (x < col - 1)防止相应边界的越界访问。
算法比较简单,就是一个DFS的算法,只需要在访问元素的过程中注意越界和遗漏边界像素的问题即可。
五、测试效果
测试代码
int main()
{
Mat test = (cv::Mat_<uchar>(8, 8) <<
0, 0, 0, 0, 1, 0, 1, 1,
1, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 0, 1, 0, 0, 1,
1, 0, 1, 0, 1, 0, 1, 1,
1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 1, 0, 1,
1, 0, 1, 0, 1, 1, 0, 1,
1, 1, 1, 1, 1, 1, 0, 0);
Mat testResult;
//Mat preConnect;
//cout<<"连通域个数为:"<<myTwoPass(test, testResult, preConnect);
cout << "testResult连通域个数为:" << myBoundaryFill(test, testResult);
cout << endl;
//Mat img = imread("../coins.png");//原图
Mat img = imread("../rice.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat rice, riceBW;
//将图像转换成二值图像,用于统计连通域
cvtColor(img, rice, COLOR_BGR2GRAY);
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
//用于生成随机颜色,用于区分不同的连通域
RNG rang(10086);
int w = img.cols;
int h = img.rows;
Mat myout,preConnect;
//int mynumber = myTwoPass(riceBW, myout, preConnect);//自定义函数
// mynumber = myTwoPass(riceBW, myout);//自定义函数
int mynumber = myBoundaryFill(riceBW, myout);//自定义函数
vector<Vec3b> mycolors;
for (int i = 0;i < mynumber;i++)
{
//使用均匀分布的随机数确定颜色
Vec3b myvec3 = Vec3b(rang.uniform(0, 256), rang.uniform(0, 256), rang.uniform(0, 256));
mycolors.push_back(myvec3);
}
//以不同的颜色标记不同的连通域
Mat myresult = Mat::zeros(rice.size(), img.type());//自定义输出图像
//cout << "mycolors number= " << mycolors.size();
for (int i = 0;i < h;i++)
{
for (int j = 0;j < w;j++)
{
int label = myout.at<int>(i, j);
if (label == 0)
{//背景颜色不改变
continue;
}
myresult.at<Vec3b>(i, j) = mycolors[label-1];
}
}
Mat out;
int number = connectedComponents(riceBW, out, 4, CV_16U);//库函数
vector<Vec3b> colors;
for (int i = 0;i < number;i++)
{
//使用均匀分布的随机数确定颜色
Vec3b vec3 = Vec3b(rang.uniform(0, 256), rang.uniform(0, 256), rang.uniform(0, 256));
colors.push_back(vec3);
}
Mat result = Mat::zeros(rice.size(), img.type());//库函数输出图像
for (int i = 0;i < h;i++)
{
for (int j = 0;j < w;j++)
{
int label = out.at<uint16_t>(i, j);
if (label == 0)
{//背景颜色不改变
continue;
}
result.at<Vec3b>(i, j) = colors[label-1];
}
}
cout << "库函数得到的连通域数:" << number << endl;
cout << "自定义函数得到的连通域数:" << mynumber << endl;
//显示结果
imshow("原图", img);
imshow("库函数输出图像", result);
imshow("自定义函数输出图像", myresult);
waitKey(0);
return 0;
}
自定义Mat变量的测试结果,图像被分为1,2,3,4一共4个连通域
图片的测试
原图
测试结果对比
计算出的连通域数量对比,库函数计算了背景所以多1。