
簡介本資源是一套面向計算機相關專業(yè)學生與初學者的行人多目標跟蹤MOT與重識別ReID一體化實踐項目基于YOLOv5檢測、DeepSORT跟蹤與FastReID特征提取三大主流框架重構實現(xiàn)聚焦視頻流中行人軌跡關聯(lián)與跨鏡像身份匹配兩大核心任務適用于課程設計、畢業(yè)設計、競賽原型開發(fā)及AI工程能力進階訓練。壓縮包共37個文件含31個Python源碼涵蓋MOT主流程、ReID特征抽取接口、模型加載與推理封裝等、2個YAML配置文件定義模型路徑與跟蹤參數(shù)、2個說明類TXT文檔及README.md等輔助文件整體僅38KB輕量易讀、結構清晰便于快速理解模塊分工與調(diào)用邏輯。已有84人下載學習項目源自高分結題實踐答辯95分所有代碼均經(jīng)實測可運行配套詳細技術文檔與使用說明提供從環(huán)境配置、視頻輸入、結果可視化到特征導出的完整鏈路支持特別適合希望深入理解MOTReID協(xié)同機制的學習者開展二次開發(fā)或功能拓展。 把Yolov5、DeepSORT、FastReID三個項目放一起跑通網(wǎng)上教程非常多但大多數(shù)停留在“能出效果”。真正的問題在于你要在視頻里同時做行人多目標跟蹤MOT和行人ReID特征提取并且把代碼、接口、文檔都做成一個可交付的工程這時候那些demo里的變量滿天飛、路徑寫死的代碼根本撐不住。我這次重構就是把三個開源項目重新拆開再組裝最后得到一個能跑、好改、經(jīng)得起追問的工程。這篇文章把我踩過的坑、改動的關鍵點、以及接口設計思路全部記錄下來希望給正在做相關課題、畢業(yè)設計或需要交付完整項目的朋友一些參考。1. 先聊清楚MOT和ReID同時出現(xiàn)時三個開源項目各自該干什么1.1 三個開源項目的明星能力與實際分工Yolov5是單階段檢測器輸出目標的邊界框和類別DeepSORT是經(jīng)典多目標跟蹤算法核心是卡爾曼濾波加匈牙利匹配負責給檢測框分配ID并維持軌跡FastReID是針對行人重識別ReID的開源庫提供強力特征提取backbone和一整套訓練、評估流程。三者合在一起正好覆蓋視頻行人MOT的完整閉環(huán)檢測到人、跨幀關聯(lián)、提取可區(qū)分的特征。但這里有一個很容易被忽略的點DeepSORT本身是帶外觀特征提取器的只是它的CNN非常弱遇到遮擋、相似衣著時頻繁ID Switch。所以我們才引入FastReID來替換DeepSORT內(nèi)部的feature extractor。這聽起來很簡單但在工程層面FastReID和DeepSORT是兩個完全獨立的生態(tài)數(shù)據(jù)格式、預處理、輸出維度、模型加載方式都不一樣。直接拼起來輕則運行報錯重則特征維度對不上匹配邏輯完全失效。1.2 原版源碼的“能跑”與“不可維護”之間的巨大落差我最初拿到那份“基于Yolov5-Deepsort-Fastreid源碼”時第一反應是這項目能跑第二反應是這項目只能“跑”沒法“用”。問題集中在這幾點依賴沖突嚴重Yolov5要求PyTorch版本不能太老FastReID又依賴它自己那套環(huán)境兩個repo放到同一個環(huán)境里經(jīng)常出現(xiàn)torch版本或者cv2版本互相打架。顯存資源失控檢測模型和ReID模型同時加載到同一塊GPU沒有顯存規(guī)劃小顯存機器直接OOM。就算能跑也是兩個模型各自吃一堆顯存實際上很多內(nèi)存是重復申請的。預處理不統(tǒng)一Yolov5用RGB歸一化DeepSORT原版resize到64x128并歸一化到[-1,1]FastReID則用ImageNet的mean/std。三者標準不一致特征分布被破壞匹配效果甚至不如直接用DeepSORT內(nèi)置的弱特征。代碼耦合嚴重main.py里面從上到下串著檢測、跟蹤、特征提取沒有模塊邊界全局變量滿天飛。想單獨調(diào)FastReID的batch size要翻好幾層函數(shù)。文檔約等于零README只有啟動命令沒有任何參數(shù)說明和設計解釋。如果這是課程作業(yè)或畢業(yè)設計評審老師隨便問一個“為什么這里用0.2的閾值”可能就答不上來。“能跑”只是底線而一個高分項目需要的是“別人拿到源碼能復現(xiàn)、能改、能回答問題”。所以我的重構目標非常明確把三個項目收斂成一個工程。1.3 重構的目標不是做學術創(chuàng)新而是做工程化收斂我給自己定了幾個硬性指標。第一檢測、跟蹤、特征提取三者解耦模塊間不直接import具體模型只面向接口編程。第二全流程配置化分辨率、置信度閾值、模型路徑、特征維度、batch size全部通過配置文件管理。第三對外提供一個統(tǒng)一的視頻處理Pipeline輸入一段視頻或圖片序列輸出帶軌跡的結果以及ReID特征序列。第四補全接口文檔、環(huán)境復現(xiàn)說明、測試數(shù)據(jù)和演示視頻。這樣做的好處是不管評審老師問“為什么選擇FastReID作為特征提取器”還是“DeepSORT匹配時用的距離是什么”都能在文檔和代碼結構中找到清晰答案。下面我把重構后的架構和關鍵改動展開講。2. 重構后的整體數(shù)據(jù)流從視頻幀到ReID特征庫的完整鏈路2.1 一次前向推理里檢測、跟蹤、特征提取的數(shù)據(jù)交接重構后的Pipeline內(nèi)部流程并不復雜但每一環(huán)的數(shù)據(jù)結構必須定義清楚。我梳理出一次前向推理的完整交接鏈讀取一幀圖像。如果需要對長邊縮放先記錄縮放比例免得后面映射回原圖坐標時出錯。送入Yolov5檢測器輸出檢測框xyxy格式、置信度、類別ID。這里只保留person類過濾掉低置信度框。把檢測框和原始幀一起傳給DeepSORT的update()方法。DeepSORT內(nèi)部先用卡爾曼濾波預測已有軌跡在下一幀的位置再通過級聯(lián)匹配和IOU匹配將檢測框與預測位置關聯(lián)。在級聯(lián)匹配過程中需要計算每個檢測框?qū)腥说耐庥^特征。這時候就調(diào)用我們重寫的ReID特征提取器批量提取特征而不是逐個人循環(huán)調(diào)用模型。DeepSORT輸出活躍軌跡的列表每個軌跡包含ID、bbox、置信度等信息。如果需要建立ReID特征庫就再根據(jù)軌跡ID從最近高質(zhì)量幀中提取特征寫入數(shù)據(jù)庫。整個過程串下來檢測器是前端跟蹤器是中樞ReID特征是外部增強。任何一環(huán)的性能下降都會影響最終ID的穩(wěn)定性。2.2 代碼層面怎么拆模塊detector、tracker、reid、pipeline我最終采用的目錄結構是這樣的project/ ├── configs/ │ ├── mot.yaml │ └── reid.yaml ├── detector/ │ ├── base_detector.py │ └── yolov5_detector.py ├── tracker/ │ ├── deepsort_tracker.py │ └── feature_extractor.py ├── reid/ │ ├── fastreid_model.py │ └── feature_bank.py ├── pipeline/ │ └── mot_pipeline.py ├── utils/ │ ├── draw.py │ └── metrics.py └── main.py每個模塊暴露的接口都盡量小。例如detector只提供detect(frame) - List[Detection]tracker只提供update(detections, frame) - List[Track]reid只提供extract(crops) - Tensor。這樣模塊之間只依賴接口不依賴具體實現(xiàn)。這樣的好處是單元測試非常容易寫。我單獨測過檢測器輸出的坐標是否正確、DeepSORT的ID切換是否在預期范圍、FastReID提取的特征能不能區(qū)分不同行人。如果全局代碼揉在一起任何一個模塊改動都可能引入隱蔽bug有了接口邊界問題定位就快得多。2.3 為什么這樣拆讓Yolov5可以被替換讓DeepSORT可以換association策略讓FastReID可以換backbone拆模塊更重要的是為了應對“替換”需求。老師可能問“如果你現(xiàn)在要把Yolov5換成另一個檢測器需要改幾行代碼”答案是只需在configs/mot.yaml里修改檢測器類型并在detector/下新增一個類實現(xiàn)BaseDetector接口。主干邏輯一行不用動。同樣FastReID的backbone從ResNet50換成Swin Transformer或者更輕量的MobileNet只需改reid.yaml里的模型配置代碼層不做改動。DeepSORT如果想換成ByteTrack或者StrongSORT也可以按相同接口封裝把跟蹤算法替換掉ReID特征提取器依然能復用。這種設計不是過度設計。如果一個項目只有幾十行代碼確實不需要拆。但在這個規(guī)模下接口收斂讓后續(xù)擴展、調(diào)試、演示都輕松得多。這也是我覺得它配得上“高分項目”這個標簽的最重要原因。3. FastReID接入DeepSORT的硬骨頭特征提取器不是調(diào)用一下那么簡單3.1 DeepSORT的feature是128維FastReID的feature是2048維怎么對齊DeepSORT原始的CNN特征提取器輸出128維FastReID基于ResNet50的模型通常輸出2048維。維度不同并不影響余弦距離計算因為余弦距離計算的是兩個向量的夾角和維度無關。但需要做兩件事一是把FastReID輸出的特征做L2歸一化二是重新標定匹配時的距離閾值。DeepSORT的級聯(lián)匹配里有一個外觀距離閾值默認是0.2表示“最大允許的余弦距離”。這個0.2是針對原版128維特征的經(jīng)驗值不能直接套到FastReID特征上。不同特征提取器輸出分布差異很大閾值不調(diào)匹配結果會變得過于嚴格或者過于寬松。我自己的做法是從公開的MOT數(shù)據(jù)集上抽取真實匹配對和不匹配對統(tǒng)計它們的特征余弦距離分布再選定一個分界點。實測下來FastReID特征的匹配閾值設在0.3左右比較合適。如果你換了模型也需要做一次類似的標定。下面是重寫后的特征提取器核心代碼class FastReIDFeatureExtractor: def __init__(self, cfg_path, weights_path, device): self.model build_fastreid_model(cfg_path, weights_path) self.model.eval().to(device) def extract(self, crops_tensor): # 輸入是一個batch的tensorshape[B,3,H,W] with torch.no_grad(): feats self.model.inference(crops_tensor) return F.normalize(feats, p2, dim1) def get_feature_dim(self): return 2048然后把FastReIDFeatureExtractor作為參數(shù)傳給DeepSORT替換掉原來的內(nèi)置extractor。3.2 預處理差異BGR/RGB、歸一化、Resize的細節(jié)這是最容易出bug但日志完全不會報錯的地方。Yolov5內(nèi)部輸入是按RGB存儲、并按Yolov5自己的方式歸一化OpenCV默認讀入的是BGRFastReID在訓練時使用ImageNet的mean/std歸一化輸入尺寸通常為256x128。三者如果不統(tǒng)一模型照樣能跑但輸出特征會出現(xiàn)系統(tǒng)性偏移ID Switch會大量增加。正確的做法是讓數(shù)據(jù)流保持清晰從視頻解碼到畫框始終用OpenCV的BGR一旦要送入檢測器或ReID模型立即轉(zhuǎn)成對應模型所需的圖像格式。FastReID的預處理最好直接用它的build_transforms函數(shù)from fastreid.data.transforms import build_transforms transform build_transforms(cfg, is_trainFalse)把檢測框裁剪出的行人圖片先轉(zhuǎn)成RGB再執(zhí)行resize和歸一化。這里尤其注意裁剪行人時不要直接resize成正方形而是按行人長寬比resize到256x128。如果拉伸變形特征質(zhì)量會下降。3.3 模型權重轉(zhuǎn)換與加載一個容易卡住的問題FastReID官方權重是配合其訓練流程的不能簡單地用torch.load直接塞進模型。正確做法是用FastReID自己的DefaultTrainer.build_model構建模型再加載權重from fastreid.config import get_cfg from fastreid.engine import DefaultTrainer cfg get_cfg() cfg.merge_from_file(configs/Market1501/bagtricks_R50.yml) cfg.MODEL.WEIGHTS path/to/best_model.pth model DefaultTrainer.build_model(cfg) model.eval()注意FastReID的模型在推理時可能會返回多個輸出一般取features字段作為ReID特征不要直接用分類logits。具體要看版本有的版本需要調(diào)用model.inference()才會走特征提取分支。我建議先跑一遍官方的demo確認拿到的是特征向量而不是類別概率再接進DeepSORT。還有一個小坑FastReID訓練時可能包含一些與DeepSORT無關的head結構加載權重時如果出現(xiàn)state_dict mismatch需要檢查是否忘加pretrainedTrue配置或版本不匹配。把這部分邏輯封裝在fastreid_model.py里后續(xù)替換權重就只改路徑。4. 檢測器側(cè)不是刷榜就行Yolov5在MOT場景下的真實調(diào)優(yōu)點4.1 視頻檢測里漏報比誤報更致命在MOT任務里檢測結果直接決定軌跡連續(xù)性。如果某一幀漏檢了一個行人DeepSORT會先對該軌跡進行遮擋處理。如果長時間匹配不上軌跡會被刪除。重新檢測到之后算法大概率會分配一個新的ID于是產(chǎn)生ID Switch和軌跡碎片。所以檢測器的recall比precision更重要。我測試時把Yolov5的置信度閾值從默認的0.25調(diào)低到0.1左右確保盡可能不漏人再讓DeepSORT通過運動匹配和外觀匹配過濾誤檢。當然這個值不是越低越好。太低會出現(xiàn)大量背景誤檢框跟蹤器計算量暴漲也更容易把背景誤當成短期軌跡。需要根據(jù)場景實測比如商場監(jiān)控和校園路口最優(yōu)閾值就完全不同。4.2 針對密集行人場景的anchor、NMS和類別過濾Yolov5的默認anchor是在COCO全部80類目標上聚類出來的對行人這種大長寬比目標并不是最優(yōu)。如果你用官方權重直接檢測行人在密集場景下小目標容易漏檢、重疊行人容易被NMS合并成一個框。我沒有重新訓練Yolov5而是通過兩個方式緩解。第一把輸入分辨率從640x640提升到1280x704小行人的漏檢明顯改善第二把NMS閾值從默認的0.45下調(diào)到0.35因為行人之間經(jīng)?;ハ嘀丿B太高的NMS會把兩個重疊的行人合并掉。但NMS過低又會保留大量冗余框需要和DeepSORT的匹配策略配合。最終我是通過MOTA指標來驗證而不是只追求檢測mAP。4.3 推理速度優(yōu)化TensorRT/ONNX的考慮以及不引入額外依賴的簡易方案如果項目只是“能跑”那不需要太追求速度。但實際演示時如果視頻像幻燈片一樣觀感會很差。我做兩個優(yōu)化第一把Yolov5導出成ONNX再用TensorRT或ONNXRuntime加速。在支持TensorRT的機器上推理時間可以從50ms降到15ms左右。第二把FastReID的特征提取改成batch推理。DeepSORT每次匹配前可能需要提取幾十個候選框的特征如果逐個循環(huán)非常慢。我把所有待提取特征的行人圖堆成一個batch一次性調(diào)用模型。如果你不想引入額外依賴也有簡單方案用PyTorch的torch.cuda.amp.autocast()開啟半精度推理顯存占用更低速度也有提升。但要注意半精度可能造成精度下降特別是特征提取部分最好是在驗證集上確認效果沒有明顯變差再開啟。5. 實測中ID Switch的一次完整復盤問題出在特征更新策略5.1 復現(xiàn)一次ID Switch的過程我拿了一段商場監(jiān)控視頻做測試鏡頭里兩個人迎面走過ID發(fā)生了互換。過程是這樣的ID1和ID2分別跟蹤左側(cè)男性和右側(cè)女性。兩人相遇時互相遮擋檢測器短暫地把兩人合并成一個框DeepSORT把這個框分配給了置信度更高的軌跡。遮擋結束后實際位置的對應關系已經(jīng)錯位但DeepSORT在遮擋期間沒有更新外觀特征軌跡特征還停留在遮擋前的樣子于是系統(tǒng)繼續(xù)用錯誤ID跟蹤直到兩人分開才可能被糾正。這個過程非常典型。它說明一個問題僅靠檢測器和跟蹤器的串聯(lián)并不能解決遮擋場景下的身份保持問題。外觀特征的更新策略至關重要。5.2 DeepSORT級聯(lián)匹配的局限與FastReID特征的關系DeepSORT的匹配距離是運動馬氏距離和外觀余弦距離的加權。運動模型假設目標近似勻速直線運動但行人經(jīng)常急停、轉(zhuǎn)身、加速運動預測誤差很大。這時外觀特征就起到?jīng)Q定性作用。FastReID的特征比DeepSORT原來的128維特征強很多能夠更好地區(qū)分不同行人。但如果特征提取的時機不對再強的特征也會失效。比如遮擋過程中提取到的是兩個人疊在一起的“融合特征”把它更新到軌跡上就等于把錯誤身份信息寫入了狀態(tài)。之后真實目標重新露出來時匹配結果自然會亂。5.3 我采用的修復措施特征緩沖、周期性更新與質(zhì)量過濾針對這個問題我做了三個修改效果比較明顯質(zhì)量過濾只有檢測框置信度和高度滿足一定條件時才提取特征并更新軌跡。低質(zhì)量框的特征不進入狀態(tài)避免污染。特征緩沖隊列每個軌跡維護最近K10個高質(zhì)量特征隊列計算新特征與隊列中已有特征的平均余弦相似度如果相似度足夠高才加入隊列否則認為是遮擋或噪聲暫時不更新。周期性強制更新每N幀強制用當前幀中該ID對應的最佳檢測框更新一次特征避免軌跡特征一直停留在很久以前的畫面。在匹配階段我不再只用當前最新特征而是用特征隊列的平均向量作為外觀特征。這樣即使單幀特征有波動也不會導致匹配結果劇烈跳變。修改之后測試視頻里的ID Switch從37次降到了9次。當然這個方案會增加計算量特征隊列要定期裁剪否則內(nèi)存會持續(xù)增長。6. 讓項目拿到“高分”的不只是代碼文檔與資料怎么組織6.1 接口文檔的寫法每個接口有輸入輸出約束、異常場景和示例評審和用代碼的人最怕的不是功能少而是不知道怎么調(diào)用。我寫文檔時不是簡單貼一遍README而是像給開源庫寫API文檔一樣明確每個接口的輸入輸出、異常行為和示例。例如MOTPipeline的接口說明class MOTPipeline: def process_frame(self, frame: np.ndarray) - dict: Args: frame: BGR uint8 array, shape (H, W, 3) Returns: dict: { tracked_objects: List[TrackObject], frame_id: int, features: Optional[dict[id, np.ndarray]] } Raises: ValueError: 當圖像尺寸為0或通道數(shù)不為3時拋出 文檔里還要寫明耗時范圍、顯存占用以及在不同硬件上的表現(xiàn)。這樣接手的人不需要從頭讀源碼。6.2 環(huán)境復現(xiàn)的一鍵化requirements、Docker、測試數(shù)據(jù)說明環(huán)境復現(xiàn)是開源項目最大的痛點之一。我把依賴拆成三部分檢測、跟蹤、ReID分別寫清楚版本要求并用一個requirements.txt固定主版本。同時提供一個Dockerfile方便一鍵構建環(huán)境。我在文檔里單獨列了“不同平臺測試情況”Windows CPU版可以跑但會慢Linux CUDA 可以接近實時。測試數(shù)據(jù)方面我放了MOT16的一個小demo序列和Market1501的部分樣本并注明版權和下載地址避免大文件拖垮整個壓縮包。6.3 全部資料包應該包含什么源碼、文檔、權重、數(shù)據(jù)、演示視頻最終我整理的“全部資料”目錄大致是這樣的├── code/ ├── docs/ │ ├── 1_項目說明.md │ ├── 2_環(huán)境配置.md │ ├── 3_算法原理.md │ ├── 4_接口文檔.md │ ├── 5_實驗結果.md │ └── 6_常見問題.md ├── weights/ ├── data/ │ └── demo/ └── videos/ └── output_demo.mp4權重單獨放一個文件夾不打進git。所有文檔都標注版本號比如v1.0_20240912。這樣哪怕壓縮包很大拿到手的人也能快速找到入口而不是在一個大repo里迷路。7. 最后分享一點個人經(jīng)驗重構開源項目的正確姿勢7.1 先跑通再重構但跑通后一定要畫數(shù)據(jù)流圖很多人一上來就想“重寫”結果被原版復雜代碼帶偏。我踩過這個坑花了兩個多月在改代碼進度緩慢。后來我把原版的項目跑通先在紙上畫數(shù)據(jù)流圖標出哪些數(shù)據(jù)結構在模塊間流動、哪些對象被誰持有、哪些隱式全局狀態(tài)存在。畫完之后重構思路立刻清晰一周時間就把主要模塊拆完了。7.2 保持對上游版本的追蹤做好本地patch記錄三個開源項目都在頻繁更新不要直接去改第三方庫的源碼。我建議把第三方庫作為子模塊或者固定到某個版本通過自己的封裝層調(diào)用。如果實在要改vendor代碼比如DeepSORT里某些內(nèi)部邏輯我會用git patch記錄而不是直接改完不留痕跡。這樣上游更新后可以快速重新合并。7.3 項目驗收看演示但更看邊界條件最后一點也是我覺得最容易被忽視的。評審老師看演示時可能會問“如果一個人被完全遮擋10秒會怎么樣”“如果檢測器漏檢3幀會怎么樣”“如果特征庫越來越大怎么辦”這些邊界條件不要求你完美解決但你要能解釋清楚當前方案的取舍和后續(xù)改進方向。我在文檔里專門加了“已知限制與改進方案”一節(jié)把我測試中遇到的一些失敗案例和原因?qū)戇M去。結果反而是這部分內(nèi)容讓項目顯得更真實、更有深度。畢竟一個敢把自己的問題拿出來講的項目比一個只展示完美結果的項目更經(jīng)得起推敲。本文還有配套的精品資源點擊獲取