從「在哪裡」到「是什麼」:描述子的任務
在前兩篇導讀中,你學會了如何找出關鍵點——像角點與斑點這類具有辨識度的位置,演算法能在不同影像之間可靠地重新定位它們。那回答了「在哪裡」這個問題:一個關鍵點就是一個位置,再加上尺度,很快還會加上方向。但光有位置,完全無法告訴你那個點看起來是什麼樣子。如果我給你某張照片中的座標 (212, 87),要你在第二張照片裡找出同一個實體點,你會卡住——因為沒有任何內容可供比對。本篇導讀要處理問題的另一半:把 影像關鍵點 周圍那一小塊像素,轉成關於「是什麼」的描述。
描述子就是答案。它是從關鍵點鄰域計算出來、精簡的數值簽章——你可以把它想成那個點的指紋。同一根指尖按出的兩枚指紋,即使手轉了個角度或光線變了,看起來幾乎一模一樣;而不同手指的兩枚指紋,看起來明顯不同。好的影像描述子也是如此:同一個實體點周圍的影像區塊,在兩張照片中都該產生幾乎相同的數字向量,而不同的點則該產生明顯不同的向量。本篇要建立的 SIFT 描述子 是最著名的例子——一串 128 個數字,用來概括一塊影像區塊。
由於描述子只是一串數字向量,比較兩個描述子就是簡單的算術:量出兩向量之間的距離。距離很小,兩塊區塊大概呈現的是同一樣東西;距離很大,則大概不是。SIFT 最常用的選擇是普通的歐氏距離——也就是 128 維空間中的直線距離。這正是配對的運作原理:要在兩張影像間找出對應點,你先為各自的關鍵點算出描述子,再把向量最接近的成對配起來。整套配對機制會在第 5 篇建立;現在只要記住,先描述、再以距離比對 就是本篇一切設計所服務的目標。
示意圖:一張影像上偵測到許多關鍵點,每個關鍵點的局部區塊被轉換成一個小長條圖向量。
方向:讓描述子具備旋轉不變性
有一個問題會讓天真的描述子徹底失敗。假設你先拍下一個咖啡杯,接著歪頭再拍一次。商標上某個關鍵點周圍的區塊,現在轉了個角度。如果我們以固定的影像座標——由左上到右下——逐一讀取像素,兩塊區塊會給出非常不同的數字,配對就此失敗,儘管那是同一個商標。SIFT 用一個巧妙的手法解決它:在描述一塊區塊之前,先給關鍵點一個主方向,然後把一切都「相對於」這個方向來度量。這就像看紙本地圖:你不是永遠把它擺成北方朝上,而是先把地圖轉到「上方」對著你前進的方向,於是地圖上的每個方位,從你的視角看都說得通。兩個朝著不同方向的人會把地圖轉成不同角度,但一旦轉好,他們讀到的相對配置是一樣的。
為了找出這個主方向,SIFT 觀察區塊內部的梯度——也就是你在第 1 篇遇過的影像梯度。回想一下,某個像素處的梯度指向亮度上升最陡的方向,其長度則衡量該處邊緣有多強。對於方向,我們在每個像素都需要兩個數字:梯度強度(多強)與梯度方向(指向哪邊)。我們以簡單的有限差分來估計它們——比較某像素左右兩側的鄰居。
由有限差分得到每個像素的梯度強度 m 與方向 θ。
我們來逐項拆解。I(x,y) 是位於第 x 行、第 y 列那個像素的亮度。I(x+1,y) − I(x−1,y) 這一項是該像素橫向的亮度變化(右鄰減左鄰),稱它為 gx;同樣地,I(x,y+1) − I(x,y−1) 是縱向的變化 gy。強度 m 就是向量 (gx, gy) 由畢氏定理算出的長度,方向 θ 則是它的角度。函數 atan2(gy, gx) 是 arctan(gy / gx) 的嚴謹版本,它會在完整的 0°–360° 範圍內回傳正確角度,而且永遠不會除以零。實例:若右鄰比左鄰亮 gx = 10、下方比上方亮 gy = 10,則 m = √(10² + 10²) ≈ 14.1,而 θ = atan2(10, 10) = 45°——一道相當強、沿對角線延伸的邊緣。
現在,對關鍵點周圍的區域,把這些方向做成一張直方圖。SIFT 使用 36 個分格(bin),每格涵蓋 10°,合起來涵蓋整個圓周。區域中的每個像素都會投票給對應其 θ 的分格,但這張票會以它的強度 m 加權(強邊緣比微弱的紋理算得更重),並再乘上一個以關鍵點為中心的高斯窗(靠近中心的像素比區域邊緣的像素算得更重)。票數最高的分格——也就是最常見、最強的梯度方向——就被定為關鍵點的主方向。有一個細節很重要:若有第二個分格達到峰值的至少 80%,SIFT 會在相同位置與尺度、但帶著那個次方向,再建立一個額外的關鍵點。因此單一個點可能產生兩個 SIFT 描述子,這在主方向曖昧不明時能改善配對。
打造 SIFT 描述子
現在我們有了一塊已定向的區塊:一個以關鍵點為中心的方形窗,其大小與關鍵點的尺度成比例(在 尺度空間 中以較粗尺度偵測到的關鍵點,會得到比細尺度更大的窗),並已旋轉到主方向朝上。從這唯一一塊標準化的區塊,我們建立 SIFT 描述子,一個由 128 個數字組成的向量。配方是一格格小型梯度直方圖——與指定方向時相同的梯度概念,但這次我們保留了一些「粗略的空間資訊」,記住每個梯度位於區塊的哪個部位。
- 取出關鍵點周圍那塊已定向、依尺度決定大小的區塊,將它切成規則的 4×4 格方形小格(共 16 格)。
- 在每個像素計算梯度強度 m 與方向 θ,做法與第 2 節完全相同,並把每個方向減去關鍵點的主方向角,使描述以相對方向為準。
- 在這 16 格的每一格中,累積一張 8 格的梯度方向直方圖(每格涵蓋 45°),把每個像素的強度——再乘上高斯窗加權——加進對應的分格。
- 把這 16 格、各 8 格的直方圖串接成一條長向量,再將它正規化(見下一道式子)以抑制光照影響。
有兩個輔助技巧讓它保持穩健。第一,覆蓋整塊區塊的高斯加權窗會降低離中心較遠像素的權重;那些外圍像素最容易在輕微視角變化下飄進或飄出區塊,因此少信任它們能讓描述子更穩定。第二個更微妙,是三線性內插。剛好落在兩格邊界上、或方向剛好介於兩個方向分格之間的像素,不該整票塞進某一格——否則區塊只要稍微平移一點,它的整票就會翻到隔壁去,描述子也跟著跳動。取而代之,每個梯度都會在三個維度上柔和地分散到鄰近的分格——兩個空間軸(落在哪一格)與方向軸(落在哪個角度分格)——分配比例取決於它離各格有多近。這種抹散消除了硬邊界造成的跳動,也是 SIFT 對小幅平移如此穩定的重要原因之一。
128 維的 SIFT 向量。
明確數一遍:4×4 = 16 個空間格,每格貢獻一張 8 個值的方向直方圖,得到 16×8 = 128 個數字——這就是標準的 SIFT 長度。選擇 4×4×8 是刻意的折衷。更多空間格(比如 8×8)能更精準地釘住每個梯度「在哪裡」,卻會讓描述子變脆:區塊只要稍微對不準,每個梯度落在哪一格就會被打亂。更少的格子(比如 2×2)對平移非常寬容,卻丟掉太多版面資訊,使得本該不同的區塊開始看起來相似。4×4 格搭配 8 個方向正好落在甜蜜點:有足夠的空間結構以保持辨識度,又有足夠的池化來原諒那些在不完美偵測下殘留的小幅平移。
為光照穩健性所做的兩階段正規化。
最後,做正規化。v 是那條 128 維向量,‖v‖ 是它的歐氏長度。除以這個長度(v ← v/‖v‖)使向量變成單位長度,這會抵消對比的整體縮放:把整塊區塊調亮,每個梯度都以相同倍數放大,但縮放回長度 1 之後,描述子毫無改變。這處理了線性的光照變化。然而強烈的方向光、或會飽和(被截斷成純白)的相機,可能把少數幾個梯度灌得極大。因此我們把每個分量截頂到至多 0.2(v_i ← min(v_i, 0.2)),壓住任何單一巨大梯度的影響,然後再次正規化回單位長度。結果是一個對亮度、對比與飽和變化幾乎無動於衷的描述子——正是我們在第 1 節列為目標的光照不變性。
# Build a 128-D SIFT descriptor from one oriented keypoint patch
def sift_descriptor(patch, dominant_angle):
# patch: square image window, already sized by the keypoint's scale
gx, gy = finite_differences(patch) # per-pixel gradients
mag = sqrt(gx**2 + gy**2) # magnitude m(x,y)
ang = (atan2(gy, gx) - dominant_angle) % 360 # rotation-relative orientation
w = gaussian_window(patch.shape) # down-weight far pixels
hist = zeros((4, 4, 8)) # 4x4 cells, 8 orientation bins
for (x, y) in patch.pixels():
cell_x, cell_y = cell_index(x, y) # which of the 4x4 cells
bin = ang[y, x] / 45.0 # which of the 8 bins (45 deg each)
vote = mag[y, x] * w[y, x] # magnitude, Gaussian-weighted
# trilinear interpolation: split 'vote' softly across the neighboring
# (cell_x, cell_y, orientation) bins in proportion to distance
add_trilinear(hist, cell_x, cell_y, bin, vote)
v = hist.flatten() # length 16 * 8 = 128
v = v / norm(v) # unit length
v = minimum(v, 0.2) # clip large components
v = v / norm(v) # renormalize
return v # the SIFT descriptor關鍵點區塊上疊著 4×4 格;每格畫著一個 8 方向的小星形,表示梯度直方圖的長度。
HOG:描述整個區域,而不只是關鍵點
方向梯度直方圖(HOG)是 SIFT 的近親,源自相同的洞見,用法卻大不相同。SIFT 只在少數稀疏、經尺度挑選的關鍵點上計算描述子,HOG 則是密集地套用:它在整個區域——一整個偵測窗——上鋪一張固定的格網,並描述其中所有內容。這使 HOG 在 2000 年代後期成為物件偵測的主力特徵,最著名的是行人偵測:把一個窗口滑過整張影像,算出它的 HOG 向量,再讓分類器判斷「是人/不是人」。
- 在窗口的每個像素計算梯度強度與方向——與先前相同的有限差分。
- 在規則格網的每個小細胞(cell)上(例如 8×8 像素的細胞),把方向池化成一張直方圖,通常用 9 個分格、涵蓋 0°–180°(無向梯度,因此方向相反者共用一格)。
- 把相鄰的細胞組成較大、彼此重疊的區塊(block)(例如 2×2 個細胞),並在每個區塊內對合併後的直方圖做正規化,以獲得對比穩健性。
- 讓區塊一次滑動一個細胞,使每個細胞在不同鄰居下被多次正規化,再把所有區塊向量串接成整個窗口的一條長描述子。
帶有穩定常數 ε 的 L2 區塊正規化。
這裡 v 是一個區塊內所有細胞直方圖堆疊起來的向量,‖v‖ 是它的歐氏長度,ε 是一個小的正常數。除以 √(‖v‖² + ε²) 會把區塊大致縮放成單位長度,從而消除局部對比的影響——一塊光線明亮的區塊,和一塊昏暗但邊緣樣式相同的區塊,正規化後會得到幾乎相同的向量。ε 是為了安全:在平坦、無紋理的區域裡所有梯度都接近零,於是 ‖v‖ ≈ 0,單純的除法會爆掉或除以零;在平方根內加上 ε² 可讓結果保持有限,並溫和地把近乎空白的區塊推向零,而不是放大它們的雜訊。關鍵的設計選擇在於區塊彼此重疊:因為每個細胞會對著不同組鄰居被正規化好幾次,描述子便能妥善應付那些使窗口各處對比改變的陰影與不均勻光照。
值得把這個對比講明白。SIFT 描述點;HOG 描述區域。 SIFT 挑出少數經尺度挑選、已對齊方向的關鍵點,各產生一個描述子,為的是在不同影像間配對同一個實體點。HOG 完全不理會關鍵點,以固定格網鋪滿整個窗口、不對個別特徵做旋轉,並為這個窗口產生一個大描述子,為的是分類它的內容。SIFT 的不變性(尺度、旋轉)正是你在同一物件以不同大小與角度出現時想要的;而 HOG 的固定幾何在你已把偵測窗口正規化成標準大小與直立姿態時則沒問題——甚至有幫助,因為這種剛性的版面本身就成了具鑑別力的線索(行人「頭在軀幹之上、軀幹在腿之上」的結構)。
一個行人形狀的偵測窗口被格網覆蓋;每格畫著有方向的梯度線條,邊緣強處較亮。
為什麼梯度直方圖如此有效
退一步看。SIFT 與 HOG 的差別在於套用的位置,但它們的穩健性都來自同樣三個反覆出現的概念。理解這些原理,比死背任一條流程更重要——幾乎每一個經典描述子,甚至卷積網路的早期層,都是同一主題的變奏。
一:使用梯度,而非原始亮度。 梯度衡量的是相鄰像素之間的差,因此它丟掉了絕對亮度,只保留亮度如何變化。把每個像素都加上一個常數——把室內燈光調亮——每個梯度都不變。光是這一個選擇,就在做任何正規化之前免費換來了大部分的光照容忍度,因為在會把原始像素值沖淡的光照變化下,邊緣與紋理仍能存活。
二:池化成直方圖。 在一個細胞內,我們不記錄「究竟是哪個像素」有某個梯度方向——我們只統計有多少梯度能量指向各個方向。這種對精確位置的刻意遺忘,正是能容忍小幅形變的關鍵:若區塊平移一兩個像素、或表面稍微彎曲,個別梯度會在細胞內到處移動,但細胞的直方圖幾乎不變。池化以一點空間精度,換來對真實相機與真實場景總會引入的小幾何抖動的大量穩健性。
三:正規化。 把描述子(或區塊)除以它自己的長度,會移除整體的對比因子,於是同一個樣式的微弱低對比畫面,與其鮮明高對比的畫面,落點幾乎相同。SIFT 中可選的截頂更進一步,壓住飽和造成的異常值。兩者合起來:梯度處理加性的亮度,正規化處理乘性的對比,描述子最終讀到的是局部結構的形狀,而非它在光度上的偶然。
優勢、代價,以及何時該動用 SIFT
該攤開一張誠實的資產負債表了。它們的優點真實存在,而且確實具有變革性:SIFT 與 HOG 產生的描述子有高度辨識度,並對旋轉、光照與適度的視角變化穩健,這讓可靠的寬基線配對、全景拼接與行人偵測首度變得實用。在許多配對與辨識任務上,它們在十多年間都是最先進的技術,而且至今仍是強而有力、無需訓練的基準。
代價是速度與大小。一個 SIFT 描述子 是一個 128 維的浮點向量,而計算它——梯度、方向、加權直方圖、三線性內插、兩次正規化——並不便宜,更別說每張影像要對數百乃至數千個關鍵點重複這一切。配對更糟:比較兩張影像意味著要對每一組候選關鍵點配對、在全部 128 維上量歐氏距離,因此工作量會隨兩邊關鍵點數量的乘積成長。在每秒 30 影格的影片串流上、或在一支 CPU 普通又得省電的手機上,這筆帳很快就要結清。
關於授權的一點,謹慎陳述:SIFT 曾經申請專利,這使許多開源專案多年來轉向替代方案。該專利此後已到期,因此 SIFT 現在可自由使用(例如它又回到了 OpenCV 的主模組中)。HOG 從未受到同樣的限制。所以今天偏好更快描述子的理由是效能,而非授權——這正是下一篇導讀的動機。