跟蹤實(shí)戰(zhàn):從原理到調(diào)參的完整指南)
簡(jiǎn)介面向計(jì)算機(jī)視覺(jué)方向畢業(yè)設(shè)計(jì)的一套完整目標(biāo)跟蹤源碼將YOLOv9檢測(cè)與DeepSORT多目標(biāo)跟蹤結(jié)合適合需要完成實(shí)時(shí)檢測(cè)跟蹤課題或?qū)W習(xí)多目標(biāo)關(guān)聯(lián)算法的學(xué)生與開(kāi)發(fā)者。壓縮包共8個(gè)文件約16.85MB含Python執(zhí)行腳本、Jupyter分步示例、類別映射表、依賴清單、環(huán)境配置與說(shuō)明文檔及2個(gè)演示gif等便于快速搭建環(huán)境并查看運(yùn)行效果。已有549人學(xué)習(xí)。源碼貫穿數(shù)據(jù)預(yù)處理、YOLOv9模型加載、卡爾曼濾波初始化、特征提取與相似度度量、逐幀跟蹤關(guān)聯(lián)及可視化后處理等關(guān)鍵環(huán)節(jié)配合Jupyter逐段演示可幫助理解檢測(cè)頭輸出如何進(jìn)入跟蹤器、如何利用重識(shí)別特征抑制ID Switch等問(wèn)題既能直接支撐畢業(yè)設(shè)計(jì)實(shí)驗(yàn)也能作為深入掌握檢測(cè)與跟蹤工程化實(shí)現(xiàn)的練習(xí)項(xiàng)目。該組合方案在實(shí)時(shí)性與精度之間取得較好平衡可擴(kuò)展到行人、車輛等常見(jiàn)目標(biāo)的持續(xù)跟蹤場(chǎng)景。1. YOLOv9DeepSort目標(biāo)跟蹤源碼先解釋為什么檢測(cè)和跟蹤要分開(kāi)看做過(guò)視頻目標(biāo)跟蹤的人多半都撞過(guò)這個(gè)畫(huà)面檢測(cè)框畫(huà)得挺準(zhǔn)但同一個(gè)目標(biāo)在連續(xù)幀里一會(huì)兒是ID 3過(guò)兩幀又變成ID 17剛穩(wěn)下來(lái)又消失。這不是檢測(cè)模型不行而是目標(biāo)跟蹤這一段沒(méi)接通。這套畢業(yè)設(shè)計(jì)級(jí)的YOLOv9DeepSort Python源碼就是用來(lái)解決這個(gè)斷點(diǎn)的。它用YOLOv9做檢測(cè)前端逐幀出框DeepSort做后端把框關(guān)聯(lián)成連續(xù)軌跡包里object_tracking.py能直接跑視頻YOLOv9_DeepSORT.ipynb適合分段調(diào)試configs和coco.names都備齊conda環(huán)境一鍵建。適合畢業(yè)設(shè)計(jì)、視覺(jué)課設(shè)以及想快速驗(yàn)證檢測(cè)加跟蹤效果的工程師。2. 檢測(cè)與跟蹤的分工邏輯YOLOv9出框DeepSort關(guān)聯(lián)成軌跡2.1 YOLOv9為什么適合當(dāng)檢測(cè)前端先分清兩件事檢測(cè)回答這一幀里有什么跟蹤回答上一幀那個(gè)目標(biāo)現(xiàn)在在哪兒。這套源碼選擇YOLOv9不是因?yàn)樗中露且驗(yàn)樗谒俣群吐z率之間取的平衡點(diǎn)比較穩(wěn)。YOLOv9沒(méi)在前向推理路徑上堆太多冗余模塊而是把重心放在backbone的梯度流動(dòng)上可編程梯度信息PGI在反向傳播時(shí)用輔助分支保住淺層特征GELAN塊又把跨層信息和高效計(jì)算拼到同一條鏈路。實(shí)際跑起來(lái)的感覺(jué)是人群、車輛這類COCO通用場(chǎng)景小目標(biāo)不輕易丟低置信度的真實(shí)物體也有機(jī)會(huì)被下一幀撈回來(lái)這對(duì)跟蹤是友好的。因?yàn)楦欉壿嬙夙槞z測(cè)漏一幀就得多等一個(gè)生命周期。工程上我更關(guān)心它的輸出形態(tài)YOLOv9的bbox帶著坐標(biāo)和置信度直接能轉(zhuǎn)給跟蹤器。這個(gè)源碼里檢測(cè)結(jié)果被整理成統(tǒng)一列表跟蹤端不關(guān)心你是否換了檢測(cè)器。如果一個(gè)算法封裝得干凈檢測(cè)和跟蹤分別是兩個(gè)函數(shù)錯(cuò)誤邊界清楚誰(shuí)出問(wèn)題一查就定位這是畢業(yè)設(shè)計(jì)最想要的工程結(jié)構(gòu)。很多人一上來(lái)就鉆進(jìn)網(wǎng)絡(luò)結(jié)構(gòu)其實(shí)對(duì)于復(fù)現(xiàn)這套源碼先把出框和關(guān)聯(lián)拆開(kāi)理解比背一堆結(jié)構(gòu)名有用得多。2.2 DeepSort的關(guān)聯(lián)鏈路預(yù)測(cè)、門(mén)控、匹配三步走DeepSort的核心鏈路可以拆成三步。第一步是卡爾曼濾波預(yù)測(cè)每個(gè)track維護(hù)一組狀態(tài)包括中心位置、長(zhǎng)寬比、高度和對(duì)應(yīng)的速度新幀到來(lái)前先按線性運(yùn)動(dòng)模型推算出這個(gè)目標(biāo)大概會(huì)在畫(huà)面的哪個(gè)位置。注意這一步預(yù)測(cè)的不是隨機(jī)猜測(cè)而是基于歷史狀態(tài)做時(shí)間更新所以當(dāng)目標(biāo)被遮擋、YOLO暫時(shí)沒(méi)有輸出時(shí)track還能靠預(yù)測(cè)值和后續(xù)觀測(cè)做關(guān)聯(lián)。第二步是門(mén)控新一幀的檢測(cè)框要和每個(gè)預(yù)測(cè)做比較運(yùn)動(dòng)馬氏距離離得太遠(yuǎn)的先淘汰外觀上用ReID網(wǎng)絡(luò)抽出一個(gè)embedding跟這個(gè)track的歷史特征隊(duì)列做余弦距離比較。第三步是匹配DeepSort把馬氏距離和余弦距離加權(quán)成綜合代價(jià)矩陣交給匈牙利算法一次性分配分配不完的檢測(cè)先暫存連續(xù)多幀沒(méi)有被匹配的track判定為丟失。為什么不能只做IOU匹配因?yàn)槟繕?biāo)一旦出畫(huà)再入畫(huà)位置跳變很大IOU直接歸零但ReID的外觀embedding還能保持相似。DeepSort訓(xùn)練ReID時(shí)用了度量學(xué)習(xí)約束同一目標(biāo)不同幀的embedding拉近不同目標(biāo)互相推遠(yuǎn)這是它比傳統(tǒng)顏色直方圖方法強(qiáng)的根本原因。這套源碼中ReID特征提取在helpers層預(yù)訓(xùn)練權(quán)重一并提供通用場(chǎng)景直接沿用沒(méi)問(wèn)題自定義數(shù)據(jù)集效果不好時(shí)需要把這個(gè)特征網(wǎng)絡(luò)換掉我在第4.3節(jié)講更換方法。還有個(gè)容易誤解的地方DeepSort并不是每幀都把檢測(cè)結(jié)果和所有track全量比較一遍。實(shí)際實(shí)現(xiàn)是先按已確認(rèn)track和未確認(rèn)track分兩級(jí)級(jí)聯(lián)匹配里先處理外觀穩(wěn)定、剛更新過(guò)的軌跡再去補(bǔ)匹配不上的。這樣做計(jì)算量小也能避免新track頻繁搶舊track的ID。幀循環(huán)里速度慢的瓶頸往往不在算法本身而在ReID推理和畫(huà)框。2.3 源碼包里的文件別只當(dāng)檔案看這套資源里不是只有一段訓(xùn)練腳本而是完整檢測(cè)跟蹤管道。拿到手建議先按職責(zé)拆開(kāi)別只盯著一個(gè)主腳本。壓縮包內(nèi)文件可以對(duì)應(yīng)到幾個(gè)層次data是輸入素材位configs和coco.names管配置object_tracking.py和helpers是核心conda.yml和requirements.txt是環(huán)境。我習(xí)慣先畫(huà)一張文件職責(zé)表排查時(shí)好定位。文件/目錄在管道中的職責(zé)排查時(shí)看什么object_tracking.py主流程讀幀、檢測(cè)、跟蹤、畫(huà)框輸出報(bào)錯(cuò)在上半段還是下半段YOLOv9_DeepSORT.ipynb分段演示檢測(cè)和跟蹤過(guò)程單段調(diào)試確認(rèn)哪一層出問(wèn)題helpers/DeepSort封裝、畫(huà)框和工具函數(shù)卡跟蹤邏輯就翻這里configs/模型路徑、尺寸、跟蹤參數(shù)調(diào)參基本都落在這層coco.namesCOCO 80類類別名稱類別和權(quán)重不匹配時(shí)先查它c(diǎn)onda.yml / requirements.txt依賴清單雙軌環(huán)境問(wèn)題先對(duì)齊這兩份文件名擺在這兒不是讓你背而是為了報(bào)錯(cuò)時(shí)不慌。比如哪天運(yùn)行object_tracking.py時(shí)提示找不到某個(gè)配置優(yōu)先看configs里路徑變量八成是相對(duì)路徑問(wèn)題而不是代碼崩了。把跟蹤器當(dāng)黑匣子扔著不管、只看最終畫(huà)框是這套源碼最容易被浪費(fèi)掉的部分。愿意打開(kāi)看一眼后面調(diào)參就是有依據(jù)的。2.4 單幀里的完整流動(dòng)從frame到track把上面概念落到一個(gè)幀循環(huán)里順序是這樣的OpenCV讀取一幀BGR圖像圖像被resize和歸一化后進(jìn)YOLOv9YOLO輸出原始預(yù)測(cè)經(jīng)過(guò)conf閾值和NMS過(guò)濾留下若干bbox每框從[x1,y1,x2,y2]轉(zhuǎn)成中心坐標(biāo)和寬高的xywh格式連同score和class拼成數(shù)組數(shù)組和原圖一起交給tracker.updatetracker內(nèi)部先卡爾曼預(yù)測(cè)再和檢測(cè)做匹配返回已確認(rèn)track的ID、坐標(biāo)和置信度最后把track結(jié)果畫(huà)到圖上并寫(xiě)進(jìn)輸出視頻。這個(gè)過(guò)程在源碼里就是object_tracking.py主循環(huán)并不需要你去動(dòng)網(wǎng)絡(luò)結(jié)構(gòu)。新手最容易懵的是為什么最終顯示的不是YOLO畫(huà)的框而是tracker畫(huà)的框。原因很簡(jiǎn)單顯示層要的是ID加框的整體結(jié)果所以源碼里畫(huà)圖函數(shù)取的是DeepSort的track輸出而不是檢測(cè)器的原始輸出如果你自己改代碼時(shí)畫(huà)了兩遍把ID疊加在檢測(cè)框上視覺(jué)上就會(huì)混亂。打開(kāi)helpers里的draw邏輯很快能看懂這一層。3. 跑通環(huán)境的三件事conda雙軌依賴、權(quán)重放位、兩種運(yùn)行入口3.1 conda.yml和requirements.txt到底該按哪個(gè)來(lái)這類視覺(jué)工程最常見(jiàn)的環(huán)境坑是兩份依賴清單各有各的版本主張。conda.yml通常把python版本和基礎(chǔ)包固定住requirements.txt則補(bǔ)齊了cv2、torch、munkres之類的依賴。常見(jiàn)做法是以conda.yml為準(zhǔn)建虛擬環(huán)境再用requirements.txt補(bǔ)裝。我建議的執(zhí)行順序是這樣的conda env create -f conda.yml conda activate yolov9_deepsort # 實(shí)際環(huán)境名以conda.yml里的name字段為準(zhǔn) pip install -r requirements.txt python -c import torch, cv2; print(torch.__version__, cv2.__version__, torch.cuda.is_available())第一行創(chuàng)建環(huán)境第二行激活第三行補(bǔ)pip依賴第四行是驗(yàn)證。最后一行最關(guān)鍵如果torch.cuda.is_available()返回False說(shuō)明GPU檢測(cè)沒(méi)生效。這時(shí)候別急著往下跑視頻否則模型推理會(huì)在CPU上慢慢爬你會(huì)以為是源碼性能差其實(shí)只是環(huán)境問(wèn)題。常見(jiàn)做法是停在這里按機(jī)器CUDA版本重裝對(duì)應(yīng)torch再跑一次同樣驗(yàn)證。3.2 權(quán)重文件和coco.names對(duì)齊才是模型能加載的前提源碼包本身是工程實(shí)現(xiàn)但YOLOv9預(yù)訓(xùn)練權(quán)重一般是單獨(dú)放的。拿到手后先建一個(gè)weights目錄把權(quán)重統(tǒng)一放進(jìn)去命名與configs里的變量保持一致。這一步遺漏最常見(jiàn)的報(bào)錯(cuò)就是加載模型時(shí)直接提示文件不存在或者給你一個(gè)看起來(lái)莫名其妙的open blob錯(cuò)誤。# 當(dāng)前目錄是工程根目錄先建權(quán)重目錄 mkdir -p weights # 權(quán)重準(zhǔn)備好之后放到配置里寫(xiě)好的路徑 cp ~/Downloads/yolov9-c.pt weights/ ls -lh weights/這段操作不是玄學(xué)而是為了讓相對(duì)路徑固定下來(lái)。我習(xí)慣把coco.names和weights都放在工程根目錄下不搞散落式路徑。排查別人代碼時(shí)卡在路徑上的時(shí)間往往比卡在參數(shù)上的還多。coco.names要跟預(yù)訓(xùn)練權(quán)重配套如果你自己訓(xùn)練的模型只有幾個(gè)類別卻仍然用80類的coco.names后面畫(huà)框和解析類別時(shí)就會(huì)對(duì)不上號(hào)。3.3 跑通第一段視頻先走最短命令別一上來(lái)就調(diào)參第一次跑不要開(kāi)一堆復(fù)雜參數(shù)就選一段5到10秒的短視頻把置信度和輸出路徑指定好其余用默認(rèn)。這是為了確認(rèn)管道是通的不是為了讓結(jié)果好看。命令行參數(shù)名在不同版本的object_tracking.py里可能略有差異運(yùn)行前先看一眼入口函數(shù)的parse_args或者執(zhí)行python object_tracking.py --help確認(rèn)這是最穩(wěn)的做法。我一般這么跑python object_tracking.py \ --source data/demo.mp4 \ --weights weights/yolov9-c.pt \ --conf 0.5 \ --output outputs/demo_result.mp4參數(shù)不復(fù)雜--source是輸入視頻--weights是檢測(cè)模型路徑--conf是檢測(cè)置信度閾值--output是結(jié)果保存位置。第一次跑就盯著終端里的FPS和track數(shù)量看FPS過(guò)低先考慮降分辨率或換tiny權(quán)重track數(shù)量一直是0說(shuō)明目標(biāo)還沒(méi)被確認(rèn)成軌跡要去看置信度和n_init這兩個(gè)參數(shù)。如果程序正常走完打開(kāi)結(jié)果視頻確認(rèn)里面每輛車的ID在連續(xù)幀之間沒(méi)有頻繁跳變這個(gè)工程就算立住了。3.4 notebook入口適合拆開(kāi)看但別依賴它做性能測(cè)試YOLOv9_DeepSORT.ipynb的作用是把整條管道拆成逐格步驟加載權(quán)重、跑單幀檢測(cè)、初始化tracker、循環(huán)視頻。如果直接跑object_tracking.py拋錯(cuò)我建議先在notebook里逐格執(zhí)行定位是加載段還是更新段的問(wèn)題。notebook的坑在于循環(huán)幀時(shí)顯示邏輯和保存邏輯會(huì)拖慢速度用它測(cè)出來(lái)的FPS不代表真實(shí)性能。所以我的習(xí)慣是排查用notebook性能驗(yàn)證用object_tracking.py兩條路徑互不替代。4. 調(diào)參實(shí)操置信度、NMS和DeepSort的max_age這樣配4.1 YOLOv9側(cè)的兩個(gè)截門(mén)conf和nms_iou檢測(cè)階段有兩個(gè)參數(shù)直接影響跟蹤質(zhì)量一個(gè)是conf一個(gè)是NMS的iou閾值。conf設(shè)得高框更干凈但容易漏檢設(shè)得低遮擋目標(biāo)能檢出來(lái)但會(huì)產(chǎn)生一堆碎框干擾跟蹤。這個(gè)取舍沒(méi)有萬(wàn)能數(shù)得按場(chǎng)景給。我自己的參數(shù)區(qū)間是這樣的參數(shù)常見(jiàn)區(qū)間場(chǎng)景判斷conf0.25 ~ 0.5人群、密集商城場(chǎng)景寧多勿漏conf0.5 ~ 0.7車輛稀疏場(chǎng)景要求框少而準(zhǔn)nms_iou0.45 ~ 0.7重疊目標(biāo)多時(shí)調(diào)低避免框互相吞并這張表只當(dāng)起點(diǎn)。血淚經(jīng)驗(yàn)是如果跟蹤結(jié)果里目標(biāo)走走停停斷斷續(xù)續(xù)先回頭查conf而不是一上來(lái)就動(dòng)DeepSort參數(shù)。因?yàn)楹芏鄑rack之所以被判定消失根本不是跟蹤器的錯(cuò)而是檢測(cè)階段壓根沒(méi)在這個(gè)目標(biāo)上輸出框。檢測(cè)端的漏檢跟蹤器背不了這個(gè)鍋。4.2 DeepSort側(cè)參數(shù)max_age、n_init和max_distDeepSort參數(shù)通常聚合在跟蹤器初始化的配置里字段名各有差異但邏輯上逃不開(kāi)幾個(gè)關(guān)鍵的。以下面這段常見(jiàn)配置為例我一般先不動(dòng)這些值跑完一輪再根據(jù)效果改# 開(kāi)源工程里很常見(jiàn)的DeepSort初始化參數(shù)字段名以你手上的版本為準(zhǔn) tracker_args { model_path: weights/deepsort.onnx, # ReID特征模型 max_dist: 0.3, # 外觀特征余弦距離上限越大越容易關(guān)聯(lián) min_confidence: 0.3, # 送入跟蹤器的檢測(cè)最低置信度 max_iou_distance: 0.7, # 外觀匹配不上時(shí)用的IOU兜底閾值 max_age: 70, # 軌跡連續(xù)丟多少幀后刪除 n_init: 3, # 連續(xù)匹配多少幀后確認(rèn)新軌跡 nn_budget: 100, # 軌跡特征樣本總數(shù)上限 }逐個(gè)說(shuō)透max_age是軌跡存活期目標(biāo)消失后track會(huì)再等max_age幀這段時(shí)間內(nèi)如果目標(biāo)重新出現(xiàn)還能續(xù)上舊ID調(diào)大它ID切換會(huì)減少但代價(jià)是誤關(guān)聯(lián)的可能性也會(huì)變高。n_init是新目標(biāo)轉(zhuǎn)正需要的連續(xù)命中幀數(shù)調(diào)小能讓新軌跡更快出現(xiàn)但震蕩框更容易被當(dāng)成目標(biāo)。max_dist控制外觀匹配的寬容度調(diào)大特征不太像的兩個(gè)框也可能配上對(duì)調(diào)小ReID嚴(yán)格ID容易斷多目標(biāo)交錯(cuò)時(shí)不容易交換身份。max_iou_distance是兜底方案外觀匹配不上時(shí)tracker會(huì)退化用位置IOU續(xù)命。實(shí)戰(zhàn)中我最常做的調(diào)整是這三步ID頻繁斷就把max_age加到100以上max_dist調(diào)到0.35先看ID切換是否下降誤匹配多把max_dist收回0.25同時(shí)調(diào)大n_init到5讓噪聲框不被立即確認(rèn)目標(biāo)出畫(huà)再入畫(huà)總是換ID就要加更強(qiáng)的ReID模型。每次只改一個(gè)參數(shù)保留同段視頻做驗(yàn)證不然你根本不知道是哪個(gè)參數(shù)起了作用。4.3 把YOLOv9換成自己的檢測(cè)器接縫只看這里有些用戶想用自己的數(shù)據(jù)集替換檢測(cè)權(quán)重后發(fā)現(xiàn)跟蹤還是按原邏輯跑。其實(shí)DeepSort并不關(guān)心分類模型長(zhǎng)什么樣它只接收檢測(cè)框列表。這份源碼里YOLOv9檢測(cè)輸出被轉(zhuǎn)成跟蹤器輸入是在主循環(huán)里完成的一小段格式一般是這種結(jié)構(gòu)# 將檢測(cè)結(jié)果統(tǒng)一轉(zhuǎn)換為[x1, y1, x2, y2, score, class]數(shù)組 bbox_xywh np.array( [[x1, y1, x2 - x1, y2 - y1, score, class_id], ...], dtypenp.float32, ) tracker.update(bbox_xywh, frame)要注意兩點(diǎn)坐標(biāo)務(wù)必轉(zhuǎn)成xywh置信度留在score位最后的class_id傳給DeepSort后只在可視化時(shí)用于顯示不影響匹配邏輯。所以你自己訓(xùn)練的模型只要在這個(gè)位置把YOLO的result對(duì)象換成統(tǒng)一列表后面的ReID和卡爾曼濾波原樣復(fù)用工程改動(dòng)量很小。5. 避坑與排查從OOM到ID跳變的五條踩坑記錄5.1 剛跑幾十幀就CUDA error: out of memory現(xiàn)象object_tracking.py啟動(dòng)后前幾幀正常過(guò)了幾十幀直接報(bào)CUDA out of memory退出。原因這類視頻工程的默認(rèn)輸入尺寸往往設(shè)得很大比如把1080p源放到960以上分辨率推理顯存占用隨分辨率呈平方級(jí)增長(zhǎng)小卡很快就滿。還有一個(gè)隱蔽因素ReID特征網(wǎng)絡(luò)也在GPU上同時(shí)跑兩段顯存疊加后直接頂穿。解決先把檢測(cè)輸入尺寸降到640很多實(shí)現(xiàn)里是一個(gè)imgsz參數(shù)如果還不行換成yolov9-tiny這類小權(quán)重。我的底線是6G顯存以下640加tiny優(yōu)先不開(kāi)大batch檢測(cè)和ReID盡量保持同一個(gè)推理會(huì)話不要同時(shí)開(kāi)多個(gè)進(jìn)程。5.2 目標(biāo)過(guò)遮擋后ID跳變不?,F(xiàn)象目標(biāo)被柱子遮住幾幀再出現(xiàn)后ID從5跳到12群體行走時(shí)更明顯。原因ReID提取外觀特征時(shí)遮擋后的畫(huà)面與遮擋前差異大余弦距離超過(guò)max_dist新舊狀態(tài)匹配不上卡爾曼濾波在目標(biāo)被擋住后會(huì)繼續(xù)外推位置偏移也逐漸加大雙重因素導(dǎo)致舊track被判死、新track建立。解決第一選擇是調(diào)大max_age讓track多活一段時(shí)間出障礙后還有機(jī)會(huì)匹配回來(lái)第二看max_dist如果恢復(fù)效果差就把距離上限調(diào)大一點(diǎn)比如0.3調(diào)0.35。這兩個(gè)方向本質(zhì)都是放寬關(guān)聯(lián)已經(jīng)頻繁出現(xiàn)ID交換時(shí)不要兩個(gè)同時(shí)亂調(diào)先動(dòng)max_age。5.3 檢測(cè)每幀都有框tracker輸出卻一直是0現(xiàn)象畫(huà)面上彩色檢測(cè)框正常在動(dòng)但標(biāo)注的ID和軌跡數(shù)量全是零日志里track數(shù)量不漲。原因跟蹤器的min_confidence把低置信度檢測(cè)過(guò)濾掉了或者n_init設(shè)置得比較大新track連續(xù)三幀才能被確認(rèn)而視頻里的目標(biāo)一直在動(dòng)中途某幀掉檢就永遠(yuǎn)轉(zhuǎn)不正。更常見(jiàn)的是檢測(cè)置信度閾值和min_confidence設(shè)了同一檔緊卡著臨界值檢測(cè)結(jié)果送不進(jìn)跟蹤器。解決先把跟蹤器里的min_confidence調(diào)到0.1做一次驗(yàn)證一般就能看到ID出現(xiàn)再把n_init從默認(rèn)3暫時(shí)降到1盡快確認(rèn)新track。邏輯是先把驗(yàn)證鏈路打通再逐檔把閾值拉回正常而不是全靠猜。5.4 部分視頻格式讀取后幀序錯(cuò)亂、時(shí)間戳卡住現(xiàn)象跑MP4一切正常換了一組AVI視頻軌跡忽前忽后甚至視頻后半段直接卡死。原因OpenCV底層的視頻解碼對(duì)不同編碼格式支持不一致。同樣是.aviMJPEG編碼和H264編碼走的是不同解碼分支處理速度和幀序表現(xiàn)差別很大。解決我的做法是統(tǒng)一先轉(zhuǎn)成H264的MP4再送進(jìn)源碼ffmpeg -i input.avi -c:v libx264 -pix_fmt yuv420p -crf 23 input_conv.mp4輸出用yuv420p是為了兼容老播放器crf 23是畫(huà)質(zhì)和碼率的平衡點(diǎn)。轉(zhuǎn)碼后直接把--source指到新文件幀序問(wèn)題基本消失。5.5 裝完requirements后torch被覆蓋cuda不能用現(xiàn)象本來(lái)在conda環(huán)境驗(yàn)證torch.cuda.is_available()是True執(zhí)行pip install -r requirements.txt后變False推理速度驟降。原因requirements.txt里可能固定了基于CPU的torch或某個(gè)舊版本pip在安裝時(shí)把已有cuda版torch覆蓋掉了。這是雙軌依賴最常見(jiàn)的坑conda和pip版本主張打架pip后裝就贏了。解決先按conda.yml建環(huán)境再用pip補(bǔ)裝requirements發(fā)現(xiàn)覆蓋后重新指定cuda版本重裝# 重新安裝適用于本機(jī)CUDA的torch版本號(hào)以官方索引為準(zhǔn) pip install torch --index-url https://download.pytorch.org/whl/cu121注意這一步不能盲抄先看nvidia-smi里DRIVER版本支持的CUDA再選對(duì)應(yīng)cu版本。我見(jiàn)過(guò)太多人環(huán)境一崩就重裝整套環(huán)境其實(shí)只要固定好torch這一根線其他就順了。6. 進(jìn)階把跟蹤結(jié)果變成業(yè)務(wù)數(shù)據(jù)——跨線計(jì)數(shù)與軌跡驗(yàn)證跟蹤的真正價(jià)值不在花哨的框而在每個(gè)track_id連續(xù)帶出的位置序列。拿這套源碼跑通后我建議你往下走一步把每幀跟蹤輸出里的track_id和中心點(diǎn)記錄成結(jié)構(gòu)化數(shù)據(jù)再在這個(gè)序列上做業(yè)務(wù)判斷。比如跨線計(jì)數(shù)是視頻車流統(tǒng)計(jì)里最常見(jiàn)的需求而它其實(shí)不需要額外模型邏輯非常樸素判斷一個(gè)目標(biāo)的中心點(diǎn)從參考線上方落到下方且該track_id從未被計(jì)數(shù)過(guò)就記一次向下穿越。import csv from collections import defaultdict line_y 720 # 參考線在畫(huà)面中的縱坐標(biāo) crossed set() # 已穿越的track_id trajectories defaultdict(list) with open(crossing_count.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame_id, track_id, cx, cy, event]) for frame_id, track_boxes in enumerate(frame_outputs): for box in track_boxes: track_id box[track_id] cx, cy box[center] trajectories[track_id].append((cx, cy)) if len(trajectories[track_id]) 2: continue prev_y trajectories[track_id][-2][1] if prev_y line_y cy and track_id not in crossed: crossed.add(track_id) writer.writerow([frame_id, track_id, cx, cy, down])這段代碼每來(lái)一個(gè)track_id就把中心點(diǎn)追加進(jìn)軌跡判定條件看前一個(gè)點(diǎn)和當(dāng)前點(diǎn)是否跨過(guò)參考線。兩點(diǎn)說(shuō)明frame_outputs在源碼里是每幀tracker返回的已確認(rèn)軌跡更新結(jié)果直接從那份輸出提取就行crossed集合保證同一個(gè)ID只在第一次穿越時(shí)計(jì)數(shù)不會(huì)因?yàn)楹罄m(xù)振蕩多次累加。如果你在真實(shí)場(chǎng)景里發(fā)現(xiàn)計(jì)數(shù)偏多大概率是檢測(cè)框在參考線附近抖動(dòng)導(dǎo)致軌跡反復(fù)穿越只要保留track_id集合去重絕大多數(shù)誤報(bào)能擋掉。驗(yàn)證方法也回到這個(gè)思路用同一段視頻跑兩遍每次看兩條曲線的ID數(shù)量和跨線事件是否一致如果不一致先回去看檢測(cè)幀再看max_age而不是懷疑計(jì)數(shù)邏輯。從那以后我每次拿到一套檢測(cè)加跟蹤的源碼都會(huì)強(qiáng)迫自己先跑一遍固定視頻的ID軌跡連續(xù)性記錄切換次數(shù)再動(dòng)參數(shù)。這套流程能擋住八成瞎調(diào)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取