简介:这是一份面向课堂场景的行为识别目标检测数据集,覆盖玩手机、阅读、写字、低头、睡觉、举手6类常见学生行为,可用于训练YOLO系列模型或进行课堂专注度分析实验。压缩包共2000个文件,以xml标注、txt标注及jpg图像三类文件为主,整体约189.74MB;其中xml采用Pascal VOC格式,txt采用YOLO格式,与图像对应,标注工具为labelImg,全部为矩形框标注,标注规范且信息完整,可直接用于常见检测框架的数据加载与模型训练。数据累计包含19768个标注框,其中reading和playphone数量最多,分别有7826、6976个,write、sleep、bowhead各有2984、1029、947个,risehand仅6个,可见阅读、玩手机等行为出现频繁,而举手样本稀缺,使用时可进行类别均衡处理。目前已有401人学习下载。用户可直接划分数据集用于模型训练与评估,也可借助标注文件转换其他检测格式,为课堂行为分析、教学质量评估等方向提供基础数据。
1. 课堂行为检测数据集:VOC与YOLO双格式你该怎么用它
做课堂行为分析最头疼的往往不是模型选型,而是找不到一份能被训练脚本直接吃进去的标注数据。这份课堂行为数据集总共671张jpg、671个VOC格式xml、671个YOLO格式txt,六个类别合计19768个矩形框,覆盖玩手机、阅读、书写、低头、睡觉、举手等课堂姿态。它最讨喜的地方是同一份标注同时给了两种格式:XML保留原始像素坐标和完整类别信息,TXT则是归一化后的YOLO标签,省掉几乎全部标注转换时间。适合正在做智慧教室、课堂专注度分析、行为识别课程设计的人群,新手拿来跑通YOLO训练链路,老手可以基于它验证类别不平衡、小样本检测这些更深的课题。
2. 梳理数据集内部结构:XML与TXT的存放逻辑和字段细节
2.1 打开压缩包后的真实目录:没有分类结构,全靠文件名对应
解压.7z之后,看到的不是常见数据集那种清爽的images/、labels/目录树,所有jpg、xml、txt文件混在同一层级里。文件名是三者之间唯一的联系纽带,以其中一张样本为例:
firc_kt_438.jpg firc_kt_438.xml firc_kt_438.txt每个jpg是训练样本本体,同名xml是Pascal VOC标准的标注文件,同名txt则是YOLO训练时真正会被DataLoader读取的标签文件。671张图对应671个xml和671个txt,整套数据实际是“一图三件”。如果解压后直接把原目录喂给训练脚本,通常不会报错,但后续要做验证集划分、做格式转换、做类别统计时,混排目录会让你每一步都多绕一圈。
我拿到后的第一件事永远是先摸清目录现状,再决定怎么整理。常见做法是新建images和labels两个目录,让图片和txt各归其位;xml目录单独保留,作为存档与转换源。不急着改原始文件的结构,所有整理操作都在副本上进行,这样就算中途操作失误,还有后悔药可吃。
2.2 解读VOC格式XML:size、object、bndbox是核心三元组
用labelImg标注并导出VOC格式后,得到的xml文件整体结构是固定的:annotation根节点下挂着filename、size和若干个object节点。打开firc_kt_438.xml能看到类似这样的内容:
<annotation> <folder>firc_ktset</folder> <filename>firc_kt_438.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>write</name> <bndbox> <xmin>312</xmin> <ymin>185</ymin> <xmax>568</xmax> <ymax>442</ymax> </bndbox> </object> <object> <name>playphone</name> <bndbox> <xmin>900</xmin> <ymin>260</ymin> <xmax>1076</xmax> <ymax>520</ymax> </bndbox> </object> </annotation>size节点里的width和height是整张jpg的原始宽高,这两个值在后面手工把VOC转成YOLO时会反复用到。object节点代表一个标注目标实例,name是类别名,bndbox里的xmin、ymin是框左上角坐标,xmax、ymax是右下角坐标,全部是整数像素值,没有做任何归一化。
同一张图中可以出现多个object,比如某个学生一边低头一边看手机,就会同时出现bowhead和playphone两个框。写解析脚本时要注意,class字段在XML里不叫class而是name,class是COCO格式的习惯叫法,两者混用最容易踩坑。
2.3 YOLO格式TXT:每行由五个相对坐标组成,与XML形成互补
打开firc_kt_438.txt,内容与上面XML表达的是同一份标注,但坐标逻辑完全不同:
4 0.343750 0.435417 0.200000 0.356944 1 0.772656 0.541667 0.137500 0.361111每行五个数字,依次是类别编号、框中心点x、中心点y、框宽度、框高度。类别编号是0到5的整数索引。中心点和宽高全部是相对值,计算方式是像素坐标除以图片宽或高,范围限定在0到1之间。
用上面的XML第一行反推验证:write是类别列表中的第五个索引,整体是4;中心点x等于xmin与xmax的平均数再除以1280,即(312+568)/2/1280=0.34375,中心点y为(185+442)/2/720=0.435417,宽度为(568-312)/1280=0.2,高度为(442-185)/720=0.356944。txt里四个浮点数值与xml的换算完全吻合。
这里要留意,YOLO格式txt不包含图片路径,也不包含图片尺寸,只有归一化坐标。train脚本无法通过txt反向推导出原图分辨率,如果jpg、xml、txt三者缺一,那张图在训练时要么被忽略,要么直接报错。
2.4 双格式对齐的校验脚本:两分钟查处缺失与错位
因为所有对应关系只靠文件名绑定,解压后的第一道工序就是做一次逐文件核对。用一个简单的集合差运算就能完成检查:
import os from pathlib import Path base = Path("t_cls") imgs = {p.stem for p in base.glob("*.jpg")} xmls = {p.stem for p in base.glob("*.xml")} txts = {p.stem for p in base.glob("*.txt")} missing_xml = imgs - xmls missing_txt = imgs - txts only_xml = xmls - imgs only_txt = txts - imgs print("jpg数量:", len(imgs)) print("缺少xml:", len(missing_xml), missing_xml) print("缺少txt:", len(missing_txt), missing_txt) print("只有xml无jpg:", len(only_xml), only_xml) print("只有txt无jpg:", len(only_txt), only_txt)这段脚本先把三个目录下的文件主名分别收集成集合,然后做差分运算。缺少xml或缺少txt的项会在训练时报DataLoader警告,只有xml没有jpg的项则属于纯标注垃圾数据,会干扰后续统计。合法数据集应打印出jpg、xml、txt数量均为671,四个差分集合均为空。
这一步看似琐碎,但对后续链路排查帮助极大。训练跑到一半报出“image without labels”或者loss曲线突然断裂,八成是这里没查干净,提前走一遍能避免后面白等几个小时的训练。
2.5 统计各类别框数:用txt快速验证明细
摘要里给出了每个类别的框数,拿到数据后建议亲自动手验一遍,顺便确认类别顺序是否一致。统计脚本很短:
from pathlib import Path class_names = ["playphone", "reading", "write", "bowhead", "sleep", "risehand"] counts = {name: 0 for name in class_names} for txt_path in Path("t_cls").glob("*.txt"): for line in txt_path.read_text().strip().splitlines(): cls_id = int(line.split()[0]) counts[class_names[cls_id]] += 1 print(counts) print("总框数:", sum(counts.values()))脚本按行读取txt,每行的第一列转成整数后映射到类别名,累加计数。正常结果应等于playphone=6976、reading=7826、write=2984、bowhead=947、sleep=1029、risehand=6,总框数19768。如果你的统计结果有出入,优先怀疑是有文件重复或类别索引错位,而不是直接开训。
统计的目的不只是核对数字,更是给后续训练定调子。当我看到risehand只有6个框时,就会立刻意识到这属于极端不平衡数据集,必须针对性地做数据增强或类别权重处理,而不是拿着默认配置硬跑。
3. 从原始文件到可训练状态:目录梳理、划分与yaml配置
3.1 约定目录结构:images与labels分离,xml留作存档
训练框架对数据目录有约定:图片归images,标签归labels,子目录按train、val区分。先在命令行里把目录骨架建好:
mkdir -p t_cls_dataset/images/train t_cls_dataset/labels/train mkdir -p t_cls_dataset/images/val t_cls_dataset/labels/val mkdir -p t_cls_dataset/xml_archive然后在Python中遍历原始目录,把三个文件按对应关系复制到目标位置。这里刻意用复制而不是移动,保留原始压缩包的解压结果作为基线:
import shutil from pathlib import Path src_dir = Path("t_cls") img_dir = Path("t_cls_dataset/images/train") label_dir = Path("t_cls_dataset/labels/train") xml_dir = Path("t_cls_dataset/xml_archive") for jpg in sorted(src_dir.glob("*.jpg")): stem = jpg.stem txt = src_dir / f"{stem}.txt" xml = src_dir / f"{stem}.xml" if txt.exists() and xml.exists(): shutil.copy(jpg, img_dir / f"{stem}.jpg") shutil.copy(txt, label_dir / f"{stem}.txt") shutil.copy(xml, xml_dir / f"{stem}.xml") else: print("不完整样本:", stem)每次循环处理一张jpg,检查同名txt与xml是否存在,都通过才复制。复制的优势在于后续就算把images/train里的文件全删了,也能从原始目录重新生成一套。对没有原始标注raw文件的数据集来说,这层保护尤其重要。目录里的xml_archive文件夹在训练时不会被读取,但如果某天需要转成COCO格式或做可视化标注检查,它就是唯一的出处。
3.2 划分训练集与验证集:固定随机种子才是可复现的前提
目标检测的划分不能简单地按文件名字母顺序切一刀。课堂场景下的图片是按时间和镜头连续拍摄的,相邻帧非常相似,如果验证集和训练集混入了同一时段画面,验证指标会虚高。划分时使用随机打散加固定种子,保证多次执行结果一致。
import random import shutil from pathlib import Path random.seed(42) src_imgs = Path("t_cls_dataset/images/train") src_txts = Path("t_cls_dataset/labels/train") val_img = Path("t_cls_dataset/images/val") val_txt = Path("t_cls_dataset/labels/val") jpg_list = sorted(src_imgs.glob("*.jpg")) random.shuffle(jpg_list) val_count = int(len(jpg_list) * 0.2) val_set = set(jpg_list[:val_count]) for jpg in jpg_list: stem = jpg.stem txt = src_txts / f"{stem}.txt" if jpg in val_set: shutil.move(jpg, val_img / jpg.name) shutil.move(txt, val_txt / txt.name) # 其余imgs留在原地,txt自然跟随 print("train:", len(jpg_list) - val_count, "val:", val_count)random.seed(42)表示固定随机序列,每次运行划分出来的名单完全一致。val_count由0.2的比例算得,最终得到537张训练、134张验证。值得提醒的是,此划分只保证文件级别的随机,不保证场景级独立性。更严谨的做法是按摄像头ID或时间段先分组再抽组,让验证集的场景与训练集错开。如果671张数据按场景强切,训练集可能只剩400张出头,是否值当取决于你的最终目标。同时也建议同步打印出验证集里各类别的框数,如果risehand恰好没分进验证集,后面评估时该类mAP必然为零,这是数据本身决定的,并非训练失误。
3.3 编写data.yaml:类别编号必须与txt第一列严格对齐
YOLO系框架通过yaml文件描述数据集的三个信息:路径、类别数、类别名。类别名的排列顺序必须与txt文件第一列的整数索引完全一致,否则训练出的模型会用错误的名字去解读输出。下面是这份数据集的data.yaml内容:
path: ./t_cls_dataset train: images/train val: images/val nc: 6 names: 0: playphone 1: reading 2: write 3: bowhead 4: sleep 5: risehandpath用相对路径比绝对路径更稳妥,换机器解压不会因为盘符不一致报错。train和val对应3.2节划分出的目录,路径是相对path写的。nc必须等于names列表的长度,多写少写都会在训练启动时报索引越界。names列表顺序尤其要谨慎,txt第一列的数字是列表的索引,比如risehand对应的数字是5,如果你把names改成先把risehand放在0号位,整个模型的类别语义就全乱了。
3.4 首次训练跑通:YOLOv8n的小迭代验证
数据齐备后,先用最小参数量模型做链路验证,不做任何调参,确认数据通路没问题再投入正式训练:
yolo detect train model=yolov8n.pt data=t_cls_data.yaml epochs=50 imgsz=640 batch=16 patience=10 seed=42yolov8n是参数量最小的版本,跑一轮速度快,用来验证数据是否正常足够。epochs=50是常规起步值,imgsz=640表示把原图缩放后送入网络,batch=16是显存4GB左右的安全值,patience=10表示连续10个epoch验证损失不下降就提前停止。训练过程中重点看两个位置:一是loss曲线起步后是否单调下降,二是验证集的Precision、Recall是否正常往上走。如果loss在某个epoch后直接变NaN,优先检查txt中是否有NaN或inf坐标。如果mAP始终很低,先回看3.2节划分时验证集的类别分布是否合理。
训练完成后,results.csv里记录了每个epoch的完整指标。我会用一句话脚本把最差的类别找出来:
import csv with open("t_cls_dataset/runs/detect/train/results.csv") as f: rows = list(csv.DictReader(f)) last = rows[-1] for k in last: if "map" in k: print(k, last[k])输出的per-class mAP能直接看出哪些类别没学好。按这份数据的分布,risehand的mAP几乎注定为0,sleep与bowhead可能偏低,reading与playphone会相对漂亮,这个结果不意外,第4章会展开说明。
3.5 保留xml的完整意义:跨框架互转和原始坐标找回
有些场景下txt反而不够用。想转成COCO格式做mmdetection训练,需要绝对坐标和图片宽高;想做Mask R-CNN的人体姿态标注,需要从原图裁剪出目标区域;想做数据清洗,要统计每个框的面积分布是否合理,这些全部依赖原始像素坐标。VOC xml正是这些信息的稳定载体,而归一化txt无法还原出原始宽高,除非你存下每张图的尺寸。
所以即使训练不读xml,也建议不要在整理阶段把它删掉。多一个xml目录只占不到几MB空间,但从它身上能派生出的格式远不止YOLO一种。后续要做跨框架验证时,xml就是一张万能底牌。
4. 六类课堂姿态的分布差异:为什么risehand只有6个框
4.1 一张表看懂类别失衡
把第2.5节的统计结果落到一张表里,问题立刻清晰:
| 类别 | 框数 | 占比 | 直观感受 |
|---|---|---|---|
| playphone | 6976 | 35.3% | 绝对多数 |
| reading | 7826 | 39.6% | 绝对多数 |
| write | 2984 | 15.1% | 中等 |
| sleep | 1029 | 5.2% | 偏少 |
| bowhead | 947 | 4.8% | 偏少 |
| risehand | 6 | 0.03% | 几乎不存在 |
reading和playphone加起来超过七成,梯度更新基本被这两个类主导。write接近3000框,属于正常训练规模。bowhead和sleep各一千出头,只能勉强维持学习。risehand只有6个框,意味着671张图中只有6次举手动作被记录,与前面几类相差三个数量级。
这种分布决定了默认配置下模型必然是偏科生。举手的特征可能完全学不出来,甚至模型会把其他误判成举手,或者干脆将所有risehand预测置信度压到极低。读懂这张表,才算真正理解为什么不能直接拿原始数据开训。
4.2 图像层的过采样:亮度变换增强时坐标不用改
针对稀有类别,最直接的手段不是调loss,而是让模型更频繁地看到它们的样本。对含risehand的图片做离线增强并复制,是通用做法。亮度、对比度、饱和度调整不会改变目标在画面中的几何位置,因此复制出来的照片直接沿用原有txt坐标即可:
import random from PIL import Image, ImageEnhance random.seed(7) rare_list = ["t_cls_dataset/images/train/firc_kt_590.jpg"] # 实际应替换为含risehand的全部图片 for path in rare_list: img = Image.open(path) txt_path = path.replace(".jpg", ".txt") origin_text = open(txt_path, encoding="utf-8").read() for k in range(3): enhanced = ImageEnhance.Brightness(img).enhance(random.uniform(0.8, 1.2)) out_img = path.replace(".jpg", f"_a{k}.jpg") out_txt = out_img.replace(".jpg", ".txt") enhanced.save(out_img) with open(out_txt, "w", encoding="utf-8") as f: f.write(origin_text)random.uniform(0.8, 1.2)让亮度在正负两成范围内浮动,避免模型对光照产生过拟合。这里只复制了三次,实操中建议将risehand图片从原始6张复制到30到50张量级,接近600到1000个正框,模型才可能有基本的学习能力。需要特别注意的是,如果增强操作里包含水平翻转或旋转,坐标必须同步修改,仅亮度变化才可以直接复用原txt。想省事的,也可以直接在训练管线里开启mosaic与copy_paste增强,但离线增强便于直观检查生成效果。
4.3 类别权重:从逆频率到clip截断的实操
除了图像层面,训练器中也能通过类别权重来平衡loss贡献。常见做法是先按逆频率算出理论权重:
class_names = ["playphone", "reading", "write", "bowhead", "sleep", "risehand"] freq = [6976, 7826, 2984, 947, 1029, 6] total = sum(freq) weights = [total / (len(freq) * f) for f in freq] print(dict(zip(class_names, [round(w, 2) for w in weights])))理论权重依次是playphone=0.47、reading=0.42、write=1.10、bowhead=3.48、sleep=3.20、risehand=548.11。直接使用这组权重,risehand会获得超出其他类别上百倍的梯度推力,模型可能出现反复把一切预测成举手的极端行为。实际经验是把权重做clip,限制在0.5到4.0之间:
clipped = [max(0.5, min(4.0, w)) for w in weights] print(dict(zip(class_names, [round(w, 2) for w in clipped])))截断后risehand权重从548降到4.0,sleep和bowhead约3.2到3.5,write约1.1,playphone和reading保留在0.5以下。这样既照顾了少数类,又不会让多数类彻底失去学习信号。在YOLOv8里可以通过传递class_weight参数或自定义loss实现,YOLOv5则通常在utils/loss.py里手动改。权重调整是锦上添花,前提是图像层已经做过过采样,不然权重再高也学不出特征。
4.4 界定训练目标:多数类优先还是多数类加少数类均衡
对于这种极端分布,需要先想清楚产品目标再定策略。如果课堂场景只需要检测玩手机、阅读和睡觉,那么放着类别权重不调也是合理选择,因为77%以上的目标都集中在head类,默认loss也能学出高召回。如果非要识别risehand,只靠这6个框无论如何训练都不现实,与其硬调参数,不如先补充数据样本或者改用更粗粒度的二分类模型,把“是否在举手”当成图像级分类问题来解。
更稳妥的路线是把任务拆成两层:先用671张图的完整数据训练一个识别playphone、reading、write、bowhead、sleep五类的模型,再把risehand作为独立专项,单独收集一段时间真实场景的举手画面来扩充标注。这种取舍避免了一损俱损,也让数据集的利用效率更高。
5. 避坑手册:训练课堂行为数据常见的五个翻车现场
5.1 坑一:txt里类别索引错位导致mAP持续低迷
现象:训练loss正常收敛,per-class mAP却始终在低位徘徊,逐一检查发现模型把bowhead大量预测成sleep,且混淆集中在特定两个类之间。
原因:手工修改过txt时的索引对应错位,比如将原先class 0的框误写成class 3,或labelImg导出时类别下拉框没切换,导致一批框全部沿用了上一个类名。txt格式只有数字索引,没有字符串,错位之后很难一眼看出来。
解决:用xml反向核对txt的类别编号,在解析脚本里交叉比对同一张图的object.name与txt第一列索引是否一致。不一致的样本输出出来单独修正,不要试图在训练后靠调阈值弥补。
from xml.etree import ElementTree as ET class_names = ["playphone", "reading", "write", "bowhead", "sleep", "risehand"] for xml_path in Path("t_cls_dataset/xml_archive").glob("*.xml"): tree = ET.parse(xml_path) root = tree.getroot() txt_path = Path("t_cls_dataset/labels") / f"{xml_path.stem}.txt" if not txt_path.exists(): print("缺txt:", xml_path.stem) continue xml_labels = [obj.findtext("name") for obj in root.findall("object")] txt_labels = [class_names[int(line.split()[0])] for line in txt_path.read_text().strip().splitlines()] if xml_labels != txt_labels: print("不一致:", xml_path.stem, xml_labels, txt_labels)5.2 坑二:txt坐标越界或为负值,训练直接断言报错
现象:训练在加载某个batch时崩溃,报出类似AssertionError: bbox must be >= 0的错误,或val阶段检测框全部落在画面之外。
原因:有人把绝对像素坐标直接写进了YOLO的txt,未除以图片宽高。绝对坐标除以一张1280×720的图后,x_center可能高达几百上千,远超1.0,框架在做边界检查时自然会中断。
解决:写一个全量扫描脚本,检查txt每行五个数值是否都在0到1范围内。发现异常值就锁定对应的jpg,回到labelImg重新导出。注意宽度与高度也必须在0到1之间,它们除以的是对应轴尺寸,不是统一常数。
from pathlib import Path bad_files = [] for txt_path in Path("t_cls_dataset/labels").glob("*.txt"): for line in txt_path.read_text().strip().splitlines(): nums = line.split() if len(nums) != 5: bad_files.append((txt_path.name, "列数错误", line)) continue cls_id, cx, cy, w, h = int(nums[0]), float(nums[1]), float(nums[2]), float(nums[3]), float(nums[4]) if not (0 <= cls_id <= 5 and 0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= w <= 1 and 0 <= h <= 1): bad_files.append((txt_path.name, "数值越界", line)) print("异常文件数:", len(bad_files)) for item in bad_files[:10]: print(item)5.3 坑三:验证集与训练集场景重叠,mAP虚高不自知
现象:训练集和验证集都在各自数据集上表现很好,mAP 50以上,但一部署到真实课堂画面,精度崩得厉害。
原因:随机划分没有考虑课堂视频的时序相关性,连续的几十帧画面几乎一样,同一场景被同时分到了train和val。模型相当于记住了训练场景的纹理,验证时和训练集高度相似,数字自然好看,换到新教室就露馅。
解决:按文件名前缀或按采集批次先分组,把同一时间段、同一镜头的图片整体划入同一侧。如果文件名的后缀数字恰好是按时间递增的,可以按范围粗暴划分,比如取数字尾号前120张做验证,其余做训练。牺牲一点验证集数量,换取更可信的泛化评估。
5.4 坑四:小目标经下采样后消失,手机和人脸全部漏检
现象:训练完的模型对远处桌面的手机几乎无响应,val阶段所有小尺度框的Recall接近零。
原因:原图通常是1280×720甚至更高,一部手机在画面里可能只有30×20像素。当imgsz设置为640时,输入下采样一半,手机目标进一步缩小到约15像素,已经低于模型特征层的有效感受野。YOLO的多个检测头里,小目标主要靠浅层高分辨率特征图,但输入尺寸过小时,这些特征图也救不回来。
解决:把imgsz提升到960或1280,让浅层特征保留更多小目标细节。显存不足时,batch减半或改成多尺度训练策略。同时可以开启mosaic增强,让模型在多目标拥挤的场景下学会分拣。
yolo detect train model=yolov8s.pt data=t_cls_data.yaml epochs=80 imgsz=1280 batch=8 mosaic=1.0imgsz=1280对这张数据集的多数目标尺寸都更友好,睡眠中低头的人头、桌上的手机都能被特征层捕获。显存不够8G时建议先降到960,批量也相应减少。
5.5 坑五:训练集的txt和图片数量对不上,反复出现警告
现象:训练日志里持续打印“found X images and Y labels”且两个数字不相等,训练能跑但loss曲线抖动异常。
原因:混排目录状态下做划分时,只移动了jpg没有同步移动txt,或者txt被误删过。YOLO要求images和labels的目录一一对应,单边有文件就会导致数据加载索引错乱。
解决:每次划分后重新跑一遍第2.4节的集合差分脚本,把训练集、验证集的images目录与labels目录分别做一次完整性校验。只有两边数量一致且stem集合相等,才允许进入训练阶段。
6. 验证与进阶:把671张数据拆出更多可见的价值
很多同学拿到数据集就直接训练,训练完看一个mAP数字就收工。我习惯多花二十分钟做两件额外的事,一件是数据可视化验证,一件是稀有类别的单独评估。
可视化验证的价值在于快速发现标注层面的硬伤。脚本不需要写得多精致,把xml里的bndbox画回原图即可:
import cv2 from xml.etree import ElementTree as ET class_names = ["playphone", "reading", "write", "bowhead", "sleep", "risehand"] xml_path = "t_cls_dataset/xml_archive/firc_kt_438.xml" img = cv2.imread("t_cls_dataset/images/train/firc_kt_438.jpg") root = ET.parse(xml_path).getroot() for obj in root.findall("object"): name = obj.findtext("name") box = obj.find("bndbox") x1, y1 = int(box.findtext("xmin")), int(box.findtext("ymin")) x2, y2 = int(box.findtext("xmax")), int(box.findtext("ymax")) cls_id = class_names.index(name) color = (0, 255, 0) if cls_id == 5 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, name, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imwrite("check_438.jpg", img)我一般随机抽30张图跑一遍可视化,重点检查risehand和bowhead两类框是否贴近真实的人体部位。这样做比看几十个统计数字直观得多,也能肉眼分辨出框是贴合在头部还是误框在桌面上。
另一件值得做的事,是把含risehand的6张图单独挑出来做评估实验。比如只用含这些样本的图片微调一个小模型,看看模型能否在小规模正样本下形成哪怕一丁点响应。也可以把这6张图直接视为后续扩展样本的起点,结合线上采集数据做增量标注。这样671张数据就不再是单纯的一次性训练素材,而是一个可持续演进的数据基础。这类数据集的价值上限其实不取决于它有多大,而取决于使用者是否清楚它的边界。
从那以后,我每次拿到新数据集都会强制自己先统计类别分布、做文件完整性校验、再跑一轮可视化回放,这三件事做完才允许进入训练。训练结果再漂亮,如果连原始标注长什么样都没见过,心里始终不踏实。希望这篇拆解能帮你在671张图上少走弯路,也期待你把这套数据集跑透之后,有自己的训练技巧回来补充。
本文还有配套的精品资源,点击获取