【youcans 的图像处理学习课】11. 形态学图像处理(中)

时间:2022-10-03 13:54:15

专栏地址:『youcans 的图像处理学习课』
文章目录:『youcans 的图像处理学习课 - 总目录』


【youcans 的 OpenCV 学习课】11. 形态学图像处理(中)

3. 形态学算法

形态学处理的主要应用是提取图像中用来表示和描述形状的元素和成分,例如提取边界、连通分量、凸壳和区域骨架。


3.1 边界提取

边界提取的原理是通过对目标图像进行腐蚀和膨胀处理,比较结果图像与原图像的差别来实现。

内边界的提取可以利用图像的腐蚀处理得到原图像的一个收缩,再将收缩结果与目标图像进行异或运算,实现差值部分的提取。

集合 A 的边界 β ( A ) \beta (A) β(A) 可以通过合适的结构元 B 腐蚀集合 A,然后求 A 与腐蚀结果的差集来实现:
β ( A ) = A − ( A ⊖ B ) \beta(A) = A -(A \ominus B) β(A)=A(AB)

常用的结构元 B 是 3*3 的全 1 核,而 5*5 的全 1 核往往可以得到2~3个像素宽度的边界。

类似地,外边界提取先对图像进行膨胀处理,然后用膨胀结果与原目标图像进行异或运算,也就是求膨胀结果与原目标图像的差集。


例程 10.10:形态算法之边界提取

    # 10.10 形态算法之边界提取
    imgGray = cv2.imread("../images/imgNetrope.png", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 25, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)  # 二值化处理

    kSize = (3, 3)  # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8)  # 生成盒式卷积核
    imgErode1 = cv2.erode(imgBin, kernel=kernel)  # 图像腐蚀
    imgBound1 = imgBin - imgErode1  # 图像边界提取

    plt.figure(figsize=(9, 5))
    plt.subplot(131), plt.axis('off'), plt.title("Origin")
    plt.imshow(imgBin, cmap='gray', vmin=0, vmax=255)
    plt.subplot(132), plt.title("Eroded kSize=(3,3)"), plt.axis('off')
    plt.imshow(imgErode1, cmap='gray', vmin=0, vmax=255)
    plt.subplot(133), plt.title("Boundary extraction"), plt.axis('off')
    plt.imshow(imgBound1, cmap='gray', vmin=0, vmax=255)
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)


3.2 孔洞填充

孔洞是被前景像素连成的边框包围的背景区域。书法作品图像中存在孔洞,在图像分割后也经常会有一些孔洞。

闭运算孔洞填充:

形态学闭运算可以用来实现孔洞填充,闭运算先膨胀后腐蚀操作,膨胀使白色高亮区域增加,孔洞会被填充,但需要准确设置核大小,因此不是通用的方法。

约束膨胀孔洞填充:

冈萨雷斯《数字图像处理(第四版)》提供了一种孔洞填充的形态学算法,构造一个元素为 0 的阵列 X 0 X_0 X0,其中对应孔洞的像素值为 1,采用迭代过程可以填充所有的孔洞:
X k = ( X k − 1 ⊕ B ) ∩ I c ,   k = 1 , 2 , 3... X_k = (X_{k-1} \oplus B) \cap I^c, \ k=1,2,3... Xk=(Xk1B)Ic, k=1,2,3...

先找到孔洞中的一个点,用结构元进行膨胀,然后用原始图像的补集进行约束(交集运算),不断迭代重复这一操作直到算法收敛,就得到孔洞填充图。

泛洪算法孔洞填充:

OpenCV 中提供了一种孔洞填充方法“泛洪填充法”,也成为“漫水填充法“。其原理是将像素点的灰度值视为高度,整个图像就像一张高低起伏的地形图,向洼地注水将会淹没低洼区域,从而实现孔洞填充。

漫水填充经常被用来标记或分离图像的一部分以便对其进行进一步处理或分析,也可以用来从输入图像获取掩码区域,掩码会加速处理过程,或只处理掩码指定的像素点,操作的结果总是某个连续的区域。

OpenCV 中的函数 cv.floodFill 可以实现漫水填充方法 。

函数说明:

cv.floodFill(image, mask, seedPoint, newVal[, loDiff[, upDiff[, flags]]]) -> retval, image, mask, rect

参数说明:

  • image:输入图像,可以为单通道或多通道,图像深度必须为8bit 或浮点类型。
  • dst:输出图像,大小和类型与 src 相同
  • mask:掩模图像,必须为单通道、8bit,且比 image 宽 2个像素、高 2个像素
  • setPoint:起始像素点
  • newVal:重绘像素区域的新的填充值(颜色)
  • rect:可选项,返回重绘区域的最小绑定矩形
  • loDiff:可选项,当前选定像素与其连通区中相邻像素中的一个像素,或者与加入该连通区的一个 seedPoint像素,二者之间的最大下行差异值。
  • upDiff:可选项,当前选定像素与其连通区中相邻像素中的一个像素,或者与加入该连通区的一个 seedPoint像素,二者之间的最大上行差异值。
  • flags:标志位,可选项,32bit 整型数据,由3部分组成: 0-7bit 表示邻接性(4邻接、8邻接);8-15bit 表示 mask 的填充颜色;16-31bit 表示填充模式:
    • cv.FLOODFILL_FIXED_RANGE:如果设置则考虑当前像素和种子像素之间的差异,否则将考虑相邻像素之间的差异
    • cv.FLOODFILL_MASK_ONLY:如果设置则不改变原始图像,并忽略 newVal,只使用上述标志位 8-16 中指定的值填充掩码。本选项仅在具有掩模图像时适用。

注意事项:

  • 原始图像 image 仅当 flags 设置为 FLOODFILL_MASK_ONLY 时不会被修改,否则原始图像会被修改。
  • 由于掩模比原始图像大,所以图像中的像素 (x,y) 对应于掩模中的像素 (x+1,y+1)。
  • Flood-filling 不能跨越掩模图像中的非 0 像素,因此边缘检测的结果可以作为mask来阻止边缘填充。
  • 泛洪填充法可以用特定的颜色填充联通区域(newVal),参见例程 10.10。

例程 10.11:约束膨胀算法实现孔洞填充

    # 10.11 约束膨胀算法实现孔洞填充
    # 本算法参考:冈萨雷斯《数字图像处理(第四版)》 9.5.2 孔洞填充
    # 图像为二值化图像,255 白色为目标物,0 黑色为背景,要填充白色目标物中的黑色空洞
    imgGray = cv2.imread("../images/imgBloodCell.png", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)  # 二值化处理
    imgBinInv = cv2.bitwise_not(imgBin)  # 二值图像的补集

    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))  # 构造 3×3 十字形结构元
    F = np.zeros(imgBin.shape, np.uint8)  # 构建阵列 F,并写入 BinInv 的边界值
    F[:, 0] = imgBinInv[:, 0]
    F[:, -1] = imgBinInv[:, -1]
    F[0, :] = imgBinInv[0, :]
    F[-1, :] = imgBinInv[-1, :]

    # 循环迭代:对 F 进行膨胀,膨胀结果与 BinInv 进行 AND 操作
    Flast = F.copy()
    for i in range(1000):
        F_dilation = cv2.dilate(F, kernel)
        F = cv2.bitwise_and(F_dilation, imgBinInv)
        if (F==Flast).all():
            break  # 结束迭代算法
        else:
            Flast = F.copy()
        if i==100: imgF100 = F  # 中间结果

    print("iter ={}".format(i))  # 迭代次数
    plt.figure(figsize=(9, 5))
    plt.subplot(131), plt.axis('off'), plt.title("Origin")
    plt.imshow(imgGray, cmap='gray', vmin=0, vmax=255)
    plt.subplot(132), plt.title("Hole filled (iter=100)"), plt.axis('off')
    plt.imshow(imgF100, cmap='gray', vmin=0, vmax=255)
    plt.subplot(133), plt.title("Hole filled (iter={})".format(i)), plt.axis('off')
    plt.imshow(F, cmap='gray', vmin=0, vmax=255)
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)


例程 10.12:泛洪算法实现孔洞填充

    # 10.12 泛洪算法实现孔洞填充 (cv2.floodFill)
    # 图像为二值化图像,255 白色为目标物,0 黑色为背景,要填充白色目标物中的黑色空洞
    imgGray = cv2.imread("../images/imgBloodCell.png", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)  # 二值化处理

    h, w = imgBin.shape[:2]
    mask = np.zeros((h+2, w+2), np.uint8)  # 掩模图像比原始图像宽 2 个像素、高 2 个像素

    imgFloodfill = imgBin.copy()
    # isbreak = False
    # for i in range(imgFloodfill.shape[0]):
    #     for j in range(imgFloodfill.shape[1]):
    #         if (imgFloodfill[i][j] == 0):  # seedPoint对应像素必须是背景
    #             seedPoint = (i, j)
    #             isbreak = True
    #             break
    #     if (isbreak):
    #         break
    # cv2.floodFill(imgFloodfill, mask, seedPoint, 255)  # 从 seedPoint 开始,必须是背景像素
    cv2.floodFill(imgFloodfill, mask, (0, 0), newVal=225)  # 算法从背景像素原点 (0, 0) 开始
    imgFloodfillInv = cv2.bitwise_not(imgFloodfill)  # 计算补集
    imgHoleFilled = imgBin | imgFloodfillInv  # 计算交集
    imgRebuild = cv2.bitwise_not(imgHoleFilled)  # 计算补集

    plt.figure(figsize=(9, 5))
    plt.subplot(131), plt.axis('off'), plt.title("Origin")
    plt.imshow(imgGray, cmap='gray', vmin=0, vmax=255)
    plt.subplot(132), plt.title("Flood filled"), plt.axis('off')
    plt.imshow(imgFloodfill, cmap='gray', vmin=0, vmax=255)
    plt.subplot(133), plt.title("Hole filled image"), plt.axis('off')
    plt.imshow(imgRebuild, cmap='gray', vmin=0, vmax=255)
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)


3.3 提取连通分量

从二值图像中提取连通分量是自动图像分析的核心步骤。

约束膨胀提取连通分量:

冈萨雷斯《数字图像处理(第四版)》提供了一种提取连通分量的形态学算法,构造一个元素为 0 的阵列 X 0 X_0 X0,其中对应连通分量的像素值为 1,采用迭代过程可以得到所有的连通分量:
X k = ( X k − 1 ⊕ B ) ∩ I ,   k = 1 , 2 , 3... X_k = (X_{k-1} \oplus B) \cap I, \ k=1,2,3... Xk=(Xk1B)I, k=1,2,3...

该算法与约束膨胀孔洞填充的思路相同,使用条件膨胀来限制膨胀的增长,但用 I I I 代替 I c I^c Ic 以寻找前景点。

对于内含多个连通分量的图像 A,从仅为连通分量 A1 内部的某个像素 B 开始,用 3*3的结构元不断进行膨胀。由于其它连通分量与 A1 之间至少有一条像素宽度的空隙,每次膨胀都不会产生位于其它连通区域内的点。用每次膨胀后的图像与原始图像 A 取交集,就把膨胀限制在 A1 内部。随着集合 B 的不断膨胀,B 的区域不断生长,但又被限制在连通分量 A1 的内部,最终就会充满整个连通分量 A1,从而实现对连通分量 A1 的提取。

提取连通分量的过程也是对连通分量的标注,通常给图像中的每个连通区分配编号,在输出图像中该连通区内的所有的像素值赋值为对应的区域编号,这样的输出图像被称为标注图像。


例程 10.13:形态算法之提取连通分量

    # # 10.13 约束膨胀算法提取连通分量
    # 本算法参考:冈萨雷斯《数字图像处理(第四版)》 9.5.3 提取连通分量
    # 图像为二值化图像,255 白色为目标物,0 黑色为背景
    imgGray = cv2.imread("../images/Fig0918a.tif", flags=0)  # flags=0 读取为灰度图像
    # 预处理
    ret, imgThresh = cv2.threshold(imgGray, 200, 255, cv2.THRESH_BINARY_INV)  # 二值化处理
    kernel = np.ones((3, 3), dtype=np.uint8)  # 生成盒式卷积核
    imgClose = cv2.morphologyEx(imgThresh, cv2.MORPH_CLOSE, kernel)  # 闭运算,消除噪点
    imgErode = cv2.erode(imgClose, kernel=kernel)  # 腐蚀运算,腐蚀亮点

    imgBin = imgErode
    imgBinCopy = imgBin.copy()  # 复制 imgBin
    xBinary = np.zeros(imgBin.shape, np.uint8)  # 大小与 img 相同,像素值为 0
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))  # 3×3结构元
    count = []  # 为了记录连通分量中的像素个数
    while imgBinCopy.any():  # 循环迭代,直到 imgBinCopy 中的像素值全部为0
        Xa_copy, Ya_copy = np.where(imgBinCopy > 0)  # imgBinCopy 中值为255的像素的坐标
        xBinary[Xa_copy[0]][Ya_copy[0]] = 255  # 选取第一个点,并将 xBinary 中对应像素值改为255

        # 约束膨胀,先对 xBinary 膨胀,再与 imgBin 执行与操作(取交集)
        for i in range(100):
            dilation_B = cv2.dilate(xBinary, kernel)
            xBinary = cv2.bitwise_and(imgBin, dilation_B)

        # 取 xBinary 值为255的像素坐标,并将 imgBinCopy 中对应坐标像素值变为0
        Xb, Yb = np.where(xBinary > 0)
        imgBinCopy[Xb, Yb] = 0

        # 显示连通分量及其包含像素数量
        count.append(len(Xb))
        lenCount = len(count)
        if lenCount == 0:
            print("无连通分量")
        elif lenCount == 1:
            print("第1个连通分量为{}".format(count[0]))
        else:
            print("第{}个连通分量为{}".format(len(count), count[-1]-count[-2]))

    # print(count)
    plt.figure(figsize=(12, 6))
    plt.subplot(231), plt.axis('off'), plt.title("origin")
    plt.imshow(imgGray, cmap='gray', vmin=0, vmax=255)
    plt.subplot(232), plt.title("threshold"), plt.axis('off')
    plt.imshow(imgBin, cmap='gray', vmin=0, vmax=255)
    plt.subplot(233), plt.title("closed image"), plt.axis('off')
    plt.imshow(imgClose, cmap='gray', vmin=0, vmax=255)
    plt.subplot(234), plt.title("eroded image"), plt.axis('off')
    plt.imshow(imgErode, cmap='gray', vmin=0, vmax=255)
    plt.subplot(235), plt.title("xBinary"), plt.axis('off')
    plt.imshow(xBinary, cmap='gray', vmin=0, vmax=255)
    plt.subplot(236), plt.title("binary copy"), plt.axis('off')
    plt.imshow(imgBinCopy, cmap='gray', vmin=0, vmax=255)
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)


3.4 凸壳(凸包)

欧氏几何中,如果连接物体 A 内任意两点的直线段都在 A 的内部,则称 A 是凸的。数字图像处理中,凸集简化为二维平面,且以离散坐标形式表达。数字集合 A 是凸的,当且仅当它的欧氏凸壳只包含属于 A 的数字点。

任意物体 A 的凸壳是包含 A 的最小凸物体。在二维图像中凸壳可以想象为一条刚好包着所有点的橡皮圈。在凸壳与物体边缘之间的部分称为凸陷(Convexity defect)。

使用形态学算法,令 B i B^i Bi 表示 4个特定的结构元,反复使用 B i B^i Bi 对 A 做击中击不中变换,直至收敛,就可以得到二值图像 I I I 中嵌入的前景图像集合 A 的凸壳的近似 C ( A ) C(A) C(A)
X k i = ( X k − 1 i ⊛ B i ) ∪ X k − 1 i ,   i = 1 , 2 , 3 , 4 ; k = 1 , 2 , 3... C ( A ) = ⋃ i = 1 4 D i X_k^i = (X_{k-1}^i \circledast B^i) \cup X_{k-1}^i, \ i=1,2,3,4;k=1,2,3... \\ C(A) = \bigcup^{4}_{i=1} D^i Xki=(Xk1iBi)Xk1i, i=1,2,3,4;k=1,2,3...C(A)=i=14Di

OpenCV 中提供了函数 cv.convexHull 可以获取轮廓的凸壳 。

函数说明:

	cv.convexHull(points[, hull[, clockwise[, returnPoints]]]) -> hull

参数说明:

  • points:输入图像,可以为单通道或多通道
  • hull:输出凸包,凸包点的索引向量,或凸包点集的向量
  • clockwise:方向标志,True 表示顺时针方向输出凸包,否则为逆时针方向输出
  • returnPoints:操作标志,True 表示返回凸包点集,否则返回凸包点的索引

进一步地,cv.convexHull 函数与 cv.drawContours 函数配合,可以用来检测物体是否存在缺陷。


例程 10.14:形态算法之凸壳(Convex hull)

说明:本例程改编自: Convex Hull using OpenCV in C++ and Python | LearnOpenCV

先使用 OpenCV 中的 cv.findContour 函数找到二值图像中的所有轮廓,然后使用 cv.convexHull 函数构造轮廓的凸包,最后使用 cv.drawContours 函数绘制轮廓和凸包 。

    # 10.14 形态算法之凸壳
    img = cv2.imread("../images/imgDemo1.png", flags=1)  # flags=1 读取为彩色图像
    imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 灰度图像
    imgBlur = cv2.blur(imgGray, (3, 3))  # 去除噪点
    ret, imgBin = cv2.threshold(imgBlur, 225, 255, cv2.THRESH_BINARY)  # 二值化处理

    img2, contours, hierarchy = cv2.findContours(imgBin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  # 寻找所有的轮廓
    hullAll = []  # 所有的凸包
    for i in range(len(contours)):
        hull = cv2.convexHull(contours[i], False)  # 计算轮廓 contours[i] 的凸包
        hullAll.append(hull)
        
    colorContours = (0, 255, 0)  # 设置轮廓的颜色
    colorConvexHull = (255, 255, 255)  # 设置凸包的颜色
    imgContours = np.zeros(img.shape, np.uint8)
    for i in range(len(contours)):  # 绘制轮廓线
        cv2.drawContours(imgContours, contours, i, colorContours, 2, 8, hierarchy)
    imgDrawing = imgContours.copy()
    for i in range(len(contours)):  # 绘制凸包线
        cv2.drawContours(imgDrawing, hullAll, i, colorConvexHull, 2, 8)

    plt.figure(figsize=(9, 6))
    plt.subplot(221), plt.axis('off'), plt.title("origin")
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.subplot(222), plt.title("binary"), plt.axis('off')
    plt.imshow(imgBin, cmap='gray', vmin=0, vmax=255)
    plt.subplot(223), plt.title("contours"), plt.axis('off')
    plt.imshow(cv2.cvtColor(imgContours, cv2.COLOR_BGR2RGB))
    plt.subplot(224), plt.title("convex hull"), plt.axis('off')
    plt.imshow(cv2.cvtColor(imgDrawing, cv2.COLOR_BGR2RGB))
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)


3.5 细化

细化是将图像的线条从多像素宽度减少到单位像素宽度的过程,也称为"骨架化"、“中轴转换"和"对称轴转换”。

形态骨架(morphological skeleton)是一种细化的结构,指图像的骨骼部分,用于描述物体的几何形状和拓扑结构,是目标物体重要的拓扑描述。图像的细化是对二值图像进行骨架提取,删除不需要的轮廓点,只保留其骨架点。

数学形态学细化算法的主要思想是源于击中击不中变换的形态学算法。在给定一系列具有一定形状的结构元素后,顺序循环地删除满足击中变换的像素。结构元 B 对前景像素集合 A 的细化,可以根据击中-击不中变换来定义:
A ⊗ B = A − ( A ⊛ B ) = A ∩ ( A ⊛ B ) c { B } = { B 1 , B 2 , . . . B n } A ⊗ { B } = ( ( . . . ( ( A ⊗ B 1 ) ⊗ B 2 ) . . . ) ⊗ B n ) A \otimes B = A - (A \circledast B) = A \cap (A \circledast B)^c \\ \{ B \} = \{ B^1, B^2, ...B^n\} \\ A \otimes \{ B \} = ((...((A \otimes B^1)\otimes B^2)...)\otimes B^n) AB=A(AB)=A(AB)c{B}={B1,B2,...Bn}A{B}=((...((AB1)B2)...)Bn)

细化过程就是对图像不断重复地逐层边界像素的过程,目标物体随着细化的进行有规律地缩小,但是目标图像边界线的连接性,方向性和特征点不变,最终使变换图像成为单像素宽的图像骨架。

细化算法应满足以下条件:
(1)将条形区域变成一条细线;
(2)细线应尽可能位于原条形区域的中间;
(3)细化应保持原图像的拓扑特性。

典型的细化算法有 Hilditch 细化算法、Deutch 细化算法、Pavlidis 细化算法、 Zhang 快速并行算法等。

细化可以突出目标的形状特点和拓扑结构,减少冗余的数据和信息,留下足够的有用信息来进行拓扑分析、形状分析或者原始对象的还原。图像细化广泛应用于文字识别、零件形状识别、指纹分类、印刷电路板检测、染色体分析等领域。


例程 10.15:形态算法之细化算法

细化是从原来的图中去掉一些点,但仍然保持原来的形状。判断一个点能否被去掉是基于 8邻域的取值进行判断:内部点不能删除,孤立点不能删除,直线端点不能删除,边界点如果去掉后不增加连通分量则可以删除。

    # 10.15 形态算法之细化算法
    def thinning(image):
        array = [0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, \
                 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, \
                 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, \
                 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, \
                 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
                 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, \
                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
                 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, \
                 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, \
                 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, \
                 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, \
                 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
                 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, \
                 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, \
                 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0]

        h, w = image.shape[0], image.shape[1]
        imgThin = image.copy()
        for i in range(h):
            for j in range(w):
                if image[i, j] == 0:
                    a = np.ones((9,), dtype=np.int)
                    for k in range(3):
                        for l in range(3):
                            if -1<(i-1+k)<h and -1<(j-1+l)<w and imgThin[i-1+k,j-1+l]==0:
                                a[k*3+l] = 0
                    sum = a[0]*1 + a[1]*2 + a[2]*4 + a[3]*8 + a[5]*16 + a[6]*32 + a[7]*64 + a[8]*128
                    imgThin[i, j] = array[sum] * 255
        return imgThin

    # 图像为灰度图像,背景为白色(255),被细化物体为黑色(0)
    image = cv2.imread("../images/imgNetrope.png", flags=0)  # flags=0 灰度图像
    ret, binary = cv2.threshold(image, 205, 255, cv2.THRESH_BINARY)  # 二值化处理

    imgThin = thinning(binary)  # 细化算法

    plt.figure(figsize=(9, 6))
    plt.subplot(131), plt.axis('off'), plt.title("origin")
    plt.imshow(image, cmap='gray', vmin=0, vmax=255)
    plt.subplot(132), plt.title("binary"), plt.axis('off')
    plt.imshow(binary, cmap='gray', vmin=0, vmax=255)
    plt.subplot(133), plt.title("thinnning"), plt.axis('off')
    plt.imshow(imgThin, cmap='gray', vmin=0, vmax=255)
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)


3.6 粗化

粗化是细化的对偶过程,定义为:
A ⊙ B = A ∪ ( A ⊛ B ) A \odot B = A \cup (A \circledast B) AB=A(AB)

通常,先细化集合的背景,然后求集合的补集,就可以得到粗化图像。


3.7 骨架

形态骨架(morphological skeleton)是一种细化的结构,指图像的骨骼部分,用于描述物体的几何形状和拓扑结构,是目标物体重要的拓扑描述。图像的细化是对二值图像进行骨架提取,删除不需要的轮廓点,只保留其骨架点。

骨架可以用腐蚀和开运算来表示,,可以用重建开运算来实现。
S ( A ) = ⋃ k = 0 K S k ( A ) S k ( A ) = ( A ⊖ k B ) − ( A ⊖ k B ) ∘ B S(A) = \bigcup ^K_{k=0} S_k(A)\\ S_k(A) = (A \ominus kB) - (A \ominus kB) \circ B S(A)=k=0KSk(A)Sk(A)=(AkB)(AkB)B

按照以上思路,构造骨骼化算法的步骤:

(1)对图像进行腐蚀,腐蚀后的物体变得更窄细;

(2)对腐蚀后图像做开运算,开运算处理时被删除的像素就是骨骼的一部分,将其加入骨骼图像;

(3)重复以上过程,直到图像被完全腐蚀。

skimage 中提供了函数 skimage.morphology.skeletonize 可以获取图像的骨骼。


例程 10.17:skimage 骨骼化算法

    # # 10.17 形态算法之骨架 (skimage.morphology.skeletonize)
    from skimage import morphology
    imgGray = cv2.imread("../images/handwriting01.png", flags=0)  # flags=0 灰度图像

    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY)  # 二值化处理
    imgBin[imgBin==255] = 1
    skeleton01 = morphology.skeletonize(imgBin)
    skeleton = skeleton01.astype(np.uint8) * 255

    plt.figure(figsize=(8, 6))
    plt.subplot(121), plt.axis('off'), plt.title("origin")
    plt.imshow(imgGray, cmap='gray', vmin=0, vmax=255)
    plt.subplot(122), plt.title("skeleton"), plt.axis('off')
    plt.imshow(skeleton, cmap='gray', vmin=0, vmax=255)
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)


例程 10.18:形态算法之提取骨架

    # 10.18:形态算法之提取骨架 (重建开运算)
    imgGray = cv2.imread("../images/handwriting05.png", flags=0)  # flags=0 灰度图像
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY)  # 二值化处理

    element = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
    skeleton = np.zeros(imgBin.shape, np.uint8)  # 创建空骨架图
    while True:
        imgOpen = cv2.morphologyEx(imgBin, cv2.MORPH_OPEN, element)  # 开运算
        subSkel = cv2.subtract(imgBin, imgOpen)  # 获得骨架子集
        skeleton = cv2.bitwise_or(skeleton, subSkel)  # # 将删除的像素添加到骨架图
        imgBin = cv2.erode(imgBin, element)  # 腐蚀,用于下一次迭代
        if cv2.countNonZero(imgBin) == 0:
            break

    plt.figure(figsize=(9, 6))
    plt.subplot(131), plt.axis('off'), plt.title("origin")
    plt.imshow(imgGray, cmap='gray', vmin=0, vmax=255)
    plt.subplot(132), plt.title("eroded completely"), plt.axis('off')
    plt.imshow(imgBin, cmap='gray', vmin=0, vmax=255)
    plt.subplot(133), plt.title("skeleton"), plt.axis('off')
    plt.imshow(skeleton, cmap='gray', vmin=0, vmax=255)
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)


3.8 提取水平和垂直线

提取图像中的水平线和垂直线,能够筛选出对自己有用的、感兴趣的部分。

腐蚀是用指定的结构元(卷积核)侵蚀图像,结构元确定像素邻域的形状,在该邻域上取最小值;膨胀是用指定的结构元(卷积核)膨胀图像,结构元确定像素邻域的形状,在该邻域上取最大值。

通过自定义的结构元素,使结构元对输入图像的一些对象敏感,而对另一些对象不敏感,就会滤去敏感对象、保留不敏感对象。对于水平或垂直线,可以通过定义水平线或垂直线的结构元素去除水平线或垂直线的干扰,也可以提取水平或垂直线。

也就是说,定义反映水平或垂直线特征的结构元,通过开操作(腐蚀+膨胀)就可以提取图像中的水平与垂直线。


例程 10.19:提取水平和垂直线

    # 10.19:提取水平和垂直线
    imgGray = cv2.imread("../images/imgLine2.png", flags=0)  # flags=0 灰度图像
    ret, imgBin = cv2.threshold(imgGray, 205, 255, cv2.THRESH_BINARY_INV)  # 二值化处理,反白
    h, w = imgBin.shape[0], imgBin.shape[1]

    # 提取水平线
    hline = cv2.getStructuringElement(cv2.MORPH_RECT, ((w//16),1), (-1,-1))  # 水平结构元
    imgOpenHline = cv2.morphologyEx(imgBin, cv2.MORPH_OPEN, hline)  # 开运算提取水平结构
    imgHline = cv2.bitwise_not(imgOpenHline)  # 恢复白色背景

    # 提取垂直线
    vline = cv2.getStructuringElement(cv2.MORPH_RECT, (1,(h//16)), (-1,-1))  # 垂直结构元
    imgOpenVline = cv2.morphologyEx(imgBin, cv2.MORPH_OPEN, vline)  # 开运算提取垂直结构
    imgVline = cv2.bitwise_not(imgOpenVline)

    lineRemoved = imgBin - imgOpenHline  # 删除水平线 (白底为 0)
    lineRemoved = lineRemoved - imgOpenVline  # 删除垂直线
    imgRebuild = cv2.bitwise_not(lineRemoved)  # 恢复白色背景

    plt.figure(figsize=(9, 6))
    plt.subplot(141), plt.axis('off'), plt.title("origin")
    plt.imshow(imgGray, cmap='gray', vmin=0, vmax=255)
    plt.subplot(142), plt.title("horizontal line"), plt.axis('off')
    plt.imshow(imgHline, cmap='gray', vmin=0, vmax=255)
    plt.subplot(143), plt.title("vertical line"), plt.axis('off')
    plt.imshow(imgVline, cmap='gray', vmin=0, vmax=255)
    plt.subplot(144), plt.title("h/v line removed"), plt.axis('off')
    plt.imshow(imgRebuild, cmap='gray', vmin=0, vmax=255)
    plt.tight_layout()
    plt.show()

【youcans 的图像处理学习课】11. 形态学图像处理(中)

版权声明:
youcans@xupt 原创作品,转载必须标注原文链接:(https://blog.csdn.net/youcans/article/details/127133796)
Copyright 2022 youcans, XUPT

欢迎关注 『youcans 的 OpenCV 学习课』 系列,持续更新
文章目录:『youcans 的图像处理学习课 - 总目录』