生低頭轉(zhuǎn)頭檢測(cè):2400張標(biāo)注數(shù)據(jù)訓(xùn)練與部署實(shí)戰(zhàn))
簡(jiǎn)介面向課堂教學(xué)行為分析場(chǎng)景一套已標(biāo)注的學(xué)生低頭、轉(zhuǎn)頭目標(biāo)檢測(cè)數(shù)據(jù)集可直接用于YOLO系列模型的訓(xùn)練與評(píng)估。數(shù)據(jù)共約2400張圖像標(biāo)注類別為低頭、轉(zhuǎn)頭兩類并已劃分好訓(xùn)練集與驗(yàn)證集降低預(yù)處理成本適合教育場(chǎng)景智能化開發(fā)者、算法入門者以及希望改進(jìn)YOLO檢測(cè)效果的工程師。包體包含2000個(gè)文件以1999個(gè)txt格式的標(biāo)注文件為主每個(gè)txt對(duì)應(yīng)一張圖像的類別與邊界框坐標(biāo)信息另有1個(gè)python可視化腳本可快速預(yù)覽標(biāo)注效果整體壓縮包約298.38MB。該資源已有130人學(xué)習(xí)下載借助標(biāo)注數(shù)據(jù)和驗(yàn)證劃分可快速訓(xùn)練學(xué)生行為檢測(cè)模型并配合作者提供的YOLOv5改進(jìn)實(shí)戰(zhàn)專欄及主頁(yè)相關(guān)項(xiàng)目便于進(jìn)一步做模型優(yōu)化、消融實(shí)驗(yàn)和場(chǎng)景適配擴(kuò)展。1. 學(xué)生低頭、轉(zhuǎn)頭行為檢測(cè)2400張YOLO標(biāo)注數(shù)據(jù)到底能做什么學(xué)生在課堂上的低頭與轉(zhuǎn)頭是最容易被攝像頭捕獲、也最能反映課堂參與度的兩類行為信號(hào)。所謂學(xué)生上課低頭、轉(zhuǎn)頭行為檢測(cè)就是通過(guò)圖像目標(biāo)檢測(cè)模型在教室畫面中實(shí)時(shí)框出頭部區(qū)域并打上標(biāo)簽再由上層邏輯統(tǒng)計(jì)成低頭時(shí)長(zhǎng)、轉(zhuǎn)頭頻次這些可量化指標(biāo)。這里的2400張已標(biāo)注圖像是整個(gè)方案的地基它決定了一個(gè)小團(tuán)隊(duì)能不能在不對(duì)數(shù)據(jù)做大規(guī)模補(bǔ)標(biāo)的情況下用YOLO系列模型快速訓(xùn)練出可用的行為識(shí)別器。適合兩類人往下讀一類是做課堂分析產(chǎn)品、要把行為指標(biāo)落到實(shí)處的算法工程師另一類是手握教室攝像頭視頻、正在糾結(jié)要不要自己標(biāo)數(shù)據(jù)的人。2. 讀懂2400張數(shù)據(jù)的標(biāo)注結(jié)構(gòu)YOLO txt每行數(shù)字的含義與類別定義拿到這類數(shù)據(jù)集第一件要做的事不是急著訓(xùn)練而是把標(biāo)注文件逐行讀明白。YOLO標(biāo)注格式下每張圖像對(duì)應(yīng)一個(gè)同名txt文件放在labels目錄里txt里每一行代表一個(gè)目標(biāo)實(shí)例。格式看起來(lái)簡(jiǎn)單但坐標(biāo)做了歸一化直接決定了后續(xù)data.yaml怎么寫、可視化腳本怎么寫以及訓(xùn)練報(bào)錯(cuò)時(shí)該往哪排查。2.1 標(biāo)注格式逐字段拆解歸一化坐標(biāo)讓數(shù)據(jù)與分辨率解耦打開一個(gè)txt你會(huì)看到類似0 0.5123 0.3845 0.0831 0.1582這樣的行。五個(gè)數(shù)字的含義分別是類別ID、目標(biāo)框中心點(diǎn)的x坐標(biāo)、中心點(diǎn)的y坐標(biāo)、框的寬度、框的高度其中坐標(biāo)全部是0到1的歸一化值不是像素絕對(duì)值。所謂歸一化是指這些數(shù)字都是相對(duì)圖像寬高的比例和圖像原始分辨率沒有關(guān)系。這個(gè)設(shè)計(jì)帶來(lái)的直接好處是同一份標(biāo)注可以喂給任意輸入分辨率。訓(xùn)練時(shí)用640后續(xù)想用960或1280做推理不需要重新標(biāo)注縮放圖像時(shí)坐標(biāo)會(huì)跟著等比換算。我在拿到一批新標(biāo)注時(shí)會(huì)先寫一個(gè)小腳本把所有txt掃一遍確認(rèn)格式和類別分布沒有明顯異常import os label_dir labels/train class_count {} total_boxes 0 bad_files [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname), r) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) ! 5: bad_files.append((fname, line)) continue cls int(parts[0]) class_count[cls] class_count.get(cls, 0) 1 total_boxes 1 print(各類別目標(biāo)數(shù):, class_count) print(總目標(biāo)框數(shù):, total_boxes) print(格式異常文件:, len(bad_files))邏輯說(shuō)明先用len(parts) ! 5過(guò)濾掉字段數(shù)不對(duì)的行再按類別ID統(tǒng)計(jì)目標(biāo)數(shù)量。這一步能同時(shí)暴露兩類問題——臟數(shù)據(jù)和類別分布嚴(yán)重失衡。如果兩個(gè)類別的目標(biāo)數(shù)相差三五倍以上訓(xùn)練時(shí)模型會(huì)偏向多數(shù)類后續(xù)要考慮調(diào)loss權(quán)重或補(bǔ)標(biāo)。參數(shù)說(shuō)明cls就是類別ID必須和data.yaml里names的順序嚴(yán)格對(duì)應(yīng)。如果這批數(shù)據(jù)約定0是低頭、1是轉(zhuǎn)頭那么names第一項(xiàng)必須是head_down、第二項(xiàng)必須是head_turn。順序?qū)懛茨P陀?xùn)練出來(lái)就是兩個(gè)類別互相調(diào)換的廢品而且指標(biāo)還看不出任何異常。2.2 目錄組織與訓(xùn)練/驗(yàn)證劃分小數(shù)據(jù)集的切分腳本和泄漏坑數(shù)據(jù)集的目錄組織常見做法是images和labels分開放各自再按train和val劃分。2400張屬于中小偏小的規(guī)模驗(yàn)證集切多了訓(xùn)練樣本不足切少了驗(yàn)證指標(biāo)波動(dòng)大。我一般按9:1左右劃分訓(xùn)練集約2160張、驗(yàn)證集約240張。如果你拿到的原始數(shù)據(jù)已經(jīng)是整理好的標(biāo)準(zhǔn)目錄跳過(guò)切分腳本直接用如果是散裝文件下面這個(gè)腳本可以快速完成劃分import os import random from shutil import move random.seed(42) os.makedirs(images/train, exist_okTrue) os.makedirs(images/val, exist_okTrue) os.makedirs(labels/train, exist_okTrue) os.makedirs(labels/val, exist_okTrue) images sorted(os.listdir(images/all)) random.shuffle(images) split_idx int(len(images) * 0.9) for i, fname in enumerate(images): part train if i split_idx else val base os.path.splitext(fname)[0] move(fimages/all/{fname}, fimages/{part}/{fname}) move(flabels/all/{base}.txt, flabels/{part}/{base}.txt)邏輯說(shuō)明先把所有圖像路徑讀進(jìn)來(lái)按9:1切分后圖像和對(duì)應(yīng)的txt一起移動(dòng)。random.seed(42)保證每次運(yùn)行切分結(jié)果一致方便復(fù)現(xiàn)。參數(shù)說(shuō)明這個(gè)隨機(jī)切分有個(gè)隱蔽的坑——如果原始數(shù)據(jù)是同一段視頻里連續(xù)抽幀來(lái)的相鄰幀內(nèi)容幾乎一樣隨機(jī)切分會(huì)導(dǎo)致訓(xùn)練集和驗(yàn)證集存在大量重復(fù)畫面驗(yàn)證指標(biāo)虛高。遇到這種情況要按視頻片段切分而不是按單幀切分比如把前70%的視頻幀都?xì)wtrain、后30%歸val再補(bǔ)一點(diǎn)不同教室的數(shù)據(jù)做驗(yàn)證。切分完成后data.yaml的寫法如下path: ./dataset train: images/train val: images/val names: 0: head_down 1: head_turn注意YOLOv8會(huì)自動(dòng)從names的長(zhǎng)度推斷nc不寫也能跑。但如果你的ultralytics版本偏舊建議補(bǔ)上nc: 2不報(bào)錯(cuò)也不影響結(jié)果。2.3 標(biāo)注質(zhì)量靜態(tài)核查先用可視化腳本抽檢再?zèng)Q定要不要補(bǔ)標(biāo)標(biāo)注質(zhì)量決定了訓(xùn)練上限。2400張圖在訓(xùn)練前值得先做一輪人工抽檢否則后期返工成本更高。我通常每50張抽1張把圖像和對(duì)應(yīng)txt框疊加畫出來(lái)用OpenCV直接可視化import cv2 img_path images/train/IMG_0001.jpg label_path labels/train/IMG_0001.txt img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f: parts list(map(float, line.strip().split())) cls, cx, cy, bw, bh parts x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) color (0, 0, 255) if int(cls) 0 else (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(int(cls)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imwrite(check.jpg, img)邏輯說(shuō)明把歸一化坐標(biāo)還原成像素坐標(biāo)并繪制檢測(cè)框。人工看畫框結(jié)果重點(diǎn)檢查三件事框是否貼合頭部、有沒有大面積漏標(biāo)、類別ID是否符合直覺。如果抽檢100張里有超過(guò)5張明顯有問題寧可把問題樣本挑出來(lái)修也不要直接訓(xùn)練。參數(shù)說(shuō)明還原坐標(biāo)最容易出錯(cuò)的地方是x1和y1要用中心點(diǎn)減半寬/半高而不是直接用cx乘w。cls只決定畫框顏色紅色是0類、綠色是1類這跟模型無(wú)關(guān)純可視化習(xí)慣。3. 用YOLOv8把行為檢測(cè)跑起來(lái)環(huán)境安裝、訓(xùn)練命令與五個(gè)必調(diào)參數(shù)數(shù)據(jù)就位后訓(xùn)練本身不復(fù)雜。Ultralytics YOLOv8是目前跑通這類小數(shù)據(jù)集最快的路徑安裝、寫配置、執(zhí)行訓(xùn)練三步就能看到第一條loss曲線。選它不是因?yàn)橐欢ū萗OLOv5指標(biāo)高而是生態(tài)太順報(bào)錯(cuò)信息直觀對(duì)2400張規(guī)模的課堂場(chǎng)景特別友好。3.1 用conda搭好環(huán)境Python版本、依賴與首次推理驗(yàn)證常見做法是用miniconda建一個(gè)獨(dú)立環(huán)境避免污染系統(tǒng)Python。在普通PC、工作站或AGX Orin這類邊緣設(shè)備上都能跑但Python版本不要低于3.9torch和ultralytics的依賴在3.8以下經(jīng)常出現(xiàn)兼容性問題conda create -n yolo_class python3.10 -y conda activate yolo_class pip install ultralytics邏輯說(shuō)明conda環(huán)境解決的是多項(xiàng)目依賴隔離。之前踩過(guò)的坑是系統(tǒng)Python里已經(jīng)裝了舊版numpy和opencv直接pip裝ultralytics會(huì)把整個(gè)環(huán)境的依賴版本攪亂。獨(dú)立環(huán)境建好后所有yolo相關(guān)包裝在新環(huán)境里互不影響。參數(shù)說(shuō)明python3.10是目前ultralytics全功能支持比較穩(wěn)的版本。ARM架構(gòu)平臺(tái)比如AGX Orin上pip會(huì)自動(dòng)拉取對(duì)應(yīng)平臺(tái)的torch版本不需要手動(dòng)處理CUDA。環(huán)境裝好后最快驗(yàn)證方式是拿一張課堂圖片跑一次推理from ultralytics import YOLO model YOLO(yolov8n.pt) results model(test.jpg) print(len(results[0].boxes))邏輯說(shuō)明用官方預(yù)訓(xùn)練權(quán)重跑一次推理沒報(bào)錯(cuò)并輸出了檢測(cè)框數(shù)量說(shuō)明依賴鏈?zhǔn)峭暾?。這一步把環(huán)境問題和數(shù)據(jù)問題分開后面訓(xùn)練報(bào)錯(cuò)時(shí)能更快定位方向。3.2 寫data.yaml與訓(xùn)練命令行imgsz、batch、epochs、patience、mosaic環(huán)境就緒后直接用ultralytics的CLI訓(xùn)練。它會(huì)自動(dòng)讀取data.yaml下載基礎(chǔ)權(quán)重然后開始訓(xùn)練循環(huán)yolo detect train \ modelyolov8n.pt \ datadataset/data.yaml \ imgsz640 \ epochs100 \ batch16 \ patience20 \ mosaic0.5 \ projectruns/train \ nameclass_behavior邏輯說(shuō)明model指定預(yù)訓(xùn)練權(quán)重yolov8n是從COCO上訓(xùn)好的基礎(chǔ)版本用它做起點(diǎn)比隨機(jī)初始化收斂快得多。data指向data.yamlultralytics會(huì)根據(jù)path字段拼接圖像和標(biāo)簽的相對(duì)路徑。參數(shù)說(shuō)明imgsz是訓(xùn)練輸入分辨率640對(duì)課堂場(chǎng)景是保守選擇。如果攝像頭是1080P或更高后排學(xué)生的頭部在畫面里往往只有30到50像素這時(shí)把imgsz提到960甚至1280小目標(biāo)召回率會(huì)有明顯提升代價(jià)是顯存占用和單epoch時(shí)間上漲約30%到50%。batch是單批次圖像數(shù)16在12G顯存下跑n模型是安全的8G顯存就降到8或4收斂會(huì)慢一點(diǎn)但不是不能訓(xùn)。epochs100對(duì)2400張的小數(shù)據(jù)集是合理設(shè)置但要不要訓(xùn)滿100輪主要看patience什么時(shí)候觸發(fā)。patience20的意思是驗(yàn)證集指標(biāo)連續(xù)20輪沒有提升就提前結(jié)束。mosaic是默認(rèn)開啟的數(shù)據(jù)增強(qiáng)把四張圖拼成一張訓(xùn)練。小數(shù)據(jù)集上容易讓模型學(xué)到過(guò)于激進(jìn)的拼接特征我一般會(huì)降到0.5。如果訓(xùn)練中發(fā)現(xiàn)驗(yàn)證指標(biāo)抖動(dòng)得很厲害把這個(gè)參數(shù)再往下降或者干脆關(guān)掉。提示如果你用的是8G顯存的老卡batch降到8同時(shí)把數(shù)據(jù)加載的workers設(shè)成2避免磁盤I/O拖慢訓(xùn)練。3.3 訓(xùn)練日志與損失曲線box_loss、cls_loss和dfl_loss怎么看訓(xùn)練時(shí)終端會(huì)滾動(dòng)輸出每一輪的GPU內(nèi)存、損失值和mAP。真正要盯的是驗(yàn)證集上的mAP50和mAP50-95以及三條loss的走勢(shì)。YOLOv8的損失函數(shù)是三項(xiàng)加權(quán)組合box_loss負(fù)責(zé)回歸檢測(cè)框的位置和大小誤差cls_loss負(fù)責(zé)類別判斷誤差dfl_loss負(fù)責(zé)邊界框的分布建模讓模型在框邊緣附近也能輸出較平滑的坐標(biāo)估計(jì)。看曲線時(shí)注意三點(diǎn)只要mAP50-95在漲單輪抖動(dòng)不用管cls_loss下降慢是正常的因?yàn)榈皖^和轉(zhuǎn)頭兩個(gè)類別本身非常相似特征區(qū)別只在頭部角度和視線方向如果訓(xùn)練后期val loss開始反彈而訓(xùn)練loss還在降說(shuō)明過(guò)擬合已經(jīng)開始了這時(shí)候提前停止或降低增強(qiáng)強(qiáng)度比硬加數(shù)據(jù)更有效。訓(xùn)練結(jié)束后runs/train/class_behavior/weights/下會(huì)生成best.pt和last.pt。best.pt是驗(yàn)證集指標(biāo)最好的那一輪權(quán)重后面做推理和部署都該拿它。4. 行為檢測(cè)訓(xùn)練避坑四條從實(shí)踐中踩出來(lái)的經(jīng)驗(yàn)這章寫的是我在課堂行為檢測(cè)上遇到的最典型的坑。每條都按現(xiàn)象、原因、解決三步走。能看到現(xiàn)象描述是因?yàn)樽约阂卜^(guò)車這些經(jīng)驗(yàn)比任何調(diào)參技巧都值錢。4.1 后排小目標(biāo)全漏檢mAP不低真實(shí)場(chǎng)景卻框不住人現(xiàn)象訓(xùn)練完驗(yàn)證集mAP50達(dá)到0.85看起來(lái)效果不錯(cuò)。但拿另一間教室的實(shí)拍視頻測(cè)試時(shí)后排學(xué)生幾乎全部漏檢檢測(cè)框密集地落在前三排。原因訓(xùn)練數(shù)據(jù)里后排樣本本身就少或者后排人頭在640分辨率下只有約25到35像素。模型沒見過(guò)足夠多的小尺度正樣本自然學(xué)不到對(duì)應(yīng)特征。驗(yàn)證集指標(biāo)好看是因?yàn)轵?yàn)證集跟訓(xùn)練集同源小目標(biāo)比例一樣稀缺mAP被前排樣本撐高了。解決優(yōu)先把imgsz提到960或1280重訓(xùn)一輪做對(duì)比。其次用切片推理工具比如SAHI這種思路把原圖切成重疊塊分別檢測(cè)再合并。還有一種做法是統(tǒng)計(jì)訓(xùn)練集中小目標(biāo)占比如果面積小于32×32像素的框占比不足5%人工補(bǔ)標(biāo)100到200張后排圖像這往往比調(diào)整網(wǎng)絡(luò)結(jié)構(gòu)更直接。4.2 低頭和轉(zhuǎn)頭互相混淆邊界樣本決定了類別邊界現(xiàn)象訓(xùn)練日志里的混淆矩陣顯示head_down和head_turn之間有10%到15%的互相誤判。實(shí)際觀察視頻發(fā)現(xiàn)模型把低頭看手機(jī)的側(cè)臉頻繁標(biāo)成轉(zhuǎn)頭。原因低頭和轉(zhuǎn)頭在視覺上存在大量過(guò)渡姿態(tài)。比如頭低下去但眼睛看向側(cè)邊或者轉(zhuǎn)頭時(shí)只有頸部偏轉(zhuǎn)、身體沒動(dòng)標(biāo)注員面對(duì)這類邊界樣本時(shí)往往憑感覺打標(biāo)導(dǎo)致同一個(gè)姿態(tài)在不同圖像里可能被標(biāo)成兩個(gè)不同類別。模型學(xué)到的類別邊界是標(biāo)注員主觀邊界的平均系統(tǒng)性偏差就這么來(lái)的。解決先統(tǒng)計(jì)全量標(biāo)注里兩個(gè)類別的框數(shù)如果數(shù)量差距過(guò)大說(shuō)明標(biāo)注口徑已失衡。再抽看被標(biāo)為不同類別但視覺特征接近的樣本統(tǒng)一口徑。更穩(wěn)妥的做法是把分類損失的權(quán)重調(diào)高一點(diǎn)比如把默認(rèn)的cls系數(shù)翻倍讓模型更重視區(qū)分這兩個(gè)容易混淆的類別但根本上還是要統(tǒng)一標(biāo)注標(biāo)準(zhǔn)。4.3 loss曲線先降后升2400張小數(shù)據(jù)集的過(guò)擬合信號(hào)現(xiàn)象訓(xùn)練到第30輪左右訓(xùn)練集的box_loss持續(xù)走低驗(yàn)證集的box_loss開始反彈但mAP50還在緩慢上升。到第50輪驗(yàn)證loss已經(jīng)比第20輪高出不少。原因2400張圖像對(duì)YOLO來(lái)說(shuō)不算多。模型早期學(xué)到的是通用特征后期開始記住訓(xùn)練圖像里的背景和噪聲模式比如特定教室的課桌椅顏色、窗戶光線。驗(yàn)證集和訓(xùn)練集如果來(lái)自同一間教室這種記憶行為不會(huì)立刻讓mAP崩盤但換到新教室就現(xiàn)原形。解決不要盲目訓(xùn)滿100個(gè)epoch用patience提前結(jié)束是最簡(jiǎn)單的后悔藥。如果確認(rèn)過(guò)擬合我把mosaic降到0甚至關(guān)掉同時(shí)適當(dāng)提高顏色增強(qiáng)參數(shù)讓模型沒法穩(wěn)定記住背景色。如果條件允許能湊到4000到5000張并覆蓋多間不同教室過(guò)擬合問題會(huì)自然緩解。4.4 訓(xùn)練報(bào)錯(cuò)loss為nan問題絕大多數(shù)出在標(biāo)注文件里現(xiàn)象訓(xùn)練跑到某個(gè)batchloss輸出nan后面所有指標(biāo)全是nan訓(xùn)練直接報(bào)廢。原因最常見的情況是標(biāo)注文件里有寬度或高度為0的框或者坐標(biāo)值大于1。這些通常是導(dǎo)出程序向上取整出錯(cuò)或者手動(dòng)編輯txt時(shí)多打了一個(gè)小數(shù)點(diǎn)。如果txt里有空行或者只有類別ID沒有坐標(biāo)的行也會(huì)讓損失計(jì)算遇到非法值。解決訓(xùn)練前用腳本把整個(gè)labels目錄掃一遍定位到非法樣本所在文件和行號(hào)import os for fname in os.listdir(labels/train): path os.path.join(labels/train, fname) with open(path) as f: for line_idx, line in enumerate(f.readlines()): parts line.strip().split() if len(parts) ! 5: print(f{fname}:{line_idx} 字段數(shù)異常) continue cls, cx, cy, bw, bh map(float, parts) if bw 0 or bh 0: print(f{fname}:{line_idx} 寬高異常 bw{bw} bh{bh}) if not (0 cx 1 and 0 cy 1): print(f{fname}:{line_idx} 中心坐標(biāo)越界)邏輯說(shuō)明腳本在訓(xùn)練前跑一遍把所有非法框全部暴露出來(lái)。定位到具體文件和行號(hào)后刪除該樣本或修正坐標(biāo)即可。參數(shù)說(shuō)明坐標(biāo)越界的判定條件0 cx 1和0 cy 1是YOLO格式的硬性規(guī)范。如果發(fā)現(xiàn)少量越界是把0.9寫成了9.9這類筆誤直接修正坐標(biāo)如果是寬高為0多半是標(biāo)注時(shí)框選操作失誤那張圖建議整個(gè)樣本重標(biāo)。5. 從檢測(cè)框到課堂指標(biāo)模型規(guī)模取舍、推理腳本與統(tǒng)計(jì)邏輯訓(xùn)練出best.pt只是第一步。課堂行為檢測(cè)真正落地的難點(diǎn)在于攝像頭輸出的是連續(xù)視頻流檢測(cè)框本身不能直接作為行為指標(biāo)中間還有模型選型、閾值設(shè)定、結(jié)果聚合這三層邏輯。5.1 模型規(guī)模怎么選n、s還是m以及邊緣部署的導(dǎo)出路徑先看硬指標(biāo)YOLOv8n參數(shù)量約300萬(wàn)s約1100萬(wàn)m約2500萬(wàn)。對(duì)課堂行為檢測(cè)這類只有兩個(gè)類別、目標(biāo)尺度相對(duì)集中的任務(wù)n在常見GPU上推理一張640分辨率圖片只需幾毫秒在邊緣設(shè)備上也能跑到實(shí)時(shí)。s精度提升有限但顯存和延遲翻倍。我的經(jīng)驗(yàn)是如果攝像頭分辨率是1080P且學(xué)生數(shù)在30人以內(nèi)用n做初版完全夠用如果要覆蓋大教室且后排占畫面比例小就上s并配imgsz1280訓(xùn)練。m和更大版本在2400張的小數(shù)據(jù)集上收益不明顯反而過(guò)擬合得更快。模型參數(shù)量640分辨率單幀延遲參考適用場(chǎng)景YOLOv8n約3M低小教室、實(shí)時(shí)互動(dòng)提醒YOLOv8s約11M中大教室、1280推理分辨率YOLOv8m約25M較高不推薦小數(shù)據(jù)容易過(guò)擬合之后如果要把模型部署到AGX Orin這類邊緣設(shè)備常見做法是先用yolo export modelbest.pt formatonnx導(dǎo)出ONNX再轉(zhuǎn)成TensorRT engine格式推理延遲會(huì)比直接跑PyTorch低一截。5.2 推理腳本與閾值過(guò)濾把best.pt變成標(biāo)簽流訓(xùn)練收斂后用best.pt做推理只需要幾行代碼。這里要做兩個(gè)工程化處理一是用confidence閾值過(guò)濾低質(zhì)量框二是把類別ID映射為可讀標(biāo)簽from ultralytics import YOLO model YOLO(runs/train/class_behavior/weights/best.pt) results model.predict( sourceclassroom.mp4, imgsz960, conf0.35, iou0.5, verboseFalse ) for frame_idx, r in enumerate(results): for box in r.boxes: cls int(box.cls[0]) conf float(box.conf[0]) label head_down if cls 0 else head_turn print(f幀 {frame_idx}: {label} 置信度 {conf:.2f})邏輯說(shuō)明model.predict直接接受視頻路徑并逐幀推理r.boxes里包含當(dāng)前幀的所有檢測(cè)框??虻腸ls給出類別索引conf給出置信度。映射成標(biāo)簽后就能進(jìn)入后續(xù)的行為統(tǒng)計(jì)邏輯。參數(shù)說(shuō)明conf0.35是經(jīng)驗(yàn)值。課堂場(chǎng)景誤檢主要來(lái)自投影屏幕上的文字和后排雜物誤檢多就上調(diào)到0.45如果出現(xiàn)大量漏檢說(shuō)明閾值偏高。iou0.5是NMS閾值決定兩個(gè)重疊框是否合并。課堂場(chǎng)景一個(gè)人只應(yīng)該有一個(gè)框保持默認(rèn)的0.5足夠。imgsz在推理時(shí)可以比訓(xùn)練時(shí)更高比如訓(xùn)練用640、推理用960能緩解小目標(biāo)漏檢但幀率會(huì)下降實(shí)時(shí)性要求高的場(chǎng)景要權(quán)衡。5.3 從單幀到統(tǒng)計(jì)檢測(cè)框如何變成低頭時(shí)長(zhǎng)和轉(zhuǎn)頭頻次單幀檢測(cè)結(jié)果沒有時(shí)間維度沒法回答這節(jié)課學(xué)生的狀態(tài)怎么樣這類問題。我常用的做法是在推理循環(huán)外面加一個(gè)基于時(shí)間窗口的統(tǒng)計(jì)層按窗口聚合輸出from collections import defaultdict window_stats defaultdict(lambda: {down_frames: 0, turn_frames: 0, total_frames: 0}) def update_window(frame_idx, label, window_id0): stats window_stats[window_id] stats[total_frames] 1 if label head_down: stats[down_frames] 1 elif label head_turn: stats[turn_frames] 1 if stats[total_frames] 300: down_ratio stats[down_frames] / stats[total_frames] turn_ratio stats[turn_frames] / stats[total_frames] print(f窗口 {window_id}: 低頭占比 {down_ratio:.2f}, 轉(zhuǎn)頭占比 {turn_ratio:.2f}) window_stats[window_id] {down_frames: 0, turn_frames: 0, total_frames: 0}邏輯說(shuō)明用固定幀數(shù)作為聚合窗口輸出窗口內(nèi)低頭幀占比和轉(zhuǎn)頭幀占比。這種統(tǒng)計(jì)方式能平滑逐幀的檢測(cè)抖動(dòng)。如果要做單人行為分析還需要用IoU匹配或ByteTrack這類跟蹤器把每一幀的框關(guān)聯(lián)到具體座位或個(gè)人這是從畫面統(tǒng)計(jì)升級(jí)到個(gè)人分析的關(guān)鍵一步。參數(shù)說(shuō)明total_frames 300 是按25fps幀率算出的12秒窗口具體數(shù)值要按你的實(shí)際幀率調(diào)整。想要更快反饋就把窗口縮到150幀想要更穩(wěn)的復(fù)盤數(shù)據(jù)就用600幀。6. 進(jìn)階用法檢測(cè)加時(shí)序平滑把行為判斷做得更貼近課堂單幀模型能輸出檢測(cè)框但老師真正關(guān)心的是這個(gè)學(xué)生低頭了多久和轉(zhuǎn)頭是否頻繁。落到真實(shí)課堂里單幀檢測(cè)結(jié)果直接被當(dāng)結(jié)論用會(huì)顯得很不穩(wěn)定因?yàn)槟P团紶枙?huì)把低頭看書的瞬間誤判成轉(zhuǎn)頭也會(huì)因?yàn)檎趽趼┑粢粠?。需要一個(gè)輕量的時(shí)序記憶系統(tǒng)來(lái)兜底。一個(gè)簡(jiǎn)單實(shí)用的方案是用滑動(dòng)窗口隊(duì)列平滑幀級(jí)決策。維護(hù)一個(gè)長(zhǎng)度為30的標(biāo)簽隊(duì)列只有當(dāng)?shù)皖^標(biāo)簽在隊(duì)列里占比超過(guò)六成時(shí)才判定為一次低頭事件開始占比低到兩成以下時(shí)判定事件結(jié)束。比起對(duì)單幀做閾值判斷誤報(bào)會(huì)少非常多from collections import deque label_history deque(maxlen30) def smooth_decision(label): label_history.append(label) if label_history.count(head_down) 18: return head_down if label_history.count(head_turn) 18: return head_turn return unknown這個(gè)做法的實(shí)質(zhì)是給檢測(cè)結(jié)果加了一道低通濾波不改模型結(jié)構(gòu)把訓(xùn)練好的best.pt當(dāng)黑匣子用就能明顯提升后端指標(biāo)的穩(wěn)定性。隊(duì)列長(zhǎng)度和閾值按幀率調(diào)整30幀對(duì)應(yīng)25fps下約1.2秒這個(gè)粒度剛好能過(guò)濾短促誤檢又不會(huì)吞掉持續(xù)1秒以上的真實(shí)行為。另一個(gè)進(jìn)階方向是引入座位區(qū)域劃分。如果攝像頭位置固定先在畫面里人工圈出每個(gè)座位區(qū)域再把檢測(cè)框中心點(diǎn)與區(qū)域做包含關(guān)系匹配。只要某個(gè)座位區(qū)域內(nèi)低頭框占比持續(xù)超過(guò)閾值就記錄為該座位的一次低頭事件。這個(gè)思路比起整幀統(tǒng)計(jì)更能回答誰(shuí)在低頭、低了多少次這類班主任真正關(guān)心的問題。我自己的教訓(xùn)是第一次做課堂行為統(tǒng)計(jì)時(shí)把全部精力花在了模型精度上mAP已經(jīng)很高但后端反饋還是不準(zhǔn)。后來(lái)才意識(shí)到老師看到的不是單幀框而是某個(gè)人在某個(gè)時(shí)間段是否頻繁低頭。這個(gè)語(yǔ)義比單幀檢測(cè)框粗放但恰好需要時(shí)序聚合才能實(shí)現(xiàn)。模型指標(biāo)是很好的起點(diǎn)但不是終點(diǎn)。如果你手頭也有一套2400張左右的已標(biāo)注數(shù)據(jù)先把第2章的數(shù)據(jù)清洗腳本跑一遍再按第3章的命令訓(xùn)練一輪最后把時(shí)序平滑接上基本就能得到一套可用的課堂行為檢測(cè)原型。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取