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

用「比對」來辨識:樣板、顏色與辨識流程

從所有辨識方法中最簡單的點子開始 — 把一張圖滑來滑去、看它在哪裡最吻合 — 並認識每個古典方法共用的那條流水線。

「辨識」一張影像到底是什麼意思?

你每天做上千次,卻渾然不覺。你瞄一眼照片,就知道那是海灘。你掃視擁擠的月台,瞬間就找到朋友的臉。你舉起手機,它解鎖了,因為它確信眼前這張臉就是你。這三件事都是「辨識」——把原始的光,變成一句對世界有把握的判斷——而讓電腦做到這些,正是整個學習路線的核心問題。在我們能造出任何會辨識的機器之前,得先把「辨識」這個詞講清楚,因為它其實偷偷藏了三份不同的工作。

第一份工作是分類(classification):這張影像裡是什麼?你用一個標籤回答整張圖——「貓」、「海灘」、「這是一個停止號誌」——就像把每張照片丟進恰好一本相簿。第二份是偵測(detection):什麼東西在哪裡?這時你得同時說出標籤與位置,在每個東西外面畫一個框——「這裡有個人,那裡有隻狗」——就像在數百張臉中找到朋友、並直接指出來。第三份是辨認與驗證(identification and verification):是哪一個特定個體?分類只在乎那是一張臉;辨認問的是這是誰的臉,而驗證問的是是非題「這和存檔的是同一個人嗎?」——這正是手機解鎖時做的事。同一張影像餵給這三份工作,會得到三種不同性質的答案。

最單純的分類:輸入一張影像,輸出一個類別標籤。

一張輸入影像流入辨識器,輸出單一類別標籤(例如「貓」),用來說明影像分類。

那麼,要怎麼親手打造一個辨識器?本篇接下來會從零開始,造出最簡單的那一個——簡單到你用一張描圖紙就能做——並鋪陳出整個學習路線每種方法都會接上去的共用生產線,也就是古典辨識流程。我們會清楚看見它在哪裡管用、在哪裡崩潰,以及這些裂縫為什麼正是後面每一篇指南存在的理由。

古典辨識流程

幾乎每一個古典辨識器都像一條工廠生產線:原始影像從一端進來,一連串工作站各自做一件小而明確定義的工作,再把結果傳給下一站。這個共用藍圖就是古典辨識流程。一旦你看懂它,這個學習路線裡的每種方法就不再像零散的把戲,而是同一個工作站上的不同選擇。我們從頭到尾走一遍這條線。

  1. 原始影像——直接來自相機或檔案的那張像素數字網格。
  2. 前處理——把它整理乾淨:縮放到標準尺寸、去除雜訊、把光照正規化,讓同一個東西的兩張照片一開始就具有可比性。
  3. 特徵擷取——把大量像素轉成一個精簡的描述子,留下重要的東西(邊緣、顏色、紋理),丟掉不重要的。
  4. 表徵——把那個描述子打包成一個固定長度的向量:對每張影像都是長度相同、有順序的一串數字,這樣才能互相比較。
  5. 比對器或分類器——把那個向量拿去和存好的範例、或和學到的規則比較,為每個候選答案算出一個分數。
  6. 決策——挑出勝出的答案(並判斷你是否有足夠把握真的下這個結論)。
  7. 後處理(可選)——把輸出整理乾淨:合併重疊的偵測框、在影片上把結果平滑化、剔除低信心的判斷。

每個工作站憑什麼佔一席之地?看一個具體任務:辨識輸送帶上產品印著的公司商標。少了前處理,在黃色倉庫燈下拍的產品看起來會和你的參考照天差地遠,把下游全部拖垮。少了特徵擷取,你就被迫直接比對數百萬個原始像素——又慢,又會被最微小的位移騙倒。特徵的重點在於摘要:商標獨特的轉角與紅色被保留,而無關的背景雜訊被丟掉。表徵必須是固定長度的向量,因為比較規則需要每個輸入有相同的形狀——你沒辦法用一串 900 個數字去減一串 12 個數字。比對器是真正進行辨識的地方,而決策則保護你不會對著一個空白箱子自信地大喊「商標!」。每個工作站存在,都是為了讓下一站的工作成為可能。

有了這個鷹架,整個學習路線就變成一場巡禮,看的是「特徵」與「比對器」這兩站的各種高明選擇。最簡單的那個選擇——也就是我們接下來要造的——幾乎沒有特徵階段:它直接比對原始像素。那就是樣板比對;它是絕佳的起點,因為它的優點與它驚人的失敗,會推動之後每一次的改良。

樣板比對:滑動一張圖、為吻合度打分

想像你手上有一張小小的透明描圖片,上面印著你要找的東西——譬如那個公司商標。你把它疊在一張大照片上滑來滑去。在每個位置你都問:這張描圖片和它底下的東西對得有多齊?重疊得最好的地方,就是商標所在之處。這就是樣板比對的全部想法。那張存好的描圖片叫做樣板(template);那張大照片叫做搜尋影像(search image);而「對得有多齊」是一個我們要算出來的數字,叫做相似度分數(similarity score)

記住:樣板和影像都只是數字網格,所以「把它們對齊」其實是逐格比較數字。

一張小影像以網格呈現,每格存著一個亮度數字,強調影像就是一張數字表格。

  1. 存下樣板:一小塊像素——你要找的東西的圖。
  2. 把樣板放在搜尋影像中的一個候選位置,讓它的左上角對齊那個點。
  3. 在那裡算出一個相似度分數,做法是把樣板的像素和它底下的影像像素相比。
  4. 移動一個像素再重複——掃過每一個可能的位置,建出一整張分數地圖。
  5. 回傳分數最佳的位置:那就是樣板吻合得最好的地方。

注意第 2、4 步到底是什麼:把樣板放在每一個位置並各自打分,這正是一個滑動視窗。我們把一個固定大小的視窗滑過整張影像,並在每一處評估它。記住這個說法——在第 3 篇,滑動視窗會成為即時人臉偵測的骨幹,唯一改變的只是「我們在視窗裡算什麼」。在這裡,我們在視窗裡算的是最簡單的東西:直接的像素比較。讓我們把「對得有多齊」講精確。這是你的第一個視覺公式,所以我們會放慢腳步。

\mathrm{SSD}(u,v)=\sum_{x,\,y}\bigl[\,I(u+x,\,v+y)-T(x,y)\,\bigr]^{2}

平方差總和——最直接的吻合分數。

把它讀成:「要為位置 (u,v) 的吻合度打分,就看樣板的每個像素,找出它和正坐在它底下的影像像素之間的差距,把差距平方,再把所有平方加起來。」逐個符號看:I 是搜尋影像,所以 I(u+x, v+y) 是某個影像像素的亮度;T 是樣板,所以 T(x,y) 是相對應的樣板像素。(u,v) 這一對是候選位置——我們把樣板左上角放在這裡。(x,y) 這一對是樣板內部的偏移量,會走遍樣板的所有像素,而那個大大的 Σ 表示「對所有這些偏移量求和」。因為差距被平方,每一項都是正的(不對的地方不會互相抵消),而的 SSD 代表的吻合——0 就是像素級完全重疊。平方還會讓少數幾個大誤差受到的懲罰遠大於許多小誤差,所以一塊嚴重出錯的區域就會毀掉整個分數。小例子:一個 2×2、全是 10 的樣板,疊在影像區塊 [[12, 9],[11, 10]] 上,得到差距 2、-1、1、0,平方後 4、1、1、0 加總為 SSD = 6——幾乎完美吻合。把它滑到一塊全是 50 的明亮區塊上,差距各是 40:SSD = 4×1600 = 6400。這個巨大的數字在大喊「不是這裡」。

\mathrm{NCC}(u,v)=\frac{\displaystyle\sum_{x,\,y}\bigl(I(u+x,\,v+y)-\bar I_{u,v}\bigr)\bigl(T(x,y)-\bar T\bigr)}{\sqrt{\displaystyle\sum_{x,\,y}\bigl(I(u+x,\,v+y)-\bar I_{u,v}\bigr)^{2}\;\sum_{x,\,y}\bigl(T(x,y)-\bar T\bigr)^{2}}}

正規化互相關——對亮度與對比都免疫的吻合分數。

SSD 有個致命缺陷:把燈調亮,每個影像像素都升高(譬如)30,於是每個差距都暴增,SSD 便宣告「不吻合」,即使那根本是同一個場景。正規化互相關(NCC)用兩步解決它。第一步,減去平均值:Ī(u,v) 是目前視窗底下那塊影像的平均亮度,T̄ 是樣板的平均亮度。各自減去平均值,就丟掉了整體亮度,只留下明暗的樣式——相對的高低起伏。第二步,大小正規化:分母是(該影像區塊的總平方變化量乘上樣板的)開根號,把兩者都重新縮放成單位大小,於是對比也不再要緊。如此一來分子就是純粹的對齊程度:當樣板裡的亮點對上影像裡的亮點、暗點對上暗點時,它又大又正。因為有正規化,NCC 永遠落在 −1 與 1 之間:值為 1 是完美吻合(樣式相同,亮度或對比任意),0 表示無關,而 −1 表示完美的照片負片。具體地說,把我們先前那塊區塊整體調亮 30——每個像素與平均值一起上升,減法剛好抵掉這個 +30,NCC 仍然是 1,宣告了那個 SSD 會錯失的吻合。

把一個小視窗滑過較大的影像、並在每一站算出一個值,正是樣板比對(以及之後的卷積)共通的運作機制。

一個小視窗一步步滑過較大的影像網格,在每個位置產生一個輸出值。

# Template matching by sum of squared differences (SSD).
# I: the search image (H x W); T: the template (h x w). Grayscale for clarity.
def template_match_ssd(I, T):
    H, W = I.shape
    h, w = T.shape
    best_score = float("inf")        # SSD: smaller is better, so start as high as possible
    best_loc = (0, 0)
    # Sweep the template's top-left corner over every valid location (u, v).
    # This double loop IS the sliding window.
    for v in range(H - h + 1):
        for u in range(W - w + 1):
            patch = I[v:v + h, u:u + w]     # the window under the template
            diff = patch - T               # per-pixel difference
            score = (diff * diff).sum()    # sum of squared differences
            if score < best_score:
                best_score = score
                best_loc = (u, v)          # remember the best fit so far
    return best_loc, best_score            # where (and how well) the template fits best
整個辨識器只要十幾行:滑動、打分、留下最好的。

為什麼純樣板比對會崩潰

那個小小的辨識器是真實而且確實有用的——拿來在螢幕上找一個固定的圖示、把同一份文件的兩次掃描對齊、或在生產線上找出已知的零件,它又快又可靠。但只要世界被允許稍微有點變化,它就崩潰,而它崩潰的每一種方式,都是指向這個學習路線後面某篇指南的路標。我們誠實面對這些失敗模式。

尺度(scale)是第一個殺手。假設你的樣板寬 60 像素。如果物件因為離相機較近而出現成 90 像素寬,像素就完全對不上了,分數崩盤——即使那明明是同一個物件。旋轉與視角(rotation and viewpoint)一樣殘酷:把物件傾斜 30 度,或從側面而非正面拍同一張椅子,逐像素比對就支離破碎。一張平面描圖片根本不知道「旋轉或縮放後的物件」是「同一個」。常見的補救是保留很多樣板——好幾種尺寸、好幾個角度——全部試過,但成本爆炸,而且你永遠存不下所有可能性。

光照(lighting)算是部分被馴服了——NCC 已經對整體亮度變化與整體對比變化聳聳肩不在乎。但它救不了你應付一道陰影斜落在物件一半上、一盞有色光投出橘色色偏、或是一塊光滑的高光反光。這些改變的是亮度的樣式,而不只是整體的水準,而樣式正是 NCC 所倚賴的。遮擋(occlusion)更糟:把物件一部分藏在一個咖啡杯後面,許多樣板像素現在坐在杯子上,注入了大量誤差,把任何逐像素分數往下拖——即使物件大部分都明明白白看得見。

但最深層的失敗——那個終結整個樣板比對美夢的——是類別內變異(intra-class variation)。一個樣板是某一個特定東西的照片。某一張特定椅子的樣板,運氣好的話,能再找到那張一模一樣的椅子。它永遠無法把椅子當成一個類別來辨識,因為辦公椅、高腳凳、扶手椅與懶骨頭沙發幾乎沒有共同的像素。臉、貓、或手寫的 7 也一樣:一個類別是一大片外觀的雲,沒有任何單一存檔範例坐落在它的正中央。樣板比對只跟一個範例、而且只跟那一個範例比較。

用顏色來辨識:色彩直方圖

樣板比對執著於每個像素在哪裡,這正是為什麼一個小位移或旋轉就毀了它。所以我們試試相反的極端,把位置整個丟掉。不要問顏色在哪裡,只問每種顏色有多少。一張成熟番茄的照片大部分是紅色、帶一點綠,不管番茄在畫面裡坐在哪、或轉向哪邊。這份「每種顏色有多少」的計數就是色彩直方圖(colour histogram)——把它想成影像的色彩指紋——而比對指紋正是色彩直方圖比對的核心。

一張彩色影像是三層疊起來的網格——紅、綠、藍。直方圖統計的是每個顏色值在這些網格中出現的頻率。

一張彩色照片被分離成紅、綠、藍三個通道並排呈現。

建一張直方圖只是簡單的計數。回想一下,每個彩色像素是三個數字——紅、綠、藍,各為 0 到 255。我們不想為全部 1670 萬種可能的顏色各設一格,所以要分箱(bin):把每個通道切成(譬如)8 個粗略的範圍,得到 8×8×8 = 512 個顏色箱。接著我們走過每個像素,判斷它的顏色落在哪個箱,就把那個箱的計數加一。最後把每個計數除以總像素數,讓所有長條加起來等於 1——這樣不同尺寸的影像就可以比較了。回報是自動的不變性:因為我們只數顏色、從不記錄位置,把物件滑過畫面、旋轉它、或稍微把它弄彎,都完全不會改變直方圖。那個曾經弄垮樣板比對的東西,在這裡根本不會被察覺。

弱點正是那個強處的鏡像:因為丟掉了位置,直方圖分不出「藍底上的紅方塊」和「紅底上的藍方塊」,而共用同一種紅的桌布與番茄也可能被混淆。它對光照(illumination)也很敏感——改變打光,顏色本身就會換箱,把指紋打亂。標準的補救是在一個「把顏色與亮度分開」的色彩空間裡計算直方圖(例如用色相,或紅/綠比值),讓指紋撐過較暗的房間。儘管有這些限制,色彩直方圖驅動了整整一代實用系統:以內容為基礎的影像檢索(image retrieval)(「幫我找更多像這張的圖」)與以顏色為基礎的追蹤(tracking),其中 CamShift 演算法靠著一幀幀追逐物件的直方圖,在影片裡跟著一個有色物件跑。要用上這一切,我們還需要一樣東西:一種為「兩份指紋有多像」打分的方法。

\cap(H_{1},H_{2})=\sum_{i}\min\!\bigl(H_{1,i},\,H_{2,i}\bigr)

直方圖交集——兩張影像共有的顏色量。

把它讀成:「一箱一箱地看,取兩張直方圖中在那一箱裡比較少的那一個,再把所有這些最小值加起來。」逐個符號看:H₁ 與 H₂ 是我們要比較的兩張正規化直方圖(各自加總為 1);i 是箱的索引,走遍所有顏色箱;H₁,ᵢ 與 H₂,ᵢ 是各自第 i 箱的高度。為什麼取最小值?因為它衡量的是重疊。在每一箱裡,共有的量不可能超過兩者中較小的那個——就像把兩疊硬幣倒進同一欄,只保留兩者都堆得到的那一段。對所有箱求和,就得到總共有的顏色:結果落在 1(指紋相同——每一箱都剛好吻合)到 0(完全不相交——只要一邊有顏色,另一邊就沒有)之間。實作例子:H₁ = [0.5, 0.3, 0.2]、H₂ = [0.4, 0.4, 0.2]。各箱的最小值是 0.4、0.3、0.2,加起來是 0.9——兩張影像共有 90% 的顏色內容,是強烈的吻合。

\chi^{2}(H_{1},H_{2})=\sum_{i}\frac{\bigl(H_{1,i}-H_{2,i}\bigr)^{2}}{H_{1,i}+H_{2,i}}

卡方距離——一種對稀有顏色給予公平權重的差異度量。

交集打的是相似度分數(越大越好);卡方打的是距離分數(越小越好,0 表示完全相同)。把它讀成:「對每一箱,把兩張直方圖之間的差距平方,再除以那一箱總共裝了多少顏色,然後加起來。」符號 H₁、H₂、i 的意思和先前相同;新花樣是分母 H₁,ᵢ + H₂,ᵢ,也就是那一箱的合計高度。為什麼要除以它?如果不除,一個巨大箱裡 0.1 的差距(兩張影像都譬如有 50% 的紅)會和一個微小箱裡 0.1 的差距(一張影像有一抹稀有的紫、另一張完全沒有)算成一樣。但那個稀有顏色其實訊息量大得多——它正是把兩張本來很像的圖區分開來的獨特細節。除以箱的合計高度,會放大這些又小又稀有的箱的貢獻,於是卡方會去注意那些有鑑別力的顏色,而不會被「碰巧淹沒整張圖的那個顏色」主宰。這就是為什麼檢索系統常常偏好它,而不用單純的平方差。

import numpy as np

def color_histogram(img, bins=8):
    # img: H x W x 3 array of uint8 RGB values (0..255).
    # Quantize each channel into `bins` coarse levels, then combine the three
    # channel indices into one bin number per pixel.
    q = (img.astype(int) * bins) // 256                 # each channel -> 0..bins-1
    idx = (q[..., 0] * bins + q[..., 1]) * bins + q[..., 2]
    hist = np.bincount(idx.ravel(), minlength=bins ** 3).astype(float)
    return hist / hist.sum()                            # normalize so the bins sum to 1

def intersection(h1, h2):
    return np.minimum(h1, h2).sum()                     # 1.0 = identical, 0.0 = disjoint

def chi_square(h1, h2):
    denom = h1 + h2
    safe = denom > 0                                    # skip empty bins (avoid 0/0)
    return ((h1[safe] - h2[safe]) ** 2 / denom[safe]).sum()   # 0.0 = identical
先建出一份色彩指紋,再用兩種不同方式比較兩份指紋。

從比對走向學習

退一步,注意樣板與色彩直方圖的共通點。兩者都是比對的形式:要找到某樣東西,你必須已經擁有你要找的那個確切的東西——一張存好的描圖片、一份存好的指紋——然後拿新影像去和那唯一的存檔範例比較。它們坐在同一條古典辨識流程上,差別只在於擷取什麼特徵、計算什麼分數。而兩者也共用我們已經撞到兩次的同一道天花板:單一存檔範例,給不了你一套有原則的方法去跨越整個類別的自然變異而推廣。給系統看一張它從沒見過的椅子,比對就無從比起。

這就引出了推動下一篇的問題:如果我們不拿每張新影像去和一個存檔範例比較,而是蒐集很多有標籤的範例——數百張椅子、數百張不是椅子的——然後讓電腦從整批資料中出一條決策規則呢?那麼「椅子性」就不再是一張圖;它是特徵空間裡由許多範例一起圈出的一塊區域。我們先前苦於無法原諒的那些變異,反而成了規則賴以建立的證據本身。