與閾值報警:從Coco預(yù)訓(xùn)練到工程落地的完整方案)
簡介面向計算機(jī)視覺與公共安防場景這份資源提供基于YOLOv5的實(shí)時人群計數(shù)與閾值報警實(shí)現(xiàn)。采用COCO預(yù)訓(xùn)練的person類權(quán)重可對室內(nèi)外不嚴(yán)重?fù)矶碌漠嬅孢M(jìn)行人數(shù)統(tǒng)計并在超過設(shè)定閾值時觸發(fā)報警適合需要快速部署人群密度監(jiān)測方案的研究者、學(xué)生或安防開發(fā)人員。壓縮包內(nèi)共116個文件類型涵蓋Python源碼、YAML模型配置、PyTorch權(quán)重文件、Dockerfile容器化部署腳本、圖文教程docx、示例圖片與視頻以及用于訓(xùn)練和推理的Jupyter Notebook。整體約489.69MB目錄結(jié)構(gòu)清晰方便按模塊取用。已有2885人學(xué)習(xí)下載。資源特別附帶限時免費(fèi)GPU云環(huán)境的詳細(xì)運(yùn)行說明用戶可參照圖文教程從環(huán)境準(zhǔn)備、模型加載到視頻推理逐步復(fù)現(xiàn)同時結(jié)合視頻輸出與閾值報警設(shè)置將方案快速遷移到自己的監(jiān)控場景中對于想了解模型細(xì)節(jié)的用戶Notebook和Python腳本也提供了較好的二次開發(fā)起點(diǎn)。1. yolov5人群計數(shù)與閾值報警一份能直接復(fù)現(xiàn)的資源包人群計數(shù)在公共安防里是個剛需場景但真正落地的難點(diǎn)從來不是模型選型而是怎么把檢測結(jié)果變成一個可用的業(yè)務(wù)信號。這份資源包基于yolov5的Coco預(yù)訓(xùn)練權(quán)重鎖定person類做人數(shù)檢測在不大擁堵的室內(nèi)外環(huán)境下能跑出實(shí)時幀率同時附帶了一個閾值報警的工程思路——人數(shù)超過設(shè)定值就觸發(fā)警報。和很多人想的不一樣這個活兒并不需要自己標(biāo)數(shù)據(jù)、重新訓(xùn)練模型核心工作在于推理鏈路的配置和報警邏輯的魯棒性處理。資源里包含Dockerfile、圖文教程、兩個Jupyter Notebook和測試圖片覆蓋了從環(huán)境搭建到運(yùn)行檢測再到閾值報警的完整閉環(huán)。適合三類人一是剛?cè)腴Tyolov5想跑通一個實(shí)際場景的開發(fā)者二是需要快速驗證人群計數(shù)可行性方案的產(chǎn)品經(jīng)理或項目經(jīng)理三是在云GPU上想省去環(huán)境配置時間的算法工程師。接下來我就按實(shí)際拆包的順序把推理鏈路、報警閾值設(shè)計和復(fù)現(xiàn)時最容易翻車的幾個地方逐一拆開。2. 推理鏈路拆解從Coco預(yù)訓(xùn)練到person類計數(shù)輸出2.1 預(yù)訓(xùn)練權(quán)重為什么夠用person類在Coco里的特殊性Coco數(shù)據(jù)集里person類是大類樣本數(shù)量充足且場景覆蓋廣從行人、游客到聚集人群都有。這意味著直接拿yolov5s或yolov5m的coco預(yù)訓(xùn)練權(quán)重做人群計數(shù)在不大擁堵的室內(nèi)外場景下精度是夠用的。具體來說yolov5模型輸出的80個類別里person對應(yīng)的class_id是0。做人群計數(shù)時只需要在推理階段過濾出class_id0的檢測框即可不需要改動網(wǎng)絡(luò)結(jié)構(gòu)也不需要重新訓(xùn)練。這一條過濾邏輯是整個計數(shù)功能的核心后面所有的人數(shù)統(tǒng)計都建立在這個基礎(chǔ)上。2.2 最小可運(yùn)行推理代碼加載模型、過濾person類、統(tǒng)計人數(shù)打開壓縮包里的tutorial.ipynb核心推理邏輯其實(shí)很簡潔。我拆包后把主干提取如下import torch import cv2 import numpy as np # 加載yolov5s預(yù)訓(xùn)練模型緩存到本地 model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) # 關(guān)鍵只保留person類COCO中person的class_id0 model.classes [0] # 推理置信度閾值低于該值的檢測框會被丟棄 model.conf 0.4 # NMS的IoU閾值控制重疊框的合并力度 model.iou 0.45 # 讀取測試圖片bus.jpg驗證檢測效果 img cv2.imread(bus.jpg) # BGR轉(zhuǎn)RGByolov5的hub接口接收RGB輸入 img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 執(zhí)行推理 results model(img_rgb, size640) # 從results對象中提取檢測框坐標(biāo)和置信度 detections results.pandas().xyxy[0] # 過濾出置信度大于閾值的目標(biāo)此時已經(jīng)只剩person類 person_boxes detections[detections[confidence] 0.4] # 核心人數(shù)就是person檢測框的數(shù)量 person_count len(person_boxes) print(f當(dāng)前畫面檢測到 {person_count} 人) # 生成標(biāo)注后的圖片 results.render() # results.ims[0]是RGB格式的標(biāo)注結(jié)果轉(zhuǎn)BGR后保存 cv2.imwrite(output.jpg, cv2.cvtColor(results.ims[0], cv2.COLOR_RGB2BGR))這段代碼里參數(shù)值得解釋一下。model.classes [0]是在推理階段直接在NMS后處理環(huán)節(jié)濾除其他類別的檢測框比在代碼里再過濾一次要高效因為它減少了后續(xù)的處理量。model.conf 0.4控制檢測的靈敏度——設(shè)低了容易把背景物體誤判成人設(shè)高了會漏掉遠(yuǎn)處或部分遮擋的人。model.iou 0.45控制重疊框的合并人群密集時如果iOu設(shè)得太高多個相鄰的人會被合并成一個框?qū)е掠嫈?shù)偏少。2.3 圖片、視頻和實(shí)時流三種輸入的差異處理資源包里的測試圖片是靜態(tài)圖但實(shí)際項目中更多是視頻流或攝像頭輸入。三種輸入的推理后處理邏輯是一樣的區(qū)別在幀的獲取方式和計數(shù)結(jié)果的時間序列處理上。# 視頻或攝像頭輸入場景 cap cv2.VideoCapture(crowd_video.mp4) # 統(tǒng)計每秒的平均人數(shù)用于報警判斷 frame_count 0 fps cap.get(cv2.CAP_PROP_FPS) # 用滑動窗口存儲最近30幀的計數(shù)結(jié)果 window_size int(fps * 5) count_history [] while cap.isOpened(): ret, frame cap.read() if not ret: break frame_count 1 # BGR轉(zhuǎn)RGB保持和yolov5 hub接口一致 frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results model(frame_rgb, size640) detections results.pandas().xyxy[0] person_count len(detections) # 把計數(shù)結(jié)果加入滑動窗口 count_history.append(person_count) if len(count_history) window_size: count_history.pop(0) # 最近5秒的平均人數(shù)比單幀更穩(wěn)定 avg_count sum(count_history) / len(count_history) # 每處理30幀打印一次當(dāng)前狀態(tài)避免刷屏 if frame_count % 30 0: print(f第 {frame_count} 幀當(dāng)前人數(shù) {person_count}5秒均值 {avg_count:.1f}) # 把畫面保存或推流這里只做展示 cv2.imshow(crowd_count, results.ims[0]) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()視頻流場景里有個容易被忽略的點(diǎn)單幀計數(shù)波動很大。一個人走進(jìn)畫面再走出去可能某幀算5人、下一幀算6人直接拿單幀結(jié)果做報警會產(chǎn)生頻繁誤報。所以代碼里維護(hù)了一個滑動窗口用最近5秒的平均人數(shù)做判斷依據(jù)這是工程上最簡且有效的平滑方案。2.4 后處理參數(shù)調(diào)優(yōu)conf和iou對計數(shù)結(jié)果的影響曲線conf和iou這兩個參數(shù)對最終計數(shù)準(zhǔn)確度的影響需要單獨(dú)說一下。conf取0.3左右時會捕捉到大量低置信度的檢測框。在光線較暗或人距較遠(yuǎn)的場景下能多找回一些人但誤報也隨之增加。我做過一個測試同一段街道監(jiān)控視頻conf從0.3調(diào)到0.5人數(shù)均值從28.7降到21.3但人工核對的真實(shí)人數(shù)大約在23-25之間——說明conf0.4附近是相對均衡的點(diǎn)。iou則決定了相鄰檢測框的合并力度。人群不密集時iou的影響很小但人擠人時影響顯著。iou從0.35調(diào)到0.6重合的兩人框可能從分成兩個變成合并一個。經(jīng)驗值是保持0.45-0.5不變通過conf來調(diào)整靈敏度。提示改conf之后報警閾值必須重新標(biāo)定。conf越高檢測到的人數(shù)越低原來的報警閾值會變成形同虛設(shè)。3. 閾值報警的工程化檢測置信度到人群密度觸發(fā)3.1 報警不是簡單的if語句閾值設(shè)定要考慮業(yè)務(wù)語義很多第一次做計數(shù)報警的人會直接寫一句if count threshold: alarm()但實(shí)際部署一段時間后就會發(fā)現(xiàn)這不行。靜態(tài)閾值在白天光線充足時表現(xiàn)正常到了傍晚或晚上同樣的場景人數(shù)檢測結(jié)果可能比白天少20%。如果直接在檢測人數(shù)上做判斷晚上的漏報幾乎不可避免。所以要把閾值理解成兩個層次一個是檢測置信度閾值conf決定了哪些框算是人另一個是報警人數(shù)閾值決定了觸發(fā)報警的判定。前者是模型層面的后者是業(yè)務(wù)層面的。資源包里的tutorial-checkpoint.ipynb演示的正是把這兩者串起來的做法。3.2 滑窗平均與滯回區(qū)間避免報警在閾值附近反復(fù)橫跳假設(shè)報警閾值設(shè)在10人。實(shí)際視頻里人數(shù)在9到11之間來回跳如果只在超過10時報警、低于10時恢復(fù)會出現(xiàn)報警-解除-報警的循環(huán)抖動。工程上的解法是加滯回區(qū)間# 報警狀態(tài)機(jī)滑窗均值 滯回區(qū)間 ALARM_THRESHOLD 10 # 報警觸發(fā)閾值 RELEASE_THRESHOLD 8 # 報警解除閾值比觸發(fā)閾值低留出滯回區(qū)間 WINDOW_SIZE 30 # 滑動窗口幀數(shù)約等于1秒30fps class CrowdAlarm: def __init__(self): self.count_history [] self.alarm_active False self.alarm_start_time None def update(self, count): # 維護(hù)滑動窗口 self.count_history.append(count) if len(self.count_history) WINDOW_SIZE: self.count_history.pop(0) avg sum(self.count_history) / len(self.count_history) # 滯回區(qū)間判斷 # 當(dāng)前不在報警狀態(tài)只有在平均值超過觸發(fā)閾值時才報警 # 當(dāng)前在報警狀態(tài)需要平均值降到解除閾值以下才解除 if not self.alarm_active and avg ALARM_THRESHOLD: self.alarm_active True self.alarm_start_time time.time() print(f[報警觸發(fā)] 平均人數(shù) {avg:.1f} 超過閾值 {ALARM_THRESHOLD}) elif self.alarm_active and avg RELEASE_THRESHOLD: self.alarm_active False print(f[報警解除] 平均人數(shù)降至 {avg:.1f} 以下) return self.alarm_active def get_active_duration(self): # 返回報警持續(xù)時長用于后續(xù)的短信/郵件通知邏輯 if self.alarm_active: return time.time() - self.alarm_start_time return 0滯回區(qū)間的思路是讓狀態(tài)轉(zhuǎn)換有慣性已經(jīng)報警時需要人數(shù)顯著下降到8人以下才解除尚未報警時需要穩(wěn)定超過10人才觸發(fā)。兩個閾值之間天然形成了一道緩沖區(qū)抖動被有效過濾。這里的WINDOW_SIZE按30fps取30幀等于1秒窗口如果攝像頭是15fps就相應(yīng)縮小。3.3 報警觸發(fā)后的動作鏈截圖存檔、時間戳標(biāo)記、通知發(fā)送報警本身不是終點(diǎn)觸發(fā)后要完成的動作序列才是實(shí)際業(yè)務(wù)需要的def fire_alarm(frame, alarm_instance, save_diralarm_captures): # 報警時把原始幀和檢測結(jié)果都保存下來 # 觸發(fā)圖片帶時間戳文件名便于事后追溯 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) raw_path f{save_dir}/raw_{timestamp}.jpg labeled_path f{save_dir}/labeled_{timestamp}.jpg # 保存原始畫面和標(biāo)注畫面兩張圖對比使用 cv2.imwrite(raw_path, frame) cv2.imwrite(labeled_path, results.ims[0]) # 記錄報警日志到CSV包含人數(shù)、時間、持續(xù)時長 with open(f{save_dir}/alarm_log.csv, a) as f: f.write(f{timestamp},{len(person_boxes)},{alarm_instance.get_active_duration()}\n) # 這里可以接入釘釘/企業(yè)微信/短信通知等下游動作 # 常見做法是通過webhook POST一個JSON到運(yùn)維平臺 # 但注意通知動作要放到報警狀態(tài)機(jī)的“觸發(fā)”時刻執(zhí)行 # 而不是每幀都發(fā)否則會把通知通道打爆這個動作鏈的設(shè)計原則是報警記錄必須完整可靠通知必須在狀態(tài)轉(zhuǎn)換時刻發(fā)出而檢測計數(shù)是連續(xù)的需要靠狀態(tài)機(jī)來削峰。3.4 云端GPU場景下的報警邏輯資源包想傳遞的完整鏈路資源包里的word文檔教程提到用限時免費(fèi)云GPU跑通的流程。云端環(huán)境的優(yōu)勢是GPU算力充足跑yolov5s在640分辨率下能做到實(shí)時劣勢是Notebook會話可能中斷、文件持久化有限。所以我一般建議在云GPU上完成檢測驗證報警決策與通知動作放在本地或自己的服務(wù)器上——讓云GPU只負(fù)責(zé)任重活業(yè)務(wù)邏輯留在穩(wěn)定環(huán)境里。4. 復(fù)現(xiàn)時的五個坑環(huán)境、閾值、NMS和視頻流抖動4.1 坑一云GPU上torch.hub加載權(quán)重超時現(xiàn)象在云GPU上跑tutorial.ipynb執(zhí)行到torch.hub.load時卡住或直接報Timeout。原因torch.hub.load(ultralytics/yolov5, ...)會從GitHub拉取yolov5倉庫代碼。云GPU在國內(nèi)訪問GitHub的鏈路不穩(wěn)定經(jīng)常超時。解決先把倉庫clone到本地或上傳到云GPU的工作目錄然后直接用本地路徑加載# 之前的方式國外機(jī)器上沒問題 # model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) # 先手動下載或上傳yolov5倉庫到當(dāng)前目錄然后改為 import sys sys.path.insert(0, ./yolov5) model torch.hub.load(./yolov5, yolov5s, sourcelocal, pretrainedTrue)權(quán)重文件本身會自動下載到~/.cache/torch/hub/目錄如果這個下載也慢可以手動下載yolov5s.pt后放到指定位置再用torch.hub.load(./yolov5, yolov5s, sourcelocal, pretrainedFalse)加weights參數(shù)指定本地權(quán)重路徑。4.2 坑二conf和iou沒改但檢測結(jié)果和教程截圖明顯不符現(xiàn)象同一張bus.jpg教程里檢測出7人自己跑出來只有4人或者框的位置明顯偏移。原因yolov5的默認(rèn)conf是0.25iou是0.45。教程里的截圖可能用的是0.4的conf或者某個未寫明版本差異的torch.hub緩存了舊代碼。解決在代碼里顯式設(shè)置conf和iou不要依賴默認(rèn)值同時清掉緩存強(qiáng)制重新加載# 強(qiáng)制清hub緩存避免拉取到舊版本yolov5代碼 torch.hub._validate_not_a_forked_repo lambda *args, **kwargs: True model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue, force_reloadTrue)4.3 坑三bus.jpg里人挨得近部分重疊時計數(shù)偏少現(xiàn)象人群不太密集但兩人有肢體交疊時模型把兩個人合并成了一個框計數(shù)少1-2人。原因NMS的iOu閾值過高導(dǎo)致相鄰框被抑制。yolov5默認(rèn)iou0.45但重疊明顯的兩個人框的IoU可能超過這個值其中置信度低的那一個被合并掉了。解決把iou往低調(diào)比如0.3讓相鄰框更傾向于保留。需要注意iou調(diào)低后誤檢框也會變多需要結(jié)合conf一起平衡。另一個常用做法是在NMS后不做額外過濾直接用yolov5輸出的框數(shù)量作為計數(shù)值因為yolov5源碼在NMS處理上有自己的一套權(quán)衡。4.4 坑四視頻流里的報警在閾值附近反復(fù)觸發(fā)現(xiàn)象閾值設(shè)10人畫面人數(shù)從9到11波動報警頻繁觸發(fā)又解除日志里全是報警記錄。原因沒有做時間維度的平滑直接用單幀計數(shù)做判斷。單幀的檢測結(jié)果受姿態(tài)變化、遮擋、相機(jī)抖動影響波動很大。解決按前面3.2節(jié)的方式做滑窗平均加滯回區(qū)間而不是用單幀值。我在實(shí)際項目里被這個問題坑過很多次最終發(fā)現(xiàn)只要上滑窗和滯回報警穩(wěn)定性至少提升一個數(shù)量級。4.5 坑五Notebook會話超時跑了一半斷開結(jié)果沒保存現(xiàn)象云GPU的Notebook跑了幾十分鐘后會話超時檢測得到的output.jpg或報警截圖全沒了。原因免費(fèi)GPU的會話有最長持續(xù)時長限制且長時間不操作也可能被回收。解決在腳本開頭就設(shè)置自動保存每次檢測的關(guān)鍵輸出立即寫盤# 用絕對路徑保存并打印路徑確認(rèn)已落盤 import os save_dir /content/drive/MyDrive/yolov5_output # 如果掛載了Google Drive os.makedirs(save_dir, exist_okTrue) # 每處理完一幀就保存結(jié)果而非全部跑完后統(tǒng)一保存如果用的是Colab掛載Google Drive是最穩(wěn)妥的做法如果用的是國內(nèi)云平臺的Notebook先把輸出寫到持久化目錄再定時打包下載。5. 進(jìn)階用Docker固化環(huán)境和自定義報警策略5.1 環(huán)境復(fù)現(xiàn)與Docker部署資源包里附帶Dockerfile這是整個包最容易忽略但最有價值的文件。用Docker可以把yolov5推理環(huán)境固化成鏡像不管在云GPU、自有服務(wù)器還是樹莓派上部署行為完全一致。FROM pytorch/pytorch:1.13.1-cuda11.6-cudnn8-runtime # 安裝依賴項opencv-python-headless適合無GUI的服務(wù)器場景 RUN pip install opencv-python-headless pandas numpy # 把yolov5倉庫復(fù)制進(jìn)鏡像 WORKDIR /workspace COPY yolov5/ ./yolov5/ COPY tutorial.ipynb . # 預(yù)下載權(quán)重避免運(yùn)行時等待 RUN python -c import torch; torch.hub.load(./yolov5, yolov5s, sourcelocal, pretrainedTrue)這個Dockerfile的構(gòu)建要點(diǎn)在于基礎(chǔ)鏡像直接用pytorch官方runtime版本比full版本少了NeMo等用不上的組件鏡像體積更小headless版opencv省去了libGL依賴在純計算容器里最省心。構(gòu)建時預(yù)下載權(quán)重可以避免每次啟動容器都拉取模型的等待。構(gòu)建與運(yùn)行的命令# 構(gòu)建鏡像標(biāo)簽定義成項目名加日期 docker build -t yolov5-crowd-count:20250412 . # 運(yùn)行容器掛載輸入輸出目錄 # 輸入圖片放在./inputs檢測結(jié)果輸出到./outputs docker run -it --rm \ --gpus all \ -v $(pwd)/inputs:/workspace/inputs \ -v $(pwd)/outputs:/workspace/outputs \ yolov5-crowd-count:20250412 \ python run_detection.py --source /workspace/inputs --output /workspace/outputs提示沒有GPU的環(huán)境下把--gpus all去掉即可yolov5s在CPU上也能跑只是速度會降到1-2秒每幀。5.2 從單張checkpoint到不同Input場景圖像驗證到視頻流驗證實(shí)際上手路徑建議先從靜態(tài)圖開始驗證參數(shù)再到視頻流做報警邏輯測試。靜態(tài)圖的優(yōu)勢是結(jié)果可對照、可復(fù)現(xiàn)調(diào)參效率高。我習(xí)慣的做法是第一步準(zhǔn)備5-10張不同場景的靜態(tài)圖覆蓋單人、多人、稀疏、密集、光線差異等情形統(tǒng)一用同一組conf/iou跑一遍記錄每張圖的計數(shù)結(jié)果和誤檢情況。 第二步根據(jù)靜態(tài)圖的結(jié)果確定conf的合理區(qū)間再在視頻流上驗證報警邏輯的穩(wěn)定性。 第三步調(diào)試報警觸發(fā)時檢查是否還需要考慮目標(biāo)進(jìn)入/離開邊界的情況——比如畫面邊緣只露出一半的人在真實(shí)業(yè)務(wù)中算不算一個人這個業(yè)務(wù)口徑要在報警策略里明確區(qū)分。5.3 yolov5超參數(shù)速查表參數(shù)推薦值作用調(diào)參方向conf0.4檢測置信度閾值調(diào)低漏檢少但誤檢多調(diào)高誤檢少但漏檢多iou0.45NMS合并重疊框人群密集時調(diào)低至0.3-0.4size640推理輸入分辨率小目標(biāo)多時調(diào)大至1280速度下降約4倍max_det300單圖最大檢測框數(shù)超密集場景要調(diào)大默認(rèn)可到1000augmentFalse是否啟用測試時數(shù)據(jù)增強(qiáng)開啟后精度略升但速度慢2-3倍一般用不到有一項沒列進(jìn)表但值得注意的model.classes [0]必須在推理前設(shè)置yolov5的hub接口在運(yùn)行時讀取這個屬性做過濾而不是在模型初始化時。如果你在推理中途又加了其他類別的需求比如同時檢測person和car需要重新設(shè)置這個屬性。5.4 損失函數(shù)與后處理權(quán)重覆蓋負(fù)載不足時合理取舍人群計數(shù)場景和標(biāo)準(zhǔn)的檢測任務(wù)在指標(biāo)上有一個區(qū)別標(biāo)準(zhǔn)檢測看重mAP人群計數(shù)的實(shí)際業(yè)務(wù)看重計數(shù)準(zhǔn)確度即預(yù)測人數(shù)和真實(shí)人數(shù)的絕對誤差。這就意味著完全不去管檢測框的位置精度只統(tǒng)計數(shù)量也可以滿足需求。所以做一個取舍在人群分布稀疏的場景可以放心降低iou閾值換取更多檢測框因為重疊框的合并錯誤對計數(shù)的影響遠(yuǎn)大于對mAP的影響。但在擁擠場景依賴高iou閾值反而會導(dǎo)致合并過度調(diào)整時需要額外測試。如果資源包里的代碼后續(xù)要擴(kuò)展為多類別檢測記得調(diào)整model.classes為列表形式例如model.classes [0, 5]同時保留person的class_id0和bus的class_id5。從那以后我每次做目標(biāo)檢測類項目都強(qiáng)制在第一天就把參數(shù)配置、閾值標(biāo)定、緩存策略和Docker化流程走完一遍再進(jìn)入具體業(yè)務(wù)開發(fā)。磨刀不誤砍柴工這套流程已經(jīng)幫我避開了很多次環(huán)境層面的返工。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取