课堂行为检测数据集:VOC与YOLO格式转换及YOLO训练实践 简介面向学生课堂行为检测场景这份VOCYOLO双格式数据集包含5622张课堂实景图片已按7个类别完成目标标注可直接用于训练课堂行为识别模型、算法评测或教学实验尤其适合需要现成标注数据的深度学习研究者。压缩包采用7z格式共2000个文件以Pascal VOC的xml标注文件为主另含1个txt说明文件整体大小268.46MB目录结构便于直接接入YOLO、Faster R-CNN等主流目标检测框架。数据覆盖dk、dx、js、tt、xt、zl、zt七种课堂状态xml标注与图片一一对应可省去手动标注环节直接用于迁移学习或从零训练。目前已有1537人学习或下载适合需要课堂行为检测数据集做训练、验证或论文实验的研究者与工程师是一份即取即用的高质量数据资源。1. 学生课堂行为检测数据集VOCYOLO双格式5622张图先看清再动手做学生课堂行为检测模型倒不是最大的门槛数据格式才是。这份VOCYOLO格式、5622张图、7类别的学生课堂行为数据集初看是捡了便宜两种主流格式都备好解压就能用。可真正跑起来你会发现双格式不等于没坑坐标归一化、类别顺序、数据集划分、7z包完整性每个环节都能让训练悄悄翻车。它解决的是课堂场景下行为目标的标注和训练输入问题适合智慧教室、教学督导、学生专注度分析这类项目也适合拿来做目标检测入门练手。下文按我实际使用的顺序拆开讲从解压到训练再到排查按这个流程走能少折腾大半天。2. 解压与目录结构7z命令、VOC和YOLO两种标注格式的差异2.1 Linux下7z解压先测完整度再解压拿到.7z压缩包第一件事不是急着解压而是先测试完整性。压缩包在传输过程中经常出现分卷丢失或字节损坏直接解压很容易在解到一半时报CRC Failed课堂数据集动辄几千张图坏一张就得从头折腾。我一般在 Linux 上先跑7z t再跑7z x。# 测试压缩包是否完整输出没有 error 才算通过 7z t 学生课堂行为检测数据集VOCYOLO格式5622张7类别.7z # 测试通过后解压到指定目录-o 后面不要留空格 7z x 学生课堂行为检测数据集VOCYOLO格式5622张7类别.7z -o/opt/datasets/classroom_behavior如果系统提示没有7z命令先装p7zipUbuntu/Debian 用sudo apt install p7zip-fullCentOS/RHEL 用sudo yum install p7zip。-o参数指定输出目录注意-o和路径之间不能有空格这是 Linux 下 7z 命令最容易看错的地方。解压完不要急着删压缩包后面校验图片还要用它。2.2 VOC格式的目录约束JPEGImages、Annotations、ImageSets/Main解压后先看 VOC 那一份目录。常见的 Pascal VOC 检测格式有固定的目录约定不管数据集作者怎么命名拿到手先确认有没有这三个目录JPEGImages放原始图片Annotations放对应的 XML 标注文件ImageSets/Main放划分好的 train/val 列表。三者缺一不可很多训练脚本默认按这个路径去读目录名不对直接报找不到文件。VOC/ ├── JPEGImages/ │ ├── IMG_0001.jpg │ ├── IMG_0002.jpg │ └── ... ├── Annotations/ │ ├── IMG_0001.xml │ ├── IMG_0002.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── ...打开一个 XML 看看结构核心标签是size和object。size/width和size/height给出图像原始宽高每个object里的name是类别名bndbox里是xmin/ymin/xmax/ymax四个绝对像素坐标。这里最常见的坑是 XML 里size和实际图片尺寸不一致作者标图用的分辨率和后来导出图片的分辨率不同就会出现框偏移。所以后面做格式转换时我一般直接读图取宽高不信任 XML 里的 size 字段。2.3 YOLO格式的txt标注和classes.txt再翻 YOLO 那一份目录结构就简洁得多。常见的是images和labels两个平级目录各自下面再分train、val另有一个classes.txt列出全部类别名一行一个类顺序就是训练时的类别 id。YOLO 标注是每个图片对应一个同名 txt 文件文件里每一行描述一个目标框。YOLO/ ├── images/ │ ├── train/ │ ├── val/ ├── labels/ │ ├── train/ │ ├── val/ └── classes.txttxt 每行固定 5 个数空格分隔class_id x_center y_center width height。注意后四个值全部是相对图像的归一化坐标范围在 0 到 1 之间。比如某行是2 0.5123 0.4411 0.0823 0.1655意思是类别 id 为 2目标框中心在图像横向 51.23% 处纵向 44.11% 处框宽占整图 8.23%高占 16.55%。这个格式不直观但训练速度快Ultralytics YOLO、darknet 都吃这一套。3. VOC与YOLO互转坐标归一化脚本与反向重建3.1 为什么训练框架只认一种格式双格式的真正价值既然数据集同时给了两种格式为什么还要自己写转换脚本两个原因。一是不同框架的输入约定不一样老一点的项目和不少检测器教程用 Pascal VOC 的 XML而 yolov8、yolov5 训练自己的数据集现成模板就是 YOLO 的 txt用 VOC 反而要额外写一个数据加载器。二是双格式数据在传输和再加工时会互相校验我拿到一份新数据集习惯先随机抽 20 张图分别用 VOC 里的 XML 和 YOLO 里的 txt 把框画出来逐张对比框不一致就说明数据集内部版本没对齐这种问题光看文件根本看不出来。所以双格式的价值不只是省事更在于给你一条交叉验证的路径。但它也有个前提两种格式的标注必须能互相转换且转换后信息不丢。下面这套脚本是我在多个数据集上改过的通用版本核心是坐标系的换算。3.2 VOC转YOLO解析XML并归一化边界框把 VOC 的 XML 转成 YOLO 的 txt核心是把xmin/ymin/xmax/ymax的绝对像素坐标换成中心点加宽高的相对坐标。注意四件事类别名字符串要映射成 id坐标要除以图像实际宽高框越界要裁剪否则训练时 loss 会出 NaN宽高小于等于 0 的框直接丢弃。import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, img_path, class_names, out_txt): xml_path: VOC标注文件 img_path: 对应图片路径用于读真实宽高 class_names: 类别列表顺序即为类别id out_txt: 输出的YOLO txt路径 # 直接读图取宽高不信任xml里的size字段 with Image.open(img_path) as img: img_w, img_h img.size tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: print(f跳过未定义类别: {name}) continue cls_id class_names.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 越界裁剪避免归一化后w或h超过1.0 x1 min(max(x1, 0), img_w) x2 min(max(x2, 0), img_w) y1 min(max(y1, 0), img_h) y2 min(max(y2, 0), img_h) if x2 x1 or y2 y1: continue cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines))这段脚本的重点在PIL读真实宽高和越界裁剪。很多转换脚本用size字段取宽高遇到尺寸标注错误的数据就产出偏移框而x2 x1的过滤能剔除反手标出的空心框。class_names的顺序必须和classes.txt完全一致否则转出来的 txt 类别 id 全错训练时不会报错但 mAP 会莫名其妙低到离谱。3.3 YOLO转VOC从txt重建xml反向转换更麻烦一点因为 txt 里没有图像宽高信息必须读图。另外要重建完整的 XML 结构folder、filename、size这些标签都要补全很多检测框架解析 XML 时缺一个字段就报错。import os import xml.etree.ElementTree as ET from PIL import Image def yolo_to_voc(txt_path, img_path, class_names, out_xml): img Image.open(img_path) img_w, img_h img.size root ET.Element(annotation) folder ET.SubElement(root, folder) folder.text VOC filename ET.SubElement(root, filename) filename.text os.path.basename(img_path) size ET.SubElement(root, size) width ET.SubElement(size, width) width.text str(img_w) height ET.SubElement(size, height) height.text str(img_h) with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.split() if len(parts) ! 5: continue cls_id, cx, cy, w, h map(float, parts) name class_names[int(cls_id)] obj ET.SubElement(root, object) name_el ET.SubElement(obj, name) name_el.text name bndbox ET.SubElement(obj, bndbox) xmin ET.SubElement(bndbox, xmin) xmin.text str(int((cx - w / 2) * img_w)) ymin ET.SubElement(bndbox, ymin) ymin.text str(int((cy - h / 2) * img_h)) xmax ET.SubElement(bndbox, xmax) xmax.text str(int((cx w / 2) * img_w)) ymax ET.SubElement(bndbox, ymax) ymax.text str(int((cy h / 2) * img_h)) tree ET.ElementTree(root) tree.write(out_xml, encodingutf-8, xml_declarationTrue)转换后顺手做一轮反向校验把原 txt 的框和 XML 解出来的框坐标做差值超过 1 个像素就告警。这个环节能提前暴露浮点精度和整数取整带来的偏移尤其是那些框特别小、宽高只有几十像素的标注取整误差放大会让目标检测的小目标识别直接失灵。3.4 转换后的最快验证把框画回原图无论哪个方向转换最终都要把框画到图上肉眼看一遍。我用 OpenCV 写了个最简单的可视化几分钟能抽查上百张。import cv2 def draw_yolo_boxes(img_path, txt_path, class_names, out_path): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.split() if len(parts) ! 5: continue cid, cx, cy, bw, bh map(float, 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, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[int(cid)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(out_path, img)这个脚本也是课堂上判断标注质量最直接的手段。画完图重点看两类问题一是框是否紧贴目标椅子背、桌面被框进去太多了二是小目标框是否清晰教室里后排学生的人头往往只有几十个像素框稍微偏一点 IoU 就崩了。VOC 和 YOLO 两份同时画对比不一致的图单独挑出来复核。4. 用5622张课堂行为数据训练YOLO划分、配置与超参4.1 数据划分按摄像头或视频源打散5622 张图看着不少但课堂行为检测有个隐蔽的数据泄露问题同一个摄像头连续拍出的帧高度相似如果随机打散进训练集和验证集验证集里会出现训练集的邻居帧mAP 虚高一换新教室立刻露馅。正确做法是按视频来源划分同一个摄像头或同一段视频的帧必须全进同一侧。如果文件命名有规律比如camera01_0001.jpg这种前缀按前缀分组就行。import os import random from collections import defaultdict IMG_DIR images/all TRAIN_RATIO 0.8 groups defaultdict(list) for f in os.listdir(IMG_DIR): prefix f.split(_)[0] # 以camera01为例 groups[prefix].append(f) train_files, val_files [], [] for prefix, files in groups.items(): random.shuffle(files) split int(len(files) * TRAIN_RATIO) train_files.extend(files[:split]) val_files.extend(files[split:]) print(f训练集 {len(train_files)} 张, 验证集 {len(val_files)} 张)如果文件名没有规律就只能靠图像哈希做相似度聚类把感知哈希相近的帧归到同一组再划分。这一步偷懒后面训练曲线再好看也是骗自己。如果你打算用这份数据做学生专注度检测建议划分完先统计一下每个行为类别在训练集和验证集里各占多少张比例差太多的类别要单独处理。4.2 编写classroom.yaml类别清单与路径yolov8 训练自己的数据集data 配置是第一步。nc是类别总数names列表必须和classes.txt顺序完全一致。这里是最容易翻车的地方只看类别数没核对顺序训练出来的类别标签全错位。# classroom.yaml path: /opt/datasets/classroom_behavior train: images/train val: images/val nc: 7 names: 0: raise_hand 1: stand 2: read 3: write 4: low_head 5: play_phone 6: sleep上面names只是示例实际类别名一定要以数据集自带的classes.txt为准逐行核对。YOLO 训练时读取标注只认数字 id如果你的names[2]在classes.txt里排第 3 位那一类的框全会被当成另一类而且模型不会给你任何报错。4.3 训练命令与关键超参imgsz、batch、epochs数据配好后训练命令本身不复杂复杂的是参数怎么定。5622 张图对目标检测来说属于中小规模我从yolov8s.pt预训练权重起步而不是从零训练。yolo train modelyolov8s.pt dataclassroom.yaml \ imgsz640 epochs120 batch16 device0几个关键参数按课堂场景的实际经验调imgsz用 640 打底如果你主要检测后排小目标提到 960 收益比改模型结构更直接batch看显存定16G 显存跑yolov8s用 16 稳稳的批量太小 BN 层会不稳epochs设 120 足够配合默认的早停机制连续 30 个 epoch 没提升会自动停。训练脚本里默认的损失函数权重一般不用动。常见的疑问是要不要调box和cls的权重我在课堂行为这类目标小、类别语义清晰的数据上调权重收益很低不如去处理数据不平衡。真出现某类召回率明显低先看是不是样本太少而不是调损失。4.4 训练曲线与混淆矩阵定位数据问题训练完先看runs/detect/train/results.png重点看三张图。box_loss和cls_loss如果训练集持续下降但验证集在某个 epoch 后反弹是过拟合5622 张图训 120 轮大概率会过拟合早停会帮你卡住。混淆矩阵更能暴露课堂行为的特点read和write经常互相混因为低头看书和低头写字在 640 分辨率下长得太像stand容易被漏检因为站姿和坐姿的框都集中在人体上半身。如果混淆矩阵里某两类几乎分不开不要急着调模型回头看一下这两类的标注框是不是本身就有大量重叠。这类问题靠损失函数权重解决不了只能通过细化类别定义或补样本解决。训练结果本身也是检验数据质量的最好工具它比任何人工统计都更诚实。5. 课堂行为检测数据集避坑与常见问题排查5.1 标注框越界训练loss突然变nan现象训练到第 20 轮左右box_loss 突然出现nan曲线断崖后面不管怎么调学习率都回不来。原因标注数据里存在越界框xmax 或 ymax 超过了图片实际宽高。VOC 格式里这类框不报错转成 YOLO 归一化坐标后 w 或 h 大于 1.0模型计算损失时对数项直接给到无穷。解决写个扫描脚本解析所有labels/train下的 txt凡是w 1.0或h 1.0的行单独列出来。我一般把这些框裁剪到边界内而不是直接丢弃因为越界往往只超出十几个像素框本身还是有效目标。注意裁剪后中心点坐标也要重新算不能只改宽高。def fix_out_of_bounds(txt_path, img_w, img_h): lines [] with open(txt_path) as f: for line in f: cid, cx, cy, w, h map(float, line.split()) x1 (cx - w / 2) * img_w y1 (cy - h / 2) * img_h x2 (cx w / 2) * img_w y2 (cy h / 2) * img_h x1 max(0, x1); y1 max(0, y1) x2 min(img_w, x2); y2 min(img_h, y2) if x2 x1 or y2 y1: continue cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{int(cid)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines))5.2 7个类别样本严重不均现象训练完看混淆矩阵某个类别 Recall 只有 0.2其他类别 0.7 以上。再看验证集标注文件那个类别总共只有几十个框。原因课堂行为数据集天然不均衡像玩手机这类行为在真实课堂里本身出现频率低标注数量远少于听讲或阅读。目标检测不像分类任务可以轻松过采样框是贴着人走的复制样本会连背景一起复制。解决先用统计脚本算出每个类别的框数占比占比低于 5% 的类别单独处理。我一般优先做逐类别的 mAP 评估确认低召回是不是集中在少数类别上然后针对这些类别做 mosaic 增强加权或者单独补采数据。最忌讳的是整体加数据增强强度结果大类过拟合、小类也没救回来。5.3 训练验证同源帧导致指标虚高现象训练集 mAP 0.82验证集 mAP 0.79看着很健康部署到新教室直接掉到 0.4。原因划分数据时随机打散了图片同一段视频的相邻帧同时出现在训练集和验证集里。相邻帧背景几乎一样人体姿势也相似验证集成了训练集的开卷考试。解决按 4.1 的方式按来源划分重做划分完不要只看张数要把两边各自包含哪些摄像头前缀也打印出来检查。这一步做完指标会明显下降但那个下降才是真实水平。5.4 反光、遮挡和低分辨率小目标现象验证集里误检集中在教室后两排玻璃反光区域经常把半个窗框识别成人前排学生遮挡后排中排漏检严重。原因课堂场景的物理约束造成的。教室灯管反光让边框特征失真后排人头目标只有 20 到 40 像素yolov8s 的 640 输入下特征层已经很难捕获。解决针对小目标把imgsz提到 960 再训一轮通常 mAP 能涨 2 到 4 个点代价是显存和时间翻倍。针对反光误检收集包含反光的 hard negative 样本加入训练集让模型见过足够多反光但没有人的场景。这一步属于数据工程范畴也是最花时间的部分。5.5 7z压缩包解压失败与损坏文件现象解压过程报CRC Failed或者解压出来图片文件数远小于 5622打开图片提示corrupt。原因压缩包在下载或拷贝过程中字节损坏也可能是磁盘空间不足导致解压中断。解决按 2.1 先7z t测完整度损坏的话重新下载或从备份磁盘拷贝。解压完成后用脚本统计图片数量和标注数量两边必须对得上还要用PIL尝试打开每张图打不开的单独挑出来。# 统计图片和标注是否一一对应 for f in labels/train/*.txt; do imgimages/train/$(basename $f .txt).jpg [ -f $img ] || echo 缺失图片: $img done6. 让模型真正在教室落地增强参数、类别统计与难例挖掘6.1 课堂场景的数据增强克制比激进更管用课堂行为检测的语义对方向敏感一个人举手和一个人托腮在视觉上只是一点点角度差异所以增强参数不能照搬 COCO 的默认模板。我一般把随机旋转压到 15 度以内90 度旋转和水平翻转直接关掉因为课堂里很少有人横躺着。亮度、对比度和饱和度的小幅扰动才是最有用的增强真实教室的灯管开关、窗帘遮挡会让光照突变模拟这个变化比几何变换收益大得多。6.2 一个类别统计脚本先摸清数据再训练拿到数据集先做一次全面的类别统计而不是直接开训。这个习惯能省掉后面大量排查时间。脚本输出每个类别的框数、平均框宽高和尺度分布用这些数字决定是否调整增强策略或划分比例。import os import glob label_dirs [labels/train, labels/val] class_count {} size_bins {small: 0, medium: 0, large: 0} def area_cat(w, h): s w * h if s 0.01: return small elif s 0.2: return medium return large for d in label_dirs: for txt in glob.glob(os.path.join(d, *.txt)): with open(txt) as f: for line in f: parts line.split() if len(parts) ! 5: continue cid int(parts[0]) w float(parts[3]) h float(parts[4]) class_count[cid] class_count.get(cid, 0) 1 size_bins[area_cat(w, h)] 1 print(按类别编号统计:, class_count) print(目标尺度分布:, size_bins)看输出时重点关注小目标占比。课堂场景里small占比如果超过 60%imgsz直接上 960不要浪费时间调增强参数。如果某个类别的框数只有个位数先评估这个类别是不是定义合理比如发呆这种肉眼都难判定的行为与其硬训不如合并到其他类别。6.3 难例挖掘用低置信度样本反哺数据集训练完用模型对验证集做一次预测专门挑出置信度在 0.3 到 0.7 之间的检测结果。这些是模型的犹豫样本要么遮挡严重、要么目标极小、要么标注本身有争议。我通常会把这些样本截图存成一个难例目录晚上花半小时人工过一遍把模型误检但确实有目标的图挑出来补标。这个流程不需要一次性做很多每轮训练后挑 100 张左右补进训练集两三轮之后小类别的召回率一般能明显改善。我做这个方向最大的教训是拿到数据集第一件事不是训练而是先写统计脚本看类别分布。数据质量在标注阶段就决定了模型的上限配置文件和超参都是在及格线上做微调。希望帮到你。本文还有配套的精品资源点击获取