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