关于连通域的寻找和分割。在python中有两个库的函数可以使用,一个是scikit-image库中的measure文件,另一个是opencv中的connectedComponentsWithStats函数。
其中,第一个库比较好使,如果环境允许,可以尽量使用第一个库函数,因此比较方便,而且这个函数中有丰富的属性可以直接使用。但是在实际使用的时候,在Linux ARM64环境下安装scikit-image包总是失败,目前官方git也没有给出比较好的解决思路https://github.com/scikit-image/scikit-image/issues/4705。所以只能采用opencv中的库函数替换。
关于连通域的寻找和分割。在python中有两个库的函数可以使用,一个是scikit-image库中的measure文件,另一个是opencv中的connectedComponentsWithStats函数。
分别介绍两个库的连通域寻找的代码
一、 skimage.measure
skimage.measure.label和skimage.measure.regionprops是很好用的组合拳。
label_map = skimage.measure.label(image)可以将二值图中,连通域区域的像素都标记为特定的数字。
regions= skimage.measure.regionprops(label_map) 这里输出的regions是一个列表,每个列表元素是一个Region对象,Region对象中保存了连通域的丰富的属性信息
下面的代码就可以图像化展示skimage.measure.label的输出结果:
labels=measure.label(data,connectivity=2) #8连通区域标记
dst=color.label2rgb(labels) #根据不同的标记显示不同的颜色
print('regions number:',labels.max()+1) #显示连通区域块数(从0开始标记)
其中measure.label的第二个参数表示搜素连通域的范围,2表示搜索周围八个像素点;1表示搜素上下左右四个像素点。
1-connectivity 2-connectivity diagonal connection close-up
[ ] [ ] [ ] [ ] [ ]
| \ | / | <- hop 2
[ ]--[x]--[ ] [ ]--[x]--[ ] [x]--[ ]
| / | \ hop 1
[ ] [ ] [ ] [ ]
skimage.measure.regionprops函数可以得到上面的每一个连通域的属性信息。常用的属性如下:
属性名称 | 类型 | 描述 |
---|---|---|
area | int | 区域内像素点总数 |
bbox | tuple | 边界外接框(min_row, min_col, max_row, max_col) |
centroid | array | 质心坐标 |
convex_area | int | 凸包内像素点总数 |
convex_image | ndarray | 和边界外接框同大小的凸包 |
coords | ndarray | 区域内像素点坐标 |
Eccentricity | float | 离心率 |
equivalent_diameter | float | 和区域面积相同的圆的直径 |
euler_number | int | 区域欧拉数 |
extent | float | 区域面积和边界外接框面积的比率 |
filled_area | int | 区域和外接框之间填充的像素点总数 |
perimeter | float | 区域周长 |
label | int | 区域标记 |
area属性是该连通域像素点之和,
bbox得到外接矩形坐标(min_row, min_col, max_row, max_col),
bbox_are属性是外接矩形面积,
filled_image大小[max_row-min_row,max_col-min_col],元素为Boolean,是外接矩形bbox中像素点是否属于连通域的标记图。所有属于连通域的像素点为True。
实际案例
比如对于表格框线的检测来说,采用分割网络可以输出每个像素点是否是表格线的[0,1]score特征图。根据阈值0.3将feature map像素变为0,1二值图。对于表格边框,feature map像素点为0,对于单元格内区域以及外部区域,像素点值为1.那么找到连通域就可以找到单元格区域。然后通过minAreaRectBox函数获得每个单元格的外接矩形局域就可以得到单元格的坐标。
from skimage import measure
import cv2
def minAreaRectBox(coords):
"""
多边形外接矩形
"""
rect = cv2.minAreaRect(coords[:, ::-1])
box = cv2.boxPoints(rect)
box = box.reshape((8,)).tolist()
box = sort_box(box)
return box
...
def get_table_cells(score):
# score是一个分割网络输出的单通道score图,0.3是阈值
tabelLabels = measure.label(score<0.3, connectivity=2)
regions = measure.regionprops(tabelLabels)
for region in regions:
# 判断这个连通域是否是整图或者近似于整图,这样的连通域是不需要的
if region.bbox_area > 0.9 * h * w:
# print("bbox_area太大了")
continue
# 过滤掉非常小的区域,这种区域可能是边界上的意外连接
elif region.area < 100 or region.bbox[2] - region.bbox[0] < 10 or region.bbox[3] - region.bbox[1] < 10:
# print("bbox-area太小了")
continue
else:
rbox = minAreaRectBox(region.coords)
rbox = list(rbox)
bboxes.append(rbox)
return bboxes
二、cv2.connectedComponentsWithStats
同样的图像连通域分割也可以通过这个函数得到cv2.connectedComponentsWithStats
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats((tmp==0).astype(np.uint8),connectivity=8)
该函数的输入必须是np.uint8类型的,而不像skimage中可以是boolean类型。第二个参数connectivity=8表示会搜索每个像素点周围八个像素点的,和measure.label函数的connectivity=8是一个意思。
输出:
该函数输入为一个二值化图像,输出为一个长为4的tuple:
第一个是连通区域的个数,
第二个是一整张图的label,这里的label标签从0开始
第三个是(x, y, width, height, area),即每个区域的每个区域的左上角坐标,宽和高,面积。
第四个是每个连通区域的中心点。
在使用skimage.measure这个库和cv2这个库并不是一样的,不能直接替换函数。需要特别注意使用cv2.connectedComponentsWithStats
获得的labels图上标签为0的像素构成的区域。这个区域是需要去除的。
比如同样需要第二章节中提到的功能,这里的函数需要这样写才能保证结果完全一致。主要的区别在于需要过滤掉label=0的区域,这个区域一般是由背景像素构成的。
import cv2
def minAreaRectBox(coords):
"""
多边形外接矩形
"""
rect = cv2.minAreaRect(coords[:, ::-1])
box = cv2.boxPoints(rect)
box = box.reshape((8,)).tolist()
box = sort_box(box)
return box
...
def get_table_cells(score):
# score是一个分割网络输出的单通道score图,0.3是阈值
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats((score>0.3).astype(np.uint8),connectivity=8)
for index, stat in enumerate(stats):
if index==0:#这一句话很重要!!!!!!
continue
x,y,cw,ch,area = stat
# 判断这个连通域是否是整图或者近似于整图,这样的连通域是不需要的
if cw*ch > 0.9 * h * w:
# print("bbox_area太大了")
continue
# 过滤掉非常小的区域,这种区域可能是边界上的意外连接
elif area < 100 or cw < 10 or ch < 10:
# print("bbox-area太小了")
continue
# 其余正常检测到表格单元的情况
else:
# print("regular boxes:{}".format((y,x,h,w)))
clabel_num = index
x_list,y_list = np.where(labels==clabel_num)
coords = np.array([x_list,y_list]).T
rbox = utils.minAreaRectBox(coords)
rbox = list(rbox)
bboxes.append(rbox)
return bboxes
cv2.connectedComponentsWithStats函数输出相较于skimage.measure.region对象来说,属性更少。比如region对象可以直接通过region.coords得到连通域的所有坐标,但是cv2.connectedComponentsWithStats需要通过对labels这个图进行一些操作才能得到特定数字的连通域的坐标信息。