2.2 邊緣偵測:Sobel、Laplacian 與 Canny 算子(1968-1986)#
2.2.1 為什麼要找邊緣#
第 2.1 節我們把影像「抹平」,本節要做相反的事:把變化劇烈的地方挑出來。但挑出來之後呢?這是本節真正想回答的問題。
邊緣(edge)在影像分析中扮演三個角色:
- 從像素到形狀的跳板。一條邊界把影像分割成兩個區域,因此邊緣是分割(segmentation)問題的自然起點;第 1.3 節的譜系裡,分割方法正是從邊緣偵測演化而來。
- 幾何資訊的載體。邊緣的位置與方向包含了物體的形狀;由邊緣可以重建輪廓,進而做形狀分析(第 3 章的 HOG 正是把局部梯度統計化的產物)。
- 幾何驗證的依據。第 2.3 節的特徵匹配需要「兩張圖的對應點」,而邊緣是尋找對應點最直觀的來源——把兩張不同角度拍攝的建築照片的邊緣疊在一起,就能看出單應性矩陣。
但「邊緣」這個詞也隱藏著一個陷阱:邊界不只來自物體,也來自雜訊、紋理、陰影、反射。一塊磚牆有幾百條邊緣,一張人臉照片卻可能只有幾十條有意義的邊緣。因此邊緣偵測的真正難題不是「找出變化」,而是「分辨哪些變化值得保留」。Canny 演算法的全部設計,都是圍繞這個問題。
一個常見誤解是「邊緣偵測就是求梯度」。求梯度只是第一步。Canny 的五個步驟裡,有三個步驟(抑制、精細化、遲滯)都是為了處理「梯度大不等於是邊緣」這個問題。
flowchart TD
A["原始影像"] --> B["步驟一: 高斯平滑"]
B --> C["步驟二: 梯度幅值與方向"]
C --> D["步驟三: 非極大值抑制"]
D --> E["步驟四: 雙閾值與遲滯"]
E --> F["步驟五: 邊緣追蹤與連通性"]
F --> G["稀疏的單像素邊緣圖"]
G --> H["分割 / 輪廓 / 特徵點候選"]2.2.2 梯度:定義、幅值與方向#
影像的一階導數#
對連續函數影像 \(I(x, y)\) ,梯度是偏導數組成的向量:
梯度具有兩個關鍵性質:
- 方向性: \(\nabla I\) 指向 \(I\) 增長最快的方向,也就是垂直於等值線(iso-intensity contour),因此垂直於邊界。
- 大小: \(\lVert \nabla I \rVert\) 度量變化速率,是「這裡有邊界嗎、邊界有多強」的量化。
常用的幅值定義有兩種:
第一種是歐氏長度(真實的變化率),第二種是曼哈頓距離(計算較快,是 Canny 原論文採用的近似)。兩者相差最多 \(\sqrt{2}\) 倍,在實務中影響不大。
方向由 \(\nabla I\) 的方向角給出:
邊緣有正反兩種梯度#
這是初學者最常混淆的地方。一條從暗到亮的垂直邊界(例如物體的左輪廓),在物體內側梯度指向右(正的 \(I_x\) ),在物體外側梯度指向左(負的 \(I_x\) )。也就是說同一條邊界在像素網格上佔兩列,兩列的梯度方向相差 180 度。
這帶來兩個實際問題:
- 邊緣位置有兩像素的模糊性。哪一列才算「邊界」?這正是 Canny 的非極大值抑制要解決的問題——它把邊界收斂到單一像素寬,方法是在梯度的垂直方向上找局部極大值。
- 做方向統計時必須考慮循環性。第 3.1 節的 HOG 方向直方圖把方向視為循環變數(0 度與 180 度是不同方向,但 359 度與 1 度幾乎相同),因此在累積時要做插值。第 2.3 節的 SIFT 描述子在把 36 個方向槽摺疊成 8 個時也要處理這個循環性。
離散化的取樣誤差#
直接用中心差分近似偏導數會有 \(O(\Delta^2)\) 的截斷誤差,且對高頻雜訊非常敏感(相鄰像素相減,雜訊被放大 2 倍)。因此實務上一律先平滑再差分——這是下一節 Sobel 與 Prewitt 的出發點。
2.2.3 Sobel 與 Prewitt:平滑與差分的組合#
為什麼要「先平滑再差分」#
考慮最簡單的中心差分:
若影像含有獨立的高斯雜訊,輸出雜訊標準差為
也就是雜訊被放大了(原本是 \(\sigma_n\) ,現在是 \(0.707 \sigma_n\) 的信號衰減加上差分的放大)。更糟的是差分會把雜訊變成高頻成分,邊緣偵測正好要放大高頻。
解法是把平滑器與差分器串聯成一個核。對核做卷積,等價於先平滑再差分,也等價於對平滑後影像的導數做運算——這是線性系統的結合律的直接應用。
Prewitt 與 Sobel 的核#
Prewitt 核(1968):
Sobel 核(1968, Irwin Sobel):
兩者的差別只在平滑方向上的權重分配:Prewitt 用等權(1,1,1),Sobel 用二項式權重(1,2,1)。Sobel 的平滑效果較好,因此抗雜訊能力較強,是實務上的預設選擇。
Sobel 的可分離性#
Sobel 核可以分解為一維平滑核與一維差分核的外積:
因此可以先做一維平滑再一維差分,每像素只需 \(2 \times 3 \times 2 = 12\) 次乘加,而非 18 次。OpenCV 的 cv2.Sobel 內部即利用此性質。
注意 \(G_y = G_x^{\mathsf{T}}\) 這個對稱性不是巧合,而是所有「等向性可分離核」的共同性質:對稱的平滑器搭配對稱的差分器,交換 \(x\) 與 \(y\) 就得到同樣的響應。
Sobel 與高斯導數:尺度可調的邊緣偵測#
這是本節最重要的一個連結。Sobel 核其實是**二維高斯偏導數 \(\partial G / \partial x\) ** 的離散近似。高斯函數的一階偏導為
它是一個「奇對稱」的 bell 形(左右異號、中間為零)。取 \(\sigma \approx 1\) 並在 \(\{-1, 0, 1\}\) 上取樣再正規化,就得到 Sobel 核的形狀。
這個觀察帶來一個重要的一般化:把 Sobel 換成任意尺度的高斯導數,就得到尺度可調的邊緣偵測器:
\(\sigma\) 的意義:
| \(\sigma\) | 效果 |
|---|---|
| 小(0.5) | 只偵測細緻結構,對雜訊與紋理極敏感,邊緣位置偏細碎 |
| 中(1.0 - 1.5) | 標準取用,Canny 預設 |
| 大(2.0 以上) | 只留下粗壯結構,細節邊緣消失,但抗雜訊與輕微模糊的能力強 |
這就是「多解析度邊緣偵測」的雛形,也是第 2.3 節尺度空間理論的直接前身。邊緣偵測器的 \(\sigma\) 選擇,本質上是在問「我想看見多細的結構」。
常見誤解:「Sobel 是邊緣偵測,Laplacian 是邊緣偵測,所以可以互相替代。」不能。Sobel 是一階導數,給出的是「邊界在哪裡、往哪個方向、對比多強」;Laplacian 是二階導數,給出的是「邊界的中點在哪裡」(零交叉),對階梯邊界更精確,但對噪聲更敏感、且無法給出方向。
2.2.4 Laplacian 與 DoG:零交叉與尺度空間#
二階導數與零交叉#
Laplacian 算子為
其離散近似常用的兩個版本:
( \(L_8\) 的四鄰角項是為了讓八方向等向; \(L_4\) 只用四鄰居。)
對一個理想階梯邊界(過渡寬度為零),一階導數在邊界處是一個尖峰,二階導數則是兩個正負相鄰的尖峰,而 Laplacian 的響應在邊界中點穿過零。因此偵測邊界的規則變成:找 Laplacian 響應的零交叉(zero crossing)。
零交叉比「取最大值」更好,因為最大值的位置取決於邊界的過渡形狀(是對稱過渡還是拖尾過渡),而零交叉的物理意義更明確:它是灰階階梯的中點。
Marr-Hildreth 與尺度空間理論(1980)#
Marr 與 Hildreth 的論點是:直接對雜訊影像求零交叉是不可靠的,必須先平滑。 而且,平滑的程度不應是任意的——它應該由影像本身的內容決定。
「由內容決定平滑程度」的想法導致了尺度空間理論的核心命題:
在所有尺度上平滑過的影像所形成的函數族(尺度空間),其零交叉結構(即邊界的位置與型態)隨尺度的變化是連續且有規律的;邊界在特徵尺度(characteristic scale)上會從邊界「分裂」成兩條零交叉——一條在物體內、一條在物體外,中間夾著一個高斯隆起。
Marr-Hildreth 因此提出 LoG(Laplacian of Gaussian):把高斯平滑與 Laplacian 結合為單一核
即「先平滑再看零交叉」。 \(\sigma\) 就是我們選擇的尺度;LoG 核的形狀是一個「墨西哥帽」(正中央為負、外圍為正環)。
Marr-Hildreth 的三個貢獻:
- 零交叉作為邊界偵測的原則,並證明它是唯一對加性雜訊有合理響應的邊界偵測算子(在某個「平均」意義下)。
- 尺度空間框架:邊界偵測必須是多尺度的,尺度參數是演算法的一部分而不是超參數的細節。
- 預測性編碼的雛形:大腦視覺皮層的處理被比喻為「在不同尺度上建立影像的描述」,這是 1980 年代神經網路與視覺生理學交會的起點。
DoG:LoG 的高效近似#
直接計算 LoG 核的成本很高,且 LoG 本身是高斯對數的導數,數值上在遠處有振盪。Lowe(1999)在 SIFT 中指出:高斯差分(Difference of Gaussians) 是 LoG 的良好近似。
定義
兩者近似的理由來自高斯的尺度導數恆等式:
對尺度做有限差分:
也就是說 DoG 就是尺度差分,而尺度差分正比於 Laplacian。DoG 還有兩個額外好處:可以透過高斯金字塔的相鄰層相減來計算(等價於拉普拉斯金字塔,複雜度 \(O(N)\) ),以及數值上比 LoG 更穩定(LoG 在遠場有 \(1/r^2\) 的振盪尾巴)。
DoG 將在第 2.3 節成為 SIFT 尺度空間的基礎。
flowchart LR
A["LoG 核"] --> B["直接卷積: 昂貴"]
C["高斯金字塔"] --> D["相鄰層相減"]
D --> E["DoG: 便宜且穩定"]
E --> F["SIFT 的尺度空間極值偵測"]
A --> F2.2.5 Canny 邊緣偵測(1986)#
John Canny 在 1986 年的論文中提出三項判準,用來評價一個邊緣偵測器:
- 正確性:偵測到的邊界應與真實邊界相符,且不應有重複的回應(每條邊界只被報一次)。
- 定位性:偵測到的邊界位置應盡可能接近真實邊界。
- 單邊性:對一條邊界,反應應是單一的局部極大(而不是一串極大)。
這三項判準直接導出了 Canny 的演算法設計。
步驟一:高斯平滑#
以 \(G_\sigma * I\) 平滑原影像, \(\sigma\) 通常取 1.0 到 1.5。理由是第 2.1 節的偏差—變異取捨:邊界偵測是差分運算,會放大雜訊,所以必須先以適當的平滑把雜訊壓到可接受水準。Canny 在論文裡明確指出,平滑是必要的,但過度平滑會破壞定位性——這是 \(\sigma\) 的主要意義。
步驟二:梯度幅值與方向#
以 Sobel 核(或高斯導數核)計算 \(I_x\) 與 \(I_y\) ,然後
或用曼哈頓近似 \(M = \lvert I_x \rvert + \lvert I_y \rvert\) 。方向為
重要實務細節:直接對原始強度求梯度會使邊界偵測對亮度偏移與對比縮放敏感( \(I \to aI + b\) 時梯度變成 \(a \nabla I\) )。因此實務上常先把影像正規化(例如除以 255,或做局部正規化),或使用已正規化的 Sobel 核(除以 8)。
步驟三:非極大值抑制(Non-Maximum Suppression)#
NMS 解決「一條邊界被報成兩條」的問題。做法:沿著梯度的方向,只保留梯度幅值的局部極大。
因為梯度方向是連續的( \(-180^\circ\) 到 \(180^\circ\) ),實作上把它四捨五入到最近的 45 度倍數(0 度、45 度、90 度、135 度),然後沿著該方向比較相鄰的兩個像素。
| 四捨五入後的方向 | 沿梯度方向的兩個比較對象 |
|---|---|
| 0 度 | \((x-1, y)\) 與 \((x+1, y)\) |
| 45 度 | \((x-1, y-1)\) 與 \((x+1, y+1)\) |
| 90 度 | \((x, y-1)\) 與 \((x, y+1)\) |
| 135 度 | \((x-1, y+1)\) 與 \((x+1, y-1)\) |
由於真正的梯度方向不是精確的 45 度倍數,被比較的相鄰像素未必正好落在梯度線上,此時就要用線性插值估計梯度線上的幅值。這是 Canny 演算法中最容易被忽略、卻直接影響邊緣品質的細節。
以 0 度方向(梯度沿 \(x\) 軸)為例,梯度線通過 \((x, y)\) 、斜率為 \(\tan\theta\) 。它穿過第 \(x-1\) 欄的列座標為 \(y - \tan\theta\) ,穿過第 \(x+1\) 欄的列座標為 \(y + \tan\theta\) 。由於四捨五入到 0 度意味著 \(|\theta| \le 22.5^\circ\) ,故 \(|\tan\theta| \le 0.414\) ,插值權重落在相鄰兩列之間:
像素被保留(標記為邊緣候選)的條件是
也就是說,像素的幅值必須同時不小於左右(沿梯度方向)兩個插值估計值。
方向為何取決於梯度而不是邊界? 這裡有個容易搞混的細節:梯度指向亮度增加的方向,而一條邊界的兩側梯度方向相反(相差 180 度)。NMS 沿梯度方向比較,是在決定「這條邊界落在哪一列像素上」——從亮的一側往暗的一側看,梯度幅值會先升後降,最大值即位於邊界的過渡中心。若改成沿邊界方向比較,會把邊界「磨平」成一條更寬的帶,違反 Canny 的單邊性判準。
NMS 之後,邊緣被壓縮成單像素寬,但仍然存在兩類問題:假陽性(雜訊造成的局部極大)與假陰性(弱邊界被壓低到低於鄰居)。這兩類問題分別由步驟四的雙閾值處理。
步驟四:雙閾值與遲滯(Hysteresis)#
為什麼單一閾值不夠? 這是 Canny 演算法的關鍵洞見。假設我們想偵測一條對比度均勻的邊界:
- 閾值設得高:整條邊界都通過,沒有假陽性,但弱對比區段的邊界會被整段漏掉(假陰性)。
- 閾值設得低:能連到弱對比區段,但雜訊產生的局部極大也會被保留(假陽性)。
兩者不可兼得,因為單一閾值無法區分「弱但真實的邊界」與「弱但假的雜訊」。Canny 的解法是使用兩個閾值並結合連通性:
- **高閾值 \(T_H\) **(例如 \(0.4 \cdot M_{\max}\) ):標記為「確定邊緣」。
- **低閾值 \(T_L\) **(例如 \(0.1 \cdot M_{\max}\) ):標記為「候選邊緣」。
- 遲滯規則:與確定邊緣連通(8 連通)的候選邊界被提升為確定邊緣;其餘候選邊界被丟棄。
直覺是:一條弱邊界如果連著一條強邊界,它大概是真的(真實邊界的對比度通常沿著邊界連續變化,而不是突然中斷);而一個孤立的弱響應多半是雜訊。這也讓輸出邊緣形成連續的曲線,而不是一堆碎點。
雙閾值比例(通常 \(T_L : T_H = 1 : 4\) 或 \(1 : 3\) )與 \(T_H\) 相對 \(M_{\max}\) 的比例,都是需要調校的參數。
步驟五:邊緣追蹤#
沿著邊緣像素的連通性追蹤,形成完整的邊緣曲線(edge chain)。實作上通常與步驟四合併,用連通元件標記(BFS/DFS 或 union-find)一次完成:標記所有 \(M > T_H\) 的像素為強邊緣,再對每個未連接到強邊界的連通元件(其中含有 \(T_L < M \le T_H\) 的像素)整個刪除。
Canny 的完整流程與參數#
flowchart TD
I["影像 I"] --> S["高斯平滑 sigma"]
S --> G["Sobel: Gx, Gy"]
G --> M["幅值 M 與方向 theta"]
M --> N["NMS: 方向四捨五入 + 線性插值比較"]
N --> T["雙閾值 TH 與 TL"]
T --> H["遲滯: 連通到強邊緣者保留"]
H --> O["輸出: 單像素寬連續邊緣"]| 參數 | 典型值 | 影響 |
|---|---|---|
| \(\sigma\) | 1.0 - 1.5 | 越大越抗噪但邊緣越粗、定位越差 |
| \(T_H / M_{\max}\) | 0.3 - 0.4 | 越大假陽性越少但邊緣變稀疏 |
| \(T_L / T_H\) | 0.2 - 0.3 | 越大邊緣越連續但假陽性增加 |
| 連通性 | 8 連通 | 決定邊緣曲線是否斷裂 |
2.2.6 評價指標:為什麼邊緣的評價與分類不同#
分類式指標的失效#
在分類問題中,正確答案只有「對」與「錯」兩個狀態,預測對了就是對。但在邊緣偵測中,「差一個像素算對還是算錯」是一個真實且重要的問題。真實邊界在離散網格上的位置有半像素的不確定性(過渡寬度決定了邊界中心落在哪一列),因此評價時必須給一個容差。
標準做法:容差內的邊緣匹配#
最常用的做法(BSDS 邊緣基準採用的精神):
- 對每個預測邊緣像素,在容差 \(\tau\) (常見 1 到 2 像素)內尋找最近的真實邊緣像素。
- 對每個真實邊緣像素,同樣尋找容差內的預測邊緣像素。
- 在匹配上的像素上計算
其中 TP 是落在容差範圍內、被雙向匹配的邊緣像素數。
- 為兼顧兩個方向且給出單一數字,常用 F 測度或 ODS(Optimal Detector Score)/ OIS(Optimal Image Scale):
ODS 使用整個資料集共用的單一閾值,OIS 允許每張影像各自挑選最佳閾值。
ODS 與 OIS 的差距是衡量參數是否依影像而異的指標:差距大表示演算法需要每張影像重新調參。
邊緣位移誤差#
另一種更細緻的指標是 edge displacement error(邊緣位移誤差):對每個預測邊緣像素,計算它到最近真實邊緣的有向距離(預測在真實邊界的哪一側),然後取絕對值的中位數。它比二元匹配更細緻,能區分「邊界位置整體偏左 1 像素」與「邊界完全錯位」。
與分類指標的哲學差異#
| 分類 | 邊緣偵測 | |
|---|---|---|
| 正確性的定義 | 離散的標籤對錯 | 連續的幾何距離 |
| 容差 | 無 | 必須,且是評估的一部分 |
| 誤報的代價 | 對稱(同等權重) | 通常假陽性的代價較低(寧可多畫) |
| 最佳化目標 | 最大化 F1 或 AUC | 通常在容差內最大化 F 或最小化位移誤差 |
實務建議:在做邊緣偵測實驗時,除了 ODS/OIS 之外,一定也要報告「在不同容差下的曲線」( \(\tau = 1, 2, 3\) ),因為單一容差下的排名可能與另一個容差下完全不同。
2.2.7 參數敏感度實驗#
下面的實驗程式碼可以讓讀者親眼看到 \(\sigma\) 與閾值比例如何影響結果,這也是理解 Canny 三項判準的最佳途徑。
import numpy as np
import cv2
def canny_manual(img, sigma=1.5, low_ratio=0.25, high_ratio=0.4):
"""從零實作 Canny(示範用;正式用途請用 cv2.Canny)。"""
# 步驟一:高斯平滑
ksize = int(2 * round(3 * sigma) + 1)
smooth = cv2.GaussianBlur(img, (ksize, ksize), sigma)
# 步驟二:梯度幅值與方向
gx = cv2.Sobel(smooth, cv2.CV_32F, 1, 0, ksize=3)
gy = cv2.Sobel(smooth, cv2.CV_32F, 0, 1, ksize=3)
mag = np.hypot(gx, gy)
ang = np.rad2deg(np.arctan2(gy, gx)) % 180.0 # 折疊到 [0, 180)
# 步驟三:非極大值抑制
nms = np.zeros_like(mag)
h, w = mag.shape
for y in range(1, h - 1):
for x in range(1, w - 1):
m = mag[y, x]
if m == 0:
continue
a = ang[y, x]
if a < 22.5 or a >= 157.5: # 0 度: 水平梯度
n1, n2 = mag[y, x - 1], mag[y, x + 1]
elif a < 67.5: # 45 度
n1, n2 = mag[y - 1, x + 1], mag[y + 1, x - 1]
elif a < 112.5: # 90 度
n1, n2 = mag[y - 1, x], mag[y + 1, x]
else: # 135 度
n1, n2 = mag[y - 1, x - 1], mag[y + 1, x + 1]
if m >= n1 and m >= n2:
nms[y, x] = m
# 步驟四:雙閾值 + 遲滯
hi = high_ratio * mag.max()
lo = low_ratio * mag.max()
strong = (nms >= hi).astype(np.uint8)
weak = (nms >= lo).astype(np.uint8)
n, labels = cv2.connectedComponents(weak, connectivity=8)
keep = np.zeros_like(strong)
for i in range(1, n):
if np.any(strong[labels == i]):
keep[labels == i] = 1
return keep
插值說明:上面的簡化版直接用四捨五入方向的鄰居比較,沒有做線性插值。在方向接近 45 度倍數的邊界處,會出現明顯的「階梯效應」。加入插值後邊緣會更平滑連續,代價是每像素約多兩次乘加。
def edge_metrics(pred, gt, tol=2):
"""容差內的 precision / recall / F(教學用簡化實作)。"""
ys, xs = np.nonzero(pred)
gy, gx = np.nonzero(gt)
if len(ys) == 0 or len(gy) == 0:
return 0.0, 0.0, 0.0
# 對每個預測點找最近的真實邊緣
hits = 0
d2 = (ys[:, None] - gy[None, :]) ** 2 + (xs[:, None] - gx[None, :]) ** 2
hits = int((d2.min(axis=1) <= tol ** 2).sum())
precision = hits / len(ys)
# 對每個真實點找最近的預測邊緣
hits2 = int((d2.min(axis=0) <= tol ** 2).sum())
recall = hits2 / len(gy)
f = 0.0 if precision + recall == 0 else 2 * precision * recall / (precision + recall)
return precision, recall, f
img = cv2.imread("canny_test.png", cv2.IMREAD_GRAYSCALE)
gt = cv2.imread("canny_gt.png", cv2.IMREAD_GRAYSCALE) > 0 # 人工標註的真實邊緣
for sigma in [0.5, 1.0, 1.5, 2.5, 4.0]:
pred = canny_manual(img, sigma=sigma)
p, r, f = edge_metrics(pred, gt)
n_edge = int(pred.sum())
print(f"sigma={sigma:4.1f} edges={n_edge:6d} P={p:.3f} R={r:.3f} F={f:.3f}")
預期觀察:
- \(\sigma\) 增大時,邊緣總數先略增後銳減(細節被抹平),邊緣變粗但更連續;F 值通常有個甜蜜點。
- \(\sigma\) 太小時,紋理區會產生大量假邊緣,precision 下降。
- 閾值比例的掃描:
for lo_r in [0.05, 0.1, 0.2, 0.3, 0.4]:
for hi_r in [0.2, 0.4, 0.6]:
if lo_r >= hi_r:
continue
pred = canny_manual(img, sigma=1.5, low_ratio=lo_r, high_ratio=hi_r)
p, r, f = edge_metrics(pred, gt)
print(f"lo={lo_r:.2f} hi={hi_r:.2f} P={p:.3f} R={r:.3f} F={f:.3f}")
這會顯示典型的取捨:低阈值提高 recall 但降低 precision,高閾值反之,而 F 值在中間某處達峰。
與 OpenCV 的對照#
img = cv2.imread("canny_test.png", cv2.IMREAD_GRAYSCALE)
mine = canny_manual(img, sigma=1.5)
cv_out = cv2.Canny(img, threshold1=40, threshold2=110) # threshold1=low, threshold2=high
diff = np.logical_xor(mine > 0, cv_out > 0)
print("disagreement pixels:", int(diff.sum()),
"ratio:", diff.mean())
差異來自三個地方:OpenCV 內部對方向使用了更精細的插值(有 4 個量化方向但用精確的梯度方向做加權)、使用了不同的 Sobel 縮放因子、以及邊界處理方式不同。自己實作一遍 Canny 的最大價值不在於取代 OpenCV,而在於把每一步的取捨暴露在眼前。
2.2.8 邊緣偵測的侷限,以及它逼出的下一個問題#
Canny 在 1990 年代幾乎是所有邊緣偵測的標準答案,但它有幾個結構性的侷限:
- 只給邊緣位置,不給「邊緣是什麼」。輸出是一張二值圖,無法區分「貓的輪廓」與「背景樹幹的邊界」。這是語義鴻溝(第 3 章、第 3.3 節)的早期徵兆。
- 依賴單一尺度的強度。 \(\sigma\) 是全域參數,同一張圖裡「文字的邊」與「建築輪廓的邊」需要不同的尺度。Canny 只能靠多跑幾個 \(\sigma\) 再合併來近似尺度空間。
- 邊緣是「線」,特徵是「點 + 描述」。對於需要知道「這裡的角是什麼形狀」的任務(例如SfM 的幾何驗證、SLAM 的資料關聯),單純的邊緣點不夠——你還需要一個局部描述子。第 2.3 節的 SIFT 正是把「邊緣偵測」升級為「局部特徵偵測」。
於是自然的問題是:能不能把邊緣偵測的「多尺度」與「區域描述」合而為一,用一個有方向的、可重複辨識的向量來代表影像中的局部結構?
這就是下一節的主題。
2.2.9 本節小結#
- 梯度是一階導數,但「邊緣偵測」不等於「求梯度」。NMS、雙閾值、遲滯三個步驟都是為了處理「梯度大不等於有意義的邊界」。
- Sobel/Prewitt 是「平滑 + 差分」的合成核,其可分離性讓運算便宜;它們等價於尺度約為 1 的高斯導數,因此「換一個 \(\sigma\) 」就得到多解析度版本。
- Laplacian 的零交叉給出邊界中點,但必須搭配平滑才有意義;LoG 把「平滑尺度」提升為演算法的一等公民,DoG 則是它的可計算近似——這條線直接通往 SIFT。
- Canny 的雙閾值與遲滯解決了「單一閾值無法同時控制假陽性與假陰性」的困境,理由是弱邊界若連著強邊界,多半是真實的。
- 邊緣的評價需要容差,因此 precision/recall/F 與 ODS/OIS 的定義與分類問題不同,報告結果時必須交代容差。
- 留下的問題:Canny 給的是「線」,我們想要的是「帶有方向與局部描述的點」——而且要能抵抗縮放、旋轉、光照與輕微形變。第 2.3 節的 SIFT 正是對這個問題的完整回答,而它同時也是全書「歸納偏誤」主軸的第一個完整案例。