8.1 侵蝕膨脹與形態學運算#
概念說明#
形態學 (morphology) 把二值影像 A 視為集合(值為 1 的像素座標的集合),用一個小集合結構元素 B(通常 3×3 或 5×5 的方形/十字/圓盤)探測它。兩個基本運算:
侵蝕 (erosion) A ⊖ B:結構元素中心放在 z 時,若 B 完全落在 A 內則輸出 1。
A ⊖ B = { z | B_z ⊆ A }
效果:白色區域縮小。邊界剝掉一圈;比結構元素小的白區整個消失;細線被切斷。用途:去除小白雜點、分離相連的物件、收縮過粗的線條。
膨脹 (dilation) A ⊕ B:只要 B 與 A 有任何接觸就輸出 1。
A ⊕ B = { z | (B̂)_z ∩ A ≠ ∅ }
效果:白色區域擴大。邊界外擴一圈;小黑孔被填掉;斷開的線被接上。用途:填補小洞、連接斷線、擴張種子區域。
兩者都不是可逆的,但可以複合成四個標準運算:
- 開運算 (opening) A ∘ B = (A ⊖ B) ⊕ B:先蝕後脹。去除小白點與突刺,大結構大小基本不變——像用 B 把輪廓「刷」圓滑。
- 閉運算 (closing) A • B = (A ⊕ B) ⊖ B:先脹後蝕。填補小黑洞與窄縫,大結構不變。
- 形態學梯度 (A ⊕ B) − (A ⊖ B):膨脹與侵蝕的差 = 區域外擴減內縮 = 邊界帶,是粗但封閉的邊緣。
- 頂帽 / 黑帽:原圖減開運算(比結構元素小的亮細節)、閉運算減原圖(暗細節)——灰階形態學用於不均勻背景的提取。
對偶性:侵蝕與膨脹互為補集運算:¬(A ⊕ B) = (¬A) ⊖ B̂。膨脹白區 = 侵蝕黑區。
灰階推廣:侵蝕 = 鄰域最小值濾波,膨脹 = 鄰域最大值濾波(3.3 節提過的最大/最小濾波就是它們),開/閉運算變成「滑動最大後滑動最小」。這讓形態學能處理灰階影像的背景不平問題。
結構元素的形狀有語義:方形適合網格狀結構、圓盤不偏向任何方向、十字形連通性溫和。尺寸決定「什麼算小、什麼算大」——半徑 r 的圓盤會消滅所有厚度小於 2r 的結構,這是形態學作為「尺度濾波器」的直覺。
演算法步驟#
- 定義結構元素 B(形狀與尺寸)。
- 侵蝕:對每個前景像素,檢查 B 覆蓋處是否全為 1;不是則設 0。
- 膨脹:對每個背景位置,檢查 B 覆蓋處是否有任一 1;有則設 1。
- 開/閉運算:按順序複合兩次運算。
- (灰階)用滑動 min/max 實現。
Python 程式碼範例#
import numpy as np
def erode(A, B):
r = B.shape[0]//2
P = np.pad(A, r, constant_values=0)
H, W = A.shape
out = np.ones_like(A)
for dy in range(-r, r+1):
for dx in range(-r, r+1):
if B[dy+r, dx+r]:
out &= P[r+dy:r+dy+H, r+dx:r+dx+W]
return out
def dilate(A, B):
r = B.shape[0]//2
P = np.pad(A, r, constant_values=0)
H, W = A.shape
out = np.zeros_like(A)
for dy in range(-r, r+1):
for dx in range(-r, r+1):
if B[dy+r, dx+r]:
out |= P[r+dy:r+dy+H, r+dx:r+dx+W]
return out
B = np.ones((3, 3), np.uint8) # 3×3 方形結構元素
# 測試影像:大方塊 + 一個 2×2 白雜點 + 一個 3×3 黑洞
A = np.zeros((20, 20), np.uint8)
A[4:16, 4:16] = 1
A[2:4, 2:4] = 1 # 小白雜點
A[9:11, 9:11] = 0 # 方塊內小黑洞
opened = dilate(erode(A, B), B)
closed = erode(dilate(A, B), B)
grad = dilate(A, B).astype(int) - erode(A, B).astype(int)
print("原圖白像素:", A.sum(), "(含雜點 4 個)")
print("開運算後:", opened.sum(), "(雜點消失)")
print("雜點是否被移除:", opened[2,2] == 0)
print("黑洞是否被填補:", closed[9,9] == 1, ",大結構仍在:", closed.sum())
print("形態學梯度邊界像素數:", int((grad == 1).sum()))
# 侵蝕對「分離相連物件」的作用
two = np.zeros((20, 20), np.uint8)
two[5:9, 4:8] = 1; two[5:9, 9:13] = 1; two[5:9, 8, ] = 1 # 兩塊由 1px 相連
print("相連物件侵蝕 3×3 後連通塊數變化:兩塊分開 =",
erode(two, B)[5, 6] == 0 and erode(two, B)[5, 11] == 1)
執行後:開運算精準移除了 2×2 雜點而大方塊幾乎原封不動;閉運算填掉了 2×2 黑洞;形態學梯度的非零像素正好是方塊的邊界環。最後一項顯示侵蝕能把僅 1 像素相連的兩個物件分開——這是連通標記(下一節)的前置處理。
複雜度分析#
樸素實作:每像素檢查 k×k 結構元素,O(N·k²)。兩個加速:(1) 分解——方形結構元素可分解成兩次 1×k,O(N·k);(2) van Herk/Gil-Werman 滑動視窗 min/max 演算法每列只需 3 次比較/像素,與 k 無關 O(N)。開/閉 = 兩次基本運算,同階。空間 O(N)。形態學的魅力在於:與卷積同樣的框架、不同的代數(Boolean/格論 instead of 線性),因此它天然並行、可分解、可組合,且結果可預測(可證明開運算具冪等性:開兩次 = 開一次)。
練習#
- 實作 5×5 圓盤結構元素,對同一張影像分別用方形與圓盤做開運算,比較對斜線邊緣的影響(哪個保留得更完整?)。
- 對一張被不均勻光照污染的文件二值圖,用「頂帽運算 + 開運算」流程還原文字,量化處理前後的連通字元數。
- 證明開運算的冪等性 (A∘B)∘B = A∘B,並用程式對隨機二值影像驗證。