数字图像处理-10-图像的细化(Zhang-Suen ),粗化以及骨架变换

1.Zhang-Suen 细化算法的逻辑

Zhang-suen细化算法是图像处理领域很经典的骨架提取技术,它的核心是把二值图像中原本较粗的线条或物体,通过迭代的方式,逐步剥离边界像素.最终转化为单像素宽度的骨架(很瘦), 同时最大程度的保留原始结构

1.1迭代机制

算法是通过不断的迭代来删除边界像素,直到没有满足条件的像素可以被删除为止.

  • 第一阶段: 标记并删除图像"东南"方向边界上的像素.
  • 第二阶段: 标记并删除图像"西北"方向边界上的像素

1.2 删除条件

在扫描图像时,算法会检查每一个前景像素点(如下图,当前是P1, 其周围是8邻域为P2-P9).一个像素点想要被安全删除,必须满足下面4个条件

	P9  P2  P3
	P8  P1  P4
	P7  P6  P5
  • 1.邻域像素数量限制:2 ≤ B(P1) ≤ 6
    B(P1) 代表 P1 周围8个邻域像素中,前景像素(非零像素)的总数。这个条件是为了防止孤立点被删除,同时也避免在过度密集的区域破坏整体结构。
  • 2.连通性保持:A(P1) = 1
    A(P1) 代表按顺时针方向(从 P2 到 P9 再回到 P2)遍历邻域时,背景像素(0)到前景像素(1)的跳变次数。等于1说明该像素处于一个单一的连通分支上,删除它不会把线条切断。说白了,就是当前像素在边界上,而且周围不是完全封闭的,删除它不会影响主体.
  • 3.第一阶段特有约束(东南):P2 × P4 × P6 = 0 且 P4 × P6 × P8 = 0
    这组条件确保在第一步中,不会过度侵蚀物体的特定边缘(如端点或拐角),保证骨架的稳定性。
  • 4.第二阶段特有约束(西北):P2 × P4 × P8 = 0 且 P2 × P6 × P8 = 0
    这组条件与第一阶段互补,确保在第二步中从另一个方向安全地剥离像素。

上面第3和4条件,通过简单的乘法逻辑(如 P2×P4×P6=0)来判断特定方向的像素是否为背景,从而决定当前像素是否可以被“侵蚀”.

2.图像的细化

原理原理:
基于 Zhang-Suen 算法扩展至 5×5 邻域:

删除像素的条件(四条全满足时删除):

  • 条件1:8邻域前景数 Num,2 ≤ Num ≤ 6 (排除孤立点和内部实心点,只处理边界像素)
  • 条件2:8邻域顺序排列时 0→1 跳变次数 == 1 (保持连通性,防止断线)
  • 条件3:若上、左、右均为前景,则检查以"上邻居"为中心的
    5×5 子区域连通性,若跳变数 == 1 则保留(不删除)
  • 条件4:若上、左、下均为前景,则检查以"左邻居"为中心的
    5×5 子区域连通性,若跳变数 == 1 则保留

通过全部条件 → 删除(置255);任意条件失败 → 保留(置0)

  • python代码
    包含辅助函数,5x5邻域提取 以及跳变次数记录函数
# ──────────────────────────────────────────────
# 辅助:提取 5×5 邻域矩阵 S
# ──────────────────────────────────────────────
def _get_s(pixels, width, row, col, threshold):
    """
    返回以 (row, col) 为中心的 5×5 邻域值(前景=1,背景=0)。
    S[m][n]:m=0 对应行-2(向上),m=4 对应行+2(向下)。
             n=0 对应列-2(向左),n=4 对应列+2(向右)。
    坐标变换:BMP 底部向上存储,此处改为顶部向下:
      row_offset = m - 2,col_offset = n - 2
    """
    S = [[0] * 5 for _ in range(5)]
    for m in range(5):
        dy = m - 2 #dy取值-2,-1,0,1,2
        for n in range(5):
            dx = n - 2 #dx取值-2,-1,0,1,2
            # 因为上面dy,dx都是以当前元素为重心 四周元素的偏移
            val = pixels[(row + dy) * width + (col + dx)]
            # 超过阈值,置为前景(黑)
            S[m][n] = 0 if val > threshold else 1
    return S
def _transitions(S):
    """
    计算 8 邻域按顺序排列时的 0→1 跳变次数(成环)。
    顺序与:
      S[1][2]→S[1][1]→S[2][1]→S[3][1]→S[3][2]→S[3][3]→S[2][3]→S[1][3]
    对应方向:上→左上→左→左下→下→右下→右→右上
    """
    seq = [
        S[1][2], S[1][1], S[2][1], S[3][1],
        S[3][2], S[3][3], S[2][3], S[1][3],
    ]
    cnt = 0
    for k in range(8):
        if seq[k] == 0 and seq[(k + 1) % 8] == 1:
            cnt += 1
    return cnt

def thin(pixels, width, height):
    """
    细化(Thinning):迭代削减前景边界,直到收敛。
    返回细化后的二值图(前景=0,背景=255)。
    """
    # 首先二值化(以 127 为阈值判断前背景)
    src = _to_binary(pixels, width, height, threshold=127)

    changed = True
    while changed:
        changed = False
        dst = bytearray(b'\xff' * width * height)  # 初始化为背景

        for row in range(2, height - 2):
            for col in range(2, width - 2):
                # 跳过背景像素
                if src[row * width + col] > 127:
                    continue

                S = _get_s(src, width, row, col, threshold=127)

                # 条件1:8邻域前景计数 2≤Num≤6
                num = (S[1][1] + S[1][2] + S[1][3] +
                       S[2][1] +            S[2][3] +
                       S[3][1] + S[3][2] + S[3][3])
                if num < 2 or num > 6:
                    dst[row * width + col] = 0  # 保留
                    continue

                # 条件2:0→1 跳变次数 == 1
                if _transitions(S) != 1:
                    dst[row * width + col] = 0  # 保留
                    continue

                # 条件3:上/左/右均为前景时,检查扩展邻域
                # S[1][2]=上, S[2][1]=左, S[2][3]=右
                if S[1][2] * S[2][1] * S[2][3] != 0:
                    # 以"上邻居" S[1][2] 为中心的 3×3 区域连通性
                    seq3 = [
                        S[0][2], S[0][1], S[1][1], S[2][1],
                        S[2][2], S[2][3], S[1][3], S[0][3],
                    ]
                    cnt3 = sum(
                        1 for k in range(8)
                        if seq3[k] == 0 and seq3[(k + 1) % 8] == 1
                    )
                    if cnt3 == 1:
                        dst[row * width + col] = 0  # 保留
                        continue

                # 条件4:上/左/下均为前景时,检查扩展邻域
                # S[1][2]=上, S[2][1]=左, S[3][2]=下
                if S[1][2] * S[2][1] * S[3][2] != 0:
                    # 以"左邻居" S[2][1] 为中心的 3×3 区域连通性
                    seq4 = [
                        S[1][1], S[1][0], S[2][0], S[3][0],
                        S[3][1], S[3][2], S[2][2], S[1][2],
                    ]
                    cnt4 = sum(
                        1 for k in range(8)
                        if seq4[k] == 0 and seq4[(k + 1) % 8] == 1
                    )
                    if cnt4 == 1:
                        dst[row * width + col] = 0  # 保留
                        continue

                # 四个条件全部通过 → 删除该边界像素
                dst[row * width + col] = 255
                changed = True

        src = dst

    return src
  • 效果图
    在这里插入图片描述

3.图像的粗化

  • 原理(对偶细化):
    粗化 = 对背景进行细化(通过削减背景来扩张前景)

  • 步骤:

    • 对图像取反(前景↔背景)
    • 对取反图像进行细化(细化原背景)
      等价关系:thicken(X) = complement(thin(complement(X)))

注意:结果图像中前景/背景颜色与原图相反

def thicken(pixels, width, height):
    """
    粗化:取反后细化,扩张前景区域。
    """
    # 步骤①:取反(像素逐字节翻转)
    inverted = bytearray(width * height)
    for i in range(width * height):
        inverted[i] = 0 if pixels[i] > 127 else 255

    # 步骤②:对取反后图像做细化
    return thin(inverted, width, height)
  • 效果图
    在这里插入图片描述

4.图像的骨架变换

原理(标准 Zhang-Suen 细化算法的双阶段形式):

每轮迭代包含两个阶段,均从同一幅源图读取:

  • 阶段1(阈值127):若满足删除条件且不满足保留条件1-3、1-4则删除
    • 保留条件1-3:S[上]*S[左]*S[下] ≠ 0(北西南全为前景)
    • 保留条件1-4:S[左]*S[下]*S[右] ≠ 0(西南东全为前景)
  • 阶段2(阈值200,S的计算使用更宽松的前景判断):
    • 保留条件2-3:S[上]*S[左]*S[右] ≠ 0(北西东全为前景)
    • 保留条件2-4:S[上]*S[下]*S[右] ≠ 0(北南东全为前景)

阶段2 结果覆盖阶段1(阶段2 对暗像素全部重新判断并写入)
仅当阶段2 删除像素时才标记 changed=True

与 thin() 的区别:

  • 不使用 5×5 扩展子区域连通性检查(条件更简洁)
  • 使用双阈值方案,结果更接近数学中轴
def skeleton(pixels, width, height):
    """
    骨架变换(中轴变换):提取图像的最细骨干线。
    """
    src = _to_binary(pixels, width, height, threshold=127)

    changed = True
    while changed:
        changed = False
        dst = bytearray(b'\xff' * width * height)

        # ---- 阶段1:阈值127,条件1-3/1-4 ----
        for row in range(2, height - 2):
            for col in range(2, width - 2):
                if src[row * width + col] > 127:
                    continue  # 背景跳过,dst 保持 255

                S = _get_s(src, width, row, col, threshold=127)

                # 条件1-1:8邻域计数
                num = (S[1][1] + S[1][2] + S[1][3] +
                       S[2][1] +            S[2][3] +
                       S[3][1] + S[3][2] + S[3][3])
                if num < 2 or num > 6:
                    dst[row * width + col] = 0; continue

                # 条件1-2:跳变数
                if _transitions(S) != 1:
                    dst[row * width + col] = 0; continue

                # 条件1-3:北/西/南均为前景 → 保留
                if S[1][2] * S[2][1] * S[3][2] != 0:
                    dst[row * width + col] = 0; continue

                # 条件1-4:西/南/东均为前景 → 保留
                if S[2][1] * S[3][2] * S[2][3] != 0:
                    dst[row * width + col] = 0; continue

                # 删除
                dst[row * width + col] = 255

        # ---- 阶段2:阈值200(更宽松的前景判断),条件2-3/2-4 ----
        # 同样从 src(本轮源图)读取,结果覆盖阶段1
        for row in range(2, height - 2):
            for col in range(2, width - 2):
                if src[row * width + col] > 127:
                    continue  # 背景:dst 保留阶段1 的值(255)

                # 注意:阈值换为 200,对邻域使用更宽松的前景判定
                S = _get_s(src, width, row, col, threshold=200)

                # 条件2-1:计数(同阶段1)
                num = (S[1][1] + S[1][2] + S[1][3] +
                       S[2][1] +            S[2][3] +
                       S[3][1] + S[3][2] + S[3][3])
                if num < 2 or num > 6:
                    dst[row * width + col] = 0; continue

                # 条件2-2:跳变数
                if _transitions(S) != 1:
                    dst[row * width + col] = 0; continue

                # 条件2-3:北/西/东均为前景 → 保留
                if S[1][2] * S[2][1] * S[2][3] != 0:
                    dst[row * width + col] = 0; continue

                # 条件2-4:北/南/东均为前景 → 保留
                if S[1][2] * S[3][2] * S[2][3] != 0:
                    dst[row * width + col] = 0; continue

                # 删除(标记本轮有变化,以便继续迭代)
                dst[row * width + col] = 255
                changed = True

        src = dst

    return src

  • 效果图
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值