3.3 銳化與非線性濾波#
概念說明#
銳化 (sharpening) 與平滑相反:增強快速變化的細節(邊緣、紋理),讓影像看起來更清晰。關鍵工具是 Laplacian——二階偏微分的離散版本。對一維訊號 f,二階微分在「變化率改變」的地方(邊緣兩側)有劇烈響應。常用的離散 Laplacian 核心有兩種:
[[0, 1, 0], [1, −4, 1], [0, 1, 0]] 或含對角的 [[1,1,1],[1,−8,1],[1,1,1]]
Laplacian 響應的特徵:在均勻區域為 0,在邊緣兩側一正一負、中心過零。直接把 Laplacian 疊回原影像即得銳化:
sharpened = I + c · ∇²I
等價的核心即「中心權重 5、四鄰居 −1」(c=1 時)。c 越大銳化越強;c 太大會在強邊緣附近產生光暈 (halo) 與雜訊放大。影像處理軟體的「unsharp mask(遮蔽銳化)」是同一件事的變形:blur = 高斯模糊(影像);sharpened = 影像 + 量·(影像 − blur),其中「影像 − blur」就是細節層。
非線性濾波不遵守「加權求和」的形式。最重要的是中值濾波 (median filter):取鄰域所有像素排序後的中位數當輸出。它的殺手級應用是脈衝雜訊(椒鹽雜訊)——黑白極端值的孤立點。線性濾波(含高斯)遇到椒鹽雜訊只是把一個極端值攤成一小片灰暈;中值濾波則因極端值永遠排在序列兩端,直接被中位數淘汰,雜訊完全消失,而且邊緣不會被模糊(中位數不會產生原本不存在的灰階)。其他非線性濾波還有最大/最小濾波(形態學的雛形,見第 8 章)與眾數濾波。
線性 vs 非線性的選擇:高斯雜訊(每個像素都偏一點)用線性平均有效,因為平均能讓隨機誤差互相抵消;椒鹽雜訊(少數像素全錯)用中值有效,因為中位數對離群值穩健 (robust)。統計上:平均數對離群值敏感、中位數穩健——濾波器的選擇就是統計估計量的選擇。
演算法步驟#
中值濾波: 1. 選奇數尺寸 k(如 3×3)。 2. 對每個像素,取出 k×k 鄰域(含邊界填充)。 3. 排序後取中位數寫入輸出。
銳化(unsharp mask): 1. blur = GaussianBlur(影像, σ)。 2. detail = 影像 − blur。 3. 輸出 = clip(影像 + 量 × detail)。
Python 程式碼範例#
import numpy as np
from PIL import Image
rng = np.random.default_rng(2)
img = np.full((60, 60), 100.0)
img[:, 30:] = 180 # 一條垂直邊緣
img[10:20, 10:20] = 0 # 一塊暗雜訊方塊(模擬椒雜訊)
img[40:50, 40:50] = 255
noisy = img.copy()
salt = rng.random(img.shape) < 0.05 # 5% 白點
pepper = rng.random(img.shape) < 0.05 # 5% 黑點
noisy[salt] = 255
noisy[pepper] = 0
print("加入椒鹽雜訊的像素數:", salt.sum() + pepper.sum())
def median_filter(img, k=3):
r = k // 2
P = np.pad(img.astype(np.float64), r, mode="edge")
H, W = img.shape
out = np.zeros((H, W))
for y in range(H):
for x in range(W):
out[y, x] = np.median(P[y:y+k, x:x+k])
return out
def gaussian1d(k, sigma):
x = np.arange(k) - k//2
g = np.exp(-x**2/(2*sigma**2)); return g/g.sum()
def blur(img, k=5, sigma=1.0):
g = gaussian1d(k, sigma)
t = np.apply_along_axis(lambda r: np.convolve(r, g, "same"), 1, img)
return np.apply_along_axis(lambda c: np.convolve(c, g, "same"), 0, t)
med = median_filter(noisy)
gau = blur(noisy)
print("雜訊後錯誤像素數:", (np.abs(noisy - img) > 50).sum())
print("中值濾波後錯誤像素數:", (np.abs(med - img) > 50).sum())
print("高斯濾波後錯誤像素數:", (np.abs(gau - img) > 50).sum())
# 銳化:對模糊邊緣影像做 unsharp mask
soft = blur(img, 7, 1.5)
detail = soft - blur(soft, 7, 1.5)
sharp = np.clip(soft + 1.5 * detail, 0, 255)
print("模糊邊緣剖面:", soft[30, 26:35].round(1))
print("銳化邊緣剖面:", sharp[30, 26:35].round(1))
執行後:高斯濾波後仍有數百個錯誤像素(雜訊被攤開成灰暈),中值濾波後趨近 0(雜訊全滅且邊緣剖面保持陡峭);銳化剖面顯示原本平緩的 4~5 像素過渡被壓縮,且兩側出現輕微過衝 (overshoot)——這正是視覺上「更銳利」的來源。
複雜度分析#
中值濾波:樸素做法每個鄰域排序 O(k² log k²),總計 O(H·W·k² log k)。用滑動直方圖 (Huang's algorithm) 可達 O(H·W),因為視窗滑動一格只需更新直方圖並移動中位數指標。銳化 = 一次模糊 + 逐像素加減,O(H·W·k)。空間均為 O(H·W)。注意中值濾波結果雖然抗離群值,但不改變直方圖排序結構以外資訊,重複套用會收斂到「根訊號」(root signal),過度使用會產生斑塊狀的外觀。
練習#
- 對同一張椒鹽雜訊影像分別做 3×3 中值兩次與 5×5 中值一次,比較雜訊清除率與細節保留,說明哪個策略較好。
- 實作 unsharp mask 函式,參數 (σ, 量),對一張照片縮圖測試 σ=1/量=1.5 與 σ=2/量=0.8 的視覺與數值差異(觀察邊緣剖面的過衝高度)。
- 5% 椒鹽雜訊下,理論上一個 3×3 視窗「過半是雜訊」的機率有多少(二項分布)?這解釋了雜訊密度多高時中值濾波開始失效。