最近在进行瑕疵检测识别中的连通域处理。主要是使用了cv2.connectedComponentsWithStats函数。本文将进行 函数介绍,使用经验,其他处理的记录。
函数介绍
'''
num_labels:所有连通域的数目
labels:图像上每一像素的标记,用数字1、2、3…表示(不同的数字表示不同的连通域)
stats:每一个标记的统计信息,是一个5列的矩阵,每一行对应每个连通区域的外接矩形的x、y、width、height和面积,示例如下: 0 0 720 720 291805
centroids:连通域的中心点
'''
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(arr_gray, connectivity=8)
对于输入的参数:
arr_gray : 是要处理的图片,要求是8位单通道的图像。
connectivity:可以选择是4连通还是8连通(不写也没事)
输出值有四个:
num_labels : 所有连通域的数量
labels :labels和你输入图像拥有同样的shape。但是里面的数值标志着属于哪一个连通域,从0开始
stats:包含5个参数,分别是,x,y,w,h,s。分别对应每个连通区域的外接矩形的左上角坐标x和y,以及其宽和高,s是指的连通域中的像素个数(不是指整个外接矩形的面积)
centroids : 是每一个连通区域的质心。
这里有一个地方我之前琢磨了好一会,就是这个输入图像,我在实际的项目中所得到的是一个已经进行完图像处理的numpy矩阵,对瑕疵进行了二值化(0与255),但是他不能作为输入图像。
我的解决方法是,我对这个numpy矩阵先转换成三通道的RGB图像,然后再把RGB图像转成灰度图就行了。
#height,width是我image的高和宽
#res_dilation 是图像处理结束后的一个二维numpy矩阵
arr = np.zeros((height, width, 3))
arr[:, :, 0] = res_dilation
arr[:, :, 1] = res_dilation
arr[:, :, 2] = res_dilation
arr = arr.astype(np.uint8)
arr_gray = cv2.cvtColor(arr, cv2.COLOR_BGR2GRAY)
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(arr_gray, connectivity=8)
因为我图像已二值化0与255,所以我对于三通道的处理就是复制3份。最后结果仍然是0与255。
然后就是对它输出值的一些分析:
我先构造一个10x10的一个图片,来模拟我之前的流程
具体测试代码如下:
import numpy as np
import cv2
a = np.array([[0,0,255,0,0,0,0,0,0,0],[0,0,255,0,0,0,0,0,0,0],[0,255,255,255,0,0,0,0,0,0],[0,0,255,0,0,0,0,0,0,0],[0,0,255,255,0,0,0,0,0,0],[0,0,255,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,255,0],[0,0,0,0,0,0,255,255,255,0],[0,0,0,0,0,0,0,255,0,0],[0,0,0,0,0,0,0,255,0,0]])
print(a)
arr = np.zeros((10, 10, 3))
arr[:, :, 0] = a
arr[:, :, 1] = a
arr[:, :, 2] = a
arr = arr.astype(np.uint8)
arr_gray = cv2.cvtColor(arr, cv2.COLOR_BGR2GRAY)
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(arr_gray, connectivity=8)
print(num_labels)
print(labels)
print(stats)
print(centroids)
上图是0与255的二值图,255也就是我项目中的瑕疵,0可以看作是背景。
可以很清楚的发现,是有2个连通域的。但是可以来看一下它的输出
可以看到,num_labels并不是2,而是3。这是因为它会把大背景作为一个大的连通域,作为0号label输出。所以 num_labels - 1才是真正的连通域数目
再看labels的输出,是一个10x10的矩阵。里面一共是0,1,2个标签,分别代表背景,1号连通域和2号连通域的各个点。
stats也是同样的,它每一行的五个参数x,y,w,h,s,上面也介绍过了。第一个是背景(不是很需要)。很明显的看出1号连通域有9个瑕疵像素点,2号连通域有6个瑕疵像素点。
centroids是每个连通域的质心坐标(第一行的是背景的,一般可以忽略)。但在实际中,我们习惯是用整数。
centroids = centroids.astype(int)
刚才说了,我们实际使用中,一般是要避免掉背景对于我们操作的影响的。所以我们可以直接去掉第一行。
stats0 = np.delete(stats, [0], axis=0)
然后对于我所需要的一些要求,我还可以对它们的x和y进行排序。
stats0 = np.delete(stats, [0], axis=0) #去掉背景的0号连通域的影响
sort_y = stats0[stats0[:, 1].argsort()] #把连通域按照y轴方向排序
min_y = sort_y[0][1]
max_y = sort_y[num_labels - 2][1]
sort_x = stats0[stats0[:, 0].argsort()] #把连通域按照x轴方向排序
min_x = sort_x[0][0]
max_x = sort_x[num_labels - 2][0]
print(min_y,min_x,max_y,max_x) # 0 1 6 6
这样的操作是为了我之后更精细的对图像进行处理。
还有就是在各连通域找到我想要的连通域,将其坐标找到,然后对其像素点进行处理。
for i in range(1,num_labels):
label_width = stats[i][2]
label_height = stats[i][3]
label_x = stats[i][0]
label_y = stats[i][1]
label = labels[label_y:label_y + label_height, label_x:label_x + label_width] # 获取label外接矩形
lab = label.reshape(-1, ) # 二维转一维
# lab = np.unique(lab) # 去掉重复
lab = np.setdiff1d(lab, 0) # 去掉0值,该外接矩阵中其余label值
print("lab")
print(lab)
for l in lab:
seeds = np.argwhere(label == i) # 找到所需要的label的点
seedlist = list(seeds)
print("l:", l)
print(seedlist)
这样就找到了具体每一个连通域的点的坐标。
接下来如果要对具体的每个连通域坐标操作,可以:
for point in seedlist:
xxxxxx具体操作xxxxxx
但有一个注意点,这个point只是指的其连通域外接矩阵中的坐标,并不是整个numpy矩阵图像上的坐标。
arr_gray[label_y + point[0], label_x + point[1]] ,这个才是其真正在矩阵中的坐标
总的一些关于连通域的应用就这么多了,如果有帮助的话,可以收藏,点赞一下,感谢。
有问题的话可以评论区交流。