JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

部件、直線與文字:可變形部件模型、霍夫轉換與 OCR

抵達深度學習前電腦視覺的頂峰 — 用彈簧連起的部件、為直線與圓投票、以及完整流程的 OCR — 再看看這些點子如何延續在今天的神經網路裡。

用彈簧連起部件:可變形部件模型

走到這條學習路徑的尾聲,你已經握有一整套辨識工具:模板比對與「顏色加流程」的觀點(第一篇)、把描述子餵給 k-NN 與 SVM 以及詞袋特徵的技巧(第二篇)、用 Haar 特徵與提升法打造的 Viola–Jones 滑動視窗人臉偵測器(第三篇),還有特徵臉、Fisher 臉這些子空間人臉模型與可變形的人臉形狀(第四篇)。這最後一篇要登上深度學習之前那個時代的頂峰,再帶你走下另一側,進入深度學習。我們先從那個多年來穩居物件偵測王座的單一方法談起:可變形部件模型(deformable part model,DPM)。

腦中要記住的畫面既簡單又有「物理感」。假設你要找出一個人。你先用一個粗略、低解析度的根(root)模板,把整個人形抓成一個模糊的 HOG 輪廓——頭在軀幹之上、軀幹在腿之上;同時你也保留幾個較小、較高解析度的部件(part)模板:一個給頭、一個給肩膀、每條腿各一個。接著用一條彈簧把每個部件連到根上。彈簧有一個偏好(錨點)位置,但它能伸縮:頭可以稍微偏高,腿可以往外擺。偵測同時問兩個問題——影像看起來像不像根與各部件,而且各部件有沒有待在彈簧想要它們待的位置附近?

這裡的每一種材料你都已經認識。模板是用 HOG 特徵做的——也就是本路徑稍早介紹過的梯度方向直方圖。每個模板由一個學到的線性濾波器評分,這正是一個 線性 SVM 權重向量與特徵做點積。而要真正在照片中找到物件,整個模型會被在許多位置與尺度上滑過影像——這就是第三篇的 滑動視窗偵測 範式。DPM 不是一個全新的基本元件;它是把本路徑已經給你的那些基本元件巧妙組裝起來,再加上一個新點子:彈簧。

\text{score}(p_0,\dots,p_n)=\sum_{i=0}^{n} F_i\cdot\phi(H,p_i)\;-\;\sum_{i=1}^{n} d_i\cdot\phi_d(p_i-p_0)

DPM 的偵測分數:外觀獎勵減去變形懲罰。

讓我們一個符號一個符號拆開,因為這一行就是整個模型。擺放位置是 p_0, p_1, …, p_n:p_0 是你放根的位置,p_1…p_n 是你放那 n 個部件的位置(各是一個座標,並落在某個選定的解析度層級)。第一個總和是外觀獎勵。F_i 是第 i 個學到的濾波器——F_0 是根濾波器,F_1…F_n 是部件濾波器——每一個都是像 SVM 那樣訓練出的權重向量。φ(H, p_i) 的意思是「從影像特徵金字塔 H 在位置 p_i 取出 HOG 特徵」。點積 F_i · φ(H, p_i) 就是 SVM 濾波器的響應:當該濾波器底下的影像區塊長得像它被訓練去喜歡的東西時,這個值會是一個大正數。對 i 求和,就是把根與每個部件的吻合程度加總起來。第二個總和是變形懲罰。φ_d(p_i − p_0) 量度部件 i 相對於根,離它理想錨點有多遠(實作上是水平與垂直位移的平方等項,dx、dx²、dy、dy²),而 d_i 是一個學到的非負權重向量——也就是那條部件彈簧的硬度。所以 d_i · φ_d(...) 會隨著部件被拉離原位而變大。那個減號是關鍵:最終分數是「一切看起來有多好」減去「我們為了讓它吻合而必須把彈簧拉彎多少」。

一個很小的具體例子能把這個權衡講活。假設頭部濾波器響應很強(比方 +5),但前提是你要把它放在根錨點上方 20 像素處,而彈簧硬度每平方像素收費 0.01。變形成本就是 0.01 × 20² = 4,所以頭部的淨貢獻是 5 − 4 = +1:仍然划算,模型接受一個稍微偏高的頭。但若要吻合那個頭需要拉伸 40 像素,成本會變成 0.01 × 40² = 16,把 +5 的獎勵淹沒掉——模型寧可不把頭放在那裡。這正是 DPM 如何容忍一個身體前傾、正在跨步或稍微側身的人:部件可以移動去追逐真正的影像證據,但只能移到「外觀增益足以抵掉彈簧成本」的程度為止。

最後一個訓練上的精妙之處,讓 DPM 配得上頂峰之位:潛在 SVM(latent-SVM)學習。你標註訓練資料時,是在整個人身上畫一個框,但你不會標出頭、肩、腿在哪——那些部件位置是隱藏的,或說潛在的(latent)。DPM 把最佳部件擺放當成潛在變數:訓練時,它對每個範例把部件滑到分數最高之處(內層最佳化),再用 SVM 式的目標函式更新濾波器與彈簧權重(外層最佳化),如此反覆。模型實際上是自行發現「這裡應該有個部件,而且它傾向待在那裡」。這就是為什麼 DPM 在 2008–2011 年前後連續好幾年穩居 PASCAL VOC 偵測競賽榜首——在深度網路到來之前,它是全世界最強的通用物件偵測器。

為結構投票:霍夫轉換

DPM 是從範例中學會一個物件長什麼樣子。現在我們切換到一個完全不同的問題:偵測幾何結構,而且完全不需要訓練。經典問題是這樣的:邊緣偵測器給了你一團散落的邊緣點雲,你想找出藏在點雲裡的直線——即使這些線有缺口(虛線的道路標線),即使有雜訊(屬於任何東西的零散邊緣像素)。「對每個點子集都試著擬合一條線」這種天真做法會在組合上爆炸。霍夫轉換 用一個漂亮的點子完全繞過這件事:讓每個點都投票

用白話說說這個投票點子。單一個邊緣點並不知道自己屬於哪條線——但它知道(無限多條)可能穿過它的線。所以與其當場表態,每個邊緣點對所有與它相容的線各投一票。我們把這些票記在一個累加器(accumulator)裡,每個可能的線對應一個格子。影像中真正的線上有許多邊緣點,它們每一個都投給同一條線,於是那個格子會累積出一個高峰。雜訊點則把票稀薄地撒得到處都是,永遠堆不出峰。偵測於是變成「找出得票最多的格子」。缺口不再重要,因為我們從不需要連續的一串點——只要沿著線的某處有足夠多的點達成一致即可。

\rho = x\cos\theta + y\sin\theta

直線的法線 (ρ, θ) 參數化——參數有界,且能表示垂直線。

這個方程式是我們不用一般的斜率截距式 y = mx + c,而為直線命名的方式。讀作:點 (x, y) 落在由 (ρ, θ) 這對參數描述的直線上。這裡 (x, y) 是影像中一個邊緣點的座標;θ 是直線法線的角度(與直線垂直的方向);ρ 則是從原點到該直線的垂直距離。平面上每一條直線都恰好對應一對 (ρ, θ),其中 θ 落在 [0, 180°)、ρ 是帶正負號的距離。何必這麼麻煩、不用 y = mx + c?因為垂直線的斜率是無限大——m 會炸開,斜率截距式根本無法表示它。(ρ, θ) 形式沒有這個盲點:垂直線就只是 θ = 0、ρ 等於它的 x 位置。而且兩個參數都有界——θ 在半圈之內,ρ 在影像對角線之內——所以一個有限大小的累加器陣列就能涵蓋所有可能的線。快速檢驗一下:水平線 y = 3 的法線是垂直的,θ = 90°,於是 ρ = x·0 + y·1 = 3,恰好是它離原點的距離。這個形式運作正常。

現在是神來一筆。固定一個邊緣點 (x, y),讓 θ 掃過它所有的值;ρ = x·cosθ + y·sinθ 於是在 (ρ, θ) 累加器中描出一條平滑的正弦曲線。所以影像空間中的單一個點,在參數空間中變成一整條曲線——那條曲線「就是」這個點在為所有穿過它的線投票。再取第二個邊緣點,它畫出自己的正弦曲線。兩條正弦曲線相交之處,那裡的 (ρ, θ) 同時滿足兩個點——也就是穿過這兩點的那條線。一整組共線的點會產生一束正弦曲線,它們全都交於同一個共同的 (ρ, θ):那個交點就是累加器中的一個高峰。因此尋峰(掃描累加器,找出超過票數門檻的局部極大值)就是偵測步驟,而每個峰的座標 (ρ, θ) 直接把一條偵測到的線交到你手上。

# Hough line voting (pseudocode)
# Inputs: edge_points = list of (x, y) from an edge detector
# Output: accumulator A indexed by (rho_bin, theta_bin)

A = zeros(num_rho_bins, num_theta_bins)
for (x, y) in edge_points:
    for theta in range(0, 180):            # sweep all line orientations
        rho = x * cos(theta) + y * sin(theta)
        r = quantize(rho)                  # map rho to its bin index
        t = quantize(theta)
        A[r, t] += 1                       # this point votes for line (rho, theta)

# Detection = peaks in the accumulator
lines = []
for (r, t) in local_maxima(A):
    if A[r, t] > vote_threshold:           # enough points agreed -> a real line
        lines.append((rho_of(r), theta_of(t)))
每個邊緣點對每個方向各加一票;累加器中明亮的格子就是線。

不只直線:圓與廣義霍夫轉換

投票其實從來都不是關於直線——它關乎的是某個參數空間裡的共識。所以我們來換個形狀。要偵測,我們只要改變「每個點投給什麼」以及「累加器在數什麼」就好。

(x-a)^2 + (y-b)^2 = r^2

一個圓有三個參數:圓心 (a, b) 與半徑 r。

讀作:邊緣點 (x, y) 落在以 (a, b) 為圓心、半徑為 r 的圓上。所以 a 和 b 是圓心座標,r 是半徑。和直線的兩個數 (ρ, θ) 相比,一個圓需要個數,這代表 霍夫轉換 的累加器現在是一個三維陣列 A(a, b, r)——每一組候選的「圓心加半徑」對應一個格子。投票邏輯在精神上一模一樣:每個邊緣點對所有可能穿過它的 (a, b, r) 圓投票,許多邊緣點達成一致的格子會亮成峰。一個以某個尺寸拍下的咖啡杯口,會在某個 (a, b, r) 周圍產生一團票;那個峰一次就把圓心與半徑都給你。

這裡有一個漂亮的省力技巧,而且它重複利用了你已經有的東西。一個好的邊緣偵測器回報的不只是邊緣在哪裡,還有它的梯度方向——而對圓上的一點來說,圓心一定落在與邊緣垂直的法線上,也就是沿著梯度方向往內(或往外)。所以與其讓一個邊緣點對各個方向的圓心都投票,你只要沿著它的梯度線、在每個候選半徑處投票就好。這把原本二維扇形的圓心投票,壓縮成每個半徑只有一條一維射線,大幅削減投出的票數與運算量,並讓峰更銳利(更少的雜散票去攪渾累加器)。同樣的點子也馴服了那個原本令人卻步的三維累加器的成本。

最後,廣義霍夫轉換(Generalized Hough Transform,GHT)讓投票完全擺脫任何方程式。假設你想偵測一個任意的剛性形狀——一個商標、一把工具的輪廓——它沒有漂亮的公式。在訓練步驟中,你在形狀內選一個單一的參考點(比方它的中心),沿著它的邊界走一圈;對每個邊界邊緣點,你記下從該邊緣點指向參考點的位移向量,並把這個位移依該邊緣點的梯度方向當作鍵,存進一張 R 表(R-table)。偵測時,每個影像邊緣點用自己的梯度方向去查 R 表,取出存好的位移,然後對它們所暗示的參考點位置投票。形狀真正所在之處,所有這些票會堆到同一個點上。再加上額外的累加器維度,同樣的機制也能對尺度與旋轉投票。

辨識文字:OCR 作為一條完整的古典流程

現在來到收尾的壓軸。光學字元辨識(OCR)——把一張文字圖片變成機器可讀的字元——是完美的終章,因為一個真正的 OCR 系統幾乎用上了整條學習路徑的每一個點子,並把它們組裝成一個真正出貨的產品。這裡沒有單一一個巧妙的模型;這裡有一條流程(pipeline),每個階段把輸出交給下一個階段,正是第一篇那種 古典辨識流程 的思維。

每個被切出的字形,最終都會交給一個分類器,把它的特徵對映到一個字元標籤——和本路徑各處看到的影像分類步驟相同。

影像分類流程示意圖:一張輸入影像被轉成特徵,再被指派一個類別標籤。

  1. 偵測與二值化:在頁面中找出文字區域,並把像素門檻化成「墨水對紙張」(白底黑字),通常先做光照校正,免得陰影被誤認成筆畫。
  2. 去傾斜:估計掃描頁面的些微旋轉,再把它轉正,讓文字行水平——常用前幾節的霍夫轉換來做,對主導的線角度投票。
  3. 切分:把頁面切成行、把行切成詞、把詞切成單一字元字形——用投影輪廓、間隙、或連通元件(相連墨水像素的群組)。
  4. 正規化:把每個字形重新縮放到標準框並置中,讓「A」不論原本字級多大,都在一個標準尺寸與位置下被比較。
  5. 抽取特徵:把每個正規化字形轉成一個描述子——分區的像素數、筆畫方向、甚至字形的 HOG——也就是第二篇的「描述」步驟。
  6. 分類:把描述子餵給 k-NN 或一個 SVM 影像分類器,判斷它是哪個字元——也就是第二篇的「決定」步驟。
  7. 後處理:用字典或語言模型清理原始字元字串,借助上下文修正分類器的錯誤。

看看有多少老朋友再次登場。對一個固定、已知的字型,辨識一個字形可以就是單純的模板比對(第一篇)——把每個候選字母模板疊上去、挑最吻合的那個。把一個滑動視窗掃過一行文字、在每個位置分類,正是把滑動視窗偵測(第三篇)套用到字元上。連通元件——對相連黑像素群做填充——是一種不需任何學習就能切出字形的快速方法。OCR 流程不是什麼新理論;它就是這整條路徑的工具箱,為了一個任務而被卡接在一起。

難的部分是一個貨真價實的雞生蛋蛋生雞:切分與辨識互相依賴。你沒辦法把詞完美切成字母,除非你知道那些字母是什麼;但你又沒辦法辨識那些字母,除非你先把它們切出來。相連的字母(「rn」看起來像「m」)、斷掉的筆畫、連字(ligature)都讓切分變得模稜兩可。更糟的是,字形在孤立時真的容易搞混:小寫「l」對數字「1」、大寫「O」對零「0」、「rn」對「m」。是上下文救了你——而這正是後處理語言模型的工作。

做這最後修正的標準工具是編輯距離(edit distance,Levenshtein 距離):把一個字串變成另一個字串所需的最少單字元編輯次數,而允許的三種編輯是插入(insertion)刪除(deletion)替換(substitution)。如果分類器把帶雜訊的字串「recognize」輸出成「rec0gnize」,字典詞「recognize」只差一次替換(編輯距離 1),而幾乎每個其他字典詞都遠得多;於是修正會挑「recognize」作為最近的合法詞。在一個句子裡,語言模型走得更遠,會權衡哪些詞可能接在哪些詞之後,所以「c0ld day」被修成「cold day」,而搞混的「l」/「1」則由「此處該是字母還是數字」來裁決。古典 OCR 就是這樣,把一串搖搖晃晃的逐字元猜測,變成乾淨、正確的文字。

古典辨識的成就 — 與它的天花板

退一步,誠實地看整條路徑。古典時代做對的事多得驚人,假裝不是這樣,既是糟糕的歷史也是糟糕的工程。它的方法是可解釋的:你真的可以把一個 Haar 特徵、一張特徵臉、一個 HOG 模板、或一個 DPM 根濾波器畫出來,看見系統對什麼有反應——毫無神秘。它們是資料高效的:模板比對或一個小 SVM 可以從少數幾個範例運作,有時甚至一個。它們又快又輕,這就是為什麼 Viola–Jones 能在 2001 年的網路攝影機上跑。而且它們是有數學基礎的——PCA、SVM 間隔、霍夫投票,全都建立在清晰、可分析的原理之上。

而且這些方法並沒有退休——它們就在你身邊。模板比對在製造產線上檢查電路板與藥丸,那裡零件固定、速度與可靠度比彈性更重要。霍夫轉換在駕駛輔助攝影機中找出車道線、在工業量規中找出圓。OCR 無所不在:支票處理、車牌辨識、文件數位化、你手機上的「掃描成文字」。當問題受限、而你又需要透明度時,古典的 辨識流程 往往仍是對的工具。

但確實有一道天花板,而這個領域撞上了它。這裡每一個方法都倚賴手工設計的特徵——是某個人決定,對這個特定問題而言,梯度(HOG)、或 Haar 矩形、或投影輪廓才是該量測的東西。那種手工設計很脆弱:為正面直立人臉調好的特徵,碰到側臉就失靈;為乾淨行人調好的 HOG,碰到嚴重遮擋、奇怪光照、運動模糊,以及真實世界那條長尾的變異就吃力。你可以繼續加更多部件、更多串接階段、用 空間金字塔比對 加更多空間分格——但報酬遞減。最具說服力的徵兆是 PASCAL VOC 的停滯:大約 2008–2011 年間,DPM 與其改良版穩居排行榜首,然而頭條準確率卻幾乎沒有逐年往上爬。整個社群只是在對著一面牆,把同一種模型磨得更亮。

通往深度學習的橋樑

我們很容易把這個故事講成一場斷裂——「2012 年,深度學習到來,舊方法被一掃而空」。那是錯的版本。真相是連續性:本路徑裡幾乎每個古典點子,都有一個直系後代活在現代網路之中,常常還在用新名字做著一模一樣的工作。理解古典方法,是讓深度方法不再像魔法的最便宜途徑。

  1. 滑動視窗 → 卷積本身。把一個濾波器滑過影像、在每個位置記錄它的響應,字面上就是卷積層在做的事;不固定裁切而密集地跑偵測器,就是全卷積偵測。
  2. HOG 與手工特徵 → 學出來的卷積濾波器。HOG 用人手量測梯度方向;CNN 的前幾層學出邊緣與方向偵測器,看起來驚人地相似——但是從資料訓練而來,再堆疊成愈來愈抽象的特徵。
  3. 詞袋特徵與空間金字塔 → 池化與全域池化。把局部描述子聚合、同時保留粗略空間佈局,演變成網路內部的最大/平均池化與全域平均池化。
  4. AdaBoost 串接與 NMS → 偵測後處理。串接「提早拒絕」的點子,以及合併重疊框的非極大值抑制,至今仍是現代偵測器的標準黏著劑。
  5. 特徵臉與子空間 → 學出來的嵌入。把一張臉投影到一個容易比對身分的低維空間,正是人臉辨識網路嵌入層在做的事——只是現在是學出來的,而非來自 PCA。
  6. 手工搭建的辨識流程 → 一條端到端學出來的流程。古典辨識流程 那些偵測 → 描述 → 分類 → 後處理的階段,被融成單一個網路,從像素到答案一起訓練。

所以這裡有一個要帶離本路徑的大師級觀點。2012 年的 AlexNet 並沒有發明一種全新的觀看方式、再把舊的扔掉。它保留了最深層的結構性點子——卷積即滑動模板比對、池化即聚合、從邊緣到物件的分層流程——而只改變了一件事:不再由人手設計特徵,網路改從資料中學出它們,端到端地學。古典時代花了數十年仔細設計「要量測什麼」;深度時代把這份設計自動化了。因此你在這五篇裡建立起來的一切,並不是過時的瑣碎知識——它是骨架,而深度方法是長在這骨架上的肌肉。

這就是那座橋。從這裡開始,學習階梯跨進深度學習的路徑:真正的卷積網路、現代的偵測器與分割器,以及更遠之處。你會與它們相遇,但不是把它們當成陌生人,而是當成你已經理解的 滑動視窗、濾波器、投票與流程的長大成熟之形。歡迎來到頂峰的另一側——這段下山路,是一座大得多的山的起點。