驗(yàn)室危險(xiǎn)區(qū)域電子圍欄:從訓(xùn)練到部署全流程)
簡(jiǎn)介基于YOLOv8的實(shí)驗(yàn)室危險(xiǎn)區(qū)域電子圍欄項(xiàng)目面向計(jì)算機(jī)相關(guān)專業(yè)學(xué)生與畢業(yè)設(shè)計(jì)/課程設(shè)計(jì)場(chǎng)景集成目標(biāo)檢測(cè)模型、可視化界面、完整數(shù)據(jù)集與部署教程。資源包含訓(xùn)練腳本、視頻檢測(cè)腳本、可視化頁面代碼、YOLO系列權(quán)重yolov8n.pt、best.pt等及說明文檔共8個(gè)文件壓縮包約15.91MB結(jié)構(gòu)精煉按README指引即可快速運(yùn)行。模型訓(xùn)練后自動(dòng)生成核心指標(biāo)曲線、混淆矩陣、F1分?jǐn)?shù)曲線、精確率-召回率曲線、驗(yàn)證集預(yù)測(cè)結(jié)果與標(biāo)簽分布圖既支撐算法效果驗(yàn)證也方便答辯展示。目前已有70人瀏覽學(xué)習(xí)適合需要完整可運(yùn)行項(xiàng)目參考的初學(xué)者或用于課程設(shè)計(jì)、大作業(yè)起步在此基礎(chǔ)上可繼續(xù)修改擴(kuò)展功能。1. 基于YOLOv8的實(shí)驗(yàn)室危險(xiǎn)區(qū)域電子圍欄這套方案值不值得做實(shí)驗(yàn)室里貼著黃黑膠帶的安全區(qū)域經(jīng)常被人誤闖光靠攝像頭肉眼盯根本盯不過來。把YOLOv8跑起來用檢測(cè)框鎖定畫面中的人再拿一個(gè)多邊形把危險(xiǎn)區(qū)圈出來人一旦進(jìn)入就觸發(fā)報(bào)警——這就是基于YOLOv8的實(shí)驗(yàn)室危險(xiǎn)區(qū)域電子圍欄最樸素的實(shí)現(xiàn)思路。這類項(xiàng)目包通常會(huì)把目標(biāo)檢測(cè)、圖像標(biāo)注、坐標(biāo)算法、桌面可視化界面都串在一起正好覆蓋了畢業(yè)設(shè)計(jì)和課程設(shè)計(jì)的全部得分點(diǎn)。你不需要從頭搭環(huán)境拿到來直接用唯一要花時(shí)間的是理解每一段代碼在干什么、參數(shù)怎么調(diào)、自己的數(shù)據(jù)怎么替換。2. 數(shù)據(jù)準(zhǔn)備與標(biāo)注先把要檢測(cè)的“人”和“區(qū)域”定義清楚2.1 電子圍欄場(chǎng)景要哪些類別實(shí)驗(yàn)室電子圍欄的目標(biāo)很簡(jiǎn)單檢測(cè)到“人”再判斷這個(gè)人的位置和危險(xiǎn)區(qū)域的關(guān)系。不要一上來就把類別拆成“戴安全帽的人”“穿白大褂的人”這些對(duì)你最終判定越界沒有幫助反而讓模型變得更難訓(xùn)練。我建議單類別“person”既能復(fù)用COCO預(yù)訓(xùn)練權(quán)重里的特征又讓數(shù)據(jù)量門檻低很多。如果你用的是已經(jīng)打包好的項(xiàng)目里面多半已經(jīng)有一份組織好的數(shù)據(jù)集。你要做的是先檢查它的目錄結(jié)構(gòu)再?zèng)Q定要不要用自己的數(shù)據(jù)重新訓(xùn)練。常見的結(jié)構(gòu)是dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── data.yamldata.yaml 內(nèi)容是類別定義和路徑path: /path/to/dataset train: images/train val: images/val names: 0: person請(qǐng)留意 path 這一段很多人訓(xùn)練時(shí)報(bào)錯(cuò)“dataset not found”最后查下來是絕對(duì)路徑寫死了別人電腦上的地址。建議改成相對(duì)路徑或者訓(xùn)練前用 pwd 確認(rèn)當(dāng)前目錄把 path 設(shè)成數(shù)據(jù)集相對(duì)于當(dāng)前目錄的位置。2.2 用 LabelMe 標(biāo)注并轉(zhuǎn)成 YOLO 格式如果你要用自己的實(shí)驗(yàn)室內(nèi)景圖片訓(xùn)練標(biāo)注工具用 LabelMe 就夠了。LabelMe 保存的是 JSON 文件而 YOLOv8 需要的是每個(gè)圖片對(duì)應(yīng)一個(gè) .txt 標(biāo)簽每行寫 class_id 和歸一化后的中心坐標(biāo)、寬高。轉(zhuǎn)換腳本是這類項(xiàng)目里必帶的核心代碼邏輯并不復(fù)雜但要考慮多邊形的特殊情況。下面是 JSON 轉(zhuǎn) YOLO 格式的腳本能處理單個(gè)多邊形直接轉(zhuǎn)矩形框的情況import json import os from glob import glob def convert_labelme_json(json_path, class_map, output_dir): json_path: LabelMe 生成的 JSON 文件路徑 class_map: 類別名到 id 的映射例如 {person: 0} output_dir: 輸出的 .txt 標(biāo)簽存放目錄 with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] if img_w 0 or img_h 0: return lines [] for shape in data[shapes]: label shape[label] if label not in class_map: continue points shape[points] # 多邊形頂點(diǎn)列表 # 取多邊形的外接矩形轉(zhuǎn)成 YOLO 的 xywh 格式 xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) box_w x_max - x_min box_h y_max - y_min x_center (x_min x_max) / 2.0 y_center (y_min y_max) / 2.0 # 歸一化到 0~1避免越界 x_center / img_w y_center / img_h box_w / img_w box_h / img_h lines.append(f{class_map[label]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) base_name os.path.splitext(os.path.basename(json_path))[0] label_path os.path.join(output_dir, base_name .txt) with open(label_path, w, encodingutf-8) as f: f.write(\n.join(lines)) # 批量轉(zhuǎn)換示例 class_map {person: 0} os.makedirs(labels, exist_okTrue) for json_file in glob(labelme_json/*.json): convert_labelme_json(json_file, class_map, labels)這段代碼用外接矩形把多邊形圍起來優(yōu)點(diǎn)是穩(wěn)定、不會(huì)丟目標(biāo)缺點(diǎn)是框里會(huì)塞進(jìn)一些背景。對(duì)于“人”這種接近直立的物體外接矩形的損失很小可以接受。如果你后續(xù)想跑實(shí)例分割才需要保留多邊形頂點(diǎn)坐標(biāo)那就是另外一套格式了。2.3 數(shù)據(jù)劃分與分布檢查訓(xùn)練集和驗(yàn)證集的劃分建議 9:1不要用 7:3目標(biāo)檢測(cè)的數(shù)據(jù)量本身就顯得少驗(yàn)證集砍太多會(huì)影響模型泛化能力評(píng)估。劃分時(shí)還要注意同一個(gè)場(chǎng)景的連續(xù)幀不能同時(shí)出現(xiàn)在訓(xùn)練集和驗(yàn)證集里否則會(huì)造成“數(shù)據(jù)泄漏”驗(yàn)證集分?jǐn)?shù)虛高。按視頻幀抽取時(shí)每隔幾幀抽一張或者按目錄分開放都能避免這個(gè)問題。劃分完以后把每類的圖片數(shù)量、標(biāo)注框數(shù)量統(tǒng)計(jì)出來。如果 person 這類的標(biāo)注框少于 500 個(gè)訓(xùn)練效果會(huì)比較勉強(qiáng)。這時(shí)優(yōu)先考慮用一個(gè)通用人形檢測(cè)權(quán)重做初始化而不是從零開始訓(xùn)練。3. YOLOv8 訓(xùn)練過程環(huán)境配置、超參數(shù)與訓(xùn)練日志3.1 環(huán)境搭建的兩種路徑CPU 版本的 YOLOv8 是可以跑的只是速度慢得讓人心慌。我用 GTX 1660 Ti 跑過一次 100 epoch 的 person 檢測(cè)每輪大約 40 秒同配置換純 CPU 會(huì)慢十倍不止。所以第一步先判斷手頭機(jī)器有沒有可用 GPU沒有的話直接用 CPU 版環(huán)境把訓(xùn)練跑通也算一種落地方式。Windows 上推薦用 Python 3.9 PyTorch 2.x 的組合conda create -n yolov8 python3.9 conda activate yolov8 pip install ultralytics -i https://pypi.tuna.tsinghua.edu.cn/simpleUbuntu 20.04 下如果只是 CPU 推理這幾行就夠pip install ultralytics python -c import torch; print(torch.cuda.is_available())安裝完成后先跑這一條驗(yàn)證環(huán)境不要直接開訓(xùn)練。torch.cuda.is_available() 返回 False 的話后續(xù)所有 GPU 訓(xùn)練命令都會(huì)靜默回退到 CPU很多人跑了一整天才發(fā)現(xiàn)模型根本沒上 GPU。還有一套關(guān)聯(lián)問題是 CUDA 版本與 PyTorch 不匹配報(bào)錯(cuò)信息里帶 “torch._C” 或者找不到 DLL 時(shí)回到 PyTorch 官網(wǎng)選對(duì)應(yīng) CUDA 版本的安裝命令重裝。3.2 訓(xùn)練命令與每個(gè)參數(shù)的實(shí)際含義訓(xùn)練入口非常簡(jiǎn)單關(guān)鍵是參數(shù)含義要清楚。拿一張 640x640 的輸入圖模型按結(jié)構(gòu)復(fù)雜度分 yolov8n/s/m/l/x 五檔實(shí)驗(yàn)室電子圍欄這種單類別任務(wù)yolov8n 或 yolov8s 就夠了。更小的模型意味著更快的推理速度對(duì)實(shí)時(shí)報(bào)警更有利。yolo train datadata.yaml modelyolov8s.pt epochs100 batch16 imgsz640 device0epochs 設(shè)為 100 是常見初值不是越大越好。我一般先用 50 快速跑通確認(rèn) loss 在下降后再加大到 100 或 150。batch 受顯存限制16G 顯存跑 yolov8s 用 16 沒有問題顯存不夠就降到 8。device0 表示第一張 GPU沒有 GPU 時(shí)寫 devicecpu速度慢但能驗(yàn)流程。遷移學(xué)習(xí)默認(rèn)是自動(dòng)的modelyolov8s.pt 會(huì)下載 COCO 預(yù)訓(xùn)練權(quán)重其中包含 person 類的特征這對(duì)小數(shù)據(jù)集幫助很大。如果你的數(shù)據(jù)集里人形姿態(tài)和實(shí)驗(yàn)室場(chǎng)景差異特別大可以試一下凍結(jié)前幾層特征只是電子圍欄項(xiàng)目很少需要做到這一步。3.3 損失曲線與訓(xùn)練日志怎么看訓(xùn)練過程會(huì)在 runs/detect/train 目錄下生成多次實(shí)驗(yàn)結(jié)果包括混淆矩陣、損失曲線和驗(yàn)證集圖片。損失曲線分三部分box_loss 是框回歸損失cls_loss 是分類損失dfl_loss 是分布損失。電子圍欄項(xiàng)目里最優(yōu)先看 box_loss因?yàn)樵浇缗卸ㄍ耆蕾嚈z測(cè)框位置的穩(wěn)定性。曲線持續(xù)下降說明模型在學(xué)習(xí)如果到第 20 輪以后還在高位震蕩優(yōu)先檢查數(shù)據(jù)標(biāo)注質(zhì)量不要急著加訓(xùn)練輪次。還有個(gè)常見誤區(qū)是只盯訓(xùn)練集 loss忽略 val 的 mAP。訓(xùn)練集 loss 降但驗(yàn)證集 mAP 沒變化說明模型在過擬合訓(xùn)練集的背景紋理。這時(shí)可以加數(shù)據(jù)增強(qiáng)參數(shù)比如yolo train datadata.yaml modelyolov8s.pt epochs100 batch16 imgsz640 hsv_h0.02 hsv_s0.5 hsv_v0.4hsv_h 是色調(diào)變化幅度0.02 表示在原來基礎(chǔ)上微調(diào)改到 0.05 以上顏色畸變明顯。電子圍欄場(chǎng)景里光照變化比較頻繁適當(dāng)?shù)?hsv_s 飽和度抖動(dòng)能提升夜間和反光場(chǎng)景的魯棒性。4. 電子圍欄判定邏輯從檢測(cè)框坐標(biāo)到越界報(bào)警4.1 危險(xiǎn)區(qū)域怎么標(biāo)定電子圍欄的“圍欄”在代碼里通常是一個(gè)或多個(gè)多邊形。你可以用鼠標(biāo)在視頻畫面上點(diǎn)擊頂點(diǎn)也可以寫在配置文件里按像素坐標(biāo)定義頂點(diǎn)。這里有一個(gè)坐標(biāo)系陷阱畫面如果是 1280x720推理時(shí)縮放成 640x640檢測(cè)框坐標(biāo)是相對(duì)于縮放后圖像的。拿這個(gè)框和標(biāo)定好的多邊形比較必須先把兩邊統(tǒng)一到同一個(gè)坐標(biāo)系。我一般按原始分辨率標(biāo)定多邊形然后把模型推理出的框按比例放大回原始分辨率再判斷。具體縮放比例scale_x frame_width / model_width scale_y frame_height / model_height4.2 用 pointPolygonTest 判斷檢測(cè)框與多邊形的關(guān)系判斷一個(gè)點(diǎn)是否在多邊形內(nèi)OpenCV 的 pointPolygonTest 是現(xiàn)成的方案。默認(rèn)判斷檢測(cè)框中心點(diǎn)太粗暴人站在邊界外身體探進(jìn)危險(xiǎn)區(qū)就漏判。更穩(wěn)的做法是取檢測(cè)框底邊中心點(diǎn)因?yàn)槿四_踩的位置最能代表“人實(shí)際站在那里”。import cv2 danger_zone np.array([[100, 200], [300, 150], [400, 400], [150, 450]], dtypenp.float32) def is_point_in_zone(point, zone): point: (x, y) 底邊中心點(diǎn) zone: 多邊形頂點(diǎn)數(shù)組 返回 True 表示點(diǎn)在區(qū)域內(nèi)或邊界上 result cv2.pointPolygonTest(zone, point, measureDistFalse) return result 0person 檢測(cè)框的底邊中心點(diǎn)結(jié)合了點(diǎn)特征和幾何特征比單純用中心點(diǎn)更符合人類對(duì)“站在某處”的直覺。如果你的檢測(cè)框特別不穩(wěn)定一幀大一幀小底邊坐標(biāo)會(huì)上下抖動(dòng)這時(shí)退一步用中心點(diǎn)和底邊中心點(diǎn)的折中位置犧牲一點(diǎn)靈敏度換穩(wěn)定性。4.3 報(bào)警觸發(fā)與去抖檢測(cè)偶爾會(huì)自動(dòng)消失一幀就立刻停止報(bào)警會(huì)讓整個(gè)系統(tǒng)非常聒噪。至少連續(xù) 3 到 5 幀判定“人在區(qū)域內(nèi)”才觸發(fā)報(bào)警這樣誤報(bào)會(huì)少很多。這個(gè)邏輯用一個(gè)計(jì)數(shù)器就能實(shí)現(xiàn)。同樣報(bào)警發(fā)生后人移出區(qū)域也建議連續(xù)幾幀判定安全才復(fù)位報(bào)警狀態(tài)避免邊界反復(fù)橫跳時(shí)蜂鳴器響個(gè)不停。status_buffer [] ALARM_FRAME_COUNT 5 def update_alarm_state(in_zone): in_zone: 當(dāng)前幀是否在區(qū)域內(nèi) 返回當(dāng)前應(yīng)不應(yīng)該觸發(fā)報(bào)警 status_buffer.append(in_zone) if len(status_buffer) ALARM_FRAME_COUNT: status_buffer.pop(0) if all(status_buffer): return True return Falsestatus_buffer 維持一個(gè)長(zhǎng)度為 5 的緩沖隊(duì)列全部為 True 才報(bào)警。這個(gè)參數(shù)配合推理幀率調(diào)整如果你每秒只處理 5 幀隊(duì)列全 True 相當(dāng)于人已經(jīng)在區(qū)域里待了一秒每秒處理 20 幀的話就相當(dāng)于 0.25 秒。拿去抖設(shè)定的幀數(shù)要匹配你的推理速度否則報(bào)警延遲會(huì)變成一大截。5. 可視化界面與部署五個(gè)容易翻車的排查點(diǎn)5.1 PyQt 與 OpenCV 視頻流的集成交互可視化界面最常用的組合是 PyQt 做窗口和控件OpenCV 負(fù)責(zé)取流和顯示幀。把推理循環(huán)放進(jìn)界面線程會(huì)直接卡死窗口因?yàn)槟P屯评硪淮慰赡芎臅r(shí)幾十毫秒畫面就在那卡著不動(dòng)。常見做法是單獨(dú)開一個(gè) QThread 跑視頻循環(huán)然后用信號(hào)把處理好的幀發(fā)給主界面刷新。from PyQt5.QtCore import QThread, pyqtSignal import cv2 class VideoThread(QThread): frame_signal pyqtSignal(object) def __init__(self, model, source0): super().__init__() self.model model self.source source self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running: ret, frame cap.read() if not ret: break results self.model(frame, conf0.45, imgsz640) # 這里把檢測(cè)框和圍欄繪制到 frame 上 annotated draw_danger_zone(frame, danger_zone) self.frame_signal.emit(annotated) cap.release()信號(hào)傳圖像對(duì)象比傳路徑更直接刷新頻率上限取決于推理速度。如果你的界面顯示跟不上可以在 signal.emit 前加一個(gè)簡(jiǎn)單的幀間隔控制用 time.sleep 限流。5.2 部署排查一模型路徑與權(quán)重不匹配現(xiàn)象訓(xùn)練完跑 demo.py程序報(bào)錯(cuò) “cannot find model”或者加載后檢測(cè)結(jié)果全為空。原因代碼里默認(rèn)加載 runs/detect/train/weights/best.pt但你把項(xiàng)目目錄移動(dòng)過或者復(fù)制了項(xiàng)目給同學(xué)絕對(duì)路徑已經(jīng)失效。另一個(gè)關(guān)聯(lián)原因是 torch.load 加載到 CPU 機(jī)器上但權(quán)重是在 GPU 上訓(xùn)練的也會(huì)出現(xiàn)加載報(bào)錯(cuò)。解決先確認(rèn) best.pt 存在并且用絕對(duì)路徑試一次加載python -c from ultralytics import YOLO; m YOLO(/abs/path/best.pt); print(ok)輸出 ok 再排查界面代碼里的相對(duì)路徑寫法。5.3 部署排查二視頻流卡死和內(nèi)存增長(zhǎng)現(xiàn)象程序運(yùn)行幾分鐘后畫面開始卡頓最后直接無響應(yīng)內(nèi)存占用一路飆升。原因預(yù)覽窗口在持續(xù)累積新的 QPixmap 對(duì)象舊的沒有被釋放?;蛘呤峭评砭€程把結(jié)果發(fā)到主線程的速度超過界面刷新速度積壓的信號(hào)把事件隊(duì)列堆滿了。第二個(gè)原因是做電子圍欄最容易踩的坑視頻幀處理完后沒能正確釋放 OpenCV 的 Mat 對(duì)象。解決不要在 while 循環(huán)里反復(fù)設(shè)置整個(gè) QGraphicsScene 的內(nèi)容改用 QLabel.setPixmap 直接刷新并確保 frame_signal 連接的是同一個(gè)刷新槽函數(shù)。最簡(jiǎn)單的驗(yàn)證方法是打印線程 id看推理線程和 UI 線程是不是同一個(gè)同一個(gè)就是線程模型寫錯(cuò)了。5.4 部署排查三內(nèi)置攝像頭編號(hào)不對(duì)現(xiàn)象界面能打開但預(yù)覽畫面是黑屏或報(bào) “Unable to open camera”。原因cv2.VideoCapture(0) 的 0 不一定是你的 USB 攝像頭筆記本自帶攝像頭和 USB 外接攝像頭的編號(hào)在不同驅(qū)動(dòng)下順序不同。還有一類原因是攝像頭被其他軟件占用OpenCV 拿不到句柄。解決先單獨(dú)測(cè)一次python -c import cv2; cap cv2.VideoCapture(0); print(cap.isOpened())如果返回 False換 VideoCapture(1) 再試。不要直接在項(xiàng)目代碼里面試編號(hào)那個(gè)過程很折磨人。5.5 部署排查四CPU 機(jī)器推理速度過慢現(xiàn)象畫面能跑起來但一幀要兩秒多報(bào)警延遲完全不可接受。原因在純 CPU 機(jī)器上跑了默認(rèn)的 yolov8s 模型且沒有限制輸入分辨率。640 輸入圖在 CPU 上推理一次大約幾百毫秒性能較差的 CPU 到一秒以上很正常。解決把模型換成 yolov8n把推理尺寸從 640 降到 480再把推理線程的輸出限在一個(gè)合理幀率例如每 0.2 秒最多推理一幀。最后的畫面流暢度由顯示線程負(fù)責(zé)報(bào)警判定基于推理結(jié)果所以降低推理頻率不會(huì)讓報(bào)警邏輯出錯(cuò)只是讓邊界判斷更新變慢而已。6. 進(jìn)階導(dǎo)出 ONNX 做加速附帶一個(gè)幀率優(yōu)化思路訓(xùn)練好的 best.pt 在推理腳本里跑沒問題但如果你想把核心檢測(cè)和圍欄邏輯移到板端比如 RK3588就得先導(dǎo)出 ONNX。導(dǎo)出指令很簡(jiǎn)單yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640 simplifyTruesimplifyTrue 會(huì)做計(jì)算圖融合減少部分算子方便后續(xù)轉(zhuǎn)換 RKNN 格式。導(dǎo)出后用 onnxruntime 驗(yàn)證一遍輸入輸出尺寸確保和 .pt 權(quán)重結(jié)果一致常見的坑發(fā)生在動(dòng)態(tài)尺寸問題上。導(dǎo)出時(shí)沒有指定固定尺寸推理時(shí)輸入維度是動(dòng)態(tài)的有些部署框架不支持動(dòng)態(tài)輸入建議 export 時(shí)加上 dynamicFalse 定死尺寸。如果你只在本地做幀率優(yōu)化一個(gè)成本很低的做法是分開縮放把 640 輸入圖的檢測(cè)結(jié)果映射回預(yù)覽畫面時(shí)預(yù)覽畫面用 960x540 顯示。這樣推理開銷降下來視覺上清晰度也沒有明顯下降。再配合跳幀推理每隔一幀跑一次模型中間一幀沿用上一幀的檢測(cè)結(jié)果報(bào)警抖動(dòng)的感知幾乎沒有差別。跑完這些以后你會(huì)在 best.pt 的 val 分?jǐn)?shù)、導(dǎo)出模型的一致性和報(bào)警延遲上花最多的調(diào)試時(shí)間。我曾經(jīng)在一個(gè)項(xiàng)目里為了把報(bào)警延遲從一秒壓到半秒最后發(fā)現(xiàn)瓶頸在攝像頭自身的 30 幀緩沖而不是模型推理。先測(cè)量瓶頸再?zèng)Q定優(yōu)化位置這是我從這個(gè)方案里學(xué)到的最大教訓(xùn)希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取