数字图像处理冈萨雷斯-形态学处理


首先明确一点形态学操作都是针对与二值图像,在灰度图像的形态学处理也是先根据一些前置操作转化为二值图像(如高帽运算后再阈值化),这是因为形态学的 任何操作都是需要击中这个概念,膨胀开闭其实可以看成包含 击中的是一个点,其他操作可以看成 击中的是一个模版!!!,在后面我会在代码中给出解释,因此当所有操作都有击中这个操作的时候就能知道为什么形态学需要二值图像而非灰度图!!!

腐蚀膨胀

腐蚀是对图像瘦小的操作,就是当模版击中的时候,图像该像素置1否则置0,(模版击中)
膨胀是对图像变胖的操作,就是当模版与对应位置的图像矩阵只要有一个点1击中,图像该像素置1否则置0,(一个点击中)
击中概念是图像与模版与运算结果都为1时为击中,在代码中可以矩阵相乘做法判断后面代码会给出
步骤分为3步:

  1. 边界处理 考虑边界是否处理
  2. 模版选择 opencv3中 有3种默认模版,矩型,十字架型,椭圆型也可以自己定义想用的模版,每个模版对图像的腐蚀影响大
  3. 卷积操作

代码如下:

# 边缘填充,边缘填充在opencv 中是个大类,有填充类别,和填充行列数这里主要为了介绍形态学处理,故填充只考虑 类别为复制,填充行列数为1
def copyMakeBorder(f):
    rows, cols = f.shape
    newF = np.zeros((rows + 2, cols + 2))
    newF[0, 1:-1] = f[0, :]
    newF[1:-1, 1:-1] = f[0:, :]
    newF[rows, 1:-1] = f[rows - 1:]
    newF[:, 0] = newF[:, 1]
    newF[:, -1] = newF[:, -2]
    return newF
    

# 腐蚀 后面的B为模版矩阵 这里的命名易读性差希望读者谅解
def imerode(f, B=np.ones(9).reshape(3, 3)):
    rowsB, colsB = B.shape
    rb = int(rowsB / 2)
    cb = int(colsB / 2)
    f = copyMakeBorder(f)
    newF = np.ndarray(f.shape)
    rows, cols = f.shape
    n = np.sum(B)
    for i in range(rb, rows - rb):
        for j in range(cb, cols - cb):
            newF[i, j] = 1 if np.sum(f[i - rb:i + rb + 1, j - cb:j + cb + 1] * B) == n else 0
    return newF[1:-1, 1:-1]


# 膨胀
def imdilate(f, B=np.ones(9).reshape(3, 3)):
    rowsB, colsB = B.shape
    rb = int(rowsB / 2)
    cb = int(colsB / 2)
    f = copyMakeBorder(f)
    newF = np.ndarray(f.shape)
    rows, cols = f.shape
    for i in range(rb, rows - rb):
        for j in range(cb, cols - cb):
            newF[i, j] = 1 if np.sum(f[i - rb:i + rb + 1, j - cb:j + cb + 1] * B) >= 1 else 0
    return newF[1:-1, 1:-1]

效果如下:
在这里插入图片描述

开闭运算

开运算是对图像整体粗细几乎不变,光滑断开的操作,先腐蚀后膨胀
闭运算是对图像整体粗细几乎不变,光滑连接的操作,先膨胀后腐蚀
tip 当进行开闭运算的时候膨胀和腐蚀根据需要可以使用不同的模版可能会带来更好的效果

代码如下:

# 开运算
def lockageon(f):
    f = f / 255 # 二值化
    mask1 = np.array([0, 1, 0, 1, 1, 1, 0, 1, 0]).reshape(3, 3)
    mask2 = np.ones(9).reshape(3, 3)
    return imdilate(imerode(f, mask1), mask2)


# 闭运算
def lockageoff(f):
	f = f / 255 #二值化
    mask1 = np.array([0, 1, 0, 1, 1, 1, 0, 1, 0]).reshape(3, 3)#十字架类型模版
    mask2 = np.ones(9).reshape(3, 3)#矩阵类型模版
    return imerode(imdilate(f, mask1), mask2)

效果如下,这里因为开和闭是两个相反的操作,故这里只给出开运算效果,闭运算科自己运行查看
在这里插入图片描述

轮廓提取

这个比较简单, o = f - ef

  1. o为轮廓图
  2. f为原图
  3. ef为腐蚀图

整个轮廓提取的过程需要注意的一点是模板的选择为矩形模板,这是为了保证每次腐蚀的元素都是图像内部元素!!!
代码如下:

contourImg = original - imerode(original, np.ones(9).reshape(3, 3))

效果如下:
在这里插入图片描述

扫描二维码关注公众号,回复: 9059856 查看本文章

击中与击不中

这个其实算是冈萨雷斯书里一个很重要的坑!!!,读者应该都知道形态学那章中击中与击不中的那个示意图,下面说一下本人所看到的一些点

  1. 案例用的是规则图形
  2. 案例其实是进行了两次击中分别是(内容击中与外部轮廓击中)

这个也就导致了我们在实际应用场景中根本就做不到外部轮廓的击中,因为在实际二值图像里我们大部分都是处理的一些边缘图,外部轮廓更是无规律可循也就完全做不到外部轮廓击中即我们实际场景中都是考虑的内容击中即可!!!这个就很容易做到,只用模板完全匹配 这个在下面的细化骨架提取得到呈现。

骨架提取

这个大类里的步骤比孔洞填充连通域的繁琐复杂因此这两个部分的内容在这里省略,这一章内容过多篇幅太长如有需要可以在评论处说明!

细化骨架提取

细化

这里其实我觉得细化和骨架提取这两个是密不可分的,这一点在冈萨雷斯这本书更体现的淋漓尽致
结构元的设定冈萨雷斯书中的8种结构元,这里可能读者会有疑问,我的一些同学也都是这样认为的是16中结构元这两种说法都是正确的下面给出我的结构元定义:

# 细化模板
    B1 = np.array([-1, -1, -1, 0, 1, 0, 1, 1, 1]).reshape(3, 3)
    B2 = np.array([0, -1, -1, 1, 1, -1, 1, 1, 0]).reshape(3, 3)
    B3 = np.array([1, 0, -1, 1, 1, -1, 1, 0, -1]).reshape(3, 3)
    B4 = np.array([1, 1, 0, 1, 1, -1, 0, -1, -1]).reshape(3, 3)
    B5 = np.array([1, 1, 1, 0, 1, 0, -1, -1, -1]).reshape(3, 3)
    B6 = np.array([0, 1, 1, -1, 1, 1, -1, -1, 0]).reshape(3, 3)
    B7 = np.array([-1, 0, 1, -1, 1, 1, -1, 0, 1]).reshape(3, 3)
    B8 = np.array([-1, -1, 0, -1, 1, 1, 0, 1, 1]).reshape(3, 3)

这8种分别对应了书中的T形和矩形的4个方位对应的结构元
这里会有两个疑问?~~?(好想打个疑惑脸)

  1. 书中为什么会定义的T形和矩形?
    这里就是为了说明细化和骨架提取的联系,这两种结构元细化可以保证最后的骨架提取不失连通性
  2. 上面的8个结构元定义是为什么?
    这8种结构元其实值为-1的位置代表了16种结构元种0的位置(好像挺绕的读者仔细体会下),这里我想表达的就是上一块的击中与击不中内容种的 “内容完全匹配(0,1都需要匹配)”!!!这样的做法只有一个好处可以减少一半的细化操作,极大减少计算量!!!

代码如下:

def refining(f):
    rows, cols = f.shape
    # 细化模板
    B1 = np.array([-1, -1, -1, 0, 1, 0, 1, 1, 1]).reshape(3, 3)
    B2 = np.array([0, -1, -1, 1, 1, -1, 1, 1, 0]).reshape(3, 3)
    B3 = np.array([1, 0, -1, 1, 1, -1, 1, 0, -1]).reshape(3, 3)
    B4 = np.array([1, 1, 0, 1, 1, -1, 0, -1, -1]).reshape(3, 3)
    B5 = np.array([1, 1, 1, 0, 1, 0, -1, -1, -1]).reshape(3, 3)
    B6 = np.array([0, 1, 1, -1, 1, 1, -1, -1, 0]).reshape(3, 3)
    B7 = np.array([-1, 0, 1, -1, 1, 1, -1, 0, 1]).reshape(3, 3)
    B8 = np.array([-1, -1, 0, -1, 1, 1, 0, 1, 1]).reshape(3, 3)
    maskList = [B1, B2, B3, B4, B5, B6, B7, B8]
    count = 0
    # skemask1 = np.array([1, 0, 1, 0, 1, 0, 1, 0, 1]).reshape(3, 3)
    while True:
        temp = f.copy
        for m in maskList:
            mas = []
            for i in range(1, rows - 1):
                for j in range(1, cols - 1):
                    if f[i, j] == 0:
                        continue
                    elif np.sum(m * f[i - 1:i + 2, j - 1:j + 2]) == 4:
                        # 击中时标记删除点
                        mas.append((i, j))
            for it in mas:
                x, y = it
                f[x, y] = 0
        if (temp == f).all:
            count += 1
        else:
            count = 0
        if count == 8:
            break
    return f

效果如下:
在这里插入图片描述

裁剪

裁剪这里其实并没有任何的理解难度,当然结构元的设定也是有迹可循的,读者可以自己想下通细化的结构元定义类似的思路

代码如下:

# 裁剪算法
def cut(f):
    rows, cols = f.shape
    f2 = f.copy()
    # 裁剪模板
    A = np.array([0, -1, -1, 1, 1, -1, 0, -1, -1]).reshape(3, 3)
    A1 = np.rot90(A)
    A2 = np.rot90(A1)
    A3 = np.rot90(A2)
    B = np.array([1, -1, -1, -1, 1, -1, -1, -1, -1]).reshape(3, 3)
    B1 = np.rot90(B)
    B2 = np.rot90(B1)
    B3 = np.rot90(B2)
    maskList = [A, A1, A2, A3, B, B1, B2, B3]
    for k in range(3):
        for m in maskList:
            mas = []
            for i in range(1, rows - 1):
                for j in range(1, cols - 1):
                    if f2[i, j] == 0:
                        continue
                    elif np.sum(m * f2[i - 1:i + 2, j - 1:j + 2]) == 2:
                        mas.append((i, j))
            for it in mas:
                x, y = it
                f2[x, y] = 0

    # 得到首位端点,即膨胀初始点
    f3 = np.zeros(f.shape)
    for i in range(1, rows - 1):
        for j in range(1, cols - 1):
            if f2[i, j] == 0:
                continue
            else:
                for m in maskList:
                    if np.sum(m * f2[i - 1:i + 2, j - 1:j + 2]) == 2:
                        f3[i, j] = 1
    # 膨胀
    H = np.ones(9).reshape(3, 3)
    rows, cols = f3.shape
    for i in range(1, rows - 1):
        for j in range(1, cols - 1):
            # 每一次膨胀与上A
            f3[i, j] = f[i, j] * (1 if np.sum(f3[i - 1:i + 2, j - 1:j + 2] * H) >= 1 else 0)

    newf = (f2 + f3) / 2
    ret, newf = cv.threshold(newf, 0, 1, cv.THRESH_BINARY)
    return newf

效果如下:其实这里的从这个图看起来并不是很理想,原因有以下几点

  1. 细化的不够好
  2. 裁剪的时候第一次细化次数
    这里的优化读者可以从这两点入手
    在这里插入图片描述

距离变换骨架提取

这部分内容只作为扩展内容,因为理论知识过多下面给出代码,有兴趣的朋友可以作为参考
代码如下:

# 距离变换骨架提取
def distance(f):
    f2 = np.zeros(f.shape)
    rows, cols = f.shape

    # 距离变换操作
    for i in range(1, rows - 1):
        for j in range(1, cols - 1):
            f[i, j] = sys.maxsize if f[i, j] == 0 else 1

    for i in range(1, rows - 1):
        for j in range(1, cols - 1):
            temp0 = f[i, j]
            temp1 = min(f[i, j - 1] + 3, temp0)
            temp2 = min(f[i - 1, j - 1] + 4, temp1)
            temp3 = min(f[i - 1, j] + 3, temp2)
            temp4 = min(f[i - 1, j + 1] + 4, temp3)
            f[i, j] = temp4

    for i in range(rows - 2, 0, -1):
        for j in range(cols - 2, 0, -1):
            temp0 = f[i, j]
            temp1 = min(f[i, j + 1] + 3, temp0)
            temp2 = min(f[i + 1, j + 1] + 4, temp1)
            temp3 = min(f[i + 1, j] + 3, temp2)
            temp4 = min(f[i + 1, j - 1] + 4, temp3)
            f[i, j] = temp4

    # 骨架提取
    for i in range(3, rows - 3):
        for j in range(3, cols - 3):
            if 5 <= f[i, j] <= 10:
                if f[i, j] == np.max(f[i - 3:i + 4, j - 3:j + 4]):
                    f2[i, j] = 1
                elif np.sum(f[i - 1, j - 1:j + 2] * [-1, -1, 1]) == 1:
                    f2[i, j] = 1
                elif np.sum(f[i - 1, j - 1:j + 2] * [-1, -1, 1]) == 0 and f[i, j + 1] > f[i, j]:
                    f2[i, j] = 1
    return f2
    
def main():
	# 边缘和距离骨架提取
    contourImg = original - imerode(original, np.ones(9).reshape(3, 3))
    out = contourImg.copy()
    distanceImg = distance(contourImg)

    plt.figure()
    show(out, "contourImg", 1, 2, 1)
    show(distanceImg, "distanceImg", 1, 2, 2)
    plt.show()

效果如下:距离变换的效果很不理想,我已经在代码里尽量保证提取后的连通性可是仍然如此
在这里插入图片描述

Hilditch算法骨架提取

这里感兴趣的朋友可以去网上看看思路以及效果和速度,这里不多做介绍,本人跑过就这张指纹图片,博客里代码优化后效果上会比Hilditch算法好,速度上从理论上就根本比不过,会比Hilditch算法慢大约两秒

总结:以上就是本人对数字图像处理形态学方面内容的浅显见解请读者多多提意见,
转载我博客应当经我允许,至少要把原文链接放在文章最前面,这是对本人辛苦原创基本的尊重。

发布了3 篇原创文章 · 获赞 0 · 访问量 409

猜你喜欢

转载自blog.csdn.net/ab136681/article/details/104239100