識(shí)別:GNN傳播源碼解析)
簡(jiǎn)介一份面向人工智能本科畢業(yè)設(shè)計(jì)的步態(tài)識(shí)別多目標(biāo)跨鏡頭跟蹤檢測(cè)系統(tǒng)源碼基于YOLOv5DeepSORT完成目標(biāo)檢測(cè)與跟蹤結(jié)合GaitSet算法實(shí)現(xiàn)步態(tài)特征識(shí)別適用于監(jiān)控場(chǎng)景下的跨鏡頭多目標(biāo)持續(xù)追蹤研究。資源共343個(gè)文件以173個(gè)Python腳本為核心算法實(shí)現(xiàn)52個(gè)YAML文件用于模型與訓(xùn)練配置另有Markdown說(shuō)明文檔、Shell部署腳本、C/CUDA擴(kuò)展模塊等覆蓋從數(shù)據(jù)預(yù)處理、模型訓(xùn)練到推理部署的完整鏈路壓縮包僅22.23MB結(jié)構(gòu)清晰便于查閱。已有4449人學(xué)習(xí)下載。適合本科畢業(yè)設(shè)計(jì)、課程項(xiàng)目或算法研究者參考可幫助理解跨鏡頭跟蹤與步態(tài)識(shí)別的工程化實(shí)現(xiàn)包含完整源碼、配置文件與部署腳本能夠直接作為實(shí)驗(yàn)基礎(chǔ)或二次開(kāi)發(fā)起點(diǎn)。1. yolov5DeepSORTGaitSet跨鏡頭步態(tài)識(shí)別這個(gè)題目到底在解決什么問(wèn)題跨鏡頭跟蹤是監(jiān)控場(chǎng)景里最尷尬的一環(huán)。單鏡頭內(nèi)的多目標(biāo)跟蹤早就有成熟方案一旦目標(biāo)走出畫(huà)面再進(jìn)入另一路攝像頭ID 就斷了。最直接的補(bǔ)救是換裝重識(shí)別但監(jiān)控場(chǎng)景下人臉看不清、衣服換個(gè)角度就變樣真正穩(wěn)定的是步態(tài)——人走路的姿態(tài)很難刻意偽裝。這套畢業(yè)設(shè)計(jì)源碼做的就是這件事用 yolov5 做目標(biāo)檢測(cè)DeepSORT 做單鏡頭內(nèi)的跟蹤再用 GaitSet 提取步態(tài)特征做跨鏡頭匹配整個(gè)鏈路里最核心的 GNN 傳播模塊也以源碼形式放在包里。如果你正在做人臉、行人重識(shí)別或多目標(biāo)跟蹤相關(guān)的課設(shè)、競(jìng)賽或者想快速拿一套能跑的跨鏡頭跟蹤基線這份資源值得先看目錄再動(dòng)手。2. 三段式架構(gòu)檢測(cè)、單鏡頭跟蹤、跨鏡頭重識(shí)別怎么接2.1 yolov5 檢測(cè)模塊不是拿來(lái)就跑先確認(rèn)輸出格式這套系統(tǒng)的第一層是 yolov5。檢測(cè)模塊負(fù)責(zé)把每一幀畫(huà)面里的行人框出來(lái)輸出的是x1, y1, x2, y2, conf, cls這種格式的張量。常見(jiàn)的做法是用 yolov5s 或者你自己訓(xùn)練的權(quán)重推理時(shí)把檢測(cè)結(jié)果直接喂給 DeepSORT。需要注意一點(diǎn)yolov5 輸出的坐標(biāo)是像素坐標(biāo)DeepSORT 的輸入要求也是像素坐標(biāo)不需要?dú)w一化但需要確保檢測(cè)框沒(méi)有被 NMS 二次壓縮過(guò)。# 以 YOLOv5 的 detect 輸出為例做檢測(cè)結(jié)果到跟蹤器的對(duì)接 import cv2 import torch model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) def detect_frame(frame): results model(frame, size640) dets results.xyxy[0].cpu().numpy() # [x1, y1, x2, y2, conf, cls] person_dets dets[dets[:, 5] 0] # COCO 類別 0 是 person return person_dets[:, :5] # 只要坐標(biāo)和置信度這里的size640是推理尺寸可以用imgsz參數(shù)覆蓋person_dets[:, :5]之所以只要前五列是因?yàn)?DeepSORT 的update()接口接收的是[x1, y1, x2, y2, score]數(shù)組。如果你用的是自定義數(shù)據(jù)集COCO 類別索引就不是 0 了需要在類別映射表里找到 person 對(duì)應(yīng)的索引否則跟蹤器會(huì)把車和樹(shù)都當(dāng)成目標(biāo)去跟蹤。2.2 DeepSORT 跟蹤層卡爾曼濾波和匈牙利匹配的參數(shù)含義DeepSORT 是整個(gè)系統(tǒng)里承上啟下的部分。它接收 yolov5 的檢測(cè)框?yàn)槊總€(gè)目標(biāo)維護(hù)一個(gè)軌跡狀態(tài)內(nèi)含卡爾曼濾波預(yù)測(cè)的位置、速度、外觀特征。核心參數(shù)有三個(gè)max_dist控制外觀特征的最大余弦距離max_iou_dist控制匹配時(shí) IoU 的閾值max_age決定軌跡丟失多少幀后刪除。畢設(shè)場(chǎng)景下這三個(gè)參數(shù)往往被忽略直接用默認(rèn)值但換數(shù)據(jù)集后它們是最影響跟蹤穩(wěn)定性的。from deep_sort_realtime.deepsort_tracker import DeepSort tracker DeepSort( max_dist0.2, # 余弦距離閾值越小越嚴(yán)格 max_iou_dist0.7, # IoU 匹配閾值 max_age30, # 軌跡丟失多少幀后刪除 nn_budget100, # 外觀特征樣本上限 embeddermobilenet # 提取外觀特征的網(wǎng)絡(luò) ) def update_tracks(frame, detections): tracks tracker.update_tracks(detections, frameframe) return [(t.track_id, t.to_ltrb()) for t in tracks if t.is_confirmed()]max_dist0.2的意思是外觀特征余弦距離大于 0.2 的檢測(cè)框和軌跡不會(huì)匹配這個(gè)值在光線變化大的監(jiān)控場(chǎng)景下往往需要放寬到 0.3。nn_budget100控制每個(gè)軌跡保留多少個(gè)歷史外觀特征超過(guò)上限會(huì)淘汰最早的這個(gè)參數(shù)在長(zhǎng)時(shí)間跟蹤里影響很大——目標(biāo)走了十分鐘后回頭如果特征池太小外觀信息已經(jīng)被擠出去了。embeddermobilenet是輕量級(jí)特征提取器速度和精度兼顧如果追求重識(shí)別準(zhǔn)確率可以換osnet但推理耗時(shí)幾乎翻倍。2.3 GaitSet 步態(tài)識(shí)別層跨鏡頭匹配的身份特征來(lái)源DeepSORT 只解決單鏡頭內(nèi)的 ID 維持跨鏡頭后外觀特征已經(jīng)不可靠這時(shí)候就要靠步態(tài)特征兜底。GaitSet 的做法是把一個(gè)行走序列的多幀輪廓圖拆成若干組對(duì)每組做集合池化再用水平金字塔映射生成步態(tài)特征。這套源碼里步態(tài)部分的關(guān)鍵是它的數(shù)據(jù)組織方式每個(gè)行人的一組輪廓圖放在同一個(gè)目錄下序列長(zhǎng)度和信息幀的選擇直接影響特征質(zhì)量。在工程對(duì)接上GaitSet 的輸出是一個(gè) 256 維或 512 維的特征向量這個(gè)向量和 DeepSORT 的外觀特征向量不在同一個(gè)空間里不能直接拼在一起做距離度量。常見(jiàn)的做法是分段判斷目標(biāo)在鏡頭 A 丟失前緩存它最后 20 幀的步態(tài)特征目標(biāo)在鏡頭 B 出現(xiàn)后先讓 DeepSORT 跑上 10 幀穩(wěn)定軌跡再提取這段軌跡的步態(tài)特征與鏡頭 A 的緩存特征做余弦相似度。相似度超過(guò)閾值就判定為同一個(gè) ID然后把這個(gè)新的軌跡 ID 映射回原來(lái)的全局 ID。這一步在源碼里對(duì)應(yīng)的是 GNN 模塊介入之前的預(yù)處理。3. GNN跨鏡頭關(guān)聯(lián)源碼走讀從 build_adjacency_matrix 到 gnn_propagate3.1 文件清單理清這套源碼跨鏡頭部分到底做了什么打開(kāi)壓縮包頂層文件里值得先看的是以下幾類gnn_propagate.cpp、build_adjacency_matrix.cpp以及對(duì)應(yīng)的.cu文件它們是跨鏡頭關(guān)聯(lián)的核心setup.cfg、isort.cfg、.flake8只是工程配置Dockerfile解決的是環(huán)境一致性。市面上大多數(shù) yolov5DeepSORT 項(xiàng)目到單鏡頭跟蹤就結(jié)束了這里的gnn_propagate和build_adjacency_matrix才是真正的畢設(shè)亮點(diǎn)。build_adjacency_matrix負(fù)責(zé)構(gòu)建圖結(jié)構(gòu)。圖的節(jié)點(diǎn)是來(lái)自不同鏡頭的軌跡片段邊的權(quán)重是軌跡之間的相似度。相似度由兩部分組成外觀特征的余弦相似度和時(shí)間空間上的共現(xiàn)約束。gnn_propagate在這個(gè)圖上做特征的迭代傳播讓每個(gè)軌跡節(jié)點(diǎn)的特征吸收鄰居節(jié)點(diǎn)的信息從而緩解單鏡頭內(nèi)外觀特征不夠判別性的問(wèn)題。這個(gè)思路來(lái)源于圖神經(jīng)網(wǎng)絡(luò)在行人重識(shí)別上的應(yīng)用畢設(shè)答辯時(shí)是一個(gè)很好的切入點(diǎn)。3.2 build_adjacency_matrix 的源碼邏輯與參數(shù)build_adjacency_matrix.cpp做的事情可以拆解成三步讀取所有軌跡片段的外觀特征和時(shí)空信息計(jì)算兩兩之間的初始相似度再根據(jù)閾值和時(shí)空約束生成稀疏鄰接矩陣。核心是相似度計(jì)算時(shí)兩個(gè)損失的權(quán)衡——外觀距離用余弦距離時(shí)空距離用起始幀和結(jié)束幀的重疊程度。下面的代碼展示了 CPU 版本的骨架邏輯。// build_adjacency_matrix.cpp 骨架邏輯 #include vector #include cmath struct Tracklet { int camera_id; int start_frame; int end_frame; std::vectorfloat feature; // 外觀特征GaitSet 或 ReID 提取 }; // 構(gòu)建鄰接矩陣 std::vectorstd::vectorfloat BuildAdjacencyMatrix( const std::vectorTracklet tracklets, float sim_threshold, float time_window) { int n tracklets.size(); std::vectorstd::vectorfloat adj(n, std::vectorfloat(n, 0.0f)); for (int i 0; i n; i) { for (int j i 1; j n; j) { // 同鏡頭不連接跨鏡頭關(guān)聯(lián)不需要同鏡頭內(nèi)的邊 if (tracklets[i].camera_id tracklets[j].camera_id) continue; // 時(shí)間窗口約束兩個(gè)軌跡在時(shí)間上必須有重疊或接近 int overlap std::min(tracklets[i].end_frame, tracklets[j].end_frame) - std::max(tracklets[i].start_frame, tracklets[j].start_frame); if (overlap time_window) continue; // 外觀特征余弦相似度 float cos_sim CosineSimilarity(tracklets[i].feature, tracklets[j].feature); if (cos_sim sim_threshold) { adj[i][j] cos_sim; adj[j][i] cos_sim; } } } return adj; }sim_threshold的取值決定了圖的稀疏程度。值設(shè)太高很多真實(shí)匹配的邊被切斷GNN 傳播時(shí)信息傳不到值設(shè)太低圖變成稠密圖計(jì)算量和噪聲同時(shí)上升。一般取 0.7 到 0.75 之間的值具體要看步態(tài)特征的質(zhì)量——如果 GaitSet 用的是 CASIA-B 數(shù)據(jù)集預(yù)訓(xùn)練權(quán)重特征判別性尚可這個(gè)閾值可以稍微放開(kāi)到 0.65。time_window這個(gè)參數(shù)很容易被誤解成時(shí)間差閾值實(shí)際上它是時(shí)間重疊度單位是幀跨鏡頭場(chǎng)景中兩個(gè)鏡頭的時(shí)間可能不同步所以錄入時(shí)最好先做時(shí)間對(duì)齊。3.3 gnn_propagate 的傳播過(guò)程與 CUDA 加速gnn_propagate在鄰接矩陣上運(yùn)行多個(gè)圖卷積層每一層把鄰居節(jié)點(diǎn)的特征加權(quán)融合到當(dāng)前節(jié)點(diǎn)。整個(gè)傳播過(guò)程涉及矩陣乘法數(shù)據(jù)量大時(shí)用 CPU 會(huì)非常慢所以源碼里額外提供了_kernel.cu的 CUDA 實(shí)現(xiàn)。GPU 版本的核心是在 kernel 函數(shù)里并行遍歷每一個(gè)節(jié)點(diǎn)聚合鄰居特征。// gnn_propagate_kernel.cu 核函數(shù)偽代碼 __global__ void PropagateKernel( const float* features, // [N, D] 特征矩陣 const float* adj, // [N, N] 鄰接矩陣 float* out_features, // [N, D] 輸出特征 int N, int D) { int row blockIdx.x * blockDim.x threadIdx.x; if (row N) return; for (int d 0; d D; d) { float sum 0.0f; for (int col 0; col N; col) { sum adj[row * N col] * features[col * D d]; } out_features[row * D d] sum; } }這段 kernel 代碼的瓶頸在內(nèi)存訪問(wèn)上。adj是稀疏矩陣但 kernel 里仍然用稠密方式遍歷N 是軌跡數(shù)量一般不會(huì)超過(guò)幾千所以問(wèn)題不大如果軌跡上萬(wàn)條N 的平方就不只是計(jì)算量的問(wèn)題內(nèi)存也會(huì)爆掉——一個(gè) 10000 x 10000 的 float 矩陣就是 400MB兩個(gè)就是 800MB。畢設(shè)規(guī)模下不需要優(yōu)化存儲(chǔ)格式但如果你要拿去跑更大的數(shù)據(jù)集建議把a(bǔ)dj改造成 CSR 格式再喂給 kernel。圖中的注釋也說(shuō)明了傳播的一層只做了線性聚合沒(méi)有加非線性激活函數(shù)這是刻意為之的——多跨鏡頭關(guān)聯(lián)的特征空間不需要引入太多非線性保持線性傳播的穩(wěn)定性比擬合能力更重要。3.4 編譯與調(diào)用setup.cfg 和 .flake8 在這里的作用這些 .cu 文件不能直接用 Python 調(diào)用需要先編譯成 PyTorch 的 C 擴(kuò)展。常見(jiàn)做法是寫(xiě)一個(gè)setup.py文件把gnn_propagate.cpp和gnn_propagate_kernel.cu通過(guò)torch.utils.cpp_extension.CUDAExtension編譯。源碼包里沒(méi)列出setup.py但列出的setup.cfg和.flake8說(shuō)明項(xiàng)目原本是用 setuptools 管理的可以依此補(bǔ)全編譯入口。# 編譯自定義 CUDA 算子的常用命令 python setup.py build_ext --inplace編譯前先確認(rèn)三件事CUDA 版本和 PyTorch 版本匹配、GPU 算力在 6.0 以上GTX 10 系及以上、TORCH_CUDA_ARCH_LIST環(huán)境變量設(shè)置了正確的算力。我見(jiàn)過(guò)不少人在這一步卡住報(bào)錯(cuò)大多是undefined symbol或者gcc: error: unrecognized command line option前者是編譯順序錯(cuò)了后者是 GCC 版本太新需要降級(jí)到 7 以下。setup.cfg里的[metadata]和[options]字段定義了包的版本、依賴和入口如果編譯不通過(guò)先檢查它和setup.py里的參數(shù)是否一致。4. 環(huán)境部署與訓(xùn)練自己的數(shù)據(jù)集Dockerfile 和參數(shù)配置4.1 Dockerfile 里的環(huán)境是怎么搭起來(lái)的這套源碼自帶 Dockerfile這是最省心的一環(huán)。讀 Dockerfile 的目的是搞明白項(xiàng)目依賴了哪些系統(tǒng)庫(kù)和 Python 包避免在本地環(huán)境里缺這個(gè)缺那個(gè)。典型的多階段構(gòu)建流程是第一步拉一個(gè) CUDA 官方鏡像比如nvidia/cuda:11.3.1-cudnn8-devel-ubuntu20.04第二步安裝 Python 3.8 和系統(tǒng)依賴第三步用 pip 裝 PyTorch、torchvision 和項(xiàng)目所需的包最后把源碼目錄復(fù)制進(jìn)容器。# 構(gòu)建并進(jìn)入容器 docker build -t gait_track:latest . docker run -it --gpus all --shm-size8g gait_track:latest /bin/bash--shm-size8g是容易忽略的參數(shù)。PyTorch 的 DataLoader 在多進(jìn)程模式下會(huì)用共享內(nèi)存做數(shù)據(jù)緩存容器默認(rèn)的/dev/shm只有 64MB數(shù)據(jù)加載稍微大一點(diǎn)就報(bào)Bus error或out of shared memory這個(gè)坑在監(jiān)控視頻數(shù)據(jù)集上特別常見(jiàn)。如果宿主機(jī)的內(nèi)存有限也可以把num_workers設(shè)為 0但那樣訓(xùn)練速度會(huì)明顯下降。4.2 數(shù)據(jù)集組織方式y(tǒng)olov5、DeepSORT、GaitSet 三種數(shù)據(jù)格式整套系統(tǒng)用到三種不同的數(shù)據(jù)標(biāo)注格式這是最容易混亂的地方需要分別準(zhǔn)備。模塊數(shù)據(jù)格式目錄組織關(guān)鍵點(diǎn)yolov5YOLO txt 格式每行cls x y w himages/和labels/按 train/val 分坐標(biāo)是歸一化后的不是像素值DeepSORT檢測(cè)結(jié)果文本每行frame_id, track_id, x, y, w, h, confMOT 風(fēng)格訓(xùn)練時(shí)不需要標(biāo)注需要的是檢測(cè)框和 IDGaitSet輪廓圖序列每段序列一個(gè)文件夾subject/sequence/按行人 ID 分需要先對(duì)原圖做前景分割提取二值輪廓GaitSet 的數(shù)據(jù)準(zhǔn)備是最耗時(shí)的。你需要一個(gè)分割模型把行人從背景中摳出來(lái)或者用背景減除算法生成輪廓圖。輪廓圖的尺寸建議統(tǒng)一為 64x44這是 GaitSet 訓(xùn)練時(shí)的常用尺寸。輪廓的質(zhì)量直接決定步態(tài)特征的好壞——輪廓上有空洞、有背景殘留特征就會(huì)帶噪聲。如果你手頭的是 CASIA-B 數(shù)據(jù)集可以直接用官方的預(yù)處理腳本如果是自采數(shù)據(jù)需要先過(guò)一遍檢測(cè)和分割再生成訓(xùn)練集。4.3 超參數(shù)調(diào)整yolov5 訓(xùn)練自己的數(shù)據(jù)集yolov5 訓(xùn)練時(shí)的超參數(shù)文件是data/hyps/hyp.scratch-low.yaml里面包含了 lr、mosaic、mixup 等參數(shù)。用預(yù)訓(xùn)練權(quán)重做遷移學(xué)習(xí)時(shí)建議把lr0從默認(rèn)的 0.01 降到 0.001因?yàn)槟愕臄?shù)據(jù)集規(guī)模和類別數(shù)都變了過(guò)大的學(xué)習(xí)率會(huì)破壞預(yù)訓(xùn)練特征。# hyp.scratch-low.yaml 關(guān)鍵參數(shù)訓(xùn)練行人檢測(cè)時(shí)推薦修改 lr0: 0.001 # 初始學(xué)習(xí)率遷移學(xué)習(xí)用小一點(diǎn) lrf: 0.01 # 最終學(xué)習(xí)率比例 warmup_epochs: 3.0 # 預(yù)熱輪數(shù) mosaic: 1.0 # mosaic 增強(qiáng)行人數(shù)據(jù)集可以保留 fliplr: 0.5 # 水平翻轉(zhuǎn)對(duì)行人檢測(cè)友好# 訓(xùn)練命令 python train.py --data person.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --device 0--batch 16是在 8GB 顯存的 GPU 上比較穩(wěn)妥的值顯存不夠就減半。--img 640和推理時(shí)的size640保持一致訓(xùn)練和推理尺寸不一致會(huì)掉點(diǎn)。5. 避坑指南跨鏡頭關(guān)聯(lián)里最容易翻車的五個(gè)點(diǎn)5.1 權(quán)重文件和 .cu 文件不匹配編譯過(guò)了但結(jié)果全錯(cuò)現(xiàn)象編譯成功import成功但 GNN 傳播的輸出全是 NaN。原因gnn_propagate_kernel.cu是在一個(gè)特定 PyTorch 版本下寫(xiě)的和你當(dāng)前的 PyTorch 版本在張量?jī)?nèi)存布局或計(jì)算圖處理上不兼容。解決先跑一個(gè) 2 節(jié)點(diǎn) 3 特征的小用例用 CPU 實(shí)現(xiàn)的結(jié)果和 GPU 實(shí)現(xiàn)的結(jié)果做逐位對(duì)比如果差異巨大降級(jí) PyTorch 版本或者檢查是否有重復(fù)定義的宏。5.2 時(shí)間對(duì)齊沒(méi)做跨鏡頭匹配率直接砍半現(xiàn)象鏡頭 A 和鏡頭 B 記錄的是同一場(chǎng)景但時(shí)間戳相差了十幾秒GNN 的鄰接矩陣?yán)飵缀跽也坏娇缭絻蓚€(gè)相機(jī)的邊。原因time_window參數(shù)判斷的是軌跡的時(shí)間重疊度兩個(gè)鏡頭的時(shí)間不同步導(dǎo)致同一個(gè)目標(biāo)在 A 鏡頭結(jié)束和 B 鏡頭開(kāi)始的幀號(hào)完全對(duì)不上。解決在預(yù)處理階段做一個(gè)時(shí)間偏移校準(zhǔn)用同一時(shí)刻出現(xiàn)在兩個(gè)畫(huà)面中的目標(biāo)哪怕是不同 ID估算幀號(hào)偏差然后統(tǒng)一到同一個(gè)時(shí)間軸再跑關(guān)聯(lián)流程。5.3 GaitSet 特征沒(méi)有歸一化相似度計(jì)算失靈現(xiàn)象余弦相似度算出來(lái)全部在 0.99 以上設(shè)什么閾值都沒(méi)區(qū)分度。原因GaitSet 輸出的特征向量模長(zhǎng)差異巨大直接算余弦相似度等于在比模長(zhǎng)而不是比方向。解決在特征輸入 GNN 之前強(qiáng)制做 L2 歸一化讓每個(gè)特征向量的模長(zhǎng)為 1。import torch import torch.nn.functional as F def normalize_features(features): # features: [N, D] 原始步態(tài)特征 return F.normalize(features, p2, dim1)5.4 小目標(biāo)檢測(cè)漏檢軌跡斷裂后跟蹤器無(wú)法恢復(fù)現(xiàn)象監(jiān)控畫(huà)面里的人離攝像頭遠(yuǎn)身體只有 20 像素高yolov5 經(jīng)常漏檢DeepSORT 的軌跡頻繁丟失。原因yolov5 在 COCO 上的小目標(biāo) AP 本來(lái)就低20 像素的行人已經(jīng)超出了常規(guī)檢測(cè)能力。解決一是把推理尺寸從 640 提高到 1280顯存夠的話二是對(duì)漏檢幀做插值。最穩(wěn)妥的做法還是換一個(gè)針對(duì)小目標(biāo)優(yōu)化的檢測(cè)器分支或者給檢測(cè)結(jié)果加一幀的容忍——上一幀有檢測(cè)框、這一幀沒(méi)有先沿用上一幀位置做卡爾曼預(yù)測(cè)。5.5 圖里節(jié)點(diǎn)全是負(fù)樣本步態(tài)識(shí)別退化成換裝重識(shí)別現(xiàn)象跨鏡頭的匹配準(zhǔn)確率還不如純 ReID加了 GNN 反而掉點(diǎn)。原因負(fù)樣本不同 ID 的軌跡在圖里占了大多數(shù)GNN 傳播時(shí)每個(gè)節(jié)點(diǎn)的特征都被大量負(fù)樣本的噪聲特征污染了正樣本的信息被稀釋。解決在build_adjacency_matrix里提高相似度閾值或者在構(gòu)建圖的時(shí)候加上一個(gè)先驗(yàn)約束——只有空間上可能銜接的鏡頭對(duì)才允許連邊。最直接的辦法是統(tǒng)計(jì)每個(gè)目標(biāo)的軌跡平均長(zhǎng)度去掉那些小于 10 幀的短軌跡——步態(tài)特征本身就需要連續(xù)幀才能穩(wěn)定提取。6. 跨鏡頭軌跡拼接的落盤驗(yàn)證用兩個(gè)攝像頭實(shí)測(cè)一輪拿到源碼后第一步不是改代碼而是先驗(yàn)證跨鏡頭拼接的質(zhì)量。我習(xí)慣的做法是準(zhǔn)備兩個(gè)模擬鏡頭的數(shù)據(jù)同一個(gè)場(chǎng)景里兩個(gè)不同角度的錄屏中間有 3 秒的交接區(qū)。跑完整個(gè)流程后看兩件事。第一件事是檢查軌跡文件里有沒(méi)有出現(xiàn)「同一個(gè)全局 ID 在不同鏡頭里時(shí)間上重疊」的情況。如果 ID 5 在鏡頭 A 的第 100 幀和鏡頭 B 的第 100 幀同時(shí)出現(xiàn)說(shuō)明 GNN 匹配錯(cuò)了。第二件事是可視化拼接結(jié)果把兩個(gè)鏡頭的畫(huà)面拼在一起用同一個(gè)顏色畫(huà)同一個(gè)人這個(gè)辦法笨但直接比任何指標(biāo)都直觀。import cv2 import numpy as np # 讀取兩個(gè)攝像頭的軌跡輸出 # 格式: frame_id, global_id, camera_id, x, y, w, h cam_a np.loadtxt(track_cam_a.txt, delimiter,) cam_b np.loadtxt(track_cam_b.txt, delimiter,) # 按全局 ID 把軌跡拆開(kāi)檢測(cè)重疊 ids_a set(cam_a[:, 1]) ids_b set(cam_b[:, 1]) overlap_errors [] for gid in ids_a ids_b: frames_a set(cam_a[cam_a[:, 1] gid][:, 0]) frames_b set(cam_b[cam_b[:, 1] gid][:, 0]) if frames_a frames_b: overlap_errors.append(gid) print(f重疊軌跡數(shù): {len(overlap_errors)})overlap_errors如果超過(guò) 3%說(shuō)明鄰接矩陣的邊建得太寬松了優(yōu)先調(diào)整sim_threshold和時(shí)間窗口。如果這個(gè)指標(biāo)沒(méi)問(wèn)題再計(jì)算匹配的準(zhǔn)確率——手動(dòng)標(biāo)注一段視頻里真實(shí)匹配的軌跡對(duì)數(shù)和 GNN 輸出的匹配結(jié)果做對(duì)比。做完這兩步你才真正摸透了這套源碼的輸出和評(píng)價(jià)維度后面無(wú)論改哪個(gè)模塊都有基準(zhǔn)可對(duì)照。這套流程里的每一步我都吃過(guò)虧。以前偷懶直接拿默認(rèn)配置跑結(jié)果跨鏡頭 ID 切換率慘不忍睹檢查了半天才發(fā)現(xiàn)是時(shí)間偏移和數(shù)據(jù)歸一化的問(wèn)題。那之后每次做跨鏡頭實(shí)驗(yàn)我都強(qiáng)制先驗(yàn)證數(shù)據(jù)對(duì)齊再跑完整鏈路。希望這篇筆記能幫你少走幾步彎路。本文還有配套的精品資源點(diǎn)擊獲取