偵測 vs 追蹤:為何「跟隨」是獨立的問題
在前幾篇導覽中,我們學會了從原始像素讀出運動——先觀察亮度如何從一個影格移動到下一個影格,再用光流精確測量那段位移。現在我們要問一個不同、卻非常「人性」的問題:不只是「哪裡有運動」,而是「我盯著的是哪一個東西,而那同一個東西跑到哪去了?」這正是物件追蹤要做的事。在第一個影格鎖定一個目標,然後一格接著一格地盯緊它,即使它移動、轉身、變小,或短暫躲起來也不放手。
我們很容易以為追蹤只是「每一格都重新偵測一次」。偵測器回答的是「這一格裡有什麼物件、在哪裡」——它每次都從零畫框,對過去毫無記憶。追蹤要回答更難的問題:「這個框,跟我剛剛看到的是同一個物件嗎?」偵測關乎「存在」,追蹤關乎「在時間中的身分」。在熱鬧街道上跑一個行人偵測器,每一格都給你一堆全新、匿名的框;而追蹤的工作,就是把這一格的第 7 號框,連到上一格的第 7 號框,讓你能說「那還是同一個行人」。
追蹤值得獨立成章,有三個具體理由。身分(Identity):下游任務——數車流、分析球員的跑位、在連續掃描影像中追腫瘤——全都需要一個始終貼在同一物件上的一致 ID,而單靠逐格偵測永遠給不了你這個。效率(Efficiency):如果我知道物件上一格在哪、又大致知道它怎麼動,我只需在它的「預測位置」附近搜尋一小塊區域,而不必對整張影像重新偵測——更省、更快。穩健性(Robustness):偵測器會漏東西、會閃爍;好的追蹤器能靠著它已知的運動資訊,撐過一次漏偵或短暫遮擋。
兩個重疊的矩形,標示出它們的交集區域與聯集區域,用以說明 IoU 比值。
本篇大部分篇幅聚焦於單物件追蹤:你在第 1 格指出一個目標(由你或偵測器畫出的框),追蹤器自此一路跟隨。把這件事做扎實後,我們再擴大到人群。而貫穿一切、值得現在就點名的,是一個反覆出現的張力:外觀模型(appearance model)(物件看起來如何——它的顏色、紋理、學到的樣板)對上運動模型(motion model)(物件應該在哪——依它一路的移動方式,它將往哪去)。最好的追蹤器會融合兩者,而接下來四節,實質上就是「如何結合兩者」的四種答案。
均值移位:追逐外觀
我們的第一個追蹤器完全倚靠「外觀」這一側。這個想法稱為均值移位追蹤,而且具體得令人愉快。在第 1 格,純粹用顏色直方圖來概括目標——數一數它的像素有多少偏紅、多少偏藍,以此類推,分進數十個顏色桶(bin)裡。那張直方圖就是目標的外觀指紋。注意:我們丟掉了每個顏色「位在框內哪裡」,只保留「每種顏色有多少」;正是這一點,讓這個方法能容忍目標彎曲、旋轉或變形。
到了下一格,我們建立一張反投影圖(back-projection map):對每一個像素,去目標直方圖裡查它的顏色,問「這個顏色有多像目標?」目標擁有很多的那些顏色,像素就得到高分;目標從未出現過的顏色,分數幾乎為零。結果是一張灰階「熱力圖」,在目標顏色密集之處發亮。如此一來,追蹤就化約成一個乾淨的問題:這張熱力圖裡最亮、最密的那一團在哪?我又該如何「爬」過去?
均值移位就是那條「爬坡規則」,而它是一個通用的尋峰(mode-seeking,找眾數)程序——本身與追蹤毫無關係。把它想成在熱力圖地形上放一顆球:均值移位讓它往最近的山峰(也就是「眾數/mode」,密度最高之處)滾上去。一步是這樣運作的:
- 把搜尋視窗(一個小框)放在目標上一格所在之處。
- 在視窗內,計算加權質心——對像素位置取平均,並以每個像素在反投影圖上「有多像目標」作為權重。
- 把視窗移動,讓它的中心落在那個質心上。
- 重複。當視窗停止移動(質心已經就在中心)時,你便抵達峰頂——那就是新的目標位置。
均值移位向量:從目前的視窗中心該往哪一步。
讓我們慢慢讀。\mathbf{x} 是目前的視窗中心(一個 (x,y) 位置)。\mathbf{x}_i 是視窗內各像素的位置。每個 w_i 是該像素的權重——也就是它在反投影圖上的值,亦即它的顏色與目標的吻合強度。分式 \frac{\sum_i w_i\,\mathbf{x}_i}{\sum_i w_i} 是像素位置的加權平均:它就是質心,會被拉向目標顏色像素最「重」的地方。減去 \mathbf{x},便把這個目標位置轉成一個位移——該往哪個方向、走多遠。所以 m(\mathbf{x}) 直白地說:「往目標顏色像素集中的地方移動。」反覆迭代,直到 m(\mathbf{x})\approx 0,意即你已經坐在峰頂上。
一個小小的算例就能讓它「咔」一聲懂了。假設視窗裡只有三個像素,位置為 \mathbf{x}_1=(0,0)、\mathbf{x}_2=(10,0)、\mathbf{x}_3=(20,0),權重為 w_1=1, w_2=1, w_3=8(最右邊那個像素的顏色非常像目標)。加權質心為 \frac{1\cdot0 + 1\cdot10 + 8\cdot20}{1+1+8} = \frac{170}{10} = 17。若目前中心 \mathbf{x} 在 10,位移就是 17-10=+7:視窗向右跳,朝目標顏色密集的那一團移動,正如直覺所要求。
均值移位的長處是快與簡單:每格只需少少幾次廉價迭代,不必訓練,且當物件彎曲或旋轉時表現從容(直方圖幾乎不變)。它的弱點則直指後面幾節。它應付不了尺度變化——固定大小的視窗無法在物件靠近鏡頭時跟著放大(CAMShift 變體修補了這點)。它也可能被與目標共享顏色的背景雜亂所欺騙:若一輛紅車駛過你穿紅衣的目標,熱力圖會在兩處發亮,視窗可能滑向錯的峰頂。光靠外觀並不夠——我們需要一種「物件應該在哪」的感覺。運動模型登場。
卡爾曼濾波器:先預測,再校正
這裡是整篇導覽的觀念核心,所以在碰任何一個矩陣之前,讓我們先把直覺建起來。想像每一格的處境。關於物件在哪,你有兩條線索。線索一:一個量測(measurement)——比方說偵測器給的邊界框。它大致正確,但會抖動;框一格往這邊抖一像素、往那邊抖兩像素。線索二:一個運動模型——根據物件一路的移動方式,預測它應該在哪。若球上一格往右移了 5 像素,「等速度」模型就賭它這次也大約往右移 5 像素。兩條線索都不完美。卡爾曼濾波器正是把它們融合成「比任一單獨線索都更好」之估計的配方。
濾波器的祕訣在於:它追蹤的不只是估計值,還有它的不確定性(uncertainty)——它有多有信心,以「散布」的形式表示(想成估計值周圍一團模糊的雲,沒把握時雲很寬,有把握時雲很緊)。這裡的等速度預測,正是一個運動模型,與我們在建模像素如何移動時遇到的運動估計是同一類想法;而卡爾曼濾波器,就是把那個運動模型,包進一套「對信任度的有原則記帳」之中。
整個濾波器就是每格重複的兩步迴圈。預測(Predict):用運動模型把目前狀態往前推——「若它原本在這、以這個速度移動,下一格它應該到那。」關鍵是,預測時不確定性會變大,因為世界有雜訊、我們的模型也只是猜測。校正/更新(Correct/update):新的量測到來;把預測往它的方向輕推,而不確定性會縮小,因為我們剛學到了新資訊。藝術在於該推多少——而這由「相對信任度」決定。
校正(更新)步驟,以及控制它的卡爾曼增益。
讓我們一個符號一個符號地解碼。\mathbf{x}_{\text{pred}} 是預測狀態(運動模型認為物件在哪)。\mathbf{z} 是新的量測(偵測器給的框)。H 是把狀態映射到量測空間的矩陣——若狀態同時含位置與速度,但偵測器只看得到位置,H 就只挑出位置那部分。所以 H\,\mathbf{x}_{\text{pred}} 是「我們預測量測應該長什麼樣」,而 (\mathbf{z} - H\,\mathbf{x}_{\text{pred}}) 是創新項(innovation)——也就是驚訝,亦即預測到頭來錯得多離譜。接著我們把這份驚訝的一個比例 K,加回到預測上。
那個比例 K 就是卡爾曼增益——把它想成一個大約介於 0 與 1 之間的信任旋鈕。看看它是怎麼組成的。R 是量測雜訊(感測器有多不可靠),P_{\text{pred}} 是預測不確定性(我們對自己預測有多沒把握)。當感測器有雜訊(R 大),分母變大,K 縮向 0,更新便幾乎不離開預測——相信模型。當我們的預測很不穩(P_{\text{pred}} 大),分子變大,K 升向 1,更新便幾乎一路跳到量測——相信感測器。增益就是那位自動裁判,決定該信哪位朋友。
用一個數字找感覺(一維位置,省去矩陣):假設預測說物件在 x_{\text{pred}}=100,量測說 z=110,所以創新項是 +10。若我們同等信任兩者,增益算出來是 K=0.5,於是 x_{\text{new}} = 100 + 0.5\times 10 = 105——剛好落在中間。若感測器很吵以致 K=0.1,則 x_{\text{new}} = 100 + 0.1\times 10 = 101——我們幾乎不離開預測。若感測器被信任以致 K=0.9,則 x_{\text{new}} = 109——我們幾乎全盤接受量測。同一條式子,三種性格,全由一個旋鈕設定。
# One Kalman cycle per frame, sketched for a 'constant velocity' box tracker.
# State x = [cx, cy, vx, vy]: centre position and its velocity.
# F = motion model (advance position by velocity). H = picks out [cx, cy].
def kalman_step(x, P, z, F, H, Q, R):
# --- PREDICT: coast forward with the motion model ---
x_pred = F @ x # where it SHOULD be next frame
P_pred = F @ P @ F.T + Q # uncertainty GROWS (Q = process noise)
# --- CORRECT: fold in the new measurement z (detector box centre) ---
innovation = z - H @ x_pred # the 'surprise': how wrong we were
S = H @ P_pred @ H.T + R # innovation covariance
K = P_pred @ H.T @ inv(S) # Kalman gain = the trust knob
x_new = x_pred + K @ innovation # nudge prediction toward measurement
P_new = (I - K @ H) @ P_pred # uncertainty SHRINKS after measuring
return x_new, P_new
# If z is missing this frame (occlusion / missed detection), skip CORRECT
# and just return x_pred, P_pred: the tracker 'coasts' on its motion model.帶走兩項收穫。第一,「量測缺席時靠預測滑行」這招(上面最後幾行)正是追蹤器撐過短暫遮擋的方法——我們會在最後一節回來談它。第二,卡爾曼濾波器是人群場景中天然的「每物件引擎」:在大規模的物件追蹤裡,你只要給每一條軌跡(track)一個自己的卡爾曼濾波器,預測每個物件會在哪,這樣就能把進來的偵測配到對的那一條。這正是第 5 節接手之處。
相關濾波器:每秒數百影格的追蹤
均值移位用直方圖描述目標;卡爾曼濾波器則對運動進行推理。第三個家族——相關濾波器追蹤,其著名成員為 MOSSE 與 KCF——走第三條路:它以「判別式」的方式學出一個小樣板。直覺很討喜。我們想要一個小濾波器,使得當你把它在搜尋區域上滑動(到處做相關)並讀出回應時,會在目標處剛好得到一個尖銳的峰值,而其他地方幾乎為零。這個濾波器是一個「在目標上發亮、在非目標處保持黑暗」的樣板。
回想 CNN 導覽裡的卷積——把一個小核(kernel)滑過影像,並在每個位置記下一個加權和。相關(correlation)是同一種「滑動並求和」的運算(只是核不翻轉的卷積);而相關濾波器,不過是一個學出來的核,任務是在目標上產生那一道明亮尖峰。聽起來目前還很昂貴:把濾波器滑過每一格的每一個位置,是大量的乘加運算。
一個小核在輸入網格上滑動,產生一張輸出回應圖,用以說明卷積/相關運算。
讓這些追蹤器快得驚人的魔術在此——在單一 CPU 上常達每秒數百影格。卷積定理(convolution theorem)告訴我們:影像域中的滑動相關,等價於傅立葉(頻率)域中一個簡單的逐元素相乘(element-wise multiply)。於是,與其把濾波器拖過數千個位置,你只需對影像塊做一次 FFT、一次逐元素相乘、一次逆 FFT——整張回應圖便一次到手。昂貴的滑動,變成廉價的逐點相乘。
套用濾波器(左)與以閉式解學出它(右)——兩者皆在傅立葉域中進行。
解碼符號:帽子(hat)表示「某物的傅立葉變換」,\odot 是逐元素相乘,上劃線(conj)是複數共軛(在頻率域做相關時一個無害的記帳細節)。左式中,\hat{X} 是影像塊的傅立葉變換;把它乘上 \overline{\hat{H}}(取共軛的濾波器),再取逆變換 \mathcal{F}^{-1} 回到影像空間——你便讀出回應圖,其最亮的像素就是新的目標位置。這一行,取代了整個滑動視窗的掃描。
右式是濾波器如何被學出的方法,而這才是真正的優雅。\hat{X} 同樣是影像塊,\hat{Y} 是期望輸出——我們想要的理想回應,一個以目標為中心、乾淨的高斯凸起。我們在解的是:「哪個濾波器 \hat{H},與這個影像塊做相關後,能產生那道完美尖峰?」在影像域這是個最小平方問題;在傅立葉域它塌縮成一個逐元素除法——一次解出,無需迭代。\lambda 是正則化:一個加在分母上的小數,讓我們永遠不會除以近乎零的值,使濾波器保持穩定。直覺上:「找出與目標相關後是單一乾淨峰值的濾波器」,靠 FFT 幾乎免費算出。
兩項精修與一個告誡。線上更新(online updating):目標的樣貌會漂移(它轉身、光線改變),所以濾波器每格都被稍微重新學一次,把新影像塊融進舊濾波器——它邊走邊適應。核技巧(KCF):KCF 不用原始像素,而是餵入更豐富的特徵(如 HOG 梯度),並用一個同樣快速的核化版本,大幅提升準確度。告誡則是老問題:固定大小的濾波器難以應付尺度變化與劇烈變形——當物件變大或扭曲,學到的樣板就不再吻合,追蹤器可能落後或滑脫。
多物件追蹤:人群中的身分
目前為止是一個目標、一個追蹤器。真實場景一次有數十個人、車或細胞,而我們需要每一個都掛著一致的 ID。這就是多物件追蹤(MOT),其主流範式是以偵測為基礎的追蹤(tracking-by-detection):每一格都跑一個物件偵測器,得到一組新的框,然後解真正的謎題——資料關聯(data association)——判斷這一格的每個偵測屬於哪一條既有軌跡。偵測給你的是匿名的框;關聯則是把對的 ID 蓋回每一個框上。
前一節的投資在這裡回收:每一條既有軌跡都帶著自己的卡爾曼濾波器,所以在我們看新偵測之前,就已經對每個被追物件這一格「應該在哪」有了一個預測。於是關聯變成兩份清單之間的配對問題——預測框(軌跡)與偵測框(觀測)。我們把每一種可能配對打分,放進一張成本矩陣(cost matrix):第 i 列 = 軌跡 i,第 j 行 = 偵測 j,每格 = 把它們配在一起會有多糟。一個自然的成本是幾何重疊,常常還會摻入外觀嵌入相似度(每個框一個學出來的特徵向量),讓身分在框短暫重疊時也能存活。
把交集除以聯集當作相似度,再轉成關聯成本。
讀法:A 是某軌跡的卡爾曼預測框,B 是一個新偵測框。\mathrm{area}(A \cap B) 是兩個矩形重疊的面積(交集);\mathrm{area}(A \cup B) 是它們合起來覆蓋的總面積(聯集)。它們的比值 \mathrm{IoU},對完全疊在一起的兩個框是 1,對毫不相觸的兩個框是 0——一個從 0 到 1 的乾淨相似度分數。因為配對器想最小化成本,我們把它翻過來:\text{cost} = 1 - \mathrm{IoU},於是極佳的重疊(IoU 接近 1)變成微小的成本(接近 0),毫無重疊則變成最大成本 1。
具體來說:假設某軌跡預測一個面積 100 的框,一個偵測面積也是 100,兩者重疊面積為 80。聯集是 100 + 100 - 80 = 120,所以 \mathrm{IoU} = 80/120 \approx 0.67,成本是 1 - 0.67 = 0.33——相當便宜、相當有把握的配對。一個只與預測重疊面積 10 的偵測,得 \mathrm{IoU} = 10/190 \approx 0.05,成本 0.95——幾乎肯定是不同的物件。把整張矩陣填滿這樣的成本,你就把指派問題乾淨地擺好了。
有了成本矩陣,我們想要的是最佳的一對一配對——把每條軌跡指派給至多一個偵測(反之亦然),使總成本最低。天真地試遍所有配對是階乘級、毫無希望的,但經典的匈牙利演算法(Hungarian algorithm)能在多項式時間內精確解出這個指派問題。這三件套就是經典管線 SORT = 卡爾曼預測 + IoU 成本 + 匈牙利配對:小巧、快速,且強得出人意料。
重疊的預測框與偵測框,標示交集與聯集區域,作為多物件追蹤的配對成本。
# SORT-style frame loop (pseudocode). Each track owns a Kalman filter.
for frame in video:
detections = detector(frame) # fresh, anonymous boxes
predictions = [t.kalman.predict() for t in tracks] # where each track expects to be
# Build cost matrix: 1 - IoU between every (track, detection) pair
cost = [[1 - iou(p, d) for d in detections] for p in predictions]
matches, unmatched_tracks, unmatched_dets = hungarian(cost, thresh=0.7)
for (t_idx, d_idx) in matches:
tracks[t_idx].kalman.update(detections[d_idx]) # correct with measurement
tracks[t_idx].keep_id() # identity preserved
for d_idx in unmatched_dets:
tracks.append(new_track(detections[d_idx])) # a new object appeared
for t_idx in unmatched_tracks:
tracks[t_idx].mark_missed() # coast on prediction;
# delete the track if it stays unmatched for too many framesSORT 的罩門是 ID 互換(ID switch):當兩條路徑交會、或一個物件遮住另一個時,一旦框疊在一起,光靠 IoU 分不清誰是誰,ID 便可能對調。DeepSORT 大幅修補了這點,它加入一個外觀描述子——每個框一個學出來的嵌入——讓關聯使用物件看起來如何(再辨識/re-identification),而不只是它位在哪。即使路徑交錯,嵌入仍認得「這是同一個穿紅外套的人」,把身分重新縫合。要誠實評斷這一切,這領域會報告 MOTA(整體準確度——同時懲罰漏偵、誤框與 ID 互換)與 IDF1(身分隨時間被保持得多一致)。我們會在收尾一節倚靠這些指標。
遮擋、漂移與誠實的評估
每一個追蹤器,無論多聰明,都會遇上同樣的現實難題,而所謂的行家,就是叫得出它們名字的人。第一個是遮擋(occlusion):目標溜到柱子、另一個人、或畫面邊緣的後面,有一段影格根本沒有量測。這正是預測/校正迴圈展現價值之處——沒有偵測可供校正時,卡爾曼濾波器繼續預測,讓框沿著最後已知的速度滑過這段盲區。它的不確定性雲團一路膨脹(沒有新證據),而當物件重新現身時,一個良好的 IoU 或外觀配對便把軌跡咬回去。對較長的消失,再辨識(re-identification)——把重現物件的外觀嵌入配回那條失聯軌跡——找回原本的 ID,而不是鑄一個新的。
第二個現實是漂移(drift)。每一格那點微小的定位誤差,單獨看很小,但誤差會累積;放任不管,框就會一點一點滑離目標,直到它在追背景。漂移對那些激進地更新外觀模型的追蹤器最嚴重:它們每一格都把框內的東西照單全收當成「目標」,所以只要框稍微歪了,它們就開始學背景——而一旦學了背景,就會去追背景。這正是著名的穩定性—可塑性兩難(stability-plasticity dilemma)。
一個結構性的區別,決定你能用哪些招。線上(因果)追蹤在影格抵達時就處理,只用過去——這是即時機器人、AR 與監控所要求的,本篇至此的一切都是線上的。離線(批次)追蹤手上握有整段影片,可以偷看未來的影格;對於事後分析(運動拆解、交通研究)這是天賜之物,因為看到一條路徑後來怎麼走,能讓你事後修正一時的曖昧,化解線上追蹤器當時只能用猜的 ID 互換。同一個問題,較鬆的限制,較好的準確度。
那麼你該動用哪一個追蹤器?一條把整篇導覽串起來的經驗法則:均值移位/相關濾波器(外觀驅動)在「單一目標、必須快速跟隨、手邊又沒有偵測器」時發光——想在 CPU 上跑到每秒數百格就用 KCF。卡爾曼濾波器(運動驅動)是你在「有雜訊量測、運動可預測」時的骨幹,也是人群場景中每條軌跡的引擎。以偵測為基礎的追蹤(SORT/DeepSORT)是「存在好偵測器」時多物件追蹤的預設——一旦 ID 互換開始造成困擾,就加上 DeepSORT 的外觀嵌入。誠實的後設心得:外觀擅長辨認,運動擅長預測,而最強的物件追蹤系統會融合兩者。
最後,用上一節的指標誠實地評斷追蹤器。MOTA 把漏偵、誤報與 ID 互換捲進一個準確度數字——但它可能美化一個「定位得好卻換錯身分」的追蹤器,所以務必同時閱讀 IDF1,它直接獎勵「讓每個身分在其整個生命中保持一致」。有了這些在手,你不只能打造一個追蹤器,更能用數字捍衛你的選擇。本軌道接下來,我們將從「跟隨一個已知物件」走向「辨認正在發生什麼」——教網路觀看影片並說出動作的名字——然後再走向密集的、逐像素的影片理解。