Python科学计算(第2版)
上QQ阅读APP看书,第一时间看更新

3.9.1 形态学图像处理

本节介绍如何使用morphology模块实现二值图像处理。二值图像中每个像素的颜色只有两种:黑色和白色。在NumPy中可以用二维布尔数组表示:False表示黑色,True表示白色。也可以用无符号单字节整型(uint8)数组表示:0表示黑色,非0表示白色。

下面的两个函数用于显示形态学图像处理的结果:

    import numpy as np
    
    def expand_image(img, value, out=None, size = 10):
        if out is None:
            w, h = img.shape
            out = np.zeros((w*
size, h*
size),dtype=np.uint8)
    
        tmp = np.repeat(np.repeat(img,size,0),size,1)
        out[:,:] = np.where(tmp, value, out)
        out[::size,:] = 0
        out[:,::size] = 0
        return out
    
    def show_image(*
imgs): 
        for idx, img in enumerate(imgs, 1):
            ax = pl.subplot(1, len(imgs), idx)
            pl.imshow(img, cmap="gray")   
            ax.set_axis_off( )    
        pl.subplots_adjust(0.02, 0, 0.98, 1, 0.02, 0)

1.膨胀和腐蚀

二值图像最基本的形态学运算是膨胀和腐蚀。膨胀运算是将与某物体(白色区域)接触的所有背景像素(黑色区域)合并到该物体中的过程。简单来说,就是对于原始图像中的每个白色像素进行处理,将其周围的黑色像素都设置为白色像素。这里的“周围”是一个模糊概念,在实际运算时,需要明确给出“周围”的定义。图3-50是膨胀运算的一个例子,其中下左图是原始图像,中间的图是四连通定义的“周围”的膨胀效果,下右图是八连通定义的“周围”的膨胀效果。图中用灰色方块表示由膨胀处理添加进物体的像素。

图3-50 四连通和八连通的膨胀运算

    from scipy.ndimage import morphology
    
    def dilation_demo(a, structure=None):
        b = morphology.binary_dilation(a, structure)
        img = expand_image(a, 255)
        return expand_image(np.logical_xor(a,b), 150, out=img)
    
    a = pl.imread("scipy_morphology_demo.png")[:,:,0].astype(np.uint8)
    img1 = expand_image(a, 255)
    
    img2 = dilation_demo(a)
    img3 = dilation_demo(a, [[1,1,1],[1,1,1],[1,1,1]])
    show_image(img1, img2, img3)

四连通包括上下左右4个像素,而八连通则还包括4个斜线方向上的邻接像素。它们都可以使用下面的正方形矩阵定义,其中正中心的元素表示当前要进行运算的像素,而其周围的1和0表示对应位置的像素是否算作其“周围”像素。这种矩阵描述了周围像素和当前像素之间的关系,被称作结构元素(structuring element)。

    四连通八连通
    0 1 0      1 1 1
    1 1 1      1 1 1
    0 1 0      1 1 1

假设数组a是一个表示二值图像的数组,可以用如下语句对其进行膨胀运算:

    binary_dilation(a)

binary_dilation( )默认使用四连通进行膨胀运算,通过structure参数可以指定其他的结构元素。下面是进行八连通膨胀运算的语句:

    binary_dilation(a, structure=[[1,1,1],[1,1,1],[1,1,1]])

通过设置不同的结构元素,能够实现各种不同的效果,图3-51显示了三种不同结构元素的膨胀效果。图中的结构元素分别为:

图3-51 不同结构元素的膨胀效果

    左中右
    0 0 0   0 1 0   0 1 0
    1 1 1   0 1 0   0 1 0
    0 0 0   0 1 0   0 0 0
    img4 = dilation_demo(a, [[0,0,0],[1,1,1],[0,0,0]])
    img5 = dilation_demo(a, [[0,1,0],[0,1,0],[0,1,0]])
    img6 = dilation_demo(a, [[0,1,0],[0,1,0],[0,0,0]])
    show_image(img4, img5, img6)

binary_erosion()的腐蚀运算正好和膨胀相反,它将“周围”有黑色像素的白色像素设置为黑色。图3-52是四连通和八连通腐蚀的效果,图中用灰色方块表示被腐蚀的像素。

图3-52 四连通和八连通的腐蚀运算

    def erosion_demo(a, structure=None):
        b = morphology.binary_erosion(a, structure)
        img = expand_image(a, 255)
        return expand_image(np.logical_xor(a,b), 100, out=img)
    
    img1 = expand_image(a, 255)
    img2 = erosion_demo(a)
    img3 = erosion_demo(a, [[1,1,1],[1,1,1],[1,1,1]])
    show_image(img1, img2, img3)

2.Hit和Miss

Hit和Miss是二值形态学图像处理中最基本的运算,因为几乎所有其他的运算都可以用Hit和Miss的组合推演出来。它对图像中的每个像素周围的像素进行模式判断,如果周围像素的黑白模式符合指定的模式,将此像素设为白色,否则设置为黑色。因为它需要同时对白色和黑色像素进行判断,因此需要指定两个结构元素。进行Hit和Miss运算的binary_hit_or_miss()的调用形式如下:

    binary_hit_or_miss(input, structure1=None, structure2=None, ...)

其中structure1参数指定白色像素的结构元素,而structure2参数则指定黑色像素的结构元素。图3-53是binary_hit_or_miss()的运算结果。其中下左图为原始图像,中图为使用下面两个结构元素进行运算的结果:

图3-53 Hit和Miss运算

    白色结构元素  黑色结构元素
      0 0 0          1 0 0
      0 1 0          0 0 0
      1 1 1          0 0 0

在这两个结构元素中,0表示不关心其对应位置的像素的颜色,1表示其对应位置的像素必须为结构元素所表示的颜色。因此通过这两个结构元素可以找到“下方三个像素为白色,并且左上像素为黑色的白色像素”。

与下右图对应的结构元素如下,通过它可以找到“下方三个像素为白色、左上像素为黑色的黑色像素”。

    白色结构元素   黑色结构元素
      0 0 0          1 0 0
      0 0 0          0 1 0
      1 1 1          0 0 0
    def hitmiss_demo(a, structure1, structure2):
        b = morphology.binary_hit_or_miss(a, structure1, structure2)
        img = expand_image(a, 100)
        return expand_image(b, 255, out=img)
    
    img1 = expand_image(a, 255)
    
    img2 = hitmiss_demo(a, [[0,0,0],[0,1,0],[1,1,1]], [[1,0,0],[0,0,0],[0,0,0]])
    img3 = hitmiss_demo(a, [[0,0,0],[0,0,0],[1,1,1]], [[1,0,0],[0,1,0],[0,0,0]])
    
    show_image(img1, img2, img3)

使用Hit和Miss运算的组合,可以实现复杂的图像处理。例如文字识别中常用的细线化运算就可以用一系列的Hit和Miss运算实现。图3-54显示了细线化处理的效果,实现程序如下:

图3-54 使用Hit和Miss进行细线化运算

    def skeletonize(img):
        h1 = np.array([[0, 0, 0],[0, 1, 0],[1, 1, 1]]) ❶
        m1 = np.array([[1, 1, 1],[0, 0, 0],[0, 0, 0]])
        h2 = np.array([[0, 0, 0],[1, 1, 0],[0, 1, 0]])
        m2 = np.array([[0, 1, 1],[0, 0, 1],[0, 0, 0]])
        hit_list = []
        miss_list = []
        for k in range(4): ❷
            hit_list.append(np.rot90(h1, k))
            hit_list.append(np.rot90(h2, k))
            miss_list.append(np.rot90(m1, k))
            miss_list.append(np.rot90(m2, k))
        img = img.copy()
        while True:
            last = img
            for hit, miss in zip(hit_list, miss_list):
                hm = morphology.binary_hit_or_miss(img, hit, miss) ❸
                # 从图像中删除hit_or_miss所得到的白色点
                img = np.logical_and(img, np.logical_not(hm)) ❹
            # 如果处理之后的图像和处理前的图像相同,则结束处理
            if np.all(img == last): ❺
                break
        return img
    
    a = pl.imread("scipy_morphology_demo2.png")[:,:,0].astype(np.uint8)
    b = skeletonize(a)

❶以图3-55所示的两个结构元素为基础,构造4个形状为(3, 3)的二维数组:h1、m1、h2、m2。其中h1和m1对应图中左边的结构元素,而h2和m2对应图中右边的结构元素,h1和h2对应白色结构元素,m1和m2对应黑色结构元素。❷将这些结构元素进行90°、180°、270°旋转之后一共得到8个结构元素。

图3-55 细线化算法的4个结构元素

❸依次使用这些结构元素进行Hit和Miss运算,❹并从图像中删除运算所得到的白色像素,其效果就是依次从8个方向删除图像的边缘上的像素。❺重复运算直到没有像素可删除为止。