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