连通域标记——实现硬币自动计件

前言

在自动计算图像中有几枚硬币的任务中,分离出前景和背景后是否就可以马上实现自动计件,如果可以,如何实现?如果不可以,为什么?
答案是否定的。二值化之后我们的得到的只是前景总像素的多少,并不知道哪些像素属于同一枚硬币。想要实现自动计件功能还需要用到连通域标记的知识。
连通域标记的方法这里我们使用种子填充法:
方法

算法步骤:

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;
}

实现效果:

原图:
1
二值图(可以看到有几个噪点,而且图像的右边和上边是白色的,这是因为原图我是截图的,边界并没有剪裁好,这点在下面的连通域标记会有影响)
2
我给属于不同连通域的物体涂上不同的颜色。
3
下面是打印出来的信息:初步得到的label是19个,其中label1就是我所说的截图边界问题。其他的几个像素个数小的就是噪点。
通过设定门限,像素个数小于500的标签物体我们将它视为噪声。最后得到的label数目正好是10,也就是硬币的数目。
4

发现的问题

连通域标记函数代码部分,可以看到我还尝试了其他两种遍历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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/378742.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

09-梯度运算

梯度运算膨胀操作-腐蚀操作&#xff0c;这里的-操作是图像的减法&#xff0c;可不是简单的加减乘除运算 为了更加形象生动&#xff0c;先将膨胀和腐蚀操作的结果进行合并展示&#xff0c;然后再与梯度运算进行比较 cv2.morphologyEx(pie,cv2.MORPH_GRADIENT,kernel) 第一个参数…

Unity-Shader-渲染队列

Unity-Shader-渲染队列 渲染简介Unity中的几种渲染队列Background (1000)最早被渲染的物体的队列。Geometry (2000) 不透明物体的渲染队列。大多数物体都应该使用该队列进行渲染&#xff0c;也就是Unity Shader中默认的渲染队列。AlphaTest (2450) 有透明通道&#xff0c;需要进…

形态学操作——腐蚀与膨胀

预备知识 结构元&#xff08;SE&#xff09; 1、结构元的中心一般来说是放在其重心位置处&#xff0c;但原则上原点的选择是依赖于你要解决的问题的。 2、对图像操作时&#xff0c;我们要求结构元是矩形阵列。&#xff08;在结构元的基础上添加较少的背景元素实现&#xff09…

10-礼帽与黑帽操作

cv2.morphologyEx(img,cv2.MORPH_TOPHAT,kernel) 第一个参数&#xff1a;图像对象名称 第二个参数&#xff1a;运算类型TOPHAT为礼帽运算 第三个参数&#xff1a;卷积核的大小 礼帽运算&#xff1a;原始的输入-开运算&#xff08;先腐蚀再膨胀&#xff09; 原始带刺儿&#xff…

形态学操作——开闭运算、顶帽底(黑)帽变换

膨胀和腐蚀运算的问题&#xff1a; 边缘形状发生了变化&#xff0c;膨胀发生了扩张&#xff0c;腐蚀发生了收缩 目标物体变形&#xff0c;对识别时的特征提取会造成影响 解决方法&#xff1a; 开操作: B对A的开操作就是先B对A腐蚀&#xff0c;紧接着用B对结果进行膨胀 先腐…

11-图像梯度-Sobel算子

图像梯度是指图像某像素在x和y两个方向上的变化率&#xff08;与相邻像素比较&#xff09;&#xff0c;是一个二维向量&#xff0c;由2个分量组成&#xff0c;X轴的变化、Y轴的变化 。 其中X轴的变化是指当前像素右侧&#xff08;X加1&#xff09;的像素值减去当前像素左侧&…

形态学操作——击中击不中变换

操作目的 HitMiss变换是形态检测的一个工具&#xff0c;通过定义形状模板可以在图像中获取同一形状物体的位置坐标。 算法讲解 1、用击中结构去腐蚀原始图像得到击中结果X&#xff08;这个过程可以理解为在原始图像中寻找和击中结构完全匹配的模块&#xff0c;匹配上了之后&…

12-图像梯度-Scharr算子和laplacian算子

Scharr算子 cv2.Scharr(img,cv2.CV_64F,1,0) 第一个参数&#xff1a;当前的图像对象名称 第二个参数&#xff1a;当前图像的深度&#xff0c;通常情况下指定为-1&#xff0c;表示输出和输入的深度是一样的&#xff1b;cv2.CV_64F可以存6字节的大小&#xff0c;为了方便后面的取…

Opencv——图像金字塔与图像尺寸缩放

主要讲解 1、resize()函数调用 函数定义&#xff1a; 调用方式&#xff1a; resize(srcImage, dstImage, Size(64, 128)); //对图片进行修改 resize(srcImage, dstImage, Size(), 0.5, 0.5);第6个参数的含义&#xff1a; INTER_NEAREST:最邻近插值 (放大好用) INTER_ARE…

13-Canny边缘检测

Canny边缘检测主要思路步骤如下&#xff1a; 1&#xff0c;使用高斯滤波器&#xff0c;以平滑图像&#xff0c;滤除噪声 2&#xff0c;计算图像中每个像素点的梯度强度和方向 3&#xff0c;应用非极大值抑制&#xff0c;以消除边缘检测带来的杂散响应 4&#xff0c;应用双阈值检…

微机原理——移位指令

例题 思路 选择移位语句&#xff0c;右移&#xff0c;将AL移出的送入DX左端&#xff0c;将BL移出的送入DX左端。循环八次 MOV AL,01100101B; MOV BL,11011010B; XOR DX,DX;两个值相同&#xff0c;异或结果为0。等效&#xff1a;MOV DX,0 MOV CX,8;count L1: SHR AL,1;逻辑右…

14-图像金字塔

由第一个图可知&#xff0c;图像金字塔这无非就是对图像进行放大和缩小罢了 1&#xff0c;高斯金字塔 向下采样方法(缩小)&#xff0c;越采样越小&#xff0c;即从金字塔底部向上采样 cv2.pyrDown(img) 向上采样方法(放大)&#xff0c;越采样越大&#xff0c;即从金字塔顶…

Eclipse C/C++开发环境搭建

1 Eclipse的安装 到http://java.sun.com/j2se/1.5.0/download.jsp 下载JRE安装&#xff1b; 到http://eclipse.org下载Eclipse安装。&#xff08;这儿可以下载Java版本的&#xff0c;也可以下载C/C 版本的&#xff09; 2 对于下载的Java版本或着只下载Eclipse IDE的&#xff0c…

微机原理——寻址方式总结

一、操作数的寻址方式 立即寻址方式 格式: 操作码 数字表达式&#xff08;将数据送入寄存器中&#xff09; 源操作数可以是8位也可以是16位。 MOV AH, F5H &#xff08;字节操作&#xff09; F5H称为立即数(8位操作数) MOV AL, 8AH &#xff08;字节操作&#xff09; 8AH称为…

15-轮廓检测

边缘是零零散散的&#xff0c;而轮廓是一个整体 cv2.findContours(img,mode,method) img&#xff1a;输入图像对象名称 mode&#xff1a;轮廓检索模式 RETR_EXTERNAL&#xff1a;只检索最外面的轮廓 RETR_LIST&#xff1a;检索所有的轮廓&#xff0c;并将其保存到一条链表当中…

抛硬币仿真实验java_探索HyperLogLog算法(含Java实现)

引言HyperLogLog算法经常在数据库中被用来统计某一字段的Distinct Value(下文简称DV)&#xff0c;比如Redis的HyperLogLog结构&#xff0c;出于好奇探索了一下这个算法的原理&#xff0c;无奈中文资料很少&#xff0c;只能直接去阅读论文以及一些英文资料&#xff0c;总结成此文…

微机原理——总线和时序

前提 8088有两个组态&#xff1a; 最大组态和最小组态&#xff0c;通过引脚MN/MX*的电平决定组态。&#xff08;*表示低电平有效&#xff09; 两种组态没有本质区别。 8088的引脚&#xff1a; 引脚可分为下面几种类别&#xff1a; 1、数据和地址引脚 2、读写控制引脚 3、中断…

PHP站内搜索:多关键字查找,加亮显示

1、SQL语句中的模糊查找LIKE条件一般用在指定搜索某字段的时候, 通过"% _" 通配符的作用实现模糊查找功能&#xff0c;通配符可以在前面也可以在后面或前后都有。搜索以PHP100开头&#xff1a; SELECT * FROM teble WHERE title LIKE PHP100% 搜索以PHP100结束&…

16-模板匹配

cv2.matchTemplate(img,template,cv2.TM_SQDIFF) 参数一&#xff1a;原图图像对象名称 参数二&#xff1a;模板图像对象名称 参数三&#xff1a;差别程度的计算方法(六选一推荐使用带归一化的) 模板匹配和卷积原理很像&#xff0c;模板从原图像上从原点开始滑动&#xff0c;计…

用C#开发Windows应用程序

To develop windows application, we need to using studio and follow some steps: 要开发Windows应用程序 &#xff0c;我们需要使用studio并遵循一些步骤&#xff1a; Step 1) First of all we launch visual studio. 步骤1)首先&#xff0c;我们启动Visual Studio。 Ste…