前言
在自动计算图像中有几枚硬币的任务中,分离出前景和背景后是否就可以马上实现自动计件,如果可以,如何实现?如果不可以,为什么?
答案是否定的。二值化之后我们的得到的只是前景总像素的多少,并不知道哪些像素属于同一枚硬币。想要实现自动计件功能还需要用到连通域标记的知识。
连通域标记的方法这里我们使用种子填充法:
算法步骤:
1、遍历一幅图像。
2、如果遇到前景且该点未被标记,说明在该点附近可能存在与该点相连通的像素点,即可能存在连通域,停止遍历。否则继续遍历。
3、以该点为seed点,遍历seed点4邻域或者8邻域。如果同为前景,将坐标存到一个栈中,并将这点贴上label,表示已经访问过该像素,避免重复访问。
4、将栈中的坐标取出,以该点为seed点,重复2操作。
5、直到栈中的所有元素都取出,说明已经遍历完了该label的所有元素。
6、label++;从一开始停止遍历的点继续遍历。
7、重复2-6直到遍历到最后一个像素
代码实现:
*--------------------------【练习】连通域标记-------------------------------------*//*参数说明:
src_img:输入图像
flag_img:作为标记的空间(在函数内部设置为单通道)
draw_img:作为输出的图像,不同的连通域的颜色不同
iFlag:作为判断属于连通域的像素目标值,一般来说我们是对二值图进行连通域分析,所以这个值为0或者255,物体是0/1,则iFlag是0/1
type: type==4 :用4邻域 type==8 :用8邻域
nums: 设定的label像素个数截断值,被标记的连通域像素个数必须大于nums才算是正确的连通域。用来防止二值化后的效果并不好的情况。
*/
void seed_Connected_Component_labeling(Mat& src_img,Mat& flag_img,Mat& draw_img, int iFlag,int type, int nums)
{int img_row = src_img.rows;int img_col = src_img.cols;flag_img = cv::Mat::zeros(cv::Size(img_col, img_row), CV_8UC1);//标志矩阵,为0则当前像素点未访问过draw_img = cv::Mat::zeros(cv::Size(img_col, img_row), CV_8UC3);//绘图矩阵Point cdd[111000]; //栈的大小可根据实际图像大小来设置long int cddi = 0;int next_label = 1; //连通域标签int tflag = iFlag;long int nums_of_everylabel[100] = { 0 }; //存放每个区域的像素个数//Mat(纵坐标,横坐标)//Point(横坐标,纵坐标)for (int j = 0; j < img_row; j++) //height{for (int i = 0; i < img_col; i++) //width{//一行一行来if ((src_img).at<uchar>(j, i) == tflag && (flag_img).at<uchar>(j, i) == 0) //满足条件且未被访问过{//将该像素坐标压入栈中cdd[cddi] = Point(i, j);cddi++;//将该像素标记(flag_img).at<uchar>(j, i) = next_label;//将栈中元素取出处理while (cddi != 0){Point tmp = cdd[cddi - 1];cddi--;//对4邻域进行标记if (type == 4){Point p[4];//邻域像素点,这里用的四邻域p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y); //左p[1] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右p[2] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上p[3] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下//顺时针//p[0] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上//p[1] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右//p[2] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下//p[3] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y); //左//逆时针//p[3] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上//p[2] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右//p[1] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下//p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y); //左for (int m = 0; m < 4; m++){if ((src_img).at<uchar>(p[m].y, p[m].x) == tflag && (flag_img).at<uchar>(p[m].y, p[m].x) == 0) //满足条件且未被访问过{//将该像素坐标压入栈中cdd[cddi] = p[m];cddi++;//将该像素标记(flag_img).at<uchar>(p[m].y, p[m].x) = next_label;}}}//对8邻域进行标记else if (type == 8){Point p[8];//邻域像素点,这里用的四邻域p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y - 1 > 0 ? tmp.y - 1 : 0); //左上p[1] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上p[2] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1,tmp.y - 1 > 0 ? tmp.y - 1 : 0); //右上p[3] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y); //左p[4] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右p[5] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//左下p[6] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下p[7] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//右下for (int m = 0; m < 7; m++){if ((src_img).at<uchar>(p[m].y, p[m].x) == tflag && (flag_img).at<uchar>(p[m].y, p[m].x) == 0) //满足条件且未被访问过{//将该像素坐标压入栈中cdd[cddi] = p[m];cddi++;//将该像素标记(flag_img).at<uchar>(p[m].y, p[m].x) = next_label;}}}}next_label++;}}}next_label = next_label - 1;int all_labels = next_label;std::cout << "labels : " << next_label <<std::endl;//给不同连通域的涂色并且记录下每个连通域的像素个数for (int j = 0;j < img_row;j++) //行循环{for (int i = 0;i < img_col;i++) //列循环{int now_label = (flag_img).at<uchar>(j, i); //当前像素的labelnums_of_everylabel[now_label]++; float scale = now_label * 1.0f / all_labels;//-------【开始处理每个像素】---------------draw_img.at<Vec3b>(j, i)[0] = 255 - 255 * scale; //B通道draw_img.at<Vec3b>(j, i)[1] = 128 - 128 * scale; //G通道draw_img.at<Vec3b>(j, i)[2] = 255 * scale; //R通道//-------【处理结束】---------------}}std::cout << "初步结论 : " << std::endl;for (int i = 1;i <= next_label;i++){std::cout << "labels : " << i<<"像素个数 " << nums_of_everylabel[i] <<std::endl;}std::cout << "最后结论 : " << std::endl;std::cout << "截断像素数目 : " << nums << std::endl;for (int i = 1;i <= next_label;i++){if (nums_of_everylabel[i] <= nums){all_labels--;}}std::cout << "labels : " << all_labels << std::endl;}int main()
{Mat flag_img;Mat draw_img;Mat srcImage = imread("D:\\opencv_picture_test\\阈值处理\\硬币.png", 0); //读入的时候转化为灰度图//Mat srcImage = imread("D:\\opencv_picture_test\\阈值处理\\黑白.jpg", 0); //读入的时候转化为灰度图namedWindow("原始图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口imshow("原始图", srcImage);cout << "srcImage.rows : " << srcImage.rows << endl; //308cout << "srcImage.cols : " << srcImage.cols << endl; //372Mat dstImage;dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);//阈值处理+二值化My_artificial(&srcImage, &dstImage, 84);// flag_img = cv::Mat::zeros(src.size(), src.type());//cvtColor(src, src, COLOR_RGB2GRAY); //这一句很重要,必须保证输入的是单通道的图,否则所读取的数据是错误的double time0 = static_cast<double>(getTickCount()); //记录起始时间seed_Connected_Component_labeling(dstImage,flag_img,draw_img,255,4,500); //白色部分被标记time0 = ((double)getTickCount() - time0) / getTickFrequency();cout << "此方法运行时间为:" << time0 << "秒" << endl; //输出运行时间imshow("dstImage", dstImage);namedWindow("draw_img", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口imshow("draw_img", draw_img);waitKey(0);return 0;
}
实现效果:
原图:
二值图(可以看到有几个噪点,而且图像的右边和上边是白色的,这是因为原图我是截图的,边界并没有剪裁好,这点在下面的连通域标记会有影响)
我给属于不同连通域的物体涂上不同的颜色。
下面是打印出来的信息:初步得到的label是19个,其中label1就是我所说的截图边界问题。其他的几个像素个数小的就是噪点。
通过设定门限,像素个数小于500的标签物体我们将它视为噪声。最后得到的label数目正好是10,也就是硬币的数目。
发现的问题
连通域标记函数代码部分,可以看到我还尝试了其他两种遍历seed周围元素的方式,分别是顺时针和逆时针。但是运算速度没有第一种快,至于原因我没有深究。希望有心人能给我讲解一波。此外,试了一下8邻域,运算速度也得到了下降。
这就是我说的剪裁错误,嘿嘿。
此外,二值化的方法我是用的人工调整,原图受到非均匀光线的照射,全局大津阈值得到的效果并不是很好,反而由于直方图双峰性比较明显,迭代法看起来还不错。不过为了连通域标记的时候能够准确一点,我就用滑条调整阈值了。
滑动条调整阈值的代码在这儿:https://blog.csdn.net/qq_42604176/article/details/104764731
迭代法、大津的代码在这儿:https://blog.csdn.net/qq_42604176/article/details/104341126
3.15更新,加入形态学腐蚀操作
首先回顾之前遇到的问题:受到噪声影响,十个硬币竟然贴了19个labels,尽管利用限制像素个数的方法来限制,但这种方法有许多弊端。
这几天学习了一些简单的形态学操作,其中腐蚀操作有个作用:去除黏连像素以及噪声。
这不就正好能解决之前遇到的问题嘛!
操作也很简单,加上两行代码就行。
、
结果运行如下(把自己简陋的限制像素函数去掉了)
效果很好啊!
关于腐蚀的详细讲解请看这边:
https://blog.csdn.net/qq_42604176/article/details/104815801