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

什麼是物件偵測?同時找出『是什麼』與『在哪裡』

認識如何為影像中每個物件畫出帶標籤的方框,並了解它為何遠比單純分類困難。

從『是什麼』到『是什麼又在哪裡』

在這個學習階梯較早的部分,你已經認識了 影像分類(image classification):你輸入一張影像,網路回傳一個標籤。在底層,這個標籤其實是一個長度固定的機率向量——例如 1000 個類別就有 1000 個數字——你只要讀出其中最大的那一個即可。關鍵在於:答案的「形狀」永遠不變。無論照片裡是貓、消防車,還是一碗湯,輸出永遠都是同樣長度的一個向量。它回答的問題很狹窄:『在我已知的類別裡,這張圖片主要是哪一樣東西?』

物件偵測 把標準提高了。它不再為整張影像給一個標籤,而是必須產生一個 清單(list),清單裡的每一項都是一個三元組:類別標籤(是什麼)、邊界框(在哪裡,用一個矩形表示)、以及信心分數(模型有多確定)。分類回答的是『這張照片裡有貓嗎?』;偵測回答的是『這裡有一隻貓、那裡有一隻狗、再過去那邊還有一台腳踏車。』它要一次把能找到的每個物件都「定位」並「命名」出來。

現在請注意那個讓偵測成為「本質上不同」問題的結構性差異。一張照片裡可能有 0 個物件(空蕩蕩的天空)、1 個物件(一張人像),或 50 個物件(擁擠的街道)。因此 輸出的數量並不固定——它取決於影像的內容,而網路事先並不知道。這一個事實,也就是「輸出數量會變動」,是貫穿整個 track 的主線。你接下來會遇到的幾乎每一個巧妙設計——錨框、查詢向量、非極大值抑制——存在的理由,都是為了應付「用一個固定大小的網路產生數量未知的答案」這個難題。

影像分類:一張影像進去,一個固定長度的機率向量出來。偵測會把這個概念延伸成一個長度可變、已定位且已標記的框清單。

一張照片進入神經網路,產生一張各類別機率的長條圖,最高的長條被標示為預測標籤。

邊界框:用座標描述的矩形

每個偵測結果中「在哪裡」這一部分,是由 邊界框 來承載的——它是一個緊貼著物件、與座標軸對齊的矩形。它是偵測的基本幾何元素:能完整框住物件的、最小的那個正立矩形。「與座標軸對齊」只是說它的邊是水平與垂直的(不會傾斜),這讓描述變得很簡單——四個數字就足以把它釘死。

在寫下那四個數字之前,我們必須先講好座標系統,因為這幾乎是每個初學者都會踩到的坑。在影像中,原點 (0,0) 位於 左上角。x 軸向右增加,這跟你預期的一樣——但 y 軸是 向下 增加的。這跟數學課堂剛好相反,數學裡 y 是向上的。所以位於 (x=10, y=5) 的像素是在影像的偏上方,而 y 增加會讓你往頁面 下方 移動。把這點記牢一次,上百個小 bug 就會消失。

\begin{aligned} &\text{corner form: } (x_1,\, y_1,\, x_2,\, y_2) \qquad \text{centre form: } (c_x,\, c_y,\, w,\, h) \\[4pt] &c_x = \tfrac{x_1 + x_2}{2}, \quad c_y = \tfrac{y_1 + y_2}{2}, \quad w = x_2 - x_1, \quad h = y_2 - y_1 \end{aligned}

邊界框的兩種標準參數化方式,以及如何從角點轉換成中心點+尺寸。

我們把它完整拆解一遍。角點形式(corner form) (x_1, y_1, x_2, y_2) 指定的是兩個對角:(x_1, y_1) 是左上角,(x_2, y_2) 是右下角。中心形式(centre form) (c_x, c_y, w, h) 則改成指定框的中心點 (c_x, c_y) 加上它的寬 w 與高 h。轉換式可以直接從幾何讀出來:中心是兩個角的平均,所以 c_x 是左右兩邊的中點,c_y 是上下兩邊的中點;寬 w = x_2 - x_1 是右邊離左邊有多遠,高 h = y_2 - y_1 是下邊在上邊之下多遠。因為 x_2x_1 的右邊,而(記得 y 向下增加)y_2y_1 的下方,所以 wh 算出來都會是 正數——出現負的寬或高就代表角點被弄反了,那是 bug,不是合法的框。

舉個具體例子。假設一張人臉位於一張 640×480 的影像中,左上角在 (x_1, y_1) = (120, 80)、右下角在 (x_2, y_2) = (200, 180)——這是角點形式。換算:c_x = (120+200)/2 = 160c_y = (80+180)/2 = 130w = 200-120 = 80h = 180-80 = 100。所以同一個框用中心形式寫就是 (160, 130, 80, 100):一個以 (160, 130) 為中心、80×100 像素的矩形。兩種描述指的是完全相同的矩形;它們只是同一個形狀的兩種語言。

為什麼網路通常預測中心形式,而不是兩個角點?有兩個原因。第一,這四個數字會變得更獨立、更有意義:『在哪裡』(中心)和『有多大』(尺寸)被乾淨地分開,網路可以先把框大致放對位置、再去微調它的大小,這正好對應偵測器實際運作的方式。預測兩個隨意的角點則會把這兩件事彆扭地耦合在一起。第二,尺寸天生是非負的、而且常常跨越很大的範圍,因此很適合搭配一些技巧(例如預測 \log w)來讓訓練更穩定——這是後面的指南會談的主題。

還有一個區別:像素座標 對上 正規化座標。到目前為止我們的數字都是原始像素(160、130……),它們綁定在某一個特定的影像尺寸上。比較好的做法常常是把每個座標除以影像的寬或高,把一切映射到 [0,1] 區間:我們的框會變成 c_x = 160/640 = 0.25c_y = 130/480 \approx 0.27w = 80/640 = 0.125h = 100/480 \approx 0.21。正規化的意思是『橫向走了 40% 的距離』而不是『進去 160 像素』,所以當你縮放影像時,同一個描述依然成立——而縮放正是網路一天到晚在做的事。它同時讓四個目標值都落在相似的、較小的尺度上,使它們更容易被學習。

在左上角為原點的座標系統中畫出的邊界框。(這張圖也預告了交集比聯集 IoU,第 2 篇指南會用它來為框打分數。)

影像中的一個物件被矩形框住,標示了左上角與右下角座標,並標出了它的中心點、寬與高。

一次做兩件事:定位與分類

仔細看一個偵測結果——單一個(類別、框、信心)三元組——你會發現它其實是兩個預測縫在一起。第一個是 定位(localization):算出框的座標。由於座標是連續的數值,這是一個迴歸(regression)問題——模型輸出像 c_x = 0.25 這樣的實數,並設法讓它們盡量貼近真正的矩形。第二個是 分類(classification):給定那個框裡的內容,判斷它是 什麼 物件,從已知的類別中選出一個。這一部分就是你已經學過的分類,只是套用在一個區域上、而不是整張影像。

還有第三個比較安靜、但偵測需要而分類從來不需要的輸出:信心分數(confidence),常稱為「物件性分數(objectness score)」。它回答一個近乎是非題的問題:『我有多確定這裡真的有一個物件,而不是空白的背景?』這之所以重要,正是因為第 1 節談到的「輸出數量不固定」問題。偵測器通常會提出許多候選框,而它必須能夠對其中絕大多數落在背景上的框說『這個是垃圾,忽略它』。信心分數就是它用來做這件事的旋鈕。模型常常會把物件性分數與類別機率融合成每個框一個數值,但在概念上它同時在問『這裡有東西嗎?』與『它是什麼?』。

負責輸出這些結果的網路元件稱為 偵測頭(detection head)。可以把網路想成兩部分:一個「身體」負責看影像、把它提煉成豐富的特徵;一個「頭」負責讀取這些共享的特徵,並針對它所考慮的每個位置,產生框的座標、以及 類別分數、以及 物件性分數。「頭」是接在共享特徵之後那個小型輸出模組的標準稱呼;在這裡它有兩個分支(一個迴歸分支負責框、一個分類分支負責標籤),兩者都取用同一份底層表徵,這很有效率——困難的視覺處理只做一次,然後被兩項工作共用。

請記住這一點,因為它是整個 track 的統一秘密:你接下來要研究的每一種偵測器——第 3 篇裡慢但仔細的兩階段 R-CNN 家族、第 4 篇裡快速的單階段 YOLO 與 SSD、以及第 5 篇裡無錨框與 Transformer 偵測器——外表看起來天差地遠,但它們最終都從偵測頭輸出同樣的東西:框、類別標籤、信心分數。各種 物件偵測 架構的差異在於「如何」產生與微調候選 邊界框,而不在於最終答案的形狀。

偵測頭讀取共享特徵後分成兩個分支:負責框座標的迴歸分支,與負責標籤的分類分支,外加一個信心/物件性輸出。

一張示意圖:共享特徵圖輸入到一個頭模組,分岔成框座標迴歸與類別分數分類兩種輸出。

偵測為何困難:輸出數量不固定的難題

值得把「偵測為何比分類難這麼多」完整盤點一遍,因為本 track 裡的每一個機制,都是對其中某一項困難的直接回應。如果你現在就理解這些問題,後面的解法就會讓你覺得是「理所當然」而不是「莫名其妙」。請記住這個比喻:偵測就像有人給你一張行數固定的表格,要你把照片裡的每一樣東西都列出來——但照片裡可能有三樣、也可能有三十樣,所以一張固定大小的表格根本裝不下這個任務。固定大小的網路與大小可變的答案之間的這個落差,正是幾乎所有麻煩的根源。

  1. 物件數量未知且會變動。影像裡可能有 0、1 或 50 個物件,而模型事先並不知道數量。一個輸出固定大小張量的網路,必須設法表示一個它無法預先得知真實長度的答案。
  2. 尺度變化極大。同一個類別可能出現得很小或很大——一個人在遠景裡可能只有 20 像素高,在近處卻有 2000 像素高——而模型必須兩者都偵測到,儘管它們在像素層級看起來非常不同。
  3. 遮擋與擁擠。物件會互相重疊、躲在彼此後面、或緊緊擠在一起,因此模型必須從局部、糾纏的線索推斷出完整的物件,並且還要把相鄰且接觸的物件分開。
  4. 嚴重的類別不平衡。任何影像中絕大多數區域都是背景,而不是物件。如果你把候選框灑滿整張影像,幾乎所有框都會落在空白區域,因此一個天真的模型只要懶惰地到處預測『背景』,就能得到不錯的分數。
  5. 配對/指派問題。當模型產生許多候選框時,我們必須在訓練時決定哪個候選框要『負責』哪個真實物件——以及在推論時哪些候選框是重複的、該被丟掉。把這套帳目算對,既微妙又關鍵。

令人安心的是:上面每一項挑戰,都有一個有名字的解法,後面的某一篇指南會把它完整講清楚。物件數量不固定,靠的是先鋪設一組固定的參考矩形,叫做 錨框(anchor box)(或在最新的模型裡,是一組固定數量、可學習的『物件查詢』),把『有幾個?』變成『這些固定格子裡哪些被觸發了?』。尺度變化由特徵金字塔(FPN)處理,它在不同解析度下分別尋找大物件與小物件。遮擋造成的重複框,由非極大值抑制來修剪。配對問題用一個叫做 IoU 的重疊度量來解決。而類別不平衡則由 focal loss 來對付,它防止容易的背景樣本淹沒掉稀有的真實物件。現在先別擔心細節——只要記住每一種痛點都有一個規劃好的解藥就好。

現代偵測器的整體樣貌

讓我們組裝出現代偵測器的鳥瞰圖——一副本 track 其餘部分會逐步填滿的骨架。它重用了你在先前 CNN tracks 裡已經學過的兩個概念。第一,一個 骨幹網路(backbone)(卷積網路)在影像上滑動已學習的濾波器,產生一張或多張 特徵圖(feature map):由特徵向量構成的網格,其中每個格子都摘要了原始影像的一小塊。第二,每個格子對輸入的視野就是它的 感受野(receptive field)——它真正能『看到』的那塊原始影像區域。一個較深的格子看到的區域較大,而這正是讓單一個格子能判斷『有一個物件的中心在我附近』的關鍵。

  1. 輸入影像。一張原始照片(例如 640×480×3)進入網路。
  2. 骨幹 CNN → 特徵圖。骨幹把影像提煉成一個緊湊、特徵豐富的網格,通常會有好幾種解析度,讓小物件與大物件都能被表示。
  3. 偵測頭 → 大量候選框。頭讀取特徵圖,吐出一大堆候選框,每個框都帶有類別分數與信心分數——候選框的數量遠多於真實物件的數量。
  4. 後處理(NMS)→ 留下最好的。非極大值抑制丟掉低信心與重複的框,為每個物件保留一個乾淨、互不重疊的框。
  5. 最終偵測結果。存活下來的就是答案:一個長度可變的(標籤、框、信心)三元組清單。
端到端的偵測器流水線:影像 → 骨幹 CNN → 特徵圖 → 偵測頭 → 後處理 → 最終的框。

一張水平的流水線示意圖:影像流經 CNN 骨幹進入特徵圖,接著偵測頭產生許多框,再由 NMS 修剪成少數幾個最終偵測結果。

特徵圖是一個由特徵向量構成的網格;每個格子摘要了輸入的一小塊,是擺放候選框的天然位置。

一張粗網格疊在影像上,其中一個被標示的格子,它的感受野被畫回到原始影像的某個區域。

# Bird's-eye pseudocode of a modern detector's forward pass.
# Every architecture in this track is a variation on these five lines.

def detect(image):
    features = backbone(image)          # CNN -> feature map(s)
    candidates = head(features)         # MANY boxes, each: (box, class_scores, objectness)

    # Turn raw outputs into scored detections
    dets = []
    for box, class_scores, objectness in candidates:
        label = argmax(class_scores)    # WHAT: pick the best class
        score = objectness * max(class_scores)  # confidence: object AND class
        if score > CONF_THRESHOLD:      # drop near-empty background boxes
            dets.append((label, box, score))

    # Remove duplicate boxes covering the same object (guide 2 explains NMS)
    final = non_max_suppression(dets, iou_threshold=0.5)
    return final                        # variable-length list of detections
同樣的五步驟骨架,用程式碼表示。兩階段、單階段與 Transformer 偵測器的差異都在 `head` 如何提出候選框——而不在這個整體形狀。

現在你有了一副骨架,而本 track 其餘部分就只是在填滿這條流水線的各個方塊。第 2 篇深入後處理與評估階段——我們如何衡量一個預測框好不好(IoU)、非極大值抑制 如何移除重複框,以及平均精度均值(mAP)如何為整個偵測器打分數。第 3 篇打開兩階段偵測器的頭,也就是仔細的 R-CNN → Faster R-CNN 系譜。第 4 篇介紹單階段偵測器(YOLO、SSD),它們一次掃描就產生所有框,並加上馴服背景不平衡的 focal loss。第 5 篇抵達現代前沿:無錨框偵測器與 Transformer 轉向,它們連候選框的產生步驟都重新思考。在這一切之中,請緊盯主線——每個設計是如何回應 物件偵測 輸出數量不固定的這個難題。