生上課狀態(tài)檢測數(shù)據(jù)集:VOC/YOLO/JSON三格式完整解析與YOLO訓(xùn)練實(shí)戰(zhàn))
簡介這是一份面向智慧課堂場景的學(xué)生上課狀態(tài)檢測數(shù)據(jù)集共包含1698張真實(shí)拍攝圖片覆蓋“認(rèn)真聽講”“睡覺”“玩手機(jī)”三類典型狀態(tài)適合用于課堂智能監(jiān)控、智慧教室及學(xué)生學(xué)習(xí)行為分析等項(xiàng)目實(shí)踐。數(shù)據(jù)集中圖片背景豐富、類別分布均勻所有標(biāo)注均經(jīng)人工精校并同時(shí)提供VOC格式xml、YOLO格式txt及JSON格式標(biāo)簽可直接接入主流目標(biāo)檢測框架使用。整個(gè)壓縮包共2000個(gè)文件包含1698張jpg原圖及對應(yīng)的xml/txt/json標(biāo)注文件體積約973.64MB目錄結(jié)構(gòu)清晰便于訓(xùn)練與驗(yàn)證。目前已有約1400人學(xué)習(xí)瀏覽數(shù)據(jù)集源自博主實(shí)際比賽與項(xiàng)目經(jīng)驗(yàn)標(biāo)注質(zhì)量較高配套標(biāo)簽格式齊全能有效降低算法適配成本適合目標(biāo)檢測方向的學(xué)生、開發(fā)者及競賽團(tuán)隊(duì)下載使用。1. 學(xué)生上課狀態(tài)檢測數(shù)據(jù)集1698張三格式標(biāo)簽到底能做什么教室監(jiān)控畫面里最常被要求檢測的三件事就是聽講、睡覺、玩手機(jī)。過去我要么自己對著視頻抽幀標(biāo)注要么到處找現(xiàn)成數(shù)據(jù)集結(jié)果常拿到一堆只有 VOC 或只有 YOLO 標(biāo)簽的包還得先花一晚上寫格式轉(zhuǎn)換腳本中間還容易出錯(cuò)。這份“學(xué)生上課狀態(tài)檢測數(shù)據(jù)集(聽講-睡覺-玩手機(jī))1698張-含voc(xml)yolo(txt)json三種格式標(biāo)簽.zip”算是把前置工作做完了1698 張課堂圖片同一批標(biāo)注同時(shí)輸出成 XML、TXT、JSON 三份解壓就能用。它能解決什么簡單說你想跑 YOLO 就用 YOLO 格式想跑 MMDetection 或 Detectron2 就用 JSON 或 VOC不需要自己造輪子。適合正在做課堂行為分析、學(xué)生專注度評估、智慧教室項(xiàng)目的人。新手拿它能完整走一遍數(shù)據(jù)到訓(xùn)練再到驗(yàn)證的流程熟手拿它能當(dāng)種子集快速跑出 baseline再補(bǔ)自己的真實(shí)場景數(shù)據(jù)。2. 拆開壓縮包VOC(xml)、YOLO(txt)、JSON 三種標(biāo)簽怎么對齊同一批圖片2.1 目錄結(jié)構(gòu)與文件命名規(guī)則拿到手先看什么拿到 zip 后的第一件事不是解壓就訓(xùn)練而是先把目錄結(jié)構(gòu)摸清楚。常見做法是解壓后看到 images或 JPEGImages與幾個(gè)標(biāo)簽?zāi)夸浳募ǔJ峭粋€(gè)前綴比如 IMG_0001.jpg 對應(yīng) IMG_0001.xml、IMG_0001.txt。先跑一條命令看看擴(kuò)展名分布確認(rèn)標(biāo)簽是不是每張圖片都齊全unzip 學(xué)生上課狀態(tài)檢測數(shù)據(jù)集*.zip -d classroom_dataset find classroom_dataset -type f | awk -F. {print $NF} | sort | uniq -c這條命令會(huì)列出壓縮包內(nèi)每種擴(kuò)展名的文件數(shù)量。如果 jpg 是 1698 個(gè)而 xml、txt 明顯小于這個(gè)數(shù)說明有圖片沒標(biāo)注后面訓(xùn)練時(shí)要么刪掉要么補(bǔ)標(biāo)。這里有個(gè)小經(jīng)驗(yàn)數(shù)據(jù)集的目錄名往往和標(biāo)注工具的導(dǎo)出設(shè)置有關(guān)有的把類別目錄嵌套在路徑里有的用中文目錄解壓后先看一遍頂層結(jié)構(gòu)再寫腳本別上來就假設(shè)。還需要順手統(tǒng)計(jì)圖片尺寸分布。課堂上不同教室的攝像頭分辨率可能混著來有的 1280x720有的 1920x1080這會(huì)影響 YOLO 歸一化坐標(biāo)的換算也會(huì)影響訓(xùn)練時(shí)是否要開 letterbox。用 Python 批量讀一下寬高from PIL import Image import glob sizes {} for p in glob.glob(classroom_dataset/images/*.jpg): w, h Image.open(p).size sizes.setdefault((w, h), 0) sizes[(w, h)] 1 print(sizes)這段代碼把圖片尺寸分布打印出來。如果尺寸種類超過三四種建議訓(xùn)練前統(tǒng)一做一次縮放或全部用 YOLO 的 letterbox讓模型少學(xué)一種無關(guān)變量。另外注意有些圖片本身帶 EXIF 旋轉(zhuǎn)信息PIL 默認(rèn)讀到的寬高可能和真實(shí)顯示方向相反這種情況要先用 ImageOps.exif_transpose 處理再記錄尺寸。提示課堂數(shù)據(jù)涉及學(xué)生隱私建議所有處理都在校園內(nèi)網(wǎng)完成對外發(fā)布前對人臉做模糊或裁剪。2.2 VOC XML 解析從 到 的關(guān)鍵字段VOC 格式的標(biāo)簽是 XML 文件寫起來雜但結(jié)構(gòu)非常固定根節(jié)點(diǎn)是 annotation里面先有 size 子節(jié)點(diǎn)記錄圖片寬高然后是若干個(gè) object 節(jié)點(diǎn)每個(gè) object 里有 name類別名和 bndbox四個(gè)坐標(biāo)。我用 xml.etree.ElementTree 解析腳本很短import xml.etree.ElementTree as ET def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) objects [] for obj in root.iter(object): name obj.find(name).text bndbox obj.find(bndbox) x1 float(bndbox.find(xmin).text) y1 float(bndbox.find(ymin).text) x2 float(bndbox.find(xmax).text) y2 float(bndbox.find(ymax).text) objects.append({ name: name, bbox: [x1, y1, x2, y2] }) return img_w, img_h, objects這里返回的是絕對像素坐標(biāo)x1、y1 是左上角x2、y2 是右下角。VOC 沒有歸一化所以解析出來可以直接用于畫框或轉(zhuǎn)其他格式。要注意的是有些標(biāo)注工具會(huì)把 bndbox 的坐標(biāo)寫成整數(shù)有些寫成帶小數(shù)統(tǒng)一轉(zhuǎn) float 比較穩(wěn)。還有的 XML 里嵌了 difficult、truncated 字段表示難例和被截?cái)嗟奈矬w。訓(xùn)練時(shí)這類框一般建議過濾掉否則模型會(huì)被拉著去學(xué)一些標(biāo)注本身就模糊的樣本。2.3 YOLO TXT 的歸一化坐標(biāo)與類別 ID 映射YOLO 的標(biāo)簽完全不同每張圖片對應(yīng)一個(gè)同名 txt 文件每行是“類別ID 中心點(diǎn)x 中心點(diǎn)y 寬度 高度”且 cx、cy、w、h 都是相對于圖片寬高的歸一化值范圍在 0 到 1 之間。它不是絕對坐標(biāo)所以讀取時(shí)必須結(jié)合圖片原始尺寸才能換算成像素框def parse_yolo(txt_path, img_w, img_h): with open(txt_path, r) as f: lines f.readlines() boxes [] for line in lines: line line.strip() if not line: continue cid, cx, cy, bw, bh map(float, line.split()) x1 (cx - bw / 2) * img_w y1 (cy - bh / 2) * img_h x2 (cx bw / 2) * img_w y2 (cy bh / 2) * img_h boxes.append((int(cid), x1, y1, x2, y2)) return boxes這里的 5 個(gè)浮點(diǎn)數(shù)決定了框的幾何信息類別 ID 只是序號本身它具體對應(yīng)“聽講、睡覺、玩手機(jī)”里的哪一個(gè)完全由訓(xùn)練時(shí)的 names 配置決定。所以拿到數(shù)據(jù)集后第一件事是確認(rèn) txt 里的 0、1、2 到底對應(yīng)哪三個(gè)類別。常見做法是隨機(jī)抽幾張圖把 txt 里的框畫出來人工看一眼。不要只看類別名因?yàn)椴煌藢?dǎo)出數(shù)據(jù)集時(shí) ID 順序可能完全不一樣這個(gè)映射錯(cuò)位是后面所有訓(xùn)練翻車的源頭。2.4 JSON 標(biāo)簽的字典結(jié)構(gòu)與 COCO 風(fēng)格轉(zhuǎn)換JSON 標(biāo)簽在不同數(shù)據(jù)集里的表現(xiàn)差異最大。有的包給的是 COCO 風(fēng)格頂層有 images、annotations、categories 三個(gè)數(shù)組適合直接喂給 Detectron2、MMDetection有的包給的是單文件列表每條記錄包含文件名和框數(shù)組。拿到 JSON 先別急著寫轉(zhuǎn)換先打印它的類型和頂層 keyimport json with open(classroom_dataset/labels/annotations.json, r) as f: data json.load(f) if isinstance(data, dict): print(keys:, data.keys()) if images in data: print(images:, len(data[images])) print(annotations:, len(data[annotations])) print(categories:, data[categories]) else: print(list length:, len(data)) print(first item:, data[0])輸出結(jié)果基本就能判斷格式。COCO 風(fēng)格里 annotation 的 bbox 字段是 [x, y, width, height]x、y 是框左上角坐標(biāo)不是 x1,y1,x2,y2 那套。categories 里通常有 category_id 對應(yīng)類別名和 YOLO 的 names 需要一一對應(yīng)。如果 JSON 是自定義列表bbox 可能是 [x1,y1,x2,y2]也可能是 [cx,cy,w,h]必須用一張圖的 ground truth 畫出來驗(yàn)證。這一步不能省我見過太多人栽在“看起來像 COCO其實(shí)是 xyxy”上。2.5 三格式一致性校驗(yàn)訓(xùn)練前必做的坐標(biāo)對齊檢查三種格式來自同一批標(biāo)注理論上框數(shù)量和坐標(biāo)完全一致但壓縮包在生成、傳輸、轉(zhuǎn)碼過程中可能丟框、改字、截?cái)?。我見過不止一次 XML 里 10 個(gè)框、同圖 TXT 里只有 8 個(gè)框的情況訓(xùn)練時(shí)雖然不報(bào)錯(cuò)但類別學(xué)習(xí)會(huì)受影響。所以訓(xùn)練前寫個(gè)簡單校驗(yàn)?zāi)_本import os, glob import xml.etree.ElementTree as ET xml_dir classroom_dataset/voc yolo_dir classroom_dataset/yolo def voc_box_count(xml_path): tree ET.parse(xml_path) return len(tree.getroot().findall(object)) def yolo_box_count(txt_path): with open(txt_path, r) as f: lines [l for l in f.read().splitlines() if l.strip()] return len(lines) mismatch [] for xml_path in sorted(glob.glob(os.path.join(xml_dir, *.xml))): stem os.path.splitext(os.path.basename(xml_path))[0] txt_path os.path.join(yolo_dir, stem .txt) if not os.path.exists(txt_path): mismatch.append((stem, missing txt)) continue vc voc_box_count(xml_path) yc yolo_box_count(txt_path) if vc ! yc: mismatch.append((stem, vc, yc)) print(mismatch files:, len(mismatch)) for m in mismatch[:10]: print(m)這段腳本先數(shù) XML 里的 object 數(shù)量再數(shù) txt 非空行數(shù)不一致的記下來。坐標(biāo)層面的比對更嚴(yán)格需要把 txt 還原成像素坐標(biāo)再與 XML 逐框比對允許 1 像素誤差。凡是校驗(yàn)不過的圖片建議直接從訓(xùn)練集剔除而不是強(qiáng)行修復(fù)因?yàn)槿輼?biāo)簽沒有對齊意味著原始標(biāo)注本身可能就有問題修好一個(gè)坑會(huì)踩出下一個(gè)坑。3. 用 YOLO 訓(xùn)練上課狀態(tài)檢測模型從數(shù)據(jù)集到第一個(gè) mAP3.1 把 VOC/JSON 轉(zhuǎn)成 YOLO 訓(xùn)練格式的標(biāo)準(zhǔn)腳本前面講了讀法這一步是批量轉(zhuǎn)格式。既然數(shù)據(jù)包自帶 YOLO 的 txt理論上可以省掉這步但實(shí)際中很多人拿到的 JSON 才是原始標(biāo)注VOC 或 YOLO 反而是轉(zhuǎn)換產(chǎn)物。這里給一個(gè)從 VOC 到 YOLO 的批量腳本邏輯是把 bndbox 的絕對坐標(biāo)換算成歸一化中心點(diǎn)與寬高import os, glob import xml.etree.ElementTree as ET class_names [聽講, 睡覺, 玩手機(jī)] class2id {name: idx for idx, name in enumerate(class_names)} def convert_voc_to_yolo(xml_path, output_dir, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in class2id: print(fskip unknown class {name} in {xml_path}) continue bb obj.find(bndbox) x1 float(bb.find(xmin).text) y1 float(bb.find(ymin).text) x2 float(bb.find(xmax).text) y2 float(bb.find(ymax).text) cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h bw (x2 - x1) / img_w bh (y2 - y1) / img_h lines.append(f{class2id[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if lines: stem os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(output_dir, stem .txt), w) as f: f.write(\n.join(lines))這里的關(guān)鍵是 class2id 映射表必須和最終訓(xùn)練用的 data.yaml 完全一致。腳本里遇到不在映射里的類別選擇跳過并打印比直接報(bào)錯(cuò)中斷更適合批量處理但前提是打印信息要認(rèn)真看別讓不認(rèn)識的類別悄悄消失。如果原始標(biāo)注是 JSON轉(zhuǎn) YOLO 的邏輯完全一樣只是讀取坐標(biāo)的部分換成從 JSON 取 [x, y, w, h]先換算成左上角和右下角x2xw、y2yh再走上面的歸一化公式。特別注意JSON 的 bbox 是 xywh直接套 VOC 公式會(huì)得到錯(cuò)得離譜的框轉(zhuǎn)換完務(wù)必抽三張圖把結(jié)果畫出來對比原圖。3.2 data.yaml 與模型選型yolov8n 還是 yolov8sYOLO 訓(xùn)練的第一步是寫好 data.yaml告訴框架圖片路徑、驗(yàn)證集路徑和類別表。對這個(gè)數(shù)據(jù)集我一般這樣配置path: /data/classroom_dataset train: images/train val: images/val names: 0: 聽講 1: 睡覺 2: 玩手機(jī)names 的順序不是隨便排的它必須和 txt 文件里行首的數(shù)字一一對應(yīng)。比如 txt 里某行是“2 0.5 0.6 0.1 0.2”那這個(gè)框就代表 names 里索引 2 的“玩手機(jī)”。如果順序錯(cuò)位模型訓(xùn)練時(shí)會(huì)把聽講的框當(dāng)玩手機(jī)學(xué)loss 照降mAP 照出但實(shí)際推理結(jié)果完全不可用這是數(shù)據(jù)格式里最安靜的黑匣子。模型選型上YOLO 系列對比下來對這個(gè) 1698 張、3 類的小規(guī)模任務(wù)yolov8n 和 yolov8s 是首選。n 參數(shù)量小、訓(xùn)練快適合先跑通全流程驗(yàn)證標(biāo)簽沒問題s 精度更高適合最終部署。不要一上來就用 x課堂場景的目標(biāo)不算大模型容量帶來的收益遠(yuǎn)小于數(shù)據(jù)質(zhì)量的影響。預(yù)訓(xùn)練權(quán)重從官方倉庫下載即可注意下載的權(quán)重版本要和訓(xùn)練腳本匹配不同版本的 ultralytics 參數(shù)略有差異跑之前先yolo checks確認(rèn)環(huán)境。注意names 的順序一旦寫入 data.yaml后續(xù)所有轉(zhuǎn)換腳本都必須沿用同一張映射表改一個(gè)數(shù)字就要重轉(zhuǎn)一遍標(biāo)簽。3.3 訓(xùn)練參數(shù)與數(shù)據(jù)劃分1698 張圖怎么分才不翻車數(shù)據(jù)劃分是課堂檢測任務(wù)里最容易翻車的環(huán)節(jié)。如果單純隨機(jī)劃分同教室、同時(shí)段的圖片會(huì)同時(shí)出現(xiàn)在訓(xùn)練集和驗(yàn)證集模型看著 mAP 很高一換教室就現(xiàn)原形。正確做法是按時(shí)間段或教室維度劃分比如前 1360 張做訓(xùn)練、后 338 張做驗(yàn)證或者按攝像頭位置分開。比例上 train:val 用 8:2 比較穩(wěn)數(shù)據(jù)量小就別再摳出測試集用驗(yàn)證集兼任即可。訓(xùn)練命令我用 ultralytics 的標(biāo)準(zhǔn)寫法yolo detect train \ modelyolov8s.pt \ dataclassroom.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ device0幾個(gè)參數(shù)值得說。epochs100 給足迭代空間配合 patience20 做早停在驗(yàn)證集不再提升時(shí)提前結(jié)束省時(shí)間也防止過擬合。imgsz640 是大多數(shù)課堂場景的合理值但如果攝像頭畫面里學(xué)生離得遠(yuǎn)、手機(jī)目標(biāo)只有 20 像素建議試試 imgsz960小目標(biāo) mAP 會(huì)明顯改善。batch16 在 16GB 顯存下跑 s 模型沒問題顯存小就降到 8。device0 指定 GPU沒有 GPU 就別硬跑 s換 n 或直接用 CPU 慢跑一遍流程驗(yàn)證腳本沒寫錯(cuò)。這里有個(gè)容易被忽略的參數(shù) cache。小數(shù)據(jù)集完全可以加 cacheTrue把圖片緩存到內(nèi)存里訓(xùn)練速度能快一倍前提是內(nèi)存足夠。1698 張的 640 分辨率圖片緩存后大約占 2-3GB 內(nèi)存別在 8GB 的小機(jī)器上開。3.4 訓(xùn)練中看什么指標(biāo)loss、mAP、混淆矩陣怎么看訓(xùn)練開始后終端會(huì)實(shí)時(shí)打印 box_loss、cls_loss、dfl_loss 三項(xiàng)損失。box_loss 衡量框的位置誤差cls_loss 衡量分類誤差dfl_loss 是分布焦點(diǎn)損失負(fù)責(zé)框邊界質(zhì)量。它們的絕對數(shù)值沒有統(tǒng)一標(biāo)準(zhǔn)沒必要糾結(jié)具體大小重點(diǎn)看曲線是否平穩(wěn)下降、驗(yàn)證集 loss 有沒有在某個(gè)點(diǎn)反彈。驗(yàn)證集 loss 反彈就是過擬合信號早停這時(shí)候就會(huì)生效。epoch 結(jié)束后框架會(huì)輸出 mAP50 和 mAP50-95。mAP50 是 IoU 閾值 0.5 下的平均精度課堂檢測場景看重它mAP50-95 更嚴(yán)苛從 0.5 到 0.95 每隔 0.05 算一次再平均目標(biāo)邊框要求更準(zhǔn)。對三類檢測mAP50 跑到 0.85 以上算是可用低于 0.7 就要回看標(biāo)簽或數(shù)據(jù)處理。訓(xùn)練完跑驗(yàn)證集的命令是yolo detect val modelruns/detect/train/weights/best.pt dataclassroom.yaml imgsz640混淆矩陣在驗(yàn)證結(jié)束后自動(dòng)生成橫軸是真實(shí)類別縱軸是預(yù)測類別。很多教程說混淆矩陣總和應(yīng)該唯一實(shí)際并不一定因?yàn)槁z的框和背景類沒有計(jì)入統(tǒng)計(jì)出現(xiàn)過擬合時(shí)對角線數(shù)字也不整齊。矩陣?yán)镒钪档每吹氖恰八X被預(yù)測成聽講”這類跨類混淆它直接告訴你該補(bǔ)哪類樣本。4. 標(biāo)簽格式轉(zhuǎn)換與訓(xùn)練避坑5 個(gè)我踩過的真實(shí)問題4.1 類別順序不一致導(dǎo)致訓(xùn)練標(biāo)簽全錯(cuò)現(xiàn)象訓(xùn)練 loss 降得挺好看但驗(yàn)證集 mAP 趨近 0畫出來的框類別全亂。原因是 txt 里行首的類別 ID 順序和 data.yaml 里的 names 順序?qū)Σ簧铣R娪趶?JSON 轉(zhuǎn) YOLO 時(shí)用了另一套排序。解決訓(xùn)練前寫個(gè)統(tǒng)計(jì)腳本把所有 txt 的第一列數(shù)字做個(gè)分布統(tǒng)計(jì)from collections import Counter import glob cnt Counter() for txt in glob.glob(classroom_dataset/yolo/*.txt): with open(txt) as f: for line in f: if line.strip(): cnt[int(line.split()[0])] 1 print(cnt)打印結(jié)果如果是 Counter({0: 800, 1: 700, 2: 600}) 這種形狀就逐個(gè)數(shù)字確認(rèn)它對應(yīng) names 里的哪個(gè)類別。這一步是純?nèi)斯ご_認(rèn)不依賴任何工具但它是整個(gè)訓(xùn)練流程里最值得花五分鐘做的事。4.2 XML 里被截?cái)嗟?bndbox坐標(biāo)越界與歸一化后負(fù)數(shù)現(xiàn)象訓(xùn)練到一半 loss 突然震蕩或者推理時(shí)框跑到圖像外。原因是標(biāo)注過程中 bndbox 坐標(biāo)被截?cái)嗷蛘迟N復(fù)制出錯(cuò)出現(xiàn) xmin xmax 或坐標(biāo)超出圖片寬高換算成歸一化坐標(biāo)后就變成負(fù)數(shù)或大于 1。解決在解析 XML 時(shí)對坐標(biāo)做 clamp 并過濾非法框x1 min(max(float(bb.find(xmin).text), 0), img_w - 1) y1 min(max(float(bb.find(ymin).text), 0), img_h - 1) x2 min(max(float(bb.find(xmax).text), 0), img_w - 1) y2 min(max(float(bb.find(ymax).text), 0), img_h - 1) if x2 x1 or y2 y1: continue參數(shù)上注意用 img_w - 1 而不是 img_w否則歸一化時(shí)可能出現(xiàn)等于 1 的邊界點(diǎn)。這段代碼放在解析函數(shù)里逐文件處理同時(shí)打印出被過濾的文件名事后回去看原始標(biāo)注再?zèng)Q定是修還是刪。4.3 JSON 與 VOC 的坐標(biāo)表示差異左上右下 vs 左上寬高現(xiàn)象用 JSON 轉(zhuǎn)換出來的框整體偏移尤其在目標(biāo)邊緣框的右下角位置明顯不對。原因是 JSON 標(biāo)的 bbox 幾乎都是 xywh左上角 x、左上角 y、寬度、高度而 VOC 是 xyxy。兩套表示法的轉(zhuǎn)換公式是 x2 x1 wy2 y1 h很多人只改了取字段名忘了加寬高。解決寫一個(gè)探測函數(shù)先從 JSON 里抽一張圖把框畫在圖上人工確認(rèn)再寫批量轉(zhuǎn)換。這個(gè)過程不要省我在這上面反復(fù)橫跳過兩次都是因?yàn)樘孕拧?.4 圖片尺寸不一致時(shí) YOLO TXT 歸一化失效現(xiàn)象某些圖片的框位置完全錯(cuò)誤但另一些圖是好的。原因是 YOLO 的歸一化坐標(biāo)依賴于圖片原始寬高如果數(shù)據(jù)集混入了縮放過的圖或 txt 是從一個(gè)統(tǒng)一尺寸版本轉(zhuǎn)換而來而 images 目錄里的實(shí)際尺寸變了坐標(biāo)就會(huì)錯(cuò)位。解決解析 txt 時(shí)不要信任目錄名或 XML size要現(xiàn)場用 PIL 讀圖片寬高。同時(shí)對尺寸差異過大的圖片做預(yù)統(tǒng)一。更隱蔽的是 EXIF 旋轉(zhuǎn)問題手機(jī)拍的圖在標(biāo)注工具里顯示是正的但訓(xùn)練框架讀到的是帶旋轉(zhuǎn)的原始數(shù)據(jù)寬高直接對調(diào)這種情況建議解壓后先統(tǒng)一清洗一遍。4.5 背景類干擾把“聽課”誤檢成“玩手機(jī)”現(xiàn)象驗(yàn)證集 mAP 不低但放到真實(shí)教室過視頻時(shí)記筆記、翻書、托腮這些動(dòng)作頻繁被檢成玩手機(jī)。原因是上課場景里手部動(dòng)作高度相似玩手機(jī)和翻書在 640 分辨率下特征差異很小模型學(xué)到的是“手在桌面下方有動(dòng)作”這個(gè)粗糙模式。解決思路有兩層。數(shù)據(jù)層把這類誤檢幀抽出來做難負(fù)樣本標(biāo)成 background 或直接作為無目標(biāo)圖片加入訓(xùn)練。邏輯層部署時(shí)加時(shí)序約束單幀檢測結(jié)果只有連續(xù) N 幀同時(shí)命中才輸出報(bào)警能壓掉一大半抖動(dòng)誤報(bào)。這算是做課堂檢測的血淚經(jīng)驗(yàn)前期標(biāo)數(shù)據(jù)時(shí)沒人提醒后期全靠補(bǔ)樣本。5. 驗(yàn)證與進(jìn)階把 1698 張數(shù)據(jù)集做成能上線的課堂檢測方案5.1 用混淆矩陣和分時(shí)統(tǒng)計(jì)驗(yàn)證模型真實(shí)可用性訓(xùn)練完成后先別急著部署。把驗(yàn)證集的預(yù)測結(jié)果落盤成 CSV統(tǒng)計(jì)每張圖的檢測結(jié)果與真實(shí)標(biāo)簽的匹配情況同時(shí)帶上圖片在課程中的時(shí)間位置按“上課前 10 分鐘、中段、后 10 分鐘”分桶。課堂場景有個(gè)明顯規(guī)律睡覺和玩手機(jī)集中在后段聽講集中在前段分時(shí)準(zhǔn)確率能直接暴露模型在哪個(gè)時(shí)段不可用。下面用 sklearn 的 confusion_matrix 做一個(gè)快速驗(yàn)證from sklearn.metrics import confusion_matrix import pandas as pd df pd.read_csv(val_results.csv) # 包含 image, true_label, pred_label labels [聽講, 睡覺, 玩手機(jī)] cm confusion_matrix(df[true_label], df[pred_label], labelslabels) print(pd.DataFrame(cm, indexlabels, columnslabels))注意這里輸入的 true_label 和 pred_label 是圖片級標(biāo)簽不是框級標(biāo)簽需要先按 IoU 把檢測框匹配到目標(biāo)。驗(yàn)證的意義在于把模型從黑匣子變成能用數(shù)字判斷的東西之后決定要不要給項(xiàng)目組交付。5.2 從三類擴(kuò)展到更多狀態(tài)的迭代思路1698 張數(shù)據(jù)能跑通但上線前最好擴(kuò)到五類加“舉手”和“趴桌”。做法不是重新標(biāo)一遍而是把現(xiàn)有檢測結(jié)果當(dāng)預(yù)標(biāo)注人工快速修正。先訓(xùn)練三類的 s 模型用它對新增教室圖片預(yù)測抽幀后人工只改框和類別改完回流給模型做下一輪訓(xùn)練。這個(gè)閉環(huán)里數(shù)據(jù)增強(qiáng)別加太猛課堂場景的平移、翻轉(zhuǎn)夠用顏色抖動(dòng)和 mosaic 增強(qiáng)對這種小目標(biāo)任務(wù)反而容易過擬合。5.3 偽標(biāo)簽與數(shù)據(jù)回流小數(shù)據(jù)集的后悔藥我的習(xí)慣是第一輪只訓(xùn)練三個(gè)原始類別然后讓模型去標(biāo)注同教室不同時(shí)段的未標(biāo)注圖片把置信度高于 0.9 的框直接作為偽標(biāo)簽0.5 到 0.9 的框人工復(fù)查低于 0.5 的丟棄。這樣 1698 張可以擴(kuò)到 3000-4000 張有效標(biāo)注成本幾乎為零。偽標(biāo)簽回流時(shí)注意只保留同一個(gè)模型能穩(wěn)定復(fù)檢的框不要混入多條來源的標(biāo)注標(biāo)準(zhǔn)。說到底這類數(shù)據(jù)集的價(jià)值不在數(shù)字本身而在于它給了你一個(gè)干凈、對齊、多格式的起點(diǎn)。拿到先解壓、統(tǒng)計(jì)、校驗(yàn)再談?dòng)?xùn)練訓(xùn)練時(shí)先定類別映射再調(diào)參數(shù)部署前先看分時(shí)混淆矩陣。這套流程我踩過不少坑才固定下來希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取