系統(tǒng)實(shí)戰(zhàn):YOLO跟蹤計(jì)數(shù)與部署避坑指南)
簡(jiǎn)介這是一份基于深度學(xué)習(xí)的人流量檢測(cè)系統(tǒng)畢業(yè)設(shè)計(jì)項(xiàng)目包含 Python 源碼與項(xiàng)目說明文檔面向計(jì)算機(jī)相關(guān)專業(yè)正在準(zhǔn)備畢業(yè)設(shè)計(jì)的學(xué)生也適合需要項(xiàng)目實(shí)戰(zhàn)練習(xí)的學(xué)習(xí)者可用于課程設(shè)計(jì)或期末大作業(yè)。項(xiàng)目經(jīng)嚴(yán)格調(diào)試后可穩(wěn)定運(yùn)行作為導(dǎo)師認(rèn)可的高分畢業(yè)設(shè)計(jì)從模型搭建、訓(xùn)練到前端檢測(cè)展示均有完整代碼目錄結(jié)構(gòu)清晰便于對(duì)照說明理解整套人流量檢測(cè)流程。資源包共 1235 個(gè)文件約 61.54MB其中 76 個(gè) Python 文件構(gòu)成核心檢測(cè)邏輯配合 382 個(gè) HTML 頁(yè)面、194 個(gè) JS 腳本、51 個(gè) CSS 樣式搭建可視化界面208 張 PNG 圖片與 186 張 GIF 動(dòng)圖用于界面元素和過程演示另有 ipynb 筆記、配置文件與模型資源方便復(fù)現(xiàn)訓(xùn)練與推理過程。該資源已有 264 人學(xué)習(xí)使用適合在已有項(xiàng)目上快速上手、二次開發(fā)也可用于學(xué)習(xí)深度學(xué)習(xí)檢測(cè)項(xiàng)目的工程組織方式。1. 人流量檢測(cè)別急著訓(xùn)模型拿到這個(gè)標(biāo)題你第一反應(yīng)多半是找個(gè)人體檢測(cè)模型數(shù)畫面里有幾個(gè)人。但我做下來的感受是人流量檢測(cè)真正難的不是檢測(cè)而是數(shù)得穩(wěn)——跨攝像頭、跨時(shí)段、有遮擋、有透視畸變時(shí)同一個(gè)人在不同幀里既可能被漏檢也可能被重復(fù)計(jì)數(shù)準(zhǔn)確率浮動(dòng)非常大。這個(gè)畢業(yè)設(shè)計(jì)項(xiàng)目的核心并不在于用深度學(xué)習(xí)框出人而在于把檢測(cè)結(jié)果、追蹤邏輯和業(yè)務(wù)口徑什么才算通過一次拼成一套能自洽的計(jì)數(shù)系統(tǒng)。適合正在做畢設(shè)或課設(shè)、想拿一個(gè)完整可演示項(xiàng)目的同學(xué)也適合剛?cè)腴T目標(biāo)檢測(cè)、想看看工程約束怎么倒逼模型選型的開發(fā)者。下面我把整套方案的選型、數(shù)據(jù)準(zhǔn)備、源碼結(jié)構(gòu)和踩坑點(diǎn)按實(shí)際開發(fā)順序拆給你。2. 檢測(cè) vs 計(jì)數(shù)兩條技術(shù)路線與選型判斷2.1 為什么單張圖檢測(cè)不夠用計(jì)數(shù)口徑才是系統(tǒng)的靈魂人流量檢測(cè)系統(tǒng)如果只做每幀輸出若干框站在答辯或驗(yàn)收視角是站不住的——演示時(shí)隨便有人交錯(cuò)走過人數(shù)就亂了。真正的流量指標(biāo)是單位時(shí)間內(nèi)有多少人穿過某個(gè)斷面這要求系統(tǒng)能區(qū)分這是剛才那個(gè)人走過去了還是新來一個(gè)人。常見做法是引入跟蹤Track來給檢測(cè)框分配 ID再配合一條虛擬計(jì)數(shù)線或區(qū)域判斷來完成計(jì)數(shù)。也就是說模型只是上游Tracking 和計(jì)數(shù)邏輯才是中游和下游三者缺一不可。我一般會(huì)把技術(shù)選型分成兩類一類是檢測(cè)類方案即 YOLO 系模型檢出人框再配 IoU 匹配做跟蹤另一類是密度回歸類方案即 CSRNet 這類網(wǎng)絡(luò)直接回歸人群密度圖再積分求人數(shù)。前者對(duì)個(gè)體定位清晰、可解釋性強(qiáng)、單人場(chǎng)景精度高但遮擋嚴(yán)重時(shí)漏檢率會(huì)陡升后者在密集人群場(chǎng)景更有優(yōu)勢(shì)但無(wú)法給出每個(gè)人在哪的框而且訓(xùn)練密度圖標(biāo)注成本極高。2.2 畢設(shè)場(chǎng)景選 YOLO 系模型的四個(gè)理由如果你的場(chǎng)景是教室門口、走廊、超市出入口這類半密集場(chǎng)景我會(huì)優(yōu)先選 YOLO 系檢測(cè)模型理由很直接預(yù)訓(xùn)練權(quán)重成熟公開數(shù)據(jù)集多換到自己場(chǎng)景只需要微調(diào)推理速度快普通 GPU 上輕松跑實(shí)時(shí)可視化效果好畫框標(biāo) ID 一眼就能看懂答辯時(shí)演示性強(qiáng)生態(tài)完善從標(biāo)注格式轉(zhuǎn)換到部署都有大量現(xiàn)成代碼可參考。相比之下CSRNet 這類密度回歸模型在 50 人以上的密集場(chǎng)景確實(shí)更強(qiáng)但標(biāo)注密度圖是逐點(diǎn)標(biāo)注人頭的工作量比畫框還大而且視覺反饋弱演示效果不容易討喜。選型還要考慮運(yùn)行環(huán)境。大部分畢設(shè)項(xiàng)目會(huì)在自己的筆記本上演示因此我建議優(yōu)先考慮 YOLOv5s 或 YOLOv8n 這類小體量版本而不是 YOLOv8x 這種重模型。同樣的訓(xùn)練設(shè)備和推理設(shè)備n/s 級(jí)別模型能保證在 CPU 上也有可接受的推理幀率這對(duì)演示現(xiàn)場(chǎng)沒有 GPU 的情況非常關(guān)鍵。如果你的機(jī)器配置好再考慮大模型。2.3 完整系統(tǒng)的最小功能骨架一個(gè)能交付的人流量檢測(cè)系統(tǒng)至少要包含五個(gè)模塊目標(biāo)檢測(cè)模塊負(fù)責(zé)從畫面中檢出人跟蹤模塊負(fù)責(zé)分配并維護(hù) ID計(jì)數(shù)邏輯模塊負(fù)責(zé)定義穿越規(guī)則并累計(jì)結(jié)果可視化模塊負(fù)責(zé)實(shí)時(shí)畫框、畫計(jì)數(shù)線和顯示人數(shù)曲線數(shù)據(jù)記錄模塊負(fù)責(zé)把計(jì)數(shù)結(jié)果落到 CSV 或數(shù)據(jù)庫(kù)里供后續(xù)分析或答辯展示。標(biāo)題里源碼項(xiàng)目說明正常情況下就應(yīng)包含這些模塊對(duì)應(yīng)的代碼文件和說明文檔。項(xiàng)目結(jié)構(gòu)我一般會(huì)這樣組織people_flow/ ├── checkpoints/ # 模型權(quán)重存放目錄 ├── configs/ # 配置文件路徑、閾值、計(jì)數(shù)線位置等 ├── data/ │ ├── dataset/ # 標(biāo)注數(shù)據(jù)與圖片 │ ├── processed/ # 轉(zhuǎn)換后的 YOLO 格式標(biāo)注 │ └── videos/ # 測(cè)試視頻 ├── scripts/ │ ├── convert_labels.py # 標(biāo)注格式轉(zhuǎn)換腳本 │ └── train.py # 訓(xùn)練入口 ├── src/ │ ├── detector.py # 檢測(cè)封裝 │ ├── tracker.py # 跟蹤封裝 │ ├── counter.py # 計(jì)數(shù)邏輯 │ ├── visualizer.py # 可視化 │ └── main.py # 主入口 └── requirements.txt為什么要拆這么細(xì)因?yàn)楫呍O(shè)答辯會(huì)問你每個(gè)文件是干什么的如果全部邏輯堆在一個(gè) main.py 里提問時(shí)很難講清楚模塊劃分。按功能拆出獨(dú)立模塊既能體現(xiàn)工程能力也方便單獨(dú)測(cè)試某一個(gè)環(huán)節(jié)比如單獨(dú)跑 detector 看檢測(cè)效果單獨(dú)跑 counter 看計(jì)數(shù)邏輯。3. 訓(xùn)練數(shù)據(jù)準(zhǔn)備從 VOC 到 YOLO 格式的轉(zhuǎn)換腳本與四個(gè)邊界坑3.1 優(yōu)先用公開數(shù)據(jù)集做遷移學(xué)習(xí)自定義數(shù)據(jù)只做微調(diào)流量檢測(cè)的數(shù)據(jù)集準(zhǔn)備有一個(gè)原則不要從零標(biāo)注幾千張圖。公開數(shù)據(jù)集里COCO 的 person 類別、CrowdHuman、MOT Challenge 系列都可以直接用來做預(yù)訓(xùn)練或微調(diào)。其中 CrowdHuman 對(duì)人群遮擋場(chǎng)景標(biāo)注比較充分MOT16/17 帶跟蹤標(biāo)注適合順帶做跟蹤評(píng)估。畢設(shè)項(xiàng)目一般不需要自己從零采集大量數(shù)據(jù)常見做法是先用公開數(shù)據(jù)訓(xùn)練一個(gè)可用的檢測(cè)器再采集一小部分目標(biāo)場(chǎng)景數(shù)據(jù)比如自己教室門口、實(shí)驗(yàn)室走廊的視頻做微調(diào)讓模型適應(yīng)當(dāng)前攝像頭的視角和光照。這樣能明顯減少標(biāo)注工作量訓(xùn)練數(shù)據(jù)量在幾百?gòu)埣?jí)別就夠了。需要提醒的是即便用了遷移學(xué)習(xí)標(biāo)注格式依然是最容易被絆倒的環(huán)節(jié)。假如你找到的公開數(shù)據(jù)集是 COCO 格式JSON而訓(xùn)練代碼用的是 YOLO 格式TXT中間必須寫轉(zhuǎn)換腳本。很多學(xué)生在網(wǎng)上找的代碼只支持一種格式數(shù)據(jù)喂進(jìn)去報(bào)錯(cuò)半天才發(fā)現(xiàn)是格式問題這個(gè)我深有體會(huì)。因此第一步先確認(rèn)你的訓(xùn)練代碼吃的是什么格式再寫轉(zhuǎn)換腳本而不是先轉(zhuǎn)完再回頭看代碼。3.2 標(biāo)注格式轉(zhuǎn)換腳本XML/TXT 與 JSON 的互相轉(zhuǎn)換這里給出一個(gè)從 COCO JSON 轉(zhuǎn) YOLO TXT 的腳本。它是整套數(shù)據(jù)流程中最常被復(fù)用的工具后續(xù)你換數(shù)據(jù)集、換場(chǎng)景大概率還要改它來適配。import json import os # COCO 數(shù)據(jù)集的 person 類別 id 通常是 1需按實(shí)際數(shù)據(jù)集確認(rèn) PERSON_CATEGORY_ID 1 def convert_coco_json_to_yolo_txt(coco_json_path, output_dir): with open(coco_json_path, r, encodingutf-8) as f: coco json.load(f) # 建立 image_id - 圖片文件名的映射 images {img[id]: img for img in coco[images]} os.makedirs(output_dir, exist_okTrue) for ann in coco[annotations]: if ann[category_id] ! PERSON_CATEGORY_ID: continue image_info images[ann[image_id]] img_w, img_h image_info[width], image_info[height] # COCO 的 bbox 是 [x, y, width, height]YOLO 需要的是歸一化中心點(diǎn)和寬高 x, y, w, h ann[bbox] cx (x w / 2) / img_w cy (y h / 2) / img_h w_norm w / img_w h_norm h / img_h # 一個(gè)圖片對(duì)應(yīng)一個(gè) txt 文件多個(gè)標(biāo)注就追加多行 txt_path os.path.join( output_dir, os.path.splitext(image_info[file_name])[0] .txt ) with open(txt_path, a, encodingutf-8) as out: out.write(f0 {cx:.6f} {cy:.6f} {w_norm:.6f} {h_norm:.6f}\n) print(f轉(zhuǎn)換完成標(biāo)注文件輸出到: {output_dir})這段腳本的邏輯不復(fù)雜對(duì)每一條 category 為 person 的標(biāo)注取出 bbox將左上角坐標(biāo)格式轉(zhuǎn)成歸一化的中心點(diǎn)加寬高格式寫入同名 TXT。注意它是按行追加寫入的所以同一個(gè)圖片有多個(gè)人時(shí)會(huì)產(chǎn)生多行標(biāo)注。如果你拿到的數(shù)據(jù)本身就是 VOC 的 XML 格式也是同樣的邏輯只是解析標(biāo)簽從 JSON 字段換成 XML 的 object/bndbox 節(jié)點(diǎn)。3.3 四個(gè)邊界坑坐標(biāo)裁剪、空標(biāo)注、類別編號(hào)、數(shù)據(jù)集劃分第一個(gè)坑是 bbox 出界。攝像頭畫面邊緣的人往往只露出一半身體標(biāo)注框的坐標(biāo)可能落在圖像邊界之外。YOLO 訓(xùn)練時(shí)若不對(duì)坐標(biāo)做 clip裁剪會(huì)算出負(fù)的寬高或超出 1 的歸一化值輕則警告重則訓(xùn)練 loss 變成 NaN。因此轉(zhuǎn)換時(shí)務(wù)必加一步x max(0, x)、x w min(img_w, x w)之類的邊界處理。第二個(gè)坑是空標(biāo)注文件。如果某張圖只標(biāo)了 person 類而實(shí)際畫面有大量人沒被標(biāo)注轉(zhuǎn)換后會(huì)生成空 TXT。把這些空文件直接丟給訓(xùn)練腳本有些框架會(huì)報(bào)no labels錯(cuò)。我的建議是轉(zhuǎn)換腳本里統(tǒng)計(jì)每個(gè) TXT 的行數(shù)自動(dòng)列出空文件讓你人工確認(rèn)是漏標(biāo)還是確實(shí)無(wú)人。第三個(gè)坑是類別編號(hào)一致性。YOLO 訓(xùn)練配置里 class 編號(hào)從 0 開始很多人從網(wǎng)上下載的標(biāo)注里 person 編號(hào)是 0但換一個(gè)數(shù)據(jù)集可能變成 1 或 15訓(xùn)練時(shí)如果不改配置文件模型會(huì)把人學(xué)成背景精度一塌糊涂。第四個(gè)坑是數(shù)據(jù)集劃分。訓(xùn)練、驗(yàn)證、測(cè)試三個(gè)集合之間不能有圖片重疊而且同一個(gè)人在不同幀里的畫面不要被分到訓(xùn)練和驗(yàn)證兩個(gè)集合里否則驗(yàn)證指標(biāo)虛高。這一點(diǎn)在按幀抽幀做人流數(shù)據(jù)時(shí)特別容易犯——視頻連續(xù)幀高度相似隨機(jī)劃分會(huì)導(dǎo)致驗(yàn)證集泄露。提示轉(zhuǎn)換腳本里建議把類別編號(hào)做成可配置參數(shù)不要寫死。你在換數(shù)據(jù)集時(shí)只需改一個(gè)變量的值而不是去代碼里到處找數(shù)字。3.4 數(shù)據(jù)增強(qiáng)怎么做才不偏離流量場(chǎng)景數(shù)據(jù)增強(qiáng)方面人流場(chǎng)景我最常用的組合是HSV 色域擾動(dòng)、隨機(jī)平移與縮放、馬賽克增強(qiáng)。HSV 擾動(dòng)可以提高不同光照下的泛化能力因?yàn)槟阌?xùn)練的可能是白天采集的視頻而實(shí)際部署可能要面對(duì)傍晚光線馬賽克增強(qiáng)則把四張圖拼成一張?zhí)嵘四P驮谌巳赫趽鯃?chǎng)景下的魯棒性。需要謹(jǐn)慎的是大角度旋轉(zhuǎn)和上下翻轉(zhuǎn)。上下翻轉(zhuǎn)在人流場(chǎng)景是禁忌因?yàn)楸O(jiān)控?cái)z像頭一般是固定的不可能出現(xiàn)倒立的人大角度旋轉(zhuǎn)也會(huì)產(chǎn)生大量現(xiàn)實(shí)中不存在的姿態(tài)。合理的增強(qiáng)策略應(yīng)該是模擬攝像頭可能遇到的真實(shí)變化而不是無(wú)腦堆疊所有增強(qiáng)手段。4. 源碼拆解與二次開發(fā)模型定義、推理主循環(huán)和評(píng)估指標(biāo)4.1 模型結(jié)構(gòu)選擇與參數(shù)設(shè)定從 YOLOv5s 到 YOLOv8n 的取舍到了源碼層面首先面對(duì)的是模型怎么定義。如果你拿到的項(xiàng)目說明基于 YOLOv5那它大概率用 yaml 文件描述網(wǎng)絡(luò)結(jié)構(gòu)如果是 YOLOv8結(jié)構(gòu)定義則內(nèi)聚在 Python 類中。兩種寫法本質(zhì)一樣都包含 Backbone、Neck、Head 三部分。畢設(shè)項(xiàng)目里我建議優(yōu)先讀 YOLOv8n 的源碼因?yàn)樗慕Y(jié)構(gòu)更清晰、接口更簡(jiǎn)潔而且文檔和示例更豐富。# yolo_v8_n.yaml 的核心結(jié)構(gòu)說明用實(shí)際文件由 ultralytics 包自動(dòng)加載 backbone: - [-1, 1, Conv, [64, 3, 2]] # 下采樣輸出 1/2 分辨率特征圖 - [-1, 1, Conv, [128, 3, 2]] # 下采樣輸出 1/4 分辨率特征圖 - [-1, 3, C2f, [128, True]] # C2f 模塊特征提取 - [-1, 1, Conv, [256, 3, 2]] # 下采樣輸出 1/8 分辨率特征圖 - [-1, 6, C2f, [256, True]] # ... 后續(xù)為 1/16、1/32 分辨率層級(jí) head: - [-1, 1, Detect, [nc]] # nc 是類別數(shù)這里固定為 1person這段描述里每一行的[-1, 1, Conv, [64, 3, 2]]含義是從上一層輸入-1 表示上一層、1 個(gè)該模塊、Conv 卷積模塊、輸出通道 64、卷積核 3x3、步長(zhǎng) 2。C2f 是 YOLOv8 的核心殘差模塊兼顧速度和精度。修改結(jié)構(gòu)時(shí)你只需要關(guān)注兩個(gè)參數(shù)depth_multiple 和 width_multiple分別控制網(wǎng)絡(luò)深度和寬度調(diào)小它們就能得到更輕量的模型調(diào)大則更準(zhǔn)但更慢。4.2 推理主循環(huán)幀讀取、檢測(cè)、跟蹤、計(jì)數(shù)如何串起來在線推理主循環(huán)是系統(tǒng)運(yùn)行的核心。下面給出一個(gè)簡(jiǎn)化但完整的推理循環(huán)骨架展示了檢測(cè)與跟蹤是如何銜接的import cv2 import torch from ultralytics import YOLO model YOLO(checkpoints/yolov8n_person.pt) tracker cv2.legacy.TrackerKCF_create() # 用 KCF 做簡(jiǎn)單兜底跟蹤 count_line_y 240 # 計(jì)數(shù)線位置需按實(shí)際畫面標(biāo)定 up_count 0 down_count 0 tracked_positions {} cap cv2.VideoCapture(data/videos/test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # 檢測(cè)conf 閾值控制誤檢與漏檢的平衡 results model(frame, conf0.4, iou0.5, classes[0]) boxes results[0].boxes.xyxy.cpu().numpy() for box in boxes: x1, y1, x2, y2 box[:4].astype(int) cx, cy (x1 x2) // 2, (y1 y2) // 2 # 框底部中心點(diǎn)更穩(wěn)定 # 判斷是否穿越計(jì)數(shù)線 if cy count_line_y: down_count 1 else: up_count 1 # 可視化 frame cv2.line(frame, (0, count_line_y), (frame.shape[1], count_line_y), (0, 255, 0), 2) cv2.imshow(people_flow, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()這段代碼的邏輯是每讀一幀送進(jìn) YOLO 檢測(cè)人框取框的底邊中心點(diǎn)作為人的落腳點(diǎn)當(dāng)落腳點(diǎn)跨過計(jì)數(shù)線時(shí)上/下行計(jì)數(shù)加一。為什么用底邊中心而不是框中心因?yàn)榭蛑行臅?huì)隨人體姿態(tài)變化而上下浮動(dòng)在彎腰、坐下等動(dòng)作下容易誤判跨越方向而底邊中心貼近地面幾何上更穩(wěn)定。注意上面這個(gè)循環(huán)為了展示骨架省略了跟蹤模塊。實(shí)際項(xiàng)目中直接用逐幀檢測(cè)做計(jì)數(shù)會(huì)產(chǎn)生嚴(yán)重重復(fù)計(jì)數(shù)——同一個(gè)停留的人會(huì)被反復(fù)累加。至少需要加一層簡(jiǎn)單的目標(biāo) ID 去重判斷當(dāng)前檢測(cè)框是否已關(guān)聯(lián)到某個(gè)已有 ID如果關(guān)聯(lián)到則只更新位置、不重復(fù)計(jì)數(shù)。4.3 項(xiàng)目說明通常包含哪些內(nèi)容評(píng)估指標(biāo)與可視化輸出項(xiàng)目說明在畢設(shè)項(xiàng)目里通常不只是代碼注釋它要包含數(shù)據(jù)集介紹、模型結(jié)構(gòu)說明、訓(xùn)練參數(shù)表、評(píng)估指標(biāo)和運(yùn)行步驟。評(píng)估指標(biāo)方面人流檢測(cè)系統(tǒng)建議至少報(bào)告三個(gè)檢測(cè)精度的 mAP0.5計(jì)數(shù)精度的 MAE平均絕對(duì)誤差和 RMSE均方根誤差。MAE 的計(jì)算很簡(jiǎn)單逐幀把預(yù)測(cè)人數(shù)與人工標(biāo)注人數(shù)做差的絕對(duì)值再取平均。在答辯時(shí)給出 mAP 只能證明你檢測(cè)得準(zhǔn)給出 MAE 才能證明你數(shù)得準(zhǔn)后者才是人流量檢測(cè)系統(tǒng)區(qū)別于普通目標(biāo)檢測(cè)項(xiàng)目的核心。def compute_mae(pred_counts, gt_counts): assert len(pred_counts) len(gt_counts) errors [abs(p - g) for p, g in zip(pred_counts, gt_counts)] return sum(errors) / len(errors) pred [3, 4, 5, 6] # 模型每幀計(jì)數(shù) gt [3, 5, 4, 6] # 人工標(biāo)注 print(compute_mae(pred, gt)) # 輸出 0.75這個(gè)函數(shù)看起來簡(jiǎn)單但在畢設(shè)項(xiàng)目中幾乎是必不可少的。因?yàn)槿肆髁繖z測(cè)的項(xiàng)目說明文檔里需要你給出本系統(tǒng)計(jì)數(shù)精度達(dá)到 MAE0.75這樣的結(jié)論性數(shù)據(jù)而不是只寫模型效果不錯(cuò)。我建議你在項(xiàng)目說明中附上逐幀的預(yù)測(cè)值和真實(shí)值對(duì)比表格再畫出變化曲線圖這種量化的呈現(xiàn)方式在答辯時(shí)非常加分。5. 部署避坑白天黑夜漂移卡頓和這些常見問題5.1 模型在白天效果好、傍晚效果差的玄學(xué)多時(shí)段數(shù)據(jù)微調(diào)我做流量檢測(cè)遇到的第一個(gè)大坑就是白天能用傍晚翻車。原因不復(fù)雜訓(xùn)練數(shù)據(jù)基本來自白天光線充足的場(chǎng)景模型學(xué)到的是亮環(huán)境下的行人外觀。到了傍晚暗光加路燈的混合光源下人臉和衣服的顏色細(xì)節(jié)都變了檢測(cè)置信度直線下降漏檢率飆升。解決思路不是加多少數(shù)據(jù)增強(qiáng)就能搞定的最有效的做法是采集目標(biāo)場(chǎng)景下多個(gè)時(shí)段早、中、晚各 10~20 分鐘視頻抽幀后做半自動(dòng)標(biāo)注再加入訓(xùn)練集微調(diào)。注意微調(diào)時(shí)不要只用傍晚數(shù)據(jù)要混合原有數(shù)據(jù)否則模型會(huì)對(duì)傍晚過擬合、白天精度反而下降。5.2 視頻里人一多就掉幀推理速度優(yōu)化三件套人流檢測(cè)最容易在演示時(shí)暴露的問題是卡頓——人一多幀率降到 15 以下畫面肉眼可見地不流暢。性能瓶頸通常在檢測(cè)模型而不在跟蹤和計(jì)數(shù)邏輯。優(yōu)化手段按性價(jià)比排序第一換小模型YOLOv8x 換成 YOLOv8n精度可能只降 2~3 個(gè)點(diǎn)速度翻倍第二輸入分辨率從 1280 降到 640這是最直接的加速方式第三打開 TensorRT 或 ONNX 導(dǎo)出做推理加速用 GPU 的話能再快不少。在這三招之后才考慮改跟蹤策略、跳幀檢測(cè)這類工程技巧因?yàn)樘鴰瑱z測(cè)會(huì)犧牲計(jì)數(shù)實(shí)時(shí)性很容易被答辯老師看穿。# 用半精度推理加速GPU 可用時(shí)能明顯提升幀率 model YOLO(checkpoints/yolov8n_person.pt) results model(frame, conf0.4, imgsz640, halfTrue)這段代碼的關(guān)鍵參數(shù)有兩個(gè)imgsz640直接降低輸入分辨率halfTrue啟用 FP16 半精度推理。如果目標(biāo)機(jī)器是 GPU這兩個(gè)參數(shù)疊加通常能把 1080p 視頻的推理幀率拉到可用水平。CPU 機(jī)器上則不建議開 half部分 CPU 對(duì) FP16 支持不好反而更慢。5.3 重復(fù)計(jì)數(shù)沒有跟蹤模塊時(shí)的經(jīng)典翻車現(xiàn)場(chǎng)重復(fù)計(jì)數(shù)是人流量檢測(cè)里最經(jīng)典也最丟分的 bug?,F(xiàn)象是一個(gè)人站在鏡頭前不動(dòng)計(jì)數(shù)數(shù)字卻不斷上漲。原因前面提過——逐幀檢測(cè)沒有去重。解決思路是引入跟蹤給每個(gè)檢測(cè)框分配穩(wěn)定的 ID。實(shí)現(xiàn)方案很多OpenCV 自帶 sort 或 BoT-SORT 都是可以選的前者輕量、適合畢設(shè)后者精度更穩(wěn)、但依賴更多。# 一個(gè)極簡(jiǎn)的基于位置去重方案當(dāng)前框的中心點(diǎn) # 若與上一幀某個(gè)已跟蹤目標(biāo)距離小于閾值則視為同一人 import math tracked_targets {} # id - (cx, cy) NEXT_ID 0 DIST_THRESH 50 # 單位像素需按實(shí)際畫面尺寸調(diào)整 for box in boxes: x1, y1, x2, y2 box[:4].astype(int) cx, cy (x1 x2) // 2, (y1 y2) // 2 matched_id None for tid, (tx, ty) in tracked_targets.items(): if math.hypot(cx - tx, cy - ty) DIST_THRESH: matched_id tid break if matched_id is None: tracked_targets[NEXT_ID] (cx, cy) NEXT_ID 1 else: tracked_targets[matched_id] (cx, cy)這個(gè)方案只能算最簡(jiǎn)單的距離匹配真實(shí)項(xiàng)目中它扛不住目標(biāo)交錯(cuò)和短暫遮擋。但作為畢設(shè)項(xiàng)目的起步它能立刻解決重復(fù)計(jì)數(shù)問題而且代碼量極小、容易講清楚。DIST_THRESH 這個(gè)參數(shù)需要按畫面里人的移動(dòng)速度調(diào)如果人走得快、兩幀之間位移大閾值小了就會(huì)把同一人判成不同人如果畫面高糊、噪聲多閾值大了又容易把兩個(gè)人合并成一個(gè) ID。5.4 計(jì)數(shù)線位置和方向設(shè)定幾何標(biāo)定的細(xì)節(jié)計(jì)數(shù)線的位置看起來是隨手畫一條線實(shí)際調(diào)參時(shí)很講究。如果線畫得太靠近畫面上邊緣人在遠(yuǎn)處尺寸很小、檢測(cè)框不穩(wěn)定容易來回橫跳導(dǎo)致重復(fù)計(jì)數(shù)如果畫得太靠下人已經(jīng)走到鏡頭正下方框的底邊中點(diǎn)受遮擋影響大。常見做法是選擇畫面中行人走動(dòng)的必經(jīng)斷面且在檢測(cè)框高度約為整幅畫面高度 1/4 的位置。線方向的選擇也影響左右計(jì)數(shù)映射要明確好畫面上方向下走對(duì)應(yīng)的是進(jìn)入還是離開一旦映射反了統(tǒng)計(jì)出的進(jìn)出人數(shù)就是反的。這個(gè)問題在評(píng)估時(shí)很容易被忽略等看數(shù)據(jù)才發(fā)現(xiàn)進(jìn)的人數(shù)比出的還少。5.5 模型權(quán)重文件的坑訓(xùn)練了一個(gè)類別的模型部署時(shí)類別過濾最后一條常見的坑是類別編號(hào)在部署時(shí)不匹配。訓(xùn)練時(shí)候如果你改成了nc1只有 person 一類那么模型的輸出類別編號(hào)就是 0推理時(shí)直接全部框都可以接受。但如果你的權(quán)重是從 YOLOv8 官方 COCO 預(yù)訓(xùn)練權(quán)重繼續(xù)微調(diào)的它的類別編號(hào) 0 仍然是 person那就沒問題。最怕的是從別人項(xiàng)目里拿的權(quán)重文件類別列表可能被改過部署時(shí)沒有檢查 classes 參數(shù)結(jié)果模型把貓、狗、車都當(dāng)成目標(biāo)框出來計(jì)數(shù)直接崩。解決方案只有一個(gè)拿到任何權(quán)重文件第一件事是用一張包含行人和非行人的測(cè)試圖跑一次推理查看輸出的類別 ID 和置信度確認(rèn)類別含義再投入使用。6. 讓準(zhǔn)確率再進(jìn)一步透視校正、區(qū)域計(jì)數(shù)與卡爾曼濾波的配合6.1 為什么要做透視校正等大的人在畫面遠(yuǎn)處更小流量檢測(cè)系統(tǒng)到了能用階段你還是會(huì)發(fā)現(xiàn)一個(gè)視覺上很明顯、數(shù)據(jù)上也很明顯的問題同一段真實(shí)人流在畫面遠(yuǎn)處檢測(cè)到的框小、漏檢多近處的框大、誤檢也多。這是因?yàn)閿z像頭是透視成像遠(yuǎn)處的人所占像素面積本來就小小目標(biāo)檢測(cè)能力就成了瓶頸。一個(gè)務(wù)實(shí)的做法是引入透視校正在畫面中選幾個(gè)參考點(diǎn)標(biāo)出實(shí)際距離的比例關(guān)系將遠(yuǎn)處目標(biāo)按比例放大后再送進(jìn)檢測(cè)器。這樣可以讓整幅畫面中人的尺寸大致與真實(shí)物理尺寸成比例提升遠(yuǎn)處小目標(biāo)的召回率。import numpy as np # 在畫面中選擇 4 個(gè)點(diǎn)對(duì)應(yīng)于地面一個(gè)矩形的 4 個(gè)角 # src 是畫面中的像素坐標(biāo)dst 是物理平面上的坐標(biāo)映射 src np.array([[200, 300], [600, 300], [900, 450], [100, 450]], dtypenp.float32) dst np.array([[0, 0], [400, 0], [400, 300], [0, 300]], dtypenp.float32) matrix cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(frame, matrix, (400, 300))透視校正的本質(zhì)是把斜視角的監(jiān)控畫面攤平成俯視視角。應(yīng)用這個(gè)變換后再對(duì) warped 圖像做檢測(cè)遠(yuǎn)處小目標(biāo)的像素尺寸會(huì)被放大檢測(cè)置信度會(huì)明顯提升。這個(gè)方案有兩個(gè)限制一是只能校正地面平面人站立時(shí)的高度信息無(wú)法恢復(fù)二是變換后畫面四周會(huì)產(chǎn)生黑邊或拉伸失真需要裁剪。因此實(shí)際部署中我一般只對(duì)計(jì)數(shù)線附近區(qū)域做透視校正而不是整幅畫面。6.2 區(qū)域計(jì)數(shù)進(jìn)入/離開判定比線計(jì)數(shù)更穩(wěn)用一條虛擬計(jì)數(shù)線做穿越判定雖然直觀但在多人并肩行走、來回走動(dòng)頻繁的場(chǎng)景里誤差很大。更穩(wěn)的做法是用區(qū)域計(jì)數(shù)劃定兩個(gè)相鄰的區(qū)域比如 A 區(qū)和 B 區(qū)當(dāng)某個(gè)目標(biāo)的軌跡從 A 區(qū)中心進(jìn)入 B 區(qū)中心時(shí)才判定為一次穿越。區(qū)域計(jì)數(shù)天然能過濾掉只在計(jì)數(shù)線附近來回試探的情況因?yàn)槟阈枰氖峭暾目鐓^(qū)軌跡而不是某一幀的跨越。實(shí)現(xiàn)上區(qū)域計(jì)數(shù)的核心是一個(gè)狀態(tài)機(jī)對(duì)每個(gè)跟蹤 ID記錄它上一次所在的區(qū)域當(dāng)檢測(cè)到區(qū)域從 A 變?yōu)?B就認(rèn)為發(fā)生了一次 A→B 的穿越。這樣對(duì)同一個(gè)人往返走也能正確記錄為兩次穿越。相比單線計(jì)數(shù)區(qū)域計(jì)數(shù)的優(yōu)點(diǎn)是對(duì)檢測(cè)框抖動(dòng)不那么敏感缺點(diǎn)是邏輯稍復(fù)雜、需要維護(hù)每個(gè) ID 的區(qū)域狀態(tài)。6.3 卡爾曼濾波讓軌跡平滑計(jì)數(shù)更準(zhǔn)確最后一個(gè)提升準(zhǔn)確率的常用技巧是卡爾曼濾波。它解決的是檢測(cè)框抖動(dòng)問題YOLO 在連續(xù)幀中對(duì)同一個(gè)人的框會(huì)有幾個(gè)像素的無(wú)規(guī)律抖動(dòng)這種抖動(dòng)疊加到計(jì)數(shù)線上就會(huì)造成重復(fù)跨越??柭鼮V波的基本思想是用上一幀的預(yù)測(cè)位置和當(dāng)前幀的觀測(cè)位置做一個(gè)加權(quán)融合讓軌跡更平滑。OpenCV 中可以用cv2.KalmanFilter實(shí)現(xiàn)。kf cv2.KalmanFilter(4, 2) # 狀態(tài) [x, y, vx, vy]觀測(cè) [x, y] kf.measurementMatrix np.array([[1, 0, 0, 0], [0, 1, 0, 0]], dtypenp.float32) kf.transitionMatrix np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]], dtypenp.float32) def filter_update(cx, cy): prediction kf.predict() measured np.array([[np.float32(cx)], [np.float32(cy)]]) filtered kf.correct(measured) return filtered[0, 0], filtered[1, 0]卡爾曼濾波在這里不是必須的如果用了區(qū)域計(jì)數(shù)通常不需要它。但如果你做的是單線計(jì)數(shù)且畫面有輕微抖動(dòng)加上它會(huì)讓計(jì)數(shù)曲線平滑很多。一個(gè)多年調(diào)參的教訓(xùn)是卡爾曼濾波的噪聲參數(shù)measurementNoiseCov、processNoiseCov對(duì)結(jié)果影響極大噪聲協(xié)方差設(shè)得太大濾波結(jié)果基本等于觀測(cè)值失去濾波意義設(shè)得太小軌跡會(huì)變得非常鈍、跟不上快速移動(dòng)的人。我通常從measurementNoiseCov1e-1、processNoiseCov1e-3開始調(diào)再根據(jù)畫面中人的移動(dòng)速度做微調(diào)。有時(shí)候現(xiàn)場(chǎng)演示時(shí)畫面上有樹葉搖晃、燈光閃爍這些干擾濾波器反而可以幫你把誤檢帶來的跳變吸收掉。這個(gè)技巧在你做項(xiàng)目效果對(duì)比時(shí)很有用——同一段視頻加不加濾波計(jì)數(shù)曲線會(huì)明顯差一個(gè)級(jí)別拿來作為優(yōu)化前后對(duì)比展示在項(xiàng)目說明里說服力很強(qiáng)。希望這些踩過的坑和調(diào)過的參數(shù)能幫你在畢設(shè)或項(xiàng)目里少走一段彎路。本文還有配套的精品資源點(diǎn)擊獲取