跳轉到

9.3 模板比對與霍夫轉換#

概念說明#

本節介紹兩個解決「全局幾何搜尋」的經典方法,它們的搜尋對象不同:模板比對找「一小塊影像」,霍夫轉換找「一組滿足參數方程的像素」。

模板比對 (template matching):給定小圖(模板 T,如 w×h),在目標影像 I 上滑動,計算每個位置的相似度,峰值即模板位置。最常用的指標是正規化互相關 (NCC):

NCC(x, y) = Σ T(x',y')·I(x+x', y+y') / √(Σ T² · Σ I²)

取值 [−1, 1],1 表示完全相同。正規化的好處是對亮度與對比的線性變化不敏感。另一常見選擇是平方差之和 SSD(越小越好)。模板比對的弱點:對旋轉、尺度、非線性光照敏感——模板稍微轉個 5° 匹配就崩壞;此時應改用特徵匹配(上一節)。它的強項是簡單、精確、可解釋,適合受控環境(工業檢測、UI 元素定位、固定場景的目標追蹤初定位)。

霍夫轉換 (Hough Transform, 1962/1972) 解決的問題:第 4 章的邊緣偵測給出一堆離散的邊緣點,哪些點構成一條直線?直接枚舉點對組合是指數級的。霍夫的洞察是對偶性:影像空間的一條直線 y = mx + b 對應參數空間 (m, b) 的一個點;而影像空間的一個點 (x₀, y₀) 對應參數空間的一條線 b = −x₀m + y₀。共線的點 → 參數空間的線相交於同一點。於是演算法變成:每個邊緣點在參數空間「投票」給所有過它的直線,得票最高的參數就是影像中的直線。

實務用極座標表示避免垂直線的 m = ∞ 問題:

ρ = x·cosθ + y·sinθ

參數空間是 (ρ, θ) 的二維累加器(θ 離散成 180 格,ρ 離散成影像對角線長度格)。每個邊緣點沿 θ 掃 180 次,各投一票。峰值搜尋(含非極大值抑制)輸出直線清單。推廣:圓 Hough(三參數 (a, b, r),用梯度方向降維加速)、廣義霍夫 (GHT,任意形狀用 R-table)。霍夫對雜訊與遮擋魯棒(斷開的虛線也能投出峰值),但累加器維度隨參數數量爆炸,三參數以上就昂貴。

演算法步驟#

模板比對:1. 模板與影像皆減去均值、除以標準差(正規化)。2. 滑動窗口逐位置算 NCC。3. 找全域最大值(可設門檻取多個)。

直線霍夫:1. 邊緣偵測 + 門檻化得邊緣點集。2. 建立累加器 A[θ][ρ] 歸零。3. 每個邊緣點對每個 θ 計算 ρ 並累加。4. 找 A 的局部峰值(超過票數門檻的極大)。5. 把 (ρ, θ) 轉回直線,可沿影像邊界截取線段。

Python 程式碼範例#

import numpy as np

# ---- 模板比對 ----
scene = np.zeros((50, 50))
scene[20:30, 30:40] = 100          # 目標:10×10 亮方塊
scene[5:12, 5:12] = 50             # 干擾:較暗方塊
tmpl = np.full((10, 10), 100.0)

def ncc(I, T):
    H, W = I.shape; h, w = T.shape
    Tz = T - T.mean()
    Tn = np.sqrt((Tz**2).sum())
    best = np.zeros((H-h+1, W-w+1))
    for y in range(H-h+1):
        for x in range(W-w+1):
            P = I[y:y+h, x:x+w]
            Pz = P - P.mean()
            best[y, x] = (Tz*Pz).sum() / (Tn*np.sqrt((Pz**2).sum()) + 1e-9)
    return best

score = ncc(scene, tmpl)
y, x = np.unravel_index(score.argmax(), score.shape)
print(f"模板比對最佳位置: ({x}, {y}), NCC = {score.max():.3f}(真實 (30,20))")
print("干擾方塊處 NCC =", score[5, 5].round(3))

# ---- 直線霍夫 ----
# 建立含兩條直線的邊緣圖
edge = np.zeros((60, 60), bool)
edge[15, 5:55] = True                    # 水平線 y=15
for i in range(40):                      # 45° 斜線
    edge[i, i+15] = True
edge[40, 45] = True                      # 雜訊點

H, W = edge.shape
theta_bins, rho_max = 180, int(np.hypot(H, W))
A = np.zeros((2*rho_max, theta_bins))
ys, xs = np.where(edge)
for theta_deg in range(theta_bins):
    t = np.radians(theta_deg)
    rho = (xs*np.cos(t) + ys*np.sin(t)).astype(int) + rho_max
    np.add.at(A[:, theta_deg], rho, 1)

# 找峰值(票數 >= 35 的極大)
votes = A.max(axis=1) - 0
peaks = []
for i in range(2*rho_max):
    for j in range(theta_bins):
        if A[i, j] >= 35 and A[i, j] == A[max(0,i-2):i+3, max(0,j-3):j+3].max():
            peaks.append((A[i, j], i - rho_max, j))
peaks.sort(reverse=True)
print("霍夫偵測到的直線 (票數, ρ, θ°):", [(int(v), r, t) for v, r, t in peaks[:4]])
print("理論: 水平線 y=15 → ρ=15, θ=90°;斜線 → ρ≈10.6, θ=135°")

執行後:NCC 在 (30, 20) 得 1.000(亮方塊與模板完全一致),暗方塊處因正規化掉到接近 0 甚至負值(模板比內容亮);霍夫的兩條真直線得到約 50 與 40 票的明顯峰值,單一雜訊點最多只投 180 個不同 (ρ,θ) 中每格 1 票、達不到門檻——「投票門檻」就是霍夫的抗雜訊機制。

複雜度分析#

模板比對:每位置 w·h 次乘加,O((W−w+1)(H−h+1)·w·h)——對 1000×1000 影像配 100×100 模板約 10¹⁰ 次運算,樸素實作很慢。加速:FFT 版互相關(卷積定理,第 5 章)把相關項降到 O(N log N);積分圖把 ΣI、ΣI² 變 O(1),NCC 的分母部分免重算。霍夫:邊緣點數 E,每點 θ_bins 次投票,O(E·180),加峰值搜尋 O(acc)。累加器空間 O(ρ·θ)(直線)但圓霍夫是 O(W·H·r) 級的三維——參數維度是指數負擔,這是霍夫的死穴。模板比對輸出「位置」,霍夫輸出「幾何參數」;當目標形狀可參數化且可能有遮擋時,霍夫幾乎是唯一解。

練習#

  1. 把模板旋轉 10° 後再做 NCC 比對,量化峰值下降程度;再對同一場景用 9.2 節的特徵匹配方法定位目標,比較兩者的旋轉容忍度。
  2. 實作 FFT 加速的互相關,測量與樸素版本在 512×512 影像、64×64 模板下的執行時間比。
  3. 對本節邊緣圖加入 100 個隨機雜訊點,測試霍夫票數門檻 30/40/50 下偵測到的假直線數,找出抗雜訊與漏檢的平衡點。