1 引言
最近使用传统方法应用于实际生活中的问题,受到了大家一致的关注。
嗯嗯,应该是一致的关注。
那么我们今天来研究一个新的好玩的方向,就是基于二值图像进行连通域标记和分析,从而解决数字分割的问题。
问题描述:
从下图左侧图像中,分割出数字1的图像,如右侧所示:
嗯捏。。。
先思考2分钟,然后我们用python来一步一步实现吧。。。
2 解决方案
2.1 读取图像
这里我们直接读取灰度图像,需要注意的是需要将其转化为float型数据.
这是因为我们后面有些操作可能会将像素值超过[0,255]范围,
代码如下:
image = cv2.imread(image_file, cv2.IMREAD_GRAYSCALE)
image = image.astype(np.float32)
结果如下:
2.2 去噪
这里我们使用一些滤波器(低通滤波器,保留低频噪声,滤除高频噪声).
以下为opencv中常用的去噪函数:
ksize = 5 # Some odd integer
blurred_image = cv2.blur(image, (ksize, ksize)) # Mean blur
blurred_image = cv2.GaussianBlur(image, (ksize, ksize), 0)
blurred_image = cv2.medianBlur(image, ksize)
blurred_image = cv2.biliteralFilter(imag, 9, 75, 75) # Read the docs
这里经过尝试,我们选用高斯滤波器,kernel size = 5 ,代码如下:
nr_image = cv2.GaussianBlur(image, (5, 5), 0)
结果如下:
2.3 生成背景图
我们发现上图中,光照不均匀,有的区域偏亮,有的区域偏暗,这里我们采用大 kernel size的滤波器来生成背景图.
经过尝试发现使用 kernel size=21的高斯滤波器依然工作良好.
相应代码如下:
background = cv2.GaussianBlur(nr_image, (21, 21), 0)
结果如下:
2.4 获取前景图
直接使用减法,原图减去背景取可以得到前景图,但是前景图的像素值均接近于0,这里采用归一化在缩放的方式将结果重新映射回去.
代码如下:
br_image = nr_image - background
br_image = 255*(br_image - np.min(br_image)) / (np.max(br_image) - np.min(br_image))
br_image = br_image.astype(np.uint8)
结果如下:
上图为我们获取的前景图,可以看出此时图像整体亮度相比之前趋于均衡.
2.5 二值化
为了获取合适的阈值,这里我们首先查看上图图像的灰度直方图,然后从中选择合适的阈值.
灰度直方图如下:
观察上图,直方图中间有一个大的尖峰,可以看作大部分为背景的像素点数.
我们感兴趣的前景物体相比背景更暗,所以我们选择保留该阈值下的像素点,同时我们需要将图像由 bool 型转换为 int 型方便后续使用.代码如下:
threshold = 149
thr_image = (br_image < threshold).astype(np.uint8)
结果如下:
需要注意的是上图中背景为黑色,我们感兴趣的前景区域为白色.
2.6 连通域标记
接下来我们使用 opencv 中的函数connectedComponentsWithStats 对上图进行连通域标记,
代码如下:
connectivity = 4
output = cv2.connectedComponentsWithStats(thr_image.astype(np.uint8),
connectivity,
cv2.CV_32S)
num_labels = output[0]
label_image = output[1] # Image with a unique label for each connected region
stats = output[2]
centroids = output[3] # Centroid indices for each connected region
函数解释如下:
- input: connectivity 连通域,默认为8连通,这里我们选择4连通
- input: cv2.CV_32S 表示输出labels类型
- output: num_labels 表示标记出的连通域的数量
- output: label_image 表示输出标记的图像
- output: stats 统计信息,每个区域的位置 宽高和面积
- output: centroids 每隔区域的中心位置坐标 cx xy
2.7 可视化
通过上述连通域标记,我们得到了好多返回值,这里我们需要重点关注的为label_image 以及 stats .其中, label_image和输入shape一样,为标记后的图像,该图像中每个连通域均被标记为唯一的一个label.
我们使用以下代码将其可视化:
from skimage.color import label2rgb
fig_num = plot_image(label2rgb(label_image), 'Labels before thresholding', fig_num)
结果如下:
同时,stats输出为一个数组,里面包含了每个连通域的统计量,具体如下:
- stats[:,0]: x,它是连通区域外接框左上角点x坐标
- stats[:,1]: y,它是连通区域外接框左上角点y坐标
- stats[:,2]: w,它是连通区域外接框的宽度
- stats[:,3]: h, 它是连通区域外接框的高度
- stats[:,4]: Area,它是每个连通区域组件的面积
2.8 分析
接着,我们来分析stats中相关的统计量的具体应用.
1)首先来分析每个连通区域的面积直方图,注意到stats[0,:]表示背景label=0的区域,这时因为相比其他连通区域,背景连通域的面积最大,这里我们将其忽略.
代码如下:
region_area = stats[1:, 4] # remove index=0 background
fig = plt.figure(fig_num)
plt.hist(region_area, 256, facecolor='black', alpha=0.75)
plt.xlabel('Region area')
结果如下:
上图为前景物体连通域面积分布直方图
2)通过观察上图,我们可以获得以下两条信息:
- 面积在200以下的区域,对应到图上为前景数字外虚线矩形框每个小虚线区域的面积
- 面积在200以上的区域,对应到图上为前景每个数字所构成的链接域的面积
基于上述两点我们编写代码实现分离虚线矩形框和相关数字功能,代码如下:
region_area = stats[:, 4]
lower_threshold = 200
upper_threshold = 450
keep_labels = np.where(np.logical_and(stats[:, 4] > lower_threshold,
stats[:, 4] < upper_threshold))
keep_label_image = np.in1d(label_image, keep_labels).reshape(label_image.shape)
ra_thr_image = np.copy(thr_image)
ra_thr_image[keep_label_image] = 255
ra_thr_image[keep_label_image == False] = 0
结果如下:
同时,面积在[50-100]的连通域的图像如下:
2.9 二次分析
观察上述面积分布直方图,我们可以看出在[200,450]之间存在5个波峰,我们分别将其取出进行可视化,结果如下:
1)连通域面积介于[245,275]之间,对应数字1,图像如下:
2)连通域面积介于[275,310]之间,对应数字7,图像如下:
3)连通域面积介于[310,340]之间,对应数字4,图像如下:
4)连通域面积介于[340,388]之间,对应数字3和5和个别9,此时仅靠面积信息进行区分已经十分困难,图像如下:
5)连通域面积介于[388,450]之间,对应剩余数字全部的8,0,2,6以及部分9,图像如下:
3 总结
本文介绍了使用传统图像处理方法进行二值图像连通域标记并实现数字分割,最后给出了完整的代码实现.
您学废了吗?
关注公众号《AI算法之道》,获取更多AI算法资讯。
注: 关注公众号,后台回复 数字 , 可获取完整代码