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
- 效果图

543

被折叠的 条评论
为什么被折叠?



