玩手机检测数据集:VOC标注解析与YOLO格式转换实战
简介面向目标检测学习者的手机使用行为检测数据集采用VOC格式的xml标注已按训练集与测试集划分完毕可直接用于YOLO、Faster R-CNN等模型的训练与评估。数据覆盖人群玩手机场景含face、drink、phone三个类别图像为分辨率300至400的RGB图片训练集共1090张图片与对应xml测试集共272张图片与对应xml另附类别字典json文件压缩包共2000个文件以1362个xml标注文件和636张jpg图片为主另有1个可视化脚本与1个json字典整体大小约53.79MB。目录按train与test分设各自包含images和labels子目录结构清晰且无需额外处理即可直接使用。已有480人学习关注随包提供的py脚本可随机传入一张图片自动绘制边界框并保存便于直观核对标注质量。省去自行采集与标注的繁琐流程适合目标检测初学者进行实战练习也可作为模型调优时的基准数据。1. 人群玩手机检测数据集先说结论再动手做安防、课堂行为分析或者工位效能监测的朋友大概率都遇到过同一种尴尬官方开源数据集里几乎没有“玩手机”这个细粒度行为自己标注又贵又慢。这个玩手机检测数据集正好补这个缺口——标准的VOC标注格式xml文件图像分辨率集中在300-400的RGB图片共1362张已经按训练集1090张、测试集272张分好目录每张图都配了同名xml。类别一共3个face、drink、phone简单说就是把人脸、喝水动作、拿手机动作分开标方便你直接做“玩手机”行为判定。适合的人群很明确想快速拿到可用数据训练检测模型的从业者以及需要做行为识别预实验的学生。不需要额外清洗解压后解析xml就能进训练流程。2. VOC标注格式解析XML的树结构和字段含义2.1 从annotation根节点开始拆解VOC格式的xml标注核心结构其实特别简单——以annotation为根节点每一个object节点就是一个目标框。下面是一份标准VOC xml的典型结构字段顺序可能有差异但含义一致annotation folderimages/folder filenameph860.jpg/filename size width360/width height400/height depth3/depth /size object namephone/name bndbox xmin128/xmin ymin156/ymin xmax212/xmax ymax238/ymax /bndbox /object /annotation解析逻辑上folder和filename是用来做文件关联的size里的width、height、depth是原图的宽、高、通道数这个信息在后期做坐标归一化时非常关键。object节点里name是类别名bndbox是边界框的四个角坐标表示左上角xmin, ymin和右下角xmax, ymax。我一般习惯用xml.etree.ElementTree来解析轻量而且不用额外装库import xml.etree.ElementTree as ET tree ET.parse(ph860.xml) root tree.getroot() for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) print(name, xmin, ymin, xmax, ymax)这个脚本本身没什么技术含量但它是所有后续工作的基础。解析xml时有个细节必须注意一定要转成int因为xml解析出来默认是字符串直接拿去算中心点会报类型错误。另外如果手头有多个xml需要批量读取建议用glob.glob匹配目录里的*.xml文件避免手动拼路径。2.2 face、drink、phone 三个类别的边界情况很多数据集类别设计得很“干净”但这个数据集的3个类别需要仔细看face、drink、phone。从场景角度理解这是在人群或办公环境中把人脸、喝水动作和使用手机三个要素分开标注。为什么要这样设计因为“玩手机”这件事本身是有歧义的——手里拿着手机贴耳边是打电话低头看屏幕是玩手机这两种行为的检测策略完全不同。把face单独标出来就可以利用人脸位置和手机框的位置关系去做行为推理比如手机框和人脸框高度重叠那是打电话手机框在躯干下方且人脸朝下那就是低头玩手机。这类多类别标注在训练时有一个常见误区直接把phone当唯一类别训练忽略face和drink。我不建议这么做因为drink和phone在视觉特征上存在相似性——手部靠近面部、手部持物如果强行合并模型会把喝水和打电话混淆。这个数据集既然把三个类别分开就说明设计者已经考虑到这种混淆风险。训练时如果只想做二分类也应该保留三个类别的原始标注在推理阶段再做逻辑合并而不是在数据层面删标注。3. 数据划分与目录结构为什么说直接能训练3.1 data目录下的train和test双分支结构这个数据集最省事的地方就是作者已经把训练集和测试集物理分好了。按照描述data目录下分为train和test两个子目录每个子目录下又有images和labels两个子文件夹。结构上的直观理解是这样的data/ ├── train/ │ ├── images/ │ │ ├── ph860.jpg │ │ ├── ph649.jpg │ │ └── ... │ └── labels/ │ ├── ph860.xml │ ├── ph649.xml │ └── ... └── test/ ├── images/ │ ├── ph1269.jpg │ └── ... └── labels/ ├── ph1269.xml └── ...训练集1090张图和1090个xml测试集272张图和272个xml文件数完全对得上。这种物理划分方式比“一个总目录加一个split.txt”的老式做法要省心得多——你不需要写脚本去split也不会出现在训练阶段误读测试文件的问题。我自己在训练时有个习惯拿到这种已划分的数据集第一件事不是急着开训而是核对两个数字训练集图片数是否等于xml数测试集是否也相等。如果两边数量不等多半是有损坏文件或者标注缺失。这个数据集两边数量一致说明基础数据完整性没问题。另外需要注意这里没有单独的验证集我的经验做法是从训练集里按0.1比例随机抽一批做验证剩下的再进训练别拿测试集调参否则最后的评估数字就不干净了。3.2 JSON字典文件类别名和索引的映射数据配套的json文件本质是一个3类别的字典文件负责把类别名映射到数字索引。这在后面要转换成YOLO格式或COCO格式时特别关键。通常内容类似这样{ face: 0, drink: 1, phone: 2 }索引顺序不是固定的但一旦确定就必须在整个训练流程中保持一致。这个json的价值在于当你把VOC格式转成其他格式时类别映射直接从这里读不用每次手工敲字典。尤其是如果后续要换模型框架比如从PaddleDetection切到YOLOv8类别顺序不一致会导致训练出来的模型在推理时标签全部错位这是特别隐蔽的坑——损失函数还在收敛但mAP永远上不去。我一般会写一行代码在训练前加载这个json打印一下类别顺序确认和模型配置文件里的names一致再开训。4. 可视化脚本验证数据随机传一张图边界框直接画出来4.1 脚本的运行方式和原理这个数据集附带的可视化py文件定位特别清晰不用改任何配置随机传一张图片路径就能把xml里的边界框绘制到图上并把结果保存在当前目录。常用于数据质量抽检。我拿到数据后第一步就是这个脚本先画个20张图看看标注是否准确。其核心逻辑通常是这样import cv2 import xml.etree.ElementTree as ET import random import os def draw_box(image_path, xml_path, output_path): img cv2.imread(image_path) tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) if name phone: color (0, 0, 255) elif name face: color (0, 255, 0) else: color (255, 0, 0) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) cv2.putText(img, name, (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(output_path, img) img_list [f for f in os.listdir(images) if f.endswith(.jpg)] sample random.choice(img_list) draw_box( os.path.join(images, sample), os.path.join(labels, sample.replace(.jpg, .xml)), visual_ sample )这段代码的逻辑就是遍历xml的所有object节点解析类别名和边界框坐标用不同颜色框出不同类别然后用cv2.rectangle画框、cv2.putText写类别名。其中random.choice负责随机抽图所以每次运行看的都是不同样本。注意sample.replace(.jpg, .xml)这种写法有个前提xml和图片文件名必须完全一致只是后缀不同。这就是这类数据集目录设计的意义——按约定后缀替换就能轻松配对。4.2 从输出图判断标注质量的三个判读点画完框之后别急着删要会看图。我总结了三个判读点一、看边界框是否贴合目标边缘。框比目标大一圈问题不大但如果框只框住了手机的一半或者把周围人的手也包进去了这种标注就是噪声需要留意后续训练是否受影响。二、看是否有漏标。一张图里如果明显有一个人低头看手机但xml里完全没有对应标注那这个数据就是“脏”的。少量漏标可以接受多了就得清洗掉否则等于在训练时告诉模型“这个场景不用检测”。三、看类别是否标错。比如phone和drink互换这在视觉上很难发现但在推理阶段影响很大。这个数据集的出图如果让我连续看20张没有明显异常我才会放心进下一步。可视化脚本本质上是把“数据是否可靠”这个问题从黑匣子里拽出来直接转化成肉眼可判断的形式。这也是我一直强调的任何数据到手先可视化再谈训练。5. 图片与XML对不上、类别大小写不一致五个常见坑与排查办法5.1 图片和XML文件名对不上现象训练脚本报错FileNotFoundError或者某张图片找不到对应的xml文件。原因最常见的就两个一是原始数据里带有多余的文件比如Thumbs.db、隐藏的MAC元数据二是在复制或解压过程中文件名被截断。这类问题在从网盘下载的数据集中反复出现。解决先遍历两个目录做一次文件名交集比对。import os img_dir train/images xml_dir train/labels img_names {f.split(.)[0] for f in os.listdir(img_dir)} xml_names {f.split(.)[0] for f in os.listdir(xml_dir)} print(仅图片有, img_names - xml_names) print(仅xml有, xml_names - img_names)如果差集不为空说明有问题。处理方式是删除多余文件而不是补齐。因为补齐意味着你可能需要一个并不存在的标注。5.2 类别名大小写不一致现象训练时发现Phone和phone被当成两个类别导致类别数从3变成4。原因标注工具或人工编辑时引入的大小写不一致。VOC格式对类别名是敏感的find(name).text读出什么就是什么模型端不会帮你归一化。解决在写数据加载脚本时强制统一类别名到小写。name obj.find(name).text.strip().lower()这个.strip().lower()就是后悔药。加上之后大小写问题一次性解决另外也能顺带去掉某些标注工具在类别名后面留下的空格。5.3 边界框坐标越界现象可视化时发现某个框画出去了或者训练时报assert错误说xmin width。原因标注时手滑或者图像通道裁剪后xml没有同步更新。坐标值超出了图像的实际宽高这在低分辨率图上尤其容易出现因为300-400的图上手抖一两个像素就出界了。解决写一个吸附函数把坐标夹紧到图像范围内。xmin max(0, min(xmin, width - 1)) xmax max(0, min(xmax, width - 1)) ymin max(0, min(ymin, height - 1)) ymax max(0, min(ymax, height - 1))注意千万别直接丢弃越界样本如果只是超出几个像素夹紧即可。如果框完全脱离图像内容比如坐标是负数而且面积很大那直接删除这个标注更安全。5.4 训练测试数据信息泄漏现象训练集表现惊艳测试集mAP却惨不忍睹怀疑数据有问题。原因有人会把同一张图既放进训练集又放进测试集也可能测试集中混入了训练样本的强相似帧。这个数据集虽然物理分开但不能保证数据集中没有近似重复帧尤其是连续拍摄的监控场景。解决做一个简单的重复度检查用图像哈希对比测试集和训练集的相似度。import cv2 import numpy as np def dhash(img, size8): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) resized cv2.resize(gray, (size 1, size)) diff resized[:, 1:] resized[:, :-1] return sum(1 i for i, v in enumerate(diff.flatten()) if v) def hamming(a, b): return bin(a ^ b).count(1)两张图的哈希汉明距离小于5就视为相似。如果发现测试集和训练集中有大量相似对就要考虑手动剔除测试集中的相似帧否则你测出来的mAP是虚高的。5.5 灰度图和损坏图片混入现象训练到一半爆出cv2.error或者loss突然跳成NaN。原因有些数据在采集或传输过程中损坏图像已经无法解码也有可能是少量灰度图混入虽然cv2能读但通道数不一致导致预处理batch报错。解决批量做一次图像完整性检查。import cv2 import os for f in os.listdir(train/images): img cv2.imread(os.path.join(train/images, f)) if img is None: print(损坏文件, f) elif img.ndim ! 3: print(非RGB图像, f)这一步放在训练之前跑一遍5分钟换你训练流程的稳定非常值得。6. 迁移到YOLO格式把VOC的XML转成TXT标签的实战脚本6.1 XML转TXT的核心代码如果你要用YOLOv8或YOLOv5训练这个数据集就用得到这一步。YOLO格式和VOC最大的区别是坐标从绝对像素值变成归一化后的中心点加宽高类别从名字变成索引。转换脚本如下import os import xml.etree.ElementTree as ET class_dict {face: 0, drink: 1, phone: 2} input_dir train/labels output_dir train/labels_yolo os.makedirs(output_dir, exist_okTrue) def convert(xml_file, out_file): tree ET.parse(xml_file) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip().lower() if name not in class_dict: continue cls_id class_dict[name] box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height w (xmax - xmin) / width h (ymax - ymin) / height lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_file, w) as f: f.write(\n.join(lines)) for f in os.listdir(input_dir): convert( os.path.join(input_dir, f), os.path.join(output_dir, f.replace(.xml, .txt)) )这段转换逻辑里最容易写错的就是归一化分母——必须用xml里size节点的宽高而不能用你“以为”的图片宽高。如果xml里的size和实际图片尺寸不一致归一化后框就全偏了。另外注意类别索引和模型配置文件里的names顺序必须一致这也是我之前专门提到json文件的原因。6.2 转换后的验证方法转换完不是直接开训先验证再训练。最有效的验证方式是随机抽一张原图和对应的txt把归一化坐标重新映射回像素值画框对比。import cv2 import random import os img_file random.choice(os.listdir(train/images)) txt_file os.path.join(train/labels_yolo, img_file.replace(.jpg, .txt)) img cv2.imread(os.path.join(train/images, img_file)) h, w img.shape[:2] with open(txt_file) as f: for line in f: cls_id, xc, yc, bw, bh map(float, line.strip().split()) xmin int((xc - bw / 2) * w) ymin int((yc - bh / 2) * h) xmax int((xc bw / 2) * w) ymax int((yc bh / 2) * h) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, str(int(cls_id)), (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(verify.jpg, img)如果画出来的框和之前VOC可视化的一致说明转换没问题可以放心去写模型的data yaml。从那以后我每次做格式转换都强制走一遍“转前可视化原xml → 转后可视化txt”的双重比对不为别的就为不让这种低级错误浪费一整天的训练时间。这个数据集本身已经整理得很干净剩下的就是你自己在格式转换和数据校验上多一份耐心。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →