別檢測(cè)系統(tǒng)實(shí)戰(zhàn)全解析)
最開始讓我下決心折騰YOLOv12的是在一個(gè)商場(chǎng)客流統(tǒng)計(jì)項(xiàng)目里被舊模型按在地上摩擦的經(jīng)歷。鏡頭從二樓俯拍下去扶梯口密密麻麻全是人檢測(cè)框一個(gè)疊一個(gè)漏檢率直接飆到我不想看統(tǒng)計(jì)報(bào)表。后來(lái)把整體方案換成“YOLOv12 密集行人數(shù)據(jù)集 PyQt5界面 登錄注冊(cè)”的完整系統(tǒng)效果才真正穩(wěn)定下來(lái)。這篇就把整個(gè)《基于深度學(xué)習(xí)YOLOv12的密集行人識(shí)別檢測(cè)系統(tǒng)》從數(shù)據(jù)準(zhǔn)備、模型訓(xùn)練、界面聯(lián)調(diào)到現(xiàn)場(chǎng)排錯(cuò)的過程完整翻出來(lái)項(xiàng)目源碼、模型、UI、登錄注冊(cè)這套東西怎么拼到一起以及我踩過的坑一次講清楚。不管你是做安防監(jiān)控、客流統(tǒng)計(jì)還是正在寫畢設(shè)/課設(shè)只要離不開“密集人群檢測(cè)”這幾個(gè)字這篇都適用。1. 密集人群檢測(cè)這個(gè)任務(wù)到底在考驗(yàn)什么1.1 為什么普通模型在密集人群里“翻車”很多人覺得行人檢測(cè)就是“把行人框出來(lái)”聽起來(lái)比車輛檢測(cè)還簡(jiǎn)單——大家都是直立行走的特征穩(wěn)定又沒有輪子。但真實(shí)項(xiàng)目里密集人群檢測(cè)恰恰是目標(biāo)檢測(cè)里最難受的場(chǎng)景之一。先說遮擋。商場(chǎng)扶梯口、景區(qū)檢票處、演唱會(huì)散場(chǎng)通道那真是人貼人。目標(biāo)被擋住40%到60%是常態(tài)頭部可能還在軀干已經(jīng)被前面的人蓋得嚴(yán)嚴(yán)實(shí)實(shí)。普通模型學(xué)的是“完整行人”的外觀特征一旦外觀殘缺置信度就嘩嘩往下掉。這時(shí)候不是模型笨是訓(xùn)練時(shí)壓根沒見過這么多“半個(gè)人”。再說尺度。同樣的一個(gè)人在1080p畫面里距離鏡頭5米和15米像素尺寸能差三四倍。密集人群里必然混著大量小目標(biāo)而小目標(biāo)在特征圖下層池化幾次以后可能只剩下幾個(gè)像素的信息很容易被模型當(dāng)成背景抹掉。第三是“群體上下文”的干擾。人一多背景就不干凈了到處都是胳膊、腿、背包、行李箱這些局部外觀和行人本身的局部很相似。模型如果把注意力放在這些重復(fù)紋理上就容易輸出一堆低質(zhì)量框。所以密集行人檢測(cè)真正要解決的問題不是“能不能檢出人”而是“在互相遮擋、尺度變化大、背景雜亂的情況下能不能穩(wěn)定地把每個(gè)人框出來(lái)”。1.2 YOLOv12帶來(lái)的關(guān)鍵變化YOLOv12是2025年推出的新版本核心變化是把注意力機(jī)制比較系統(tǒng)地放進(jìn)了YOLO架構(gòu)里。傳統(tǒng)YOLO系列尤其是大家熟悉的v5、v8骨干網(wǎng)絡(luò)基本是CNN堆疊靠卷積感受野一點(diǎn)一點(diǎn)往上擴(kuò)。CNN的優(yōu)點(diǎn)是快、穩(wěn)、部署方便缺點(diǎn)是對(duì)“全局關(guān)系”的建模偏弱。一個(gè)人被擋住半邊CNN看到的還是局部紋理拼湊而帶注意力機(jī)制的模型會(huì)在特征層面去建?!斑@個(gè)區(qū)域是不是一個(gè)完整目標(biāo)的局部”相當(dāng)于把散落的拼圖塊關(guān)聯(lián)起來(lái)。YOLOv12并不是簡(jiǎn)單粗暴地整個(gè)Transformer搬進(jìn)來(lái)而是做了工程化折中在不太犧牲推理速度的前提下把注意力模塊嵌在骨干和特征融合的關(guān)鍵位置。實(shí)際跑下來(lái)的感覺是普通街景場(chǎng)景下它跟v8、v11的差距沒有想象中大但一旦進(jìn)入擁擠場(chǎng)景漏檢率和誤檢率確實(shí)能拉開一個(gè)身位。對(duì)大目標(biāo)、整體輪廓被破壞的目標(biāo)注意力帶來(lái)的收益尤其明顯。另外YOLOv12訓(xùn)練時(shí)可以原生使用Ultralytics系列的訓(xùn)練管線數(shù)據(jù)標(biāo)注格式、評(píng)估指標(biāo)、命令行入口基本可以無(wú)縫切換。我這次沒有用魔改分支直接基于官方代碼倉(cāng)庫(kù)來(lái)跑數(shù)據(jù)格式還是熟悉的YOLO txt格式省去了很多適配成本。1.3 這套系統(tǒng)圍繞什么場(chǎng)景設(shè)計(jì)這套系統(tǒng)從一開始就不是給人臉檢測(cè)或者自動(dòng)駕駛做的它的核心目標(biāo)場(chǎng)景有三個(gè)第一是出入口客流統(tǒng)計(jì)比如商場(chǎng)出入口、地鐵閘機(jī)、展覽館門禁需要在多人同時(shí)通過的瞬間快速計(jì)數(shù)并且保留每個(gè)人的軌跡特征。第二是人群聚集預(yù)警比如景區(qū)廣場(chǎng)、車站候車廳當(dāng)某個(gè)區(qū)域瞬時(shí)人數(shù)超過閾值時(shí)系統(tǒng)要給出提示這時(shí)候除了檢測(cè)框還需要一種簡(jiǎn)單的區(qū)域人數(shù)統(tǒng)計(jì)算法。第三是安防檢索輔助比如事后翻查監(jiān)控錄像想快速找到“某個(gè)時(shí)間段某個(gè)人是否出現(xiàn)在畫面里”需要把檢測(cè)結(jié)果連同時(shí)間戳、畫面幀一起落盤。這三個(gè)場(chǎng)景決定了系統(tǒng)不能只是“跑個(gè)模型出個(gè)框”它必須有視頻源接入能力、實(shí)時(shí)畫面展示、檢測(cè)結(jié)果統(tǒng)計(jì)和存儲(chǔ)還要有一個(gè)讓人操作的門面。這就是為什么這套系統(tǒng)除了模型還會(huì)配套UI界面和登錄注冊(cè)。后面我會(huì)把每一塊都拆開講。2. 系統(tǒng)拆解技術(shù)棧選型與模塊邊界2.1 為什么用Python PyTorch OpenCV而不是純C部署很多做嵌入式或者C部署的朋友會(huì)問識(shí)別系統(tǒng)不是應(yīng)該用TensorRT C嗎精度高、延遲低、好落地。但你要看項(xiàng)目階段的訴求。這套系統(tǒng)要用于算法驗(yàn)證、效果演示、現(xiàn)場(chǎng)參數(shù)快速調(diào)整迭代速度遠(yuǎn)比極致性能重要。Python生態(tài)的優(yōu)點(diǎn)在算法驗(yàn)證階段太明顯了Ultralytics的檢測(cè)接口可以直接調(diào)用OpenCV負(fù)責(zé)攝像頭讀取和畫框PyQt5寫界面SQLite存賬號(hào)數(shù)據(jù)整個(gè)鏈路不需要編譯改一個(gè)閾值參數(shù)馬上能看到效果。等到算法徹底定型再考慮用ONNX或TensorRT導(dǎo)出到C部署也不遲。我在這個(gè)系統(tǒng)里保留了ONNX導(dǎo)出端口后面實(shí)測(cè)部分會(huì)細(xì)說。PyTorch的選擇沒有懸念。模型訓(xùn)練、推理、遷移學(xué)習(xí)都靠它特別是torch的DataLoader和amp混合精度在訓(xùn)練階段能省下大量時(shí)間。OpenCV承擔(dān)的任務(wù)比較雜讀取本地視頻、連接RTSP攝像頭、BGR與RGB轉(zhuǎn)換、圖像縮放、在畫面上疊加檢測(cè)框和文字。它雖然不是最高效的但兼容性和社區(qū)資料是最穩(wěn)的。2.2 UI層選PyQt5還是WebUI有兩條路桌面端用PyQt5/PySide6Web端用Flask/FastAPI 前端框架。我這次選擇的是PyQt5。原因并不復(fù)雜。當(dāng)下的需求是單機(jī)運(yùn)行、操作直觀、低延遲顯示視頻流。PyQt5的QLabel可以直接刷OpenCV的幀實(shí)時(shí)性好鼠標(biāo)拖動(dòng)縮放、窗口布局都很好控制不依賴瀏覽器和網(wǎng)絡(luò)服務(wù)。現(xiàn)場(chǎng)項(xiàng)目里給安保人員一臺(tái)裝了界面的電腦打開就能用比開瀏覽器輸?shù)刂分庇^得多。如果是做遠(yuǎn)程訪問或者多終端同時(shí)看那Web方案更合理但系統(tǒng)復(fù)雜度會(huì)上一個(gè)層級(jí)要處理并發(fā)問題。這套系統(tǒng)的定位是“本地一體機(jī)”PyQt5夠用了。如果你打算改成Web檢測(cè)引擎部分可以完全復(fù)用只把UI替換成Flask WebSocket流推送。2.3 一個(gè)干凈的目錄結(jié)構(gòu)怎么設(shè)計(jì)工程最怕寫到后面文件亂飛。這個(gè)系統(tǒng)我用的目錄結(jié)構(gòu)是這樣的pedestrian_detection_system/ ├── app.py # 程序入口 ├── config.yaml # 全局配置 ├── requirements.txt # 依賴包清單 ├── models/ # 訓(xùn)練好的模型權(quán)重 ├── dataset/ # 原始數(shù)據(jù)集與標(biāo)簽 ├── ui/ │ ├── login_window.py # 登錄注冊(cè)界面 │ ├── main_window.py # 主界面 │ └── resources/ # 圖標(biāo)、樣式表 ├── core/ │ ├── detector.py # YOLOv12推理封裝 │ ├── video_source.py # 本地/RTSP視頻源管理 │ ├── stats.py # 檢測(cè)統(tǒng)計(jì) │ └── database.py # SQLite用戶與日志 └── utils/ ├── draw.py # 畫框、畫統(tǒng)計(jì)信息 └── logger.py # 日志記錄這樣拆的好處是檢測(cè)邏輯和界面完全解耦。你想換成別的數(shù)據(jù)集重訓(xùn)一個(gè)模型只需要替換models目錄下的權(quán)重和config里的模型配置你想換個(gè)UI框架core目錄完全不需要?jiǎng)印?. 數(shù)據(jù)準(zhǔn)備從開源數(shù)據(jù)集到Y(jié)OLO格式的真實(shí)細(xì)節(jié)3.1 哪些公開數(shù)據(jù)集值得用模型效果的上限一半是由數(shù)據(jù)決定的。我這次訓(xùn)練數(shù)據(jù)主要來(lái)自三塊第一塊是COCO數(shù)據(jù)集的person類。COCO覆蓋面廣場(chǎng)景雜但很多圖是日常街景密集程度不算高。它適合做底料讓模型學(xué)到一個(gè)比較通用的“行人”概念。第二塊是VisDrone的部分行人類別。VisDrone是無(wú)人機(jī)視角人群密度高目標(biāo)小遮擋嚴(yán)重正好補(bǔ)COCO的不足。它的標(biāo)注是MS COCO格式需要轉(zhuǎn)一次。剛開始模型在密集場(chǎng)景下無(wú)力就是缺少這類“高壓數(shù)據(jù)”的毒打。第三塊是從MOT Challenge等行人跟蹤數(shù)據(jù)集中篩出來(lái)的幀。跟蹤數(shù)據(jù)集天然包含連續(xù)幀人挨著人的畫面特別多拿來(lái)補(bǔ)充遮擋樣本很合適。我自己的經(jīng)驗(yàn)是不要只拖一個(gè)數(shù)據(jù)集用到底。把上面三個(gè)按比例混合效果最好我這里大概是COCO 6成、VisDrone 3成、MOT幀1成?;旌现跋冉y(tǒng)一類別全部只保留person這一類。3.2 YOLO標(biāo)注格式和轉(zhuǎn)換腳本的坑YOLO格式的標(biāo)注是每張圖一個(gè)同名txt文件每行代表一個(gè)目標(biāo)格式為“類別id x_center y_center width height”全部坐標(biāo)都是相對(duì)圖片寬高的歸一化數(shù)值。這個(gè)格式看起來(lái)簡(jiǎn)單轉(zhuǎn)換時(shí)卻有三個(gè)容易出錯(cuò)的點(diǎn)。第一個(gè)是類別id要對(duì)齊。COCO的person類是0但有些數(shù)據(jù)集person是別的id。轉(zhuǎn)換腳本里最容易翻車的就是這里訓(xùn)練時(shí)發(fā)現(xiàn)模型什么都不檢先看是不是標(biāo)簽標(biāo)簽里混著499和500這種非法id我踩過。第二個(gè)是坐標(biāo)歸一化。有些開源數(shù)據(jù)給的是左上角坐標(biāo)和寬高要自己算成中心點(diǎn)坐標(biāo)。四舍五入保留幾位也影響不大但別用int取整會(huì)損失小目標(biāo)的框精度。第三個(gè)是無(wú)效標(biāo)簽。有些json標(biāo)注里會(huì)出現(xiàn)空標(biāo)簽文件也就是一張圖沒有任何目標(biāo)。訓(xùn)練時(shí)如果用了多尺度、Mosaic增強(qiáng)空標(biāo)簽會(huì)報(bào)錯(cuò)。轉(zhuǎn)換腳本里統(tǒng)一過濾掉這種圖片就好。下面是我當(dāng)年寫的數(shù)據(jù)轉(zhuǎn)換核心片段注釋標(biāo)出了最容易錯(cuò)的地方import json import os from PIL import Image def coco_to_yolo(coco_json_path, image_dir, output_dir, valid_category_ids(0,)): with open(coco_json_path, r, encodingutf-8) as f: coco_data json.load(f) image_map {img[id]: img for img in coco_data[images]} category_map {cat[id]: cat[name] for cat in coco_data[categories]} annotations_by_image {} for ann in coco_data[annotations]: if ann[category_id] not in valid_category_ids: continue annotations_by_image.setdefault(ann[image_id], []).append(ann) for image_id, ann_list in annotations_by_image.items(): img_info image_map[image_id] img_path os.path.join(image_dir, img_info[file_name]) # 注意讀取寬高時(shí)優(yōu)先從圖片本身獲取不要完全依賴json里的寬高字段 with Image.open(img_path) as img: img_w, img_h img.size txt_path os.path.join(output_dir, os.path.splitext(img_info[file_name])[0] .txt) with open(txt_path, w, encodingutf-8) as f: for ann in ann_list: x, y, w, h ann[bbox] # coco bbox給的是左上角坐標(biāo)和寬高 center_x x w / 2.0 center_y y h / 2.0 # 關(guān)鍵所有坐標(biāo)除以圖片寬高得到0~1之間的歸一化值 center_x_norm center_x / img_w center_y_norm center_y / img_h w_norm w / img_w h_norm h / img_h # category_id這里要映射成你自己的類別id不能直接抄寫原數(shù)據(jù)集的id f.write(f0 {center_x_norm:.6f} {center_y_norm:.6f} {w_norm:.6f} {h_norm:.6f}\n)轉(zhuǎn)換完以后我會(huì)隨機(jī)抽20張圖把標(biāo)注框畫回去人工檢查一遍。這一步不能省因?yàn)槟_本寫錯(cuò)時(shí)數(shù)據(jù)比例往往看著正常一眼根本看不出來(lái)。3.3 針對(duì)密集場(chǎng)景的數(shù)據(jù)增強(qiáng)策略YOLO系列自帶Mosaic增強(qiáng)也就是把四張圖拼成一張訓(xùn)練圖。這在常規(guī)場(chǎng)景里很好用但密集人群場(chǎng)景下我還額外加了兩個(gè)操作隨機(jī)貼片模擬遮擋隨機(jī)從別的圖里扣一塊行人區(qū)域貼到當(dāng)前圖上蓋住部分目標(biāo)。這能強(qiáng)迫模型學(xué)著用可見局部識(shí)別目標(biāo)。隨機(jī)裁剪縮放對(duì)高密度區(qū)域按0.5到0.8的隨機(jī)比例裁剪后再縮回訓(xùn)練尺寸。這能讓模型適應(yīng)不同尺度的擁擠畫面也相當(dāng)于做了尺度增強(qiáng)。增強(qiáng)不是越多越好。訓(xùn)練后期你會(huì)發(fā)現(xiàn)增強(qiáng)太重會(huì)讓模型很難收斂loss降不下去。我的習(xí)慣是前一半epoch用完整增強(qiáng)后一半逐步降低增強(qiáng)強(qiáng)度讓模型在干凈數(shù)據(jù)上“收一收”。Ultralytics里可以用超參配置調(diào)控例如mosaic: 0.8 mixup: 0.2 fliplr: 0.5 scale: 0.5這個(gè)配置是根據(jù)我的場(chǎng)景定的你不用照抄但思路可以借用密集場(chǎng)景優(yōu)先保mosaic和scaleflip保持0.5mixup不要調(diào)太高否則動(dòng)態(tài)模糊反而讓目標(biāo)邊界更亂。3.4 數(shù)據(jù)劃分按場(chǎng)景切而不是隨機(jī)切很多新手訓(xùn)練模型時(shí)會(huì)隨機(jī)把圖片分成train和val。這樣做出來(lái)的指標(biāo)好看但實(shí)際部署就露餡。原因很簡(jiǎn)單隨機(jī)劃分會(huì)讓同一視頻里的連續(xù)幀同時(shí)出現(xiàn)在訓(xùn)練集和驗(yàn)證集模型等于變相“看過答案”。我這套系統(tǒng)的做法是按采集場(chǎng)景劃分。同一個(gè)攝像頭、同一個(gè)地點(diǎn)、同一段時(shí)間的素材全部歸到同一個(gè)集合里絕不讓它們跨集。比如A商場(chǎng)的素材全部進(jìn)訓(xùn)練B商場(chǎng)的素材全部進(jìn)驗(yàn)證C項(xiàng)目現(xiàn)場(chǎng)素材最后拿來(lái)壓測(cè)。這樣驗(yàn)證結(jié)果才能代表真實(shí)場(chǎng)景。另外密集人群驗(yàn)證集的比例我故意調(diào)高到30%。這類任務(wù)如果驗(yàn)證集里沒有大量擁擠畫面mAP再高都說明不了問題。4. 模型訓(xùn)練參數(shù)、監(jiān)控與評(píng)估4.1 訓(xùn)練環(huán)境配置訓(xùn)練時(shí)我用的是單卡環(huán)境顯存24G跑YOLOv12的中型模型比較寬裕。如果你是學(xué)生或者在普通機(jī)器上跑建議直接選nano或small版本效果差不了太多訓(xùn)練時(shí)間能省一半以上。PyTorch版本建議2.xCUDA對(duì)應(yīng)裝好就行Ultralytics對(duì)版本兼容性做得比較好一般不會(huì)卡在環(huán)境上。依賴清單長(zhǎng)這樣python3.10 torch2.1.0 torchvision0.16.0 ultralytics8.3.x opencv-python4.8.* PyQt55.15.* pyyaml6.*這里有個(gè)細(xì)節(jié)不要貪最新版本。Ultralytics家更新很快有時(shí)候新版會(huì)改默認(rèn)行為你照著教程跑結(jié)果不一樣不一定是代碼錯(cuò)可能是框架行為變了。我在項(xiàng)目里會(huì)把requirements.txt鎖死版本方便以后重建環(huán)境。4.2 訓(xùn)練參數(shù)怎么定訓(xùn)練命令直接用Ultralytics的CLI就行yolo detect train \ --model yolo12s.yaml \ --data dataset.yaml \ --epochs 200 \ --batch 16 \ --imgsz 1280 \ --device 0 \ --optimizer AdamW \ --lr0 0.001 \ --weight_decay 0.0005 \ --amp幾個(gè)參數(shù)我說下選擇的理由。imgsz對(duì)密集小目標(biāo)影響最大。1080p畫面里的人群目標(biāo)普遍偏小如果縮到640再送進(jìn)網(wǎng)絡(luò)很多目標(biāo)只有十幾個(gè)像素神仙模型也難救。我把訓(xùn)練尺寸調(diào)到1280之后小目標(biāo)的召回率提升非常明顯。代價(jià)是訓(xùn)練時(shí)間翻倍、顯存壓力變大。如果機(jī)器跑不動(dòng)先640跑通流程再用1280精調(diào)。batch受顯存約束不是越大越好。batch太大會(huì)導(dǎo)致每個(gè)epoch的迭代次數(shù)少模型反而欠擬合。16在24G顯存下配合1280輸入剛剛好。epochs我設(shè)了200但實(shí)際在140左右就用早停收工了。密集人群訓(xùn)練后期很容易過擬合表現(xiàn)為訓(xùn)練loss繼續(xù)降、驗(yàn)證mAP不再漲甚至掉。建議開著早停patience設(shè)30。4.3 訓(xùn)練過程的觀察要點(diǎn)訓(xùn)練時(shí)不要只盯屏幕等進(jìn)度條一定要看訓(xùn)練曲線的三個(gè)信號(hào)第一是box_loss有沒有相對(duì)平滑地下降。如果前10個(gè)epoch loss紋絲不動(dòng)先查學(xué)習(xí)率、查標(biāo)簽有沒有問題不要頭鐵繼續(xù)跑。第二是驗(yàn)證集mAP是否穩(wěn)步上升。如果訓(xùn)練集已經(jīng)漲得很高、驗(yàn)證集遲遲不漲基本就是過擬合該減epoch或者加數(shù)據(jù)增強(qiáng)。第三是看特定類別的AP這里只有person類所以就是整體AP。如果mAP50能到85%以上但mAP50-95偏低說明模型框的位置還不夠準(zhǔn)可以朝調(diào)高輸入尺寸、優(yōu)化錨框/標(biāo)簽分配方向想。密集行人場(chǎng)景我個(gè)人的手感是mAP50能到88%以上密集畫面基本可用mAP50-95到60%以上就是很好的水平了不必死磕。4.4 密集人群下的評(píng)估指標(biāo)別只用mAPmAP是一個(gè)綜合指標(biāo)但密集人群場(chǎng)景里它不夠直觀。我評(píng)估模型好不好用還會(huì)加兩個(gè)指標(biāo)漏檢率也就是標(biāo)注里“人”被完全漏掉的比例。統(tǒng)計(jì)一下畫面里有100個(gè)真目標(biāo)模型只給出80個(gè)框漏檢率就是20%。這個(gè)數(shù)直接影響客流統(tǒng)計(jì)的準(zhǔn)確性。誤檢率也就是模型把拉桿箱、垃圾桶、廣告牌上的假人認(rèn)成人的比例。這個(gè)數(shù)直接影響預(yù)警系統(tǒng)會(huì)不會(huì)沒事亂響。某個(gè)模型mAP看起來(lái)很高但漏檢和誤檢可能同時(shí)很高——它可能輸出一堆重復(fù)框、覆蓋率高但位置漂移的框mAP計(jì)算時(shí)重復(fù)框會(huì)被NMS處理掉所以看不出來(lái)。我之前就吃過這個(gè)虧模型在COCO上mAP50有90%拿到密集現(xiàn)場(chǎng)一看一個(gè)人可能被框了三遍客流數(shù)直接爆表。所以代碼里除了調(diào)用evaluate得到mAP我還單獨(dú)寫了一個(gè)腳本統(tǒng)計(jì)漏檢率和誤檢率專門用來(lái)判斷“這個(gè)模型能不能上線”。5. 登錄注冊(cè)與UI一個(gè)“能用”的產(chǎn)品該有的樣子5.1 登錄注冊(cè)模塊要不要做有人會(huì)問一個(gè)本機(jī)的檢測(cè)工具搞登錄注冊(cè)是不是多此一舉我的答案是看項(xiàng)目交期和使用對(duì)象。如果只是自己跑著玩登錄確實(shí)可以砍掉。但如果有多個(gè)操作人員交替使用或者要把這個(gè)系統(tǒng)交給甲方驗(yàn)收登錄注冊(cè)就是必要功能。它至少解決了三件事賬號(hào)權(quán)限隔離不是每個(gè)人都能改模型參數(shù)和檢測(cè)閾值。操作留痕誰(shuí)在幾點(diǎn)登錄、運(yùn)行了哪些檢測(cè)都有記錄。給人“這是一個(gè)完整系統(tǒng)”的觀感甲方看到登錄界面對(duì)項(xiàng)目的完成度評(píng)價(jià)會(huì)高一個(gè)檔次。5.2 本地賬號(hào)體系SQLite就夠了這個(gè)系統(tǒng)無(wú)需聯(lián)網(wǎng)賬號(hào)體系本地化最合適。我用SQLite建了一張用戶表密碼不存明文而是用哈希加鹽方式存儲(chǔ)。下面這段示例展示了哈希校驗(yàn)的核心思路import hashlib import sqlite3 import os def hash_password(password: str, salt: str None) - str: if salt is None: salt os.urandom(16).hex() digest hashlib.sha256((salt password).encode(utf-8)).hexdigest() return f{salt}${digest} def verify_password(stored: str, password: str) - bool: salt, digest stored.split($, 1) return hash_password(password, salt) stored這里沒有使用bcrypt這類重量級(jí)庫(kù)是因?yàn)楸緳C(jī)系統(tǒng)被攻破的風(fēng)險(xiǎn)面本來(lái)很小用SHA256加鹽已經(jīng)足夠防止“看一眼數(shù)據(jù)庫(kù)就知道密碼”的情況。系統(tǒng)越簡(jiǎn)單越容易維護(hù)。同時(shí)我用數(shù)據(jù)庫(kù)記錄登錄日志登錄時(shí)間、登錄結(jié)果、檢測(cè)時(shí)長(zhǎng)。運(yùn)行幾次以后這套日志對(duì)排查問題很有幫助。5.3 主界面該放什么控件主界面我用PyQt5實(shí)現(xiàn)布局分成四塊區(qū)域左側(cè)視頻顯示區(qū)用來(lái)展示原始畫面和檢測(cè)結(jié)果畫框。視頻源可以切換本地文件、USB攝像頭、RTSP協(xié)議流地址。右側(cè)實(shí)時(shí)統(tǒng)計(jì)區(qū)顯示當(dāng)前幀檢測(cè)人數(shù)、峰值人數(shù)、每秒幀數(shù)以及總檢測(cè)數(shù)。還可以用一個(gè)折線圖展示最近N秒人數(shù)變化趨勢(shì)。底部控制區(qū)放開始/停止按鈕、置信度閾值滑塊、IOU閾值滑塊、截圖按鈕、導(dǎo)出報(bào)表按鈕。菜單欄放模型選擇、日志查看、用戶管理入口。置信度閾值和IOU閾值這兩個(gè)滑塊一定要放在界面上不要寫死在代碼里。原因很簡(jiǎn)單不同攝像頭角度、不同密度的畫面最優(yōu)閾值差別很大。比如俯拍場(chǎng)景置信度閾值0.3就夠平視場(chǎng)景可能要調(diào)到0.55才能壓住誤檢?,F(xiàn)場(chǎng)人員可以在不重啟程序的情況下實(shí)時(shí)調(diào)整這才是“能用”的產(chǎn)品。5.4 檢測(cè)引擎和UI解耦很多人寫PyQt界面時(shí)習(xí)慣把檢測(cè)循環(huán)直接塞進(jìn)界面的槽函數(shù)里結(jié)果界面一卡一卡的拖一下窗口畫面就卡死。原因很直接檢測(cè)是耗時(shí)操作放在UI主線程里界面必然等它。正確做法是單獨(dú)開一個(gè)工作線程跑檢測(cè)通過信號(hào)把處理好的幀傳回界面。PyQt5里可以用QThread或者更輕量的QThreadPool。我把檢測(cè)循環(huán)封裝在了core/detector.py里它只依賴opencv和模型自己不知道UI的存在。UI線程只負(fù)責(zé)接收檢測(cè)結(jié)果并刷新畫面。class DetectionThread(QThread): frame_ready pyqtSignal(object, object) def __init__(self, video_source, detector, conf_thresh, iou_thresh): super().__init__() self.video_source video_source self.detector detector self.conf_thresh conf_thresh self.iou_thresh iou_thresh self.running True def run(self): for frame in self.video_source.read_frames(): if not self.running: break results self.detector.predict(frame, self.conf_thresh, self.iou_thresh) annotated self.detector.draw_boxes(frame, results) self.frame_ready.emit(annotated, results)這個(gè)線程把“讀一幀、推理、畫框”整套流程局限在工作線程里界面主線程收到frame_ready信號(hào)后只做一次label圖片更新流暢度和穩(wěn)定性都有保證。6. 實(shí)測(cè)與排錯(cuò)記錄6.1 漏檢嚴(yán)重時(shí)先別急著換模型剛把YOLOv12配置跑通時(shí)我在一個(gè)商場(chǎng)扶梯的測(cè)試視頻上發(fā)現(xiàn)問題上半段人群稀疏檢測(cè)很好但扶梯出口處的密集區(qū)域漏檢一片。當(dāng)時(shí)第一反應(yīng)是“模型不行”后來(lái)一步步排查才發(fā)現(xiàn)問題出在輸入尺寸上。測(cè)試代碼里我沿用了自己習(xí)慣的imgsz640這個(gè)尺寸對(duì)普通場(chǎng)景夠用但對(duì)密集場(chǎng)景就是致命傷。扶梯出口處一個(gè)人可能只有20像素高640輸入下特征圖上的響應(yīng)極弱。把推理尺寸改成1280后漏檢率肉眼可見地降下來(lái)。所以如果你也遇到密集場(chǎng)景漏檢先檢查輸入尺寸再檢查置信度閾值最后才輪到模型架構(gòu)。6.2 幫你少走彎路的一些調(diào)參筆記置信度閾值密集人群場(chǎng)景建議從0.25開始調(diào)如果誤檢多就往上加到0.45左右如果漏檢多就往下減。IOU閾值建議保持在0.45到0.5之間過高比如0.7會(huì)導(dǎo)致同一個(gè)人的多個(gè)重疊框都保留計(jì)數(shù)虛高過低又會(huì)讓兩個(gè)人的框被合并成一個(gè)計(jì)數(shù)漏掉。推理尺寸訓(xùn)練和推理尺寸要盡量一致不要訓(xùn)練1280、推理640效果損失非常大。數(shù)據(jù)增強(qiáng)如果場(chǎng)景光照變化大可以在優(yōu)化階段把光照擾動(dòng)、模糊擾動(dòng)加上這些對(duì)行人檢測(cè)的魯棒性很有幫助。密度極高場(chǎng)景如果人群密集到只露出一片人頭可以考慮單獨(dú)訓(xùn)練一個(gè)detection head或使用人頭檢測(cè)模型做二次過濾整體效果會(huì)明顯好過單個(gè)全身模型。6.3 GPU推理速度與界面流暢度系統(tǒng)在GTX級(jí)別顯卡上推理1280分辨率單幀大約20到40毫秒。但界面刷新幀率不高原因在畫框和圖像格式轉(zhuǎn)換。OpenCV默認(rèn)是BGRPyQt顯示需要轉(zhuǎn)成RGB再轉(zhuǎn)QImage這個(gè)開銷比想象中大。優(yōu)化方式是只在結(jié)果返回時(shí)才做那個(gè)幀的格式轉(zhuǎn)換不要把視頻流每一幀都轉(zhuǎn)減少無(wú)謂開銷。另外如果推理幀率和顯示幀率不一致不要強(qiáng)行同步。檢測(cè)線程按自己的節(jié)奏跑界面按固定的刷新間隔顯示最新幀就能讓畫面看起來(lái)流暢。沒有必要讓UI去等待檢測(cè)速度。6.4 視頻源兼容問題本地視頻文件基本沒問題USB攝像頭要看驅(qū)動(dòng)型號(hào)Windows下容易遇到權(quán)限或占用問題。RTSP流是最容易踩坑的不同廠家的攝像頭對(duì)OpenCV的兼容性不一樣有些流用H.265編碼OpenCV默認(rèn)支持不好畫面根本打不開。解決辦法是先用VLC確認(rèn)這個(gè)流能正常播放再在OpenCV里嘗試?yán)?。如果?shí)在不行用FFmpeg轉(zhuǎn)封裝再讀也是個(gè)不錯(cuò)的備選。7. 一點(diǎn)后續(xù)可以順手做的小擴(kuò)展系統(tǒng)主體跑通以后我強(qiáng)烈建議你加一個(gè)“檢測(cè)結(jié)果落盤”的功能每一幀的檢測(cè)框、置信度、人數(shù)統(tǒng)計(jì)、時(shí)間戳都寫進(jìn)CSV同時(shí)對(duì)超過人數(shù)閾值的關(guān)鍵幀自動(dòng)截圖。這個(gè)擴(kuò)展能極大提升系統(tǒng)的實(shí)用性用來(lái)做事后統(tǒng)計(jì)、軌跡分析都是現(xiàn)成的數(shù)據(jù)基礎(chǔ)。落盤代碼用一個(gè)簡(jiǎn)單的格式就夠with open(detect_log.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) for box in results.boxes: writer.writerow([ frame_id, datetime.now().isoformat(), box.cls.item(), box.conf.item(), box.xyxy[0, 0].item(), box.xyxy[0, 1].item(), box.xyxy[0, 2].item(), box.xyxy[0, 3].item() ])這條日志會(huì)幫你省很多事。現(xiàn)場(chǎng)出了問題回看日志基本能判斷是模型漏檢、閾值設(shè)置不合適還是視頻源本身的問題。8. 項(xiàng)目實(shí)戰(zhàn)中最重要的幾點(diǎn)總結(jié)這套系統(tǒng)做下來(lái)最值得記住的經(jīng)驗(yàn)有三條。第一密集人群檢測(cè)的數(shù)據(jù)比模型更重要把時(shí)間花在數(shù)據(jù)篩選、清洗和場(chǎng)景劃分上比反復(fù)換模型變體劃算得多。第二推理尺寸、置信度閾值、IOU閾值這些參數(shù)要根據(jù)真實(shí)場(chǎng)景反復(fù)試不要期望一組參數(shù)通吃所有攝像頭。第三界面和檢測(cè)邏輯一定要分層解耦用線程和信號(hào)組織數(shù)據(jù)流否則系統(tǒng)永遠(yuǎn)卡頓也就談不上實(shí)用。如果你正在復(fù)現(xiàn)這套系統(tǒng)我建議按這個(gè)順序推進(jìn)先把YOLOv12模型在公開數(shù)據(jù)上跑通推理再把數(shù)據(jù)轉(zhuǎn)換和訓(xùn)練流程搭好模型效果穩(wěn)定之后再去做UI和登錄注冊(cè)。千萬(wàn)不要一上來(lái)就鋪開做界面后面發(fā)現(xiàn)模型效果不行所有的界面代碼都要返工。先把“模型能不能在密集場(chǎng)景里站穩(wěn)”這個(gè)問題解決掉其他都是水到渠成的事。