图像连通域搜寻skimage和opencv方法

关于连通域的寻找和分割。在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
     [ ]           [ ]  [ ]  [ ]
image.png

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这个图进行一些操作才能得到特定数字的连通域的坐标信息。

你可能感兴趣的:(图像连通域搜寻skimage和opencv方法)