首页 / 资讯中心 / 文章详情

VOC摩托车电动车数据集5424张:从标注解析到YOLO训练实战

VOC摩托车电动车数据集5424张:从标注解析到YOLO训练实战 ★ FEATURED ARTICLE
简介面向目标检测与深度学习入门者的摩托车识别专用数据集采用Pascal VOC标准格式全部由jpg原图与对应xml标注文件成对构成。数据源自园区闸口进出方向实拍接近真实安防与交通管理场景共5424张图片、5424个标注文件边界框总量6261个类别统一为motorcycle使用labelImg完成矩形框勾画标注规范可直接用于训练YOLO、SSD等主流检测模型。资源压缩包约916.43MB含2000个文件条目主要文件类型为jpg原始图像、对应xml标注文件及1个说明txt其中xml完整保留目标类别与边界框坐标txt用于核对使用前注意事项目录按采集顺序组织便于筛选与二次整理。数据集类别单一而标注数量充足适合搭建基准实验、验证检测算法或开展园区出入口摩托车流量统计等预研任务。已有623人学习浏览对缺乏自采数据的研究者而言是一份可直接落地的基础数据资产。1. “VOC正版摩托车电动车数据集5424张”到底是什么先回答三个最实际的问题做城市交通或园区安防检测的同行多半遇过这类尴尬跑通用检测模型行人、汽车识别得很稳一到两轮车就现原形——摩托车和电动车混在一起共享单车乱入夜间灯影里更是分不清谁是谁。这套“VOC正版摩托车电动车数据集5424张”解决的就是这一类问题数据按照 VOC 格式组织聚焦摩托车、电动车两个类别共5424张实拍图标注字段与 PASCAL VOC 原版保持一致下载下来不需要清洗目录结构就能直接进入标注解析环节。它的价值在于规模和格式都卡在“能直接用”的位置这个量级足够训练一个可用的两轮车检测模型又不至于像大规模数据集那样要花大量时间做数据管理。它适合两类人一是准备用 YOLOv8/YOLOv5 训练自己检测模型的从业者二是在校学生需要一个结构干净的 VOC 数据集做课程设计或毕设。2. 先看货再动手VOC目录结构、xml标注与类别分布盘点拿到数据集先别急着开训。我习惯把目录结构和标注文件完整过一遍因为“正版”在数据集圈子里意味着标注格式规整但规整不等于类别均衡、不等于没有脏数据。用二轮车数据做过训练的人都有体会摩托车和电动车是一对天生难兄难弟外形相似、遮挡重叠、角度多变数据质量直接决定模型上限而不是训练参数。我在接手这类 VOC 格式数据时一般会按“看目录 → 读 xml → 跑统计”三步走每一步都能提前暴露问题。这一章把这三步逐一展开最后附一个可以直接跑的类别分布盘点脚本。2.1 JPEGImages与Annotations一张图对应一个xml的硬约束VOC 格式的目录结构是固定的这套数据集既然标称“正版”目录组织应当与早期 PASCAL VOC 竞赛保持一致。常见的目录布局如下dataset/ ├── JPEGImages/ # 原图jpeg 格式文件名与 xml 严格同名 │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... ├── Annotations/ # 每张图对应一个 xml 标注 │ ├── 0001.xml │ ├── 0002.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt # 训练集图片名清单 ├── val.txt # 验证集图片名清单 └── test.txt # 测试集图片名清单JPEGImages 与 Annotations 通过文件名一一对应这是 VOC 格式的硬约束。xml 文件名必须和图片文件名完全一致差一个字符都会在后续划分数据集时静默丢失样本。ImageSets/Main 下的 txt 文件每行写一个不带扩展名的图片名用来区分训练、验证、测试三份清单PASCAL VOC 2007/2012 原版就是这么组织的。看这个目录结构时有个小技巧先统计 JPEGImages 和 Annotations 两个目录的文件数是否一致。如果不一致说明源头数据集存在“有图无标注”或“有标注无图”的情况这种数据集就算格式再正版训练前也必须做一次差集清理。2.2 读懂xmlbndbox坐标、difficult标记与name字段随便打开一个 xml 文件内容长这样annotation folderJPEGImages/folder filename0042.jpg/filename size width1280/width height720/height depth3/depth /size object namemotorcycle/name difficult0/difficult bndbox xmin310/xmin ymin180/ymin xmax612/xmax ymax530/ymax /bndbox /object /annotation三个关键字段必须看懂。第一个是size里的 width 和 height这是后续把绝对像素坐标换算成 YOLO 归一化坐标的分母写转换脚本时最容易被忽略的就是这里——有人直接拿图片实际尺寸去算却没注意到 xml 里记录的尺寸和图片真实尺寸不一致这类脏数据一旦混入生成的归一化坐标会系统性偏移。第二个是bndbox它记录的是左上角 (xmin, ymin) 到右下角 (xmax, ymax) 的绝对像素坐标不是中心点加宽高转 YOLO 时要用(xmin xmax) / 2这类公式换算。第三个是name和difficult。name 是类别字符串必须和既定类别清单严格一致“正版”VOC 即便标错类别也不会出现“motorcycle1”“电瓶车”这种别名。difficult 标记是老版 VOC 用来区分难易样本的值为 1 表示该目标极难辨认训练时常规做法是过滤掉。2.3 用一段脚本盘点类别分布二轮车数据最容易踩“类别失衡”的雷import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter ann_dir Path(Annotations) class_img_cnt Counter() # 每个类别出现在多少张图中 class_box_cnt Counter() # 每个类别一共有多少个框 total_boxes 0 for xml_path in ann_dir.glob(*.xml): tree ET.parse(xml_path) root tree.getroot() seen_in_this_img set() for obj in root.findall(object): name obj.findtext(name).strip() class_img_cnt[name] 1 class_box_cnt[name] 1 seen_in_this_img.add(name) total_boxes 1 print(类别 - 出现图片数 / 总框数) for name in class_box_cnt: print(f{name}: {class_img_cnt.get(name, 0):4d} imgs / {class_box_cnt[name]:4d} boxes) print(总框数:, total_boxes)这段脚本用 xml.etree.ElementTree 解析所有 Annotations 下的 xml统计每个类别出现的图片数和边界框总数。跑完之后重点看两件事第一两个类别的框数是否接近 1:1。如果摩托车 4000 框、电动车只有 1500 框训练出来的模型会明显偏向摩托车电动车类别的 mAP 会低一截。第二name 字段是否出现了计划外的值。如果打印结果里混进了“bicycle”“tricycle”之类的额外类别说明源头数据里混入了其他车型需要先做一次筛选而不是直接开训。我还习惯顺手统计平均每张图的目标数。对于两轮车数据集平均每图 12 个框是正常的如果平均每图到 4 个以上往往意味着图片密集场景多这类场景下摩托车和电动车的互相遮挡会非常严重标注质量更难保证训练时需要重点观察小目标和中度遮挡目标的召回率。3. 把VOC转成YOLO格式转换脚本与四个边界坑VOC 格式本身不直接用于 YOLO 训练这是新手最容易卡住的第一道门槛。很多人拿到数据集第一反应是直接把.xml改成.txt这是典型的翻车操作。VOC 和 YOLO 的标注格式在数学上完全不同必须写脚本做坐标换算。3.1 为什么非要转YOLO格式TXT归一化坐标与类别索引YOLO 系列训练时读取的标注文件是.txt文本每一行代表一个目标格式为class_id x_center y_center width height其中坐标全部是归一化数值范围在 0 到 1 之间相对于图片宽高计算。而 VOC 的 xml 记录的是绝对像素坐标且表达方式是左上角和右下角。两者差异不是文件后缀的区别而是坐标系和表达方式的根本差异。另外COCO 数据集用的是 JSON 格式一条标注嵌在多层嵌套的字典结构里和 VOC、YOLO 又是另一套逻辑——如果后续要在 COCO 格式和 VOC 格式之间迁移也需要单独做一轮转换。为什么要归一化核心原因是不同分辨率的图片可以共用同一份标注。一张 1920×1080 和一张 640×480 的图如果标注文件里写的是绝对像素坐标换分辨率就要重写标注归一化之后坐标是相对比例缩放图片不影响标注有效性。YOLO 训练时对输入图片做了 letterbox 缩放归一化坐标天然适配这个流程这也是 YOLO 全家桶都坚持用 txt 的原因。3.2 voc_to_yolo.py单脚本完成比例换算与文件落盘下面这个脚本是我在多个二轮车数据集上反复用过的版本直接保存为voc_to_yolo.py放数据集根目录运行即可import xml.etree.ElementTree as ET from pathlib import Path import random # 类别顺序会直接决定 YOLO 的 class id必须固定写死 CLASSES [motorcycle, electric_bicycle] def convert_xml_to_yolo(xml_path, out_label_path, classes): tree ET.parse(xml_path) root tree.getroot() img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) lines [] for obj in root.findall(object): name obj.findtext(name).strip() if name not in classes: continue cls_id classes.index(name) difficult int(obj.findtext(difficult, 0)) if difficult: continue xmin float(obj.findtext(bndbox/xmin)) ymin float(obj.findtext(bndbox/ymin)) xmax float(obj.findtext(bndbox/xmax)) ymax float(obj.findtext(bndbox/ymax)) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 边界保护防止坐标越界导致训练时报警 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_label_path.write_text(\n.join(lines) \n) # 主流程以 xml 为主循环避免出现有图没标注却不知道的情况 ann_dir Path(Annotations) out_dir Path(labels) out_dir.mkdir(exist_okTrue) for xml_path in sorted(ann_dir.glob(*.xml)): label_path out_dir / (xml_path.stem .txt) convert_xml_to_yolo(xml_path, label_path, CLASSES) print(转换完成生成文件数:, len(list(out_dir.glob(*.txt))))脚本逻辑分三段。第一段定义CLASSES列表这一步看似简单实际是全局最重要的决策点YOLO 不认识字符串类别名只认索引motorcycle对应 0electric_bicycle对应 1。这个顺序一旦确定会固化到 data.yaml、训练日志、评估结果里后期改顺序等于所有标注全部作废所以必须一次性写死。第二段是单个 xml 的转换函数核心公式就四行中心点坐标取 xmin 和 xmax 的平均值再除以图宽宽高直接用绝对差值归一化。第三段主流程遍历所有 xml 生成同名 txt。参数方面有两点值得注意。一是difficult过滤这里做了continue把难例样本直接跳过因为它们在 PASCAL VOC 的评测体系里有特殊待遇而在 YOLO 训练中是干扰项。如果训练目标是追求极致鲁棒性、想把难例也纳入训练可以单独保留这些样本但初版训练建议过滤。二是小数位数用了 6 位这是 YOLO 系列的通行惯例精度足够且文件体积可控。3.3 四个边界坑归一化越界、类别索引错位、漏xml、图片和xml失配转换脚本看着简单实际在真实数据集上跑大概率会遇到下面的问题。第一个坑是归一化越界。现象是转换后某些 txt 里出现1.000001或-0.000001这种数YOLO 训练时会在日志里打出警告严重时这个目标会被忽略。原因在于一部分 xml 的 bndbox 坐标超出了size里记录的图像尺寸可能是标注软件的手误也可能是标注时图片被裁剪过而标注没同步。解决方式是转换脚本里加上min(max())边界保护同时在转换后跑一遍检查脚本把所有出现越界标注的图片名打印出来人工复核原始图。如果这类样本超过总量的 1%建议找源头重新导出。第二个坑是类别索引错位。现象是模型训练完跑验证集发现摩托车全被识别成电动车反过来也一样而且 mAP 很低。原因是转换脚本里CLASSES顺序和 data.yaml 里的names顺序不一致两边对不上。这个问题在多机协作或二次转换时特别容易发生——有人手动改了 data.yaml 的 names忘了改分类脚本里的 CLASSES。解决方式是把类别清单单独存成一个classes.txt转换脚本和训练配置都从这个文件读取保证单一数据源。第三个坑是漏 xml。现象是训练时经常报No labels found in ...或者是某个类别的目标数量明显少于预期。原因是以图片目录为主循环做转换有些图片文件在但对应 xml 被误删了。解决方式就是上面脚本的做法——以 xml 目录为主循环天然规避这个问题转换后确认生成 txt 数量与 xml 数量一致即可。第四个坑是图片和 xml 失配。现象是训练和验证时某些图片里明显有摩托车但模型始终没学会打开标注文件发现对应的 txt 内容为空或者标注的是别的车的框。原因是数据增强阶段对图片做了翻转、裁剪但标注没有同步变换。解决方式是用这段差集检查代码先排除文件名层面的问题from pathlib import Path jpg_files {p.stem for p in Path(JPEGImages).glob(*.jpg)} xml_files {p.stem for p in Path(Annotations).glob(*.xml)} print(有图无标注:, len(jpg_files - xml_files)) print(有标注无图:, len(xml_files - jpg_files))如果两个差集都不为 0先清理再训练不要抱着侥幸心理往下走。数据集里的每一处失配样本最后都会变成验证集上某个百思不得其解的掉点。4. 用YOLOv8/YOLOv5训练自己的数据集data.yaml配置与三个必调参数格式转换完成之后离真正跑起来只差一个 data.yaml 和几个训练参数。这一章按 YOLOv8 的命令行接口来讲YOLOv5 的思路完全一致只是命令格式略有差异。4.1 data.yaml怎么写train/val路径、nc与names的对应关系在数据集根目录创建data.yaml内容如下path: /home/user/moto_ev_dataset # 数据集根目录的绝对路径 train: images/train # 相对 path 的训练图目录 val: images/val # 相对 path 的验证图目录 test: images/test # 可选测试图目录 nc: 2 # 类别数必须和 names 列表等长 names: [motorcycle, electric_bicycle]写这个文件有两个容易踩的细节。一是path建议用绝对路径不要用相对路径。YOLOv8 的 DataLoader 在处理相对路径时依赖当前工作目录如果训练命令是在项目根目录发起的相对路径会拼错。二是train和val指向的是图片目录YOLO 会自动去同级的labels目录找标注文件。比如train: images/train标注目录必须是labels/train目录结构错了训练时直接报No labels found。nc和names的对应关系是另一个高频翻车点。nc: 2表示有两个类别names列表必须正好两个元素顺序要和转换脚本里CLASSES完全一致。这里如果写反了训练不会报错但摩托车会被当作电动车学而且整个训练过程中模型会努力拟合一个错误映射训完才发现问题相当痛苦。4.2 三个必调参数imgsz、batch、epochs结合5424张给出建议值训练命令里最值得花心思调的参数就三个其他可以先用默认值。下面这张表是结合 5424 张、双类别数据的规模给的建议值参数建议值说明imgsz640两轮车目标的中等尺寸场景640 是速度和精度的平衡点batch168G显存/ 3224G显存显存允许范围内尽量大影响收敛稳定性epochs100 配 patience205424 张数据量不至于需要几百轮重点是早停imgsz是输入分辨率。640 是 YOLOv8 的默认值对大多两轮车场景够用。如果你的应用场景是远距离监控、目标在画面里偏小可以试 768但训练时间和显存占用会显著上升收益未必明显。batch主要受显存约束8G 显存用 16 比较稳24G 可以上 32。这里有个容易被忽略的细节batch 太小BN 层的统计量会不稳模型收敛会变慢所以没必要为了省显存把 batch 压到 4。epochs不建议一上来就给 300。5424 张图不算大100 轮左右通常能收敛配合早停patience2020 轮没有提升就自动结束省时间也省显存。4.3 训练命令与日志里要盯的四个指标先给可复制的训练命令yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ patience20用yolov8s.pt作为预训练权重起步比从零训练收敛快很多这是迁移学习的常规收益。训练开始后终端会持续滚动每个 epoch 的输出主要盯四个指标Pprecision查准率、Rrecall查全率、mAP0.5和mAP0.5:0.95。前三个是日常参考最后一个更严格它要求预测框和真实框的 IoU 从 0.5 到 0.95 取平均两轮车这种中等尺寸目标一般做到 0.6 以上算合格。训练结束后不要只看终端最后一行去runs/detect/train目录打开results.png看 P/R/mAP 三条曲线的收敛形状。正常是稳步上升后走平如果曲线震荡剧烈、mAP 反复上下跳大概率是学习率偏大或者数据里有噪声样本。还要打开confusion_matrix.png这个图能直接看出摩托车和电动车之间的混淆程度——后面第 6 章会专门讲怎么看。如果你用的是 YOLOv5命令换成python train.py --data data.yaml --weights yolov5s.pt --epochs 100 --imgsz 640 --batch 16参数含义完全一样。新版 YOLOv26 的命令接口也维持了这一套风格data.yaml的结构没有任何变化迁移成本很低。5. 数据质量避坑摩托车与电动车识别最常见的5条踩坑记录训练跑起来之后真正花时间的不是调参而是面对各种奇怪的结果。二轮车检测的数据质量坑比通用目标检测更隐蔽因为两个类别太像了。以下 5 条踩坑记录每一条都是我在实际训练中遇到过的按“现象 → 原因 → 解决”写清楚。第 1 条摩托车和电动车类别互相串模型把电动车全识别成摩托车。现象是验证集上电动车的 recall 很高但 precision 很低摩托车类别刚好反过来打开错检图发现模型对同一辆车在不同帧里给出了不同类别。原因基本可以锁定在标注口径不统一有人按“有没有脚踏板”区分有人按“车身是否有电池仓”区分还有人把带人的电动车标成摩托车导致数据本身自相矛盾。解决方式是重新定义类别口径写成一份简单的标注规范明确规定摩托车必须是燃油动力、有排气管电动车必须有电池仓或脚踏板两者都具备的按主要外观特征归类。然后对标注数据进行抽样复核重点看那些连人都分不清的模糊样本这类样本是模型决策边界混乱的根源。第 2 条骑手被框进目标模型学会了把行人检测框连到车上。现象是模型输出的检测框普遍偏大框住了车身还带着骑手的上半身IoU 算下来反而吃亏。原因是 VOC 的 bndbox 是轴对齐正矩形两轮车在画面里倾斜时骑手和车身在矩形区域内天然有重叠标注员图省事就把整个外接矩形画上了。解决方式是先做一次几何过滤转换完成后用脚本统计每个框的宽高比正常两轮车侧视角宽高比大约在 1.5 到 3.0 之间如果出现大量宽高比接近 1.0 的框说明正对着车头或车尾的样本太多需要人工确认。这里要提醒一句VOC 格式做不了旋转框mmrotate 这类旋转检测工具要用的是 DOTA 格式这套数据集没有角度标注解决不了倾斜目标框不准的问题只能回归到“框到底怎么画”的标注规范上。第 3 条difficult1 的样本没过滤训练时 mAP 虚高测试时崩掉。现象是训练日志里 mAP0.5 一路涨到 0.9 以上换成独立测试集直接掉到 0.6。原因是我早期转换脚本里没有过滤 difficult 标记把大量“严重遮挡”“极小目标”“逆光模糊”的样本当作普通正样本参与了训练。模型在训练时死记硬背了这些难例的特征但真实场景里的难例形态完全不同泛化自然崩。解决方式很简单转换脚本里保留if difficult: continue过滤逻辑把难例单独存一个目录训练结束后用它们做难例挖掘而不是混进训练集。第 4 条mosaic 增强后小目标消失模型对远处车辆完全没有响应。现象是训练曲线前期正常到了后期 recall 卡在某个值上不去远距离的摩托车基本检测不到。原因是 YOLOv8 默认开启的 mosaic 增强会把四张图缩放到一张图里每张图只占原图的四分之一区域两轮车本来就小缩放之后可能只剩下十几个像素标注框也跟着缩小过小的真值框在计算 loss 时权重被摊薄。解决方式是把 mosaic 的启用概率从默认值调低在 ultralytics 的配置里设置mosaic0.5或者干脆第一阶段关掉 mosaic、第二阶段再开启YOLOv5 的老版本就有这个两阶段训练的设计。对于 5424 张这个量级轻中度增强足够不需要把所有增强手段全开。第 5 条混入非两轮车样本模型学会了检测“带轮子的物体”。现象是验证集上出现大量自行车、三轮车的误检而且误检框非常自信置信度都在 0.7 以上。原因是数据来源混杂初筛时没把“摩托车电动车”之外的车辆清干净部分图片里包含了自行车、三轮车甚至滑板车标注时单独开了新类别或者干脆没标但图片参与训练后作为背景干扰。解决方式是训练前先做一次快速人工抽检每个类别随机抽 50 张图按“是否有排气管、是否有电池仓、是否有脚踏板”快速筛选发现有非目标类别直接剔除。这个操作看起来原始却是避免模型学到错误概念的性价比最高的方式。另一个方法是在训练后用检测模型回扫全部训练集把置信度高但类别异常的输出框抓出来这也是一种自动化的数据清洗手段。6. 验证与进阶用混淆矩阵和跨域数据找出两个类别的真实分界模型训练完别急着部署。二轮车检测真正的难点在验证阶段摩托车和电动车在视觉上的分界远比标注文档里写的模糊得多你需要用工具把模型的“真实认知边界”逼出来。6.1 用yolo的val模式生成混淆矩阵与错检图训练完成后用验证集跑一次独立的评估yolo detect val \ datadata.yaml \ modelruns/detect/train/weights/best.pt \ imgsz640 \ batch16跑完去runs/detect/val目录找confusion_matrix.png。这张图是 2×2 的混淆矩阵加上背景类重点看motorcycle行和electric_bicycle列交叉处的数值。如果模型把 15% 以上的摩托车误判成电动车说明类别决策边界偏向电动车一侧此时调置信度阈值作用有限真正要做的是回到标注数据找出那些连人都需要想一下才能归类的样本重新统一标注口径。同时打开val_batch*.jpg这是带预测框的可视化结果滚动浏览一遍你会直观看到模型在哪些场景下“想当然”——比如把外卖箱当成车身特征或者夜间把车灯当成了整辆车。6.2 跨域验证拿BDD100K和CCPD测一测泛化边界一个数据集训出来的模型早晚要接受真实世界的检验。我一般会做两个跨域实验一是拿 BDD100K 数据集里的摩托车子集做验证它包含大量白天、夜间、雨天场景如果你的模型在夜间场景明显掉点说明训练集中夜间样本占比不足需要补数据而不是继续调参。二是拿 CCPD 车牌数据集测试误检风险CCPD 是纯车牌数据画面里都是车的前脸和尾部如果模型在这些图上输出了大量高置信度的“摩托车/电动车”框说明它学到的是“车体亮色区域”这类粗粒度特征而不是两轮车的结构特征这时候就要回头检查训练集是否过于单一。这两步做下来模型的真实水平就清楚了。我自己的血泪经验是第一版模型在训练集上 mAP 能到 0.88觉得稳了结果拿到夜间监控场景一测掉到 0.6原因就是训练集中夜间和逆光样本太少增广根本补不回来这类光照分布差异。所以我现在养成的习惯是任何模型交付前必须跑一轮跨域验证哪怕只是快速看一眼错检图。数据集的规模是 5424 张还是 2 万张并不是决定性因素决定性因素是你有没有把模型的认知边界摸清楚。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站