ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

玩手机检测数据集:从VOC格式到YOLO训练的完整指南

玩手机检测数据集:从VOC格式到YOLO训练的完整指南 简介面向目标检测学习与研究人员的“玩手机检测”数据集采用VOC格式的xml标注文件已经完成训练集与测试集划分可直接用于YOLO、SSD、Faster R-CNN等常见检测框架的训练与验证。数据共包含3个类别face、drink、phone图像为分辨率300-400的RGB图片场景为人群玩手机检测贴近实际安防与行为分析需求。压缩包共2000个文件体积约54MB其中包含1362个xml标注文件、636张jpg图片以及1个可视化脚本和1个类别字典json文件目录下按train/test划分images和labels子文件夹结构规范无需额外处理即可投入训练。目前已有480人学习下载。资源另附可直接运行的Python可视化脚本随机传入一张图片即可绘制边界框并保存结果便于快速检查标注质量对初学者理解VOC标注格式和开展检测实验都很实用。1. 玩手机检测比想象中更需要一份干净的VOC数据集玩手机检测在工位安全、驾驶行为分析、课堂管理等场景里几乎是标配需求但把目标检测模型跑起来并不难难的是标注数据本身。一份把图像、VOC标注格式xml、训练集与测试集划分都准备好的玩手机检测数据集意味着你不需要从零收集图片、不需要自己画框可以直接进入模型训练和迭代。这也是它在标题里被反复强调的原因给的不是“一张图一个标注”而是一套能直接喂给训练管线的完整工程。VOCPASCAL VOC格式到今天仍被广泛使用是因为它把标注信息以xml方式组织尺寸、类别、坐标一目了然人类可读、脚本可解析几乎所有目标检测框架都提供从VOC到自有训练格式的适配。标题里这份数据集已经完成train/test划分省下的不只是标注工时还有划分时最容易踩的泄漏陷阱。下面我按自己拿到类似数据集时会做的步骤展开先盘目录和xml内容再做一次标注体检验证划分是否合理最后转成YOLO训练格式并在训练前做一轮可视化抽查。2. 玩手机检测数据集的VOC目录与xml文件结构2.1 一个标准VOC数据集从目录结构开始拿到一份玩手机检测数据集先别急着解压后直接开始训练。标准做法是先看目录确认图像、标注、划分文件三者是否存在且能对上。一个典型的VOC数据集通常由三个目录和一个Main子目录组成路径内容说明JPEGImages/原始图像jpg/png文件名即图像唯一标识Annotations/与图像同名的xml标注文件VOC格式保存类别和坐标ImageSets/Main/train.txt / val.txt / test.txt每行一个文件名不带扩展名labels/ 或 images/可选部分数据集会附带已转换的YOLO格式这种组织的价值在于图像和标注分离通过txt文件索引来实现训练集、测试集切换不需要复制粘贴图像文件。一套JPEGImages可以同时支持多个划分方案只要ImageSets/Main下多放几个txt即可。看目录时我先执行一个快速命令确认图像与xml数量是否一致ls JPEGImages | wc -l ls Annotations | wc -l正常情况下两侧数字应完全相等。如果不一致说明有图像缺少标注或有标注缺少图像这类数据进入训练会直接影响loss计算和验证指标。2.2 xml文件里到底存了什么展开一个Annotations下的xml文件能看到完整标注信息annotation folderJPEGImages/folder filenameimg_20240315_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namephone/name difficult0/difficult bndbox xmin842/xmin ymin331/ymin xmax1037/xmax ymax514/ymax /bndbox /object /annotation这里最值得关注的是size和bndbox两个字段。size记录原始图像的物理宽高是后续做归一化坐标的基础bndbox是绝对像素坐标VOC标准定义左上角(xmin, ymin)、右下角(xmax, ymax)。类别名在name中玩手机检测数据集里通常固定为phone、cellphone或handphone但也可能出现不一致后面会专门检测。difficult是一个容易忽略的字段。在PASCAL VOC评价体系里difficult为1的目标在计算mAP时会被跳过因为样本本身难以辨认。对玩手机检测来说遮挡严重、距离太远、仅有半边手机的目标都可能被标为difficult训练时建议保留评估时先搞清楚框架是否自动过滤。2.3 ImageSets/Main下的划分文件怎么用ImageSets/Main里放的是纯文本文件每行一个文件名且不带扩展名例如train.txt内容可能是img_20240315_001 img_20240315_002 img_20240315_003这段索引的作用是把JPEGImages与Annotations映射到训练集或测试集。脚本读取时不需要拼接路径只要用文件名去两个目录里查找对应文件即可。我通常先用一段Python查看划分文件的行数和内容头确认格式没有异常from pathlib import Path main_dir Path(ImageSets/Main) for txt in main_dir.glob(*.txt): lines txt.read_text().strip().splitlines() print(f{txt.name}: {len(lines)} 条记录) print(lines[:3])这里先列出每个txt的文件数再打印前几行用来验证文件不是空的、没有重复行、扩展名确实被去掉。注意有些数据集会把train.txt和val.txt合并成一个trainval.txt训练时需要自己拼接。3. 玩手机检测xml解析与标注体检动手前先扫一遍数据3.1 一个最小可用的xml解析脚本VOC的xml结构规整用Python标准库xml.etree.ElementTree就能解析不需要额外依赖。先写一个脚本统计整个Annotations目录下的目标数量、类别和图片尺寸分布import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter ann_dir Path(Annotations) class_counter Counter() size_counter Counter() total_boxes 0 for xml_file in ann_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) size_counter[(width, height)] 1 for obj in root.findall(object): name obj.find(name).text class_counter[name] 1 total_boxes 1 print(类别统计:, class_counter) print(图片尺寸分布:, size_counter) print(总标注框数:, total_boxes)用ET.parse读入xml树root.find(size)定位到尺寸节点再逐层取文本。root.findall(object)会返回该xml下所有目标框包括同一张图里的多个手机。如果发现size_counter里出现两种以上分辨率后续归一化转换时就必须以每张图自己的宽高为基准不能拿统一值去套。对比使用findall和直接遍历子节点的差别findall(object)只找直接子节点VOC结构中object位于根节点下这样写最准确不要用iter(object)它会递归查找虽然在VOC里结果相同但遇到结构变体时可能误匹配。3.2 标注统计里最常见的坑类别名不一致VOC数据集常由多人标注或多批数据合并而成同一类目标可能被标成phone、cellphone、mobile phone甚至Phone大写开头。目标检测框架通常按类别名的字符串做映射拼写不一致会直接导致类别数量倍增训练时把同一类物体当成多个类学。运行上面的统计脚本后如果class_counter里出现多个高相似度名称需要统一。下面这段代码把常见变体归一化为phoneimport xml.etree.ElementTree as ET from pathlib import Path name_map {cellphone: phone, mobile: phone, handphone: phone} for xml_file in Path(Annotations).glob(*.xml): tree ET.parse(xml_file) root tree.getroot() changed False for obj in root.findall(object): name obj.find(name).text if name in name_map: obj.find(name).text name_map[name] changed True if changed: tree.write(xml_file, encodingutf-8, xml_declarationTrue)name_map字典定义了原始类别名到标准类别名的映射tree.write会覆盖原文件执行前建议备份整个Annotations目录。注意xml声明格式原始VOC文件通常不带?xml version1.0?写入时强行加声明对多数解析器没有影响但如果后续用旧版工具处理建议先拿一个文件测试。3.3 xmin/ymin/xmax/ymax 边界值体检坐标越界是VOC数据集的常见问题越界通常是因为标注工具限制、旋转框转换错误或后期裁剪图像时没有同步坐标。越界的框在训练时可能产生负数宽高导致loss变成NaN或导致增强阶段计算目标区域失败。写一个扫描脚本找出所有不满足条件的标注框import xml.etree.ElementTree as ET from pathlib import Path error_list [] for xml_file in Path(Annotations).glob(*.xml): tree ET.parse(xml_file) root tree.getroot() img_name root.find(filename).text size_node root.find(size) img_w int(size_node.find(width).text) img_h int(size_node.find(height).text) for obj in root.findall(object): 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 xmin 0 or ymin 0 or xmax img_w or ymax img_h: error_list.append((img_name, xmin, ymin, xmax, ymax)) if xmax xmin or ymax ymin: error_list.append((img_name, 非正宽高, xmin, ymin, xmax, ymax)) print(异常框数量:, len(error_list)) for e in error_list[:20]: print(e)对越界框合理的处理方式是clip到图像边界即把xmin小于0的置为0、xmax大于宽度的置为宽度而不是删除整张图。删除会减少数据量而且如果一张图里同时有正常框和越界框删除整图等于丢失了一部分有效标注。3.4 手机目标的大小分布值得先算一遍玩手机检测的特殊性在于手机在画面中的尺寸差异极大。驾驶场景里手机可能占据几十个像素课堂场景里可能占到画面五分之一。模型对尺度很敏感训练前统计框面积占比可以帮助决定是否使用多尺度训练、是否加入mosaic增强、是否单独处理小目标。写一个统计脚本打印面积占比的中位数和分位数import xml.etree.ElementTree as ET from pathlib import Path import numpy as np ratios [] for xml_file in Path(Annotations).glob(*.xml): root ET.parse(xml_file).getroot() size_node root.find(size) area_img int(size_node.find(width).text) * int(size_node.find(height).text) for obj in root.findall(object): box obj.find(bndbox) w int(box.find(xmax).text) - int(box.find(xmin).text) h int(box.find(ymax).text) - int(box.find(ymin).text) ratios.append(w * h / area_img) print(中位数占比: {:.4f}.format(np.median(ratios))) print(10%分位: {:.4f}.format(np.percentile(ratios, 10))) print(90%分位: {:.4f}.format(np.percentile(ratios, 90)))如果10%分位低于0.01说明有大量小目标。此时YOLO系列默认的输入尺寸可能不够需要在上采样或训练分辨率上做调整或者使用支持小目标的模型变体。反过来如果90%分位接近0.5说明存在近景大目标这会影响anchor设置值得在训练前记录下来。4. 训练集与测试集划分为什么“已经划分”反而要先验证4.1 划分的价值不只在比例一份已经划分好训练集和测试集的数据集真正的价值不在于省去你执行random.shuffle的那几行代码而在于避免数据泄漏。数据泄漏在目标检测里有三种常见形态同一张图片同时出现在训练集和测试集高度相似的图像连拍帧、同一场景不同角度被切到两侧标注框在划分时被复制粘贴导致计数错乱。无论哪一种都会让验证指标虚高模型上线后表现远差于预期。因此拿到已经有划分的玩手机检测数据集第一步不是信任它而是验证它。验证分三个层次文件名集合是否与xml对应、训练集与测试集是否有交集、类别分布是否一致。4.2 检查划分文件与xml/JPEGImages是否对得上先写一段代码把ImageSets/Main里的txt与Annotations目录里的xml做集合比较from pathlib import Path ann_names {p.stem for p in Path(Annotations).glob(*.xml)} img_names {p.stem for p in Path(JPEGImages).glob(*.jpg)} for split_file in [train.txt, test.txt, val.txt]: path Path(ImageSets/Main) / split_file if not path.exists(): print(f{split_file} 不存在) continue split_names set(path.read_text().strip().splitlines()) missing_xml split_names - ann_names missing_img split_names - img_names print(f{split_file}: {len(split_names)} 条, 缺xml: {len(missing_xml)}, 缺图片: {len(missing_img)})set操作是这里的关键差集可以直接找出引用了不存在的xml或图像的记录。如果missing_xml非空说明划分文件引用了坏数据如果missing_img非空说明图像文件缺失。任何一个集合非空都建议修复后再训练否则训练中会遇到FileNotFoundError或静默跳过某张图的情况。4.3 关键一查训练集与测试集是否有交集标题明确说已经做了训练集和测试集划分大多数情况下划分是互斥的但实践中我见过多次因为划分脚本生成逻辑有误导致两边出现重叠文件名的情况。检查方法很简单from pathlib import Path def read_split(name): path Path(ImageSets/Main) / name return set(path.read_text().strip().splitlines()) if path.exists() else set() train_set read_split(train.txt) test_set read_split(test.txt) val_set read_split(val.txt) overlap train_set test_set if overlap: print(train与test重叠:, len(overlap)) print(list(overlap)[:10]) if val_set: overlap2 train_set val_set print(train与val重叠:, len(overlap2))这里使用集合求交集运算符效率远高于两层for循环几万条数据瞬间完成。发现重叠时最稳妥的做法是从测试集中移除重叠项而不是从训练集中移除因为训练集删图可能连带影响类别均衡。还要注意一种特殊形态的泄漏文件名不同、内容相同。这种情况在从视频抽帧生成的数据集里很常见比如train.txt里的frame_0012.jpg和test.txt里的frame_0013.jpg是相邻两帧画面高度相似。文件名交集检查发现不了需要图像去重这个放到第6章专门处理。4.4 验证训练集和测试集的目标数量分布分布一致性影响的是mAP的可信度。如果训练集里手机目标占图像面积的中位数是0.05而测试集里是0.3那么即便测试指标优秀也无法说明模型在小目标场景真实可用。统计每个划分集合中每张图片的目标数量然后对比from pathlib import Path import xml.etree.ElementTree as ET def count_boxes_for_split(split_names): counts [] for name in split_names: xml_path Path(Annotations) / f{name}.xml if not xml_path.exists(): continue root ET.parse(xml_path).getroot() counts.append(len(root.findall(object))) return counts train_counts count_boxes_for_split(train_set) test_counts count_boxes_for_split(test_set) import numpy as np print(train 每图目标数均值:, np.mean(train_counts), 中位数:, np.median(train_counts)) print(test 每图目标数均值:, np.mean(test_counts), 中位数:, np.median(test_counts))如果两者差异超过20%说明划分不均衡。此时我一般不会直接重新划分整个数据集而是先检查ImageSets/Main下是否还有val.txt或trainval.txt可以用。很多数据集提供的是三份划分train和val用于训练阶段test单独留作最终评估标题只说做了训练集和测试集划分可能是省略了val。这种情况下我会从train.txt里再切出约10%作为验证集。shuf -n $(( $(wc -l train.txt) / 10 )) train.txt val.txt comm -23 (sort train.txt) (sort val.txt) train_new.txtshuf随机抽取十分之一行写入val.txtcomm按排序结果计算出train_new.txt两条命令配合使用不产生重复记录。shuf生成的val.txt是随机的如果把可复现性看得比较重可以在运行前用export RANDOM42固定随机种子。5. 把玩手机检测VOC数据集转成YOLO训练格式5.1 为什么YOLO系列不直接吃xmlyolov5、yolov8训练自己的数据集标准标注格式是每张图对应一个txt文件每一行记录一个目标class_id x_center y_center width height坐标全部归一化到0到1之间。VOC的xml用绝对像素坐标且一张图的所有目标集中在一个xml里两种格式差异明显必须转换。VOC格式为什么没有成为训练通用格式原因是训练框架通常把图像解码、坐标映射、增强绑定在一起txt格式便于在dataloader里直接读取和做归一化不需要在每次迭代解析xml再换算坐标。对玩手机检测这种单类别数据集来说转换成本很低转换一次以后不用再动。5.2 完整的VOC转YOLO标注脚本下面这个脚本可以直接运行它会根据ImageSets/Main中的划分文件生成YOLO格式的标签文件和目录from pathlib import Path import xml.etree.ElementTree as ET import random random.seed(42) CLASS_NAMES [phone] # 必须与数据集类别完全一致 output_root Path(yolo_dataset) def get_images_from_split(split_path): return split_path.read_text().strip().splitlines() if split_path.exists() else [] def convert_annotation(xml_path, output_label_path, img_w, img_h): root ET.parse(xml_path).getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_NAMES: continue class_id CLASS_NAMES.index(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) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) output_label_path.write_text(\n.join(lines)) for split_name in [train, val, test]: split_file Path(ImageSets/Main) / f{split_name}.txt img_names get_images_from_split(split_file) if not img_names: continue out_img_dir output_root / images / split_name out_label_dir output_root / labels / split_name out_img_dir.mkdir(parentsTrue, exist_okTrue) out_label_dir.mkdir(parentsTrue, exist_okTrue) for name in img_names: xml_path Path(Annotations) / f{name}.xml img_path Path(JPEGImages) / f{name}.jpg if not xml_path.exists() or not img_path.exists(): print(f跳过缺失文件: {name}) continue root ET.parse(xml_path).getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) convert_annotation(xml_path, out_label_dir / f{name}.txt, img_w, img_h) # 这里用符号链接而不是复制避免浪费磁盘空间 (out_img_dir / f{name}.jpg).symlink_to(img_path.resolve()) print(转换完成)转换逻辑分三段get_images_from_split读取划分文件convert_annotation解析xml并归一化坐标主循环为每个划分建立独立目录并写入标签。归一化公式是中心点坐标除以图像宽高得到的是0到1之间的小数保存6位小数足够训练使用不需要更高精度。CLASS_NAMES [phone]是唯一需要根据实际数据集调整的地方。如果第3章统计出类别名是cellphone这里必须改成一致并且位置顺序决定class_id编号。YOLO的class_id从0开始编号如果数据集有多个类别顺序错一个整个训练就乱了。5.3 输出目录组织与data.yaml配置转换完成后目录结构如下yolo_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/训练时还需要一个数据集配置文件YOLOv8用data.yaml描述路径和类别path: /absolute/path/to/yolo_dataset train: images/train val: images/val test: images/test nc: 1 names: [phone]path建议写绝对路径写相对路径在IDE里跑和命令行跑会得到不同结果。train和val的取值是相对于path的路径不需要带完整前缀。nc是类别数量names列表里的类别要与CLASS_NAMES保持一致。5.4 转换完成后如何确认没转错转换完成不等于转换正确。最直接的验证方法是对比一个样本的xml与生成的txtcat Annotations/img_20240315_001.xml cat yolo_dataset/labels/train/img_20240315_001.txt手动验算一个框假设图像宽度1920xmin为842、xmax为1037中心点x为(8421037)/2939.5归一化后为939.5/1920≈0.4893检查生成的txt第一行第三个字段是否接近这个值。也可以写一段反向验证代码找出所有生成标签中坐标大于1或小于0的行这类行必然说明归一化基准或宽高取错了。6. 训练前最后一步可视化抽查与图像去重6.1 三行脚本在图上画框指标统计再完整也不如亲眼看几个框位置准。写一个快速可视化脚本从测试集里抽样画框import cv2 import xml.etree.ElementTree as ET from pathlib import Path sample_names list(Path(ImageSets/Main).joinpath(test.txt).read_text().strip().splitlines())[:5] for name in sample_names: img cv2.imread(fJPEGImages/{name}.jpg) root ET.parse(fAnnotations/{name}.xml).getroot() for obj in root.findall(object): 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) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.imwrite(fcheck_{name}.jpg, img)检查重点是框是否贴住手机边缘、是否漏掉被遮挡的机身、是否把类似手机外形的物体误标。边框过大过小在指标上不一定反应得出来但视觉上非常直观。6.2 技巧用感知哈希检查跨集重复图像文件名不同的重复或近似图像是VOC数据集中最难发现的泄漏。对玩手机检测数据来说从同一段监控视频连续抽帧的可能性非常高相邻帧之间人眼几乎看不出差别模型却可能把它们当作相同样本学习。检查方法是计算每张图像的感知哈希pHash再比较测试集和训练集之间的哈希距离。import cv2 def phash(img, hash_size8): resized cv2.resize(img, (hash_size * 4, hash_size * 4)) gray cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) avg gray.mean() return .join([1 if pixel avg else 0 for row in gray for pixel in row]) train_hashes {} for name in train_set: img cv2.imread(fJPEGImages/{name}.jpg) train_hashes[name] phash(img) for name in test_set: img cv2.imread(fJPEGImages/{name}.jpg) hash_test phash(img) for train_name, hash_train in train_hashes.items(): diff sum(a ! b for a, b in zip(hash_test, hash_train)) if diff 8: print(f疑似重复: {name} 与 {train_name}, 汉明距离 {diff})感知哈希把图像压缩为固定长度的二进制串汉明距离越小说明图像越相似。diff 8阈值对64位哈希来说已经足够宽松能区分出轻微旋转和亮度变化。对几万张图做全量两两比较会比较慢可以先用glob把图像读入内存再使用numpy批量计算哈希距离加速。6.3 训练前的最后一条验证命令完成转换和检查后确认标签文件与xml数量一致find yolo_dataset/labels -name *.txt | wc -l ls Annotations | wc -l两侧数字相等后即可启动训练YOLOv8的命令示例yolo detect train datayolo_dataset/data.yaml modelyolov8s.pt epochs100 batch16 imgsz640batch-size按显存从上到下试一圈imgsz优先保持640不变等到第一轮loss收敛稳定后再考虑调整。本文还有配套的精品资源点击获取
返回列表