VOC数据集转YOLO训练全流程:摩托车电动车标注实战
发布时间:2026/9/29 1:37:39来源:尧图网络
简介这是一份面向目标检测任务的标准Pascal VOC格式摩托车数据集适合计算机视觉研究者、算法工程师及高校学生用于训练和评估车辆检测模型尤其适合需要快速获取真实场景数据的入门与进阶项目。数据采集自园区闸口进出方向真实覆盖道闸出入口场景包含5424张jpg原图与一一对应的5424个xml标注文件另附1份使用说明txt压缩包整体约916MB文件结构清晰便于直接划分训练集与验证集。所有图片均使用labelImg工具手工标注仅设置motorcycle一个类别累计6261个矩形框标注规则清晰、框体贴合目标可直接用于Faster R-CNN、YOLO、SSD等主流检测框架的训练与验证。当前已有622人学习/下载适用于智慧园区、门禁安防、交通流量统计等真实场景下的模型微调、迁移学习与算法效果对比可显著节省数据采集和标注时间。1. 摩托车电动车数据集5424 张 VOC 标注能干什么做目标检测的人都知道VOC 格式的数据集在自动驾驶、安防监控、城市交通治理这些场景里几乎是默认的交换格式。这份摩托车电动车数据集一共 5424 张实拍图片完整度很正全部带 Pascal VOC 标准的 XML 标注覆盖摩托车和电动车两类目标直接拿来训练 YOLOv5/YOLOv8 或者 CenterNet 都行不用再花时间手工框标签。我拆完之后的感觉是它对“电动车进电梯”“摩托车违停抓拍”这类窄场景特别友好因为目标角度和遮挡情况足够杂比网上那些只有正面车辆的公开数据集更能练出抗造模型。适合手里有监控视频但缺标注样本的算法工程师也适合做毕设、竞赛需要一份现成 VOC 标注的在校生。2. 数据集的底细目录结构、XML 标注与类别分布拿到任何 VOC 数据集第一步不是急着训练而是先把它拆开看结构。VOC 格式有一套约定俗成的目录规范这份数据也按同一套组织方式排列。VOC 被大量标注工具默认支持LabelImg 导出的就是这种结构所以确认目录完整之后后面标注转换才不容易翻车。2.1 目录结构先确认 Annotations 和 JPEGImages 能对上标准的 Pascal VOC 目录至少包含三个部分JPEGImages存放原始图片jpg 格式Annotations存放对应 XML 标注文件每个 XML 对应一张图片ImageSets/Main存放划分好的 train.txt、val.txt 文件记录参与训练和验证的图片文件名不含扩展名。打开数据集后按下面的命令快速核验图片和标注文件数量是否一致# 进入数据集根目录 ls JPEGImages | wc -l # 图片数量应等于 5424 ls Annotations | wc -l # 标注数量应等于 5424 # 找出有图没标注的文件 for img in JPEGImages/*.jpg; do f$(basename $img .jpg) [ -f Annotations/$f.xml ] || echo missing: $f done这段脚本的逻辑很简单先数两边文件总数再遍历每张图片检查同名 XML 是否存在。如果出现有图无标注或者有标注无图多半是数据在拷贝过程中丢文件后面训练时 YOLO 会报“找不到图片”或“找不到标签”的错。我一般会顺手再跑一条find Annotations -name *.xml | wc -l双保险因为有的压缩包解压后会在 Annotations 里多出__MACOSX等系统文件导致数量对不上。还有一个细节值得看JPEGImages 里的图片尺寸。不同监控摄像头出来的图分辨率差异很大有 1920x1080 的也有 1280x720 的。训练前用 Python 快速统计一下尺寸分布能判断是否需要对全数据集统一缩放。from PIL import Image import os, collections img_dir JPEGImages sizes collections.Counter() for f in os.listdir(img_dir): with Image.open(os.path.join(img_dir, f)) as im: sizes[im.size] 1 for size, cnt in sizes.most_common(10): print(size, cnt)这一步在数据集质量检查里属于必做动作因为后续归一化 bbox 坐标时XML 里的 width 和 height 必须和图片真实尺寸一致。如果图片被外部工具批量压缩过但 XML 没更新就会出现框偏移的隐蔽问题训练时看着 loss 正常实际框全是偏的。2.2 XML 标注字段逐个拆解随便打开一个 XML 文件内容基本是下面这样的结构annotation folderJPEGImages/folder filename00001.jpg/filename path/data/VOC_moto/JPEGImages/00001.jpg/path sourcedatabaseUnknown/database/source size width1280/width height720/height depth3/depth /size segmented0/segmented object namemotorcycle/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin210/xmin ymin180/ymin xmax560/xmax ymax510/ymax /bndbox /object /annotation把关键字段列成一张表方便对照字段含义训练时的用途filename图片文件名定位对应图像转换脚本按它找图size/width, height图片宽高bbox 归一化必须依赖它错了全盘错object/name类别名映射到类别编号truncated目标是否被截断截断严重的目标建议过滤difficult是否难例difficult1 的目标在评测中默认不计bndbox目标框 xmin, ymin, xmax, ymax转换 YOLO 格式的坐标来源值得注意的坑XML 里的path字段经常写的是标注者本机路径换了一台电脑就失效。所以后续转换脚本我从不读 path只读 filename 加自己拼的目录前缀这样数据集拷到任何环境都能跑。另外filename的大小写也要留意有的标注工具会生成IMG_001.JPG而实际文件是img_001.jpg在 Windows 上能跑换到 Linux 就找不到图片。2.3 类别分布决定能不能直接训练在训练之前先把所有 XML 里的类别名统计一遍确认一共几类、每类多少目标避免出现“类别名不一致”的低级问题。常见情况是同一类目标被标成了moto和motorcycle两种写法统计一次就能发现。import xml.etree.ElementTree as ET import glob, collections classes collections.Counter() obj_count 0 for xml in glob.glob(Annotations/*.xml): root ET.parse(xml).getroot() for obj in root.findall(object): classes[obj.find(name).text] 1 obj_count 1 print(total objects:, obj_count) for c, n in classes.most_common(): print(c, n)这段代码遍历所有 XML累加每个 object 的 name。输出既能看类别名是否统一也能看每类的目标数量是否均衡。如果两类目标数量差超过 5 倍训练时模型会偏向多的一类少的那类 mAP 往往垫底。这时常见做法是给少的那类做数据增强或者调整 loss 里的类别权重而不是直接开训。另一个影响训练结果的是难例。把 difficult1 和 truncated1 的目标数量也统计出来如果占比超过 10%说明数据里大量目标是遮挡或者小目标。训练时下采样倍数要调小imgsz建议从默认的 640 提到 960否则小目标基本学不到特征。3. VOC 转 YOLO转换脚本与三个边界细节YOLO 系列训练不接受 VOC 的 XML只吃每张图片对应一个 txt 标签文件的格式。txt 每行是class_ix cx cy w h前两个数字是归一化后的中心点坐标后两个是归一化后的宽高。这一步是 VOC 数据集落地的必经环节转换脚本写清楚后面能省一整天的排错时间。3.1 转换脚本XML 到 YOLO txt我一般用下面这个脚本完成转换它只依赖 Python 标准库不需要额外安装 lxmlimport xml.etree.ElementTree as ET import os # 类别映射按数据集实际标注类名填写 class_mapping {motorcycle: 0, electric_vehicle: 1} annotations_dir Annotations labels_dir labels os.makedirs(labels_dir, exist_okTrue) for xml_file in os.listdir(annotations_dir): if not xml_file.endswith(.xml): continue root ET.parse(os.path.join(annotations_dir, xml_file)).getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) out_lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_mapping: continue cls_id class_mapping[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h out_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) img_name os.path.splitext(xml_file)[0] with open(f{labels_dir}/{img_name}.txt, w) as f: f.write(\n.join(out_lines))这里有几个参数必须按自己的数据改class_mapping的键名要和 XML 里 object/name 完全一致值从 0 开始编号width和height是从 XML 的 size 节点读的不是从图片文件读的所以前面 2.1 节强调的尺寸一致性在这里体现。坐标全部做归一化因为 YOLO 在训练时会把图片统一缩放到 640x640归一化后的框比例不受 resize 影响这也是为什么不能直接写像素坐标的硬性原因。3.2 数据划分train/val 怎么切分合理转换出 txt 后还要生成 train.txt 和 val.txt 两个清单文件。这里用随机切分常见比例是 8:25424 张图大概就是 4339 张训练、1085 张验证。注意验证集要固定住不要每次训练重新切否则做实验对比时不同轮次用的验证集不一样结果不可比。mkdir -p ImageSets/Main # 先把所有文件名去扩展名写进一个列表 ls JPEGImages/ | sed s/\.jpg$// | shuf all.txt # 前 80% 做训练后 20% 做验证 total$(wc -l all.txt) train_n$((total * 8 / 10)) head -n $train_n all.txt ImageSets/Main/train.txt tail -n $((train_n 1)) all.txt ImageSets/Main/val.txt这段命令的逻辑是sed去掉扩展名shuf打乱顺序再用head/tail按行数切开。我习惯把 train.txt 和 val.txt 同时保留在 ImageSets/Main 下一是方便以后回溯用了哪些图二是 YOLO 的训练脚本也支持直接读这两个清单生成最终的数据集文件。如果不想用命令行也可以在 Python 里用random.shuffle做同样的事但能几十行命令解决就直接用命令。提示train.txt 和 val.txt 里的文件名不能带扩展名YOLO 读取时会自动拼接.jpg和.txt多带了反而找不到文件。3.3 类别编号和归一化坐标的边界转换过程最容易出问题的三个边界类别编号必须从 0 开始。YOLO 的 class id 从 0 数起如果你映射成 1 和 2训练时类别数会被推断成 3 类多出一个空类导致混淆矩阵里永远有一列是 0。归一化坐标不能超过 1。如果 XML 里 bndbox 的 xmax 大于 width说明标注框越界归一化后会大于 1YOLO 训练时 loss 可能变成 NaN。空标注文件不能删。有些图中没有任何目标对象VOC 数据里这类图也存在转换后 txt 是 0 字节。这种情况不要直接删掉对应图片否则训练时图片列表和标签列表对不上。写转换脚本时顺手加一个检查程序能把越界坐标和空标注都找出来# 检查 labels 里是否存在超界坐标 import os labels_dir labels for f in os.listdir(labels_dir): for line in open(os.path.join(labels_dir, f)): parts line.strip().split() if len(parts) ! 5: print(bad line:, f, line) continue cx, cy, bw, bh map(float, parts[1:]) if cx 0 or cx 1 or cy 0 or cy 1 or bw 0 or bh 1: print(out of range:, f, line)这个校验脚本建议每条数据都跑一遍特别是数据集在网络上二次传播过的情况下很多 XML 被手工改过坐标越界概率比想象的高。跑完再进训练阶段能过滤掉一大类训练 loss 突然炸掉的玄学问题。4. 训练配置与常见问题排查五条血泪踩坑记录把 5424 张 VOC 数据集转到 YOLO 后不是马上就能跑出好结果。我在复现过程中遇到大概五类高频问题每条都按“现象 → 原因 → 解决”的方式梳理一下这些坑几乎换任何 VOC 数据集都会遇到。4.1 标签文件路径错了训练时报 No labels found现象训练脚本一启动就提示找不到标签文件或者训练时每个 epoch 都在重新扫描数据速度极慢。原因YOLO 默认是在 images 目录的同级 labels 目录下找 txt 文件。如果图片放在JPEGImages而标签放在labels需要在数据集 yaml 里把路径写对。更隐蔽的情况是图片文件名带大写字母而标签文件名是小写Linux 下直接匹配失败。解决构建数据集目录时推荐统一成images/和labels/两个子目录并且文件名统一转小写mkdir -p images labels for f in JPEGImages/*.jpg; do base$(basename $f .jpg | tr A-Z a-z) cp $f images/${base}.jpg [ -f labels/${base}.txt ] cp labels/${base}.txt labels/${base}.txt done做完这一步再检查 labels 目录里非空文件数量确认转换脚本产出的 txt 没有全被拷丢然后再写 yaml。文件名统一小写这个习惯值得长期保持因为很多预训练权重和数据集的命名风格不一致统一小写能减少平台迁移时的路径问题。4.2 类别数对不上两种类名写法同一个类现象训练正常启动但 mAP 只有 0.3 左右打开混淆矩阵发现同一类目标被劈成两行比如moto和motorcycle。原因标注者不统一一部分人写全称一部分人写缩写。转换脚本的 class_mapping 只映射了其中一种拼写另一种拼写被跳过或者被当成新类别了。解决在 2.3 节的类别统计结果里把所有拼写变体统一合并再做映射。常见做法是在转换脚本里加一个别名映射层把同义类名归一name_alias { moto: motorcycle, 摩托: motorcycle, 电动车: electric_vehicle, } raw_name obj.find(name).text name name_alias.get(raw_name, raw_name)这里的关键点是合并要在统计类别分布之前做而不是在转换之后做。转换之后的 txt 只存数字没法再做类名纠错只能重新生成。如果你发现数据集里同时存在中英文标注这一步几乎是必须的。4.3 坐标越界导致 loss NaN现象训练第几个 batch 后 loss 突然变成 nan然后整个训练报废。原因XML 里的 bndbox 坐标大于图片宽高归一化后的 cx 或 bw 超过 1。常见于标注工具在图像 resize 后没有同步更新 XML 的 size 和 bndbox。解决转换前跑一遍 3.3 节的校验脚本把越界记录全部打印出来。处理方式有两种如果只是轻微越界比如 xmax 比 width 大几个像素直接把 xmax 裁到 width 再转如果偏差很大说明标注质量有问题建议删掉这条样本。我的习惯是永远选择裁剪而不是删样本因为监控场景里目标出画框本来就是常态截断目标对训练有小幅收益。xmax min(float(box.find(xmax).text), w) ymax min(float(box.find(ymax).text), h) xmin max(float(box.find(xmin).text), 0) ymin max(float(box.find(ymin).text), 0)处理完越界坐标后再跑一次校验脚本确认没有任何out of range输出再开训。这个动作能帮你避开 80% 的 NaN 问题剩下 20% 通常出在学习率过高或数据本身有损坏那是后话。4.4 小目标太多640 分辨率下基本学不到现象训练完验证集 mAP 尚可但拿到实际监控视频里检测效果明显偏漏尤其是远处的小目标一帧都没检出。原因数据集中在监控视角大量目标在图像里只占 20x20 像素左右。YOLO 默认输入 640x640下采样 32 倍后小目标的特征图区域不足 1 个像素特征基本丢失。解决把训练图像尺寸提到 960 或 1280同时开启 YOLO 的 mosaic 增强。显存不够时降低 batch size 到 8 或 4并用--rect开启矩形训练减少无效填充yolo detect train \ datadataset.yaml \ modelyolov8s.pt \ imgsz960 \ batch8 \ rectTrue \ epochs150imgsz960是这里最关键的参数它直接决定小目标保留多少特征。换过之后同一个验证集的 mAP50 通常能提升 3 到 5 个点。注意代价是训练速度慢将近一倍显存占用翻倍。如果显存实在不够960 也跑不动另一个可用的做法是把大图切成 640x640 的 patch 分别推理训练时用 SAHI 这类工具做切片辅助效果接近直接提分辨率。4.5 验证集和训练集重叠指标虚高不自知现象训练时 mAP 一路涨到 0.95部署后实测差得离谱误检漏检一堆。原因划分 train/val 时用了带相同前缀的图片或者随机种子没有固定。监控视频的相邻帧画面高度相似如果相邻帧分别掉进训练集和验证集验证指标就是“背题”成绩完全没有参考价值。解决划分时按视频或时间段分组保证同一场景的画面只进入一个集合。数据集中图片如果是连续帧命名比如 00001、00002切分前按文件名前缀分桶。固定随机种子也同样重要shuf --random-source(yes 42) all.txt shuffled.txt--random-source让 shuf 用固定种子打乱这样每次运行划分结果一致。从这个坑之后我每次划分完都会抽几张验证集图片去训练集里找相同内容肉眼扫一遍。虽然费时间但能避免在假指标上浪费一整周的调参时间。5. 训练完成后的验证mAP、混淆矩阵与坏样本排查训练结束不等于项目结束。我一般会先用官方验证命令跑一次测试集再去看几个容易骗人的指标最后把错误预测的图片挑出来人眼过一遍。这一步能决定模型到底是“能交差”还是“能上线”。第一个动作是用验证集做标准评测yolo detect val \ modelruns/detect/train/weights/best.pt \ datadataset.yaml \ imgsz960输出里重点看三列mAP50、mAP50-95、per-class mAP。mAP50 高但 mAP50-95 低说明模型对框的位置精度不足常见于小目标数据集。两个指标相差超过 20 个点就优先去调 NMS 阈值或者回归损失权重而不是继续加 epoch。第二个动作是打开混淆矩阵。YOLO 训练输出目录里有confusion_matrix.png每个类别对应一行一列。重点关注对角线之外的最大数值如果摩托车和电动车互相误检大概率是两个类别外形接近这时候该考虑的是合并类别或者增加难负样本而不是继续调超参。如果背景列数值偏高说明误检多优先检查类别不平衡。第三个动作是我个人的习惯把验证集预测错误的图集中抽出来写一个小脚本保存预测框和真实框重叠度低于 0.5 的样本yolo detect val \ modelruns/detect/train/weights/best.pt \ datadataset.yaml \ save_confTrue \ save_txtTrue之后在runs/detect/val/labels里看每个文件的预测结果配合原图确认是漏检该框没框出、错检框到了路边垃圾桶、还是定位偏差框一半在目标外。这三种情况对应的修法完全不同漏检加分辨率或加训练轮数错检加负样本定位偏差调 IoU 阈值或回归权重。我这次复现摩托车电动车数据集时错检集中出现在逆光阴影下把阴影当成了电动车最终通过把训练集里加了几百张同场景的纯阴影负样本解决。从那以后我每次拿到 VOC 格式数据都会强制走一遍全流程先看目录对不齐再统计类别和难例接着转 YOLO 时跑越界校验最后训练完不只看 mAP还要翻一遍坏样本图。这套动作看起来很笨但确实帮我避掉过多次“指标好看、上线翻车”的事件希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网