ARTICLE DETAIL

资讯详情

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

VOC车辆检测数据集制作与转换:从标注到YOLOv8训练的全流程避坑指南

VOC车辆检测数据集制作与转换:从标注到YOLOv8训练的全流程避坑指南 简介这是面向视觉目标检测学习者与算法工程师的车辆检测标注数据集包含 bus、car、suv、taxi、truck 五类常见车辆。图片按类别前缀统一命名并同时提供 txt 与 xml 两套标注文件适合用于 YOLO 系列、SSD 或 Faster R-CNN 等模型的训练与效果验证也可作为数据格式转换练习的素材。资源共 2000 个文件其中原始图片 699 张 jpg对应标注 699 个 xml 与 700 个 txt压缩包整体约 466.3MB。xml 为 PASCAL VOC 标注格式txt 为 YOLO 格式归一化坐标标注两者分别存放在独立文件夹中便于按需选用。目前已有 961 人学习下载常用于车辆检测的教学实验与小型项目验证。下载后可直接获得成体系的车辆检测数据既可用于训练自定义检测模型也可对比两种主流标注格式的差异省去自行采集与转换标注的时间成本。1. 为什么说“VOC 各种类型车辆检测数据集”不是下载即用的成品深夜一点val 集里两百辆卡车被模型认成轿车mAP 掉到 0.33。打开 xml 一看训练集 bus 只有 117 张car 却有一万八千张——这就是接触“VOC 各种类型车辆检测数据集”的人最常见的开局。它指的不是某个打包好的压缩包而是一套以 Pascal VOC 的目录结构和 xml 标注为核心、覆盖多类车辆对象的检测数据组织方案通常包含 car、bus、truck、motorcycle、bicycle 等类别服务于 YOLO、Faster R-CNN、SSD 这类检测模型的训练。适合谁手上有图像或视频素材想做成能被复现、能迭代、能换模型不换标注的数据资产的人。先记住一个反直觉结论网上能下到的“VOC 车辆数据集”大概率只有 JPEGImages 和 Annotations没有划分好的 train/val类别也可能只有 car 一类剩下全靠自己补。2. VOC 数据集的目录结构与车辆类别划分从 Annotations 到 labelmap 的落地约定2.1 一个规范的 VOC 目录JPEGImages、Annotations 与 ImageSets 的分工VOC 格式的核心不是单个 xml 文件而是整套目录约定。做车辆检测时我一般会先按下面的结构把数据落盘后面对接任何训练框架都靠这套结构兜底VOCdevkit/ └── VOC2007/ # 年份只是协议标识不影响解析 ├── JPEGImages/ # 原始图像统一 .jpg文件名与 xml 一一对应 ├── Annotations/ # 每张图像一个 .xmlPascal VOC 标注 ├── ImageSets/ │ └── Main/ # train.txt / val.txt / trainval.txt / test.txt │ # 每行一个不带后缀的文件名例如 000001 └── labels/ # 非 VOC 标准目录很多团队自己加放 YOLO txtJPEGImages 和 Annotations 必须严格同名ImageSets/Main 里的 txt 只写文件名不带扩展名这是 VOC 协议给训练脚本的接口。一个典型血泪经验是有人把标注文件存成 .XML 大写后缀Linux 下 ET.parse 找不到文件排查半天才发现是大小写问题。图像统一转成 .jpg 而不是 png不只是省空间还因为很多检测框架按扩展名判断解码路径混着用容易在 dataloader 报错。VOC 没有强制图像尺寸但同一批训练数据最好控制在接近的分辨率范围否则 batch 里一边是 1920×1080一边是 640×480resize 后目标尺度差异会直接反映在 AP 上。2.2 车辆类别怎么从 VOC 的 20 类缩到我们自己的 labelmapPascal VOC 原生 20 类里跟车辆强相关的只有 bicycle、bus、car、motorbikeaeroplane 偶尔算作空中目标truck、van、pickup 这些在 VOC 里没有独立类别全部并进 car。做多类型车辆检测时这点最需要提前约定自定义类别来源落地建议carVOC 的 car注意 VOC 把 SUV、三厢轿车全算 car训练前按业务决定是否拆分busVOC 的 bus直接可用truckVOC 无对应需从 car 重标或从其他数据集引入卡车和客车容易混淆标注规范里要写清厢式货车算 truckmotorcycleVOC 的 motorbike类别名保留 motorcycle映射时做一次改名bicycleVOC 的 bicycle直接可用vanVOC 无对应需扩展标注封闭厢式车在街景中出现率高建议单列确定了 labelmap 后第一步是写一个干净的映射表而不是直接改 xml。我习惯把映射独立成文件比如生成一个 label_map.json转换脚本只读它。原因很简单后续加类别、合并类别时只改一处不用重新遍历标注。2.3 为什么不直接下“现成交付的 VOC 车辆包”很多第三方发布的 VOC 车辆数据集实际是用脚本从 VOC trainval 里筛出 car、bus 等类别打包的。直接用有三个风险一是分布不对齐VOC 的 car 大量是街景角落里几十像素的小目标而你的业务可能是高速卡口大目标二是类别覆盖不够van、truck 基本没有三是来源不可追溯xml 可能是爬虫抓的图重新标注的商用授权无法证明。这也是 BDD100K、UA-DETRAC、KITTI 这些自动驾驶数据集更常被选作底料的原因——不是它们标注更准而是它们的类别定义和场景分布更接近真实车辆检测任务。需要注意的是这些数据集的标注格式都不是 VOCBDD100K 是 json 风格KITTI 是自定义 txtUA-DETRAC 带轨迹属性全部要走到第 4 章的转换流程。另外 CCPD 这类数据集框的是车牌不是整车训练整车检测时别混用它的 bbox 范围完全不是你要的目标。3. 从零搭建车辆数据集数据来源选型、抽帧初筛与 VOC 标注生成3.1 公开车道数据集怎么选先看类别分布再看标注质量搭建车辆数据集最省力的路径是在公开自动驾驶数据集基础上做类别裁剪和清洗而不是从零找图标注。我接触过的几个常用来源如下数据集规模量级车辆类别覆盖标注格式适合方向BDD100K10 万帧car、truck、bus、bike、rider 等BDD json 风格夜间、雨天、城区复杂场景占比高UA-DETRAC约 14 万帧car、bus、van 等带轨迹的框城市路口、遮挡严重场景KITTI约 1.5 万张car、truck、van、tramKITTI label城郊、高速目标视距变化大自行采集看资源完全可控任意业务强相关的特殊车型选型逻辑不是哪个数据集大就选哪个。夜间场景的检测任务优先看 BDD100K因为它的夜间帧占比明显高于 KITTI路口遮挡多就选 UA-DETRAC只跑结构化道路KITTI 就够用。我的建议是主数据源选 1 个辅助数据源选 1 个用辅助源补充主源缺失的类别和天气组合。注意这些数据集图像分辨率差异很大BDD100K 是 1280×720KITTI 是 1242×375训练前要么统一 resize要么在转换时保留原始尺寸让 dataloader 自行处理。3.2 视频抽帧别让连续帧淹掉你的硬盘如果自己采集视频第一步是抽帧。常见错误是把视频所有帧都存下来一段 10 分钟 30fps 的视频就是 18000 张九成是重复背景。我一般按间隔抽帧同时把视频文件名写进帧名方便后面做时序划分import cv2 import os def sample_frames(video_path, out_dir, sample_interval30): cap cv2.VideoCapture(video_path) os.makedirs(out_dir, exist_okTrue) idx 0 base os.path.splitext(os.path.basename(video_path))[0] while True: ret, frame cap.read() if not ret: break if idx % sample_interval 0: out_file os.path.join(out_dir, f{base}_{idx:06d}.jpg) cv2.imwrite(out_file, frame) idx 1 cap.release() print(fprocessed {idx} frames, saved to {out_dir}) # 参数说明 # sample_interval30 表示每 30 帧取 1 帧30fps 视频下约每秒取 1 张 # 帧名带视频名和帧号是为了后续按时间段切 train/val防止信息泄漏sample_interval 取多少要看目标运动速度。高速公路场景 10 帧取 1 帧可能漏掉快速变道的瞬间城市路口 30 帧取 1 帧足够。保守做法是先抽一版人工扫一遍如果同一个车在相邻帧里位置几乎没变就加大间隔。抽完帧之后建议跑一遍清晰度筛选把模糊帧删掉。OpenCV 的 Laplacian 方差是个便宜的指标方差低于阈值的帧直接丢不要留给标注员浪费时间。3.3 使用 LabelImg 标注并生成标准 VOC xml标注阶段我仍推荐 LabelImg它原生支持 PascalVOC 和 YOLO 两种导出格式。选 PascalVOC 导出因为 xml 带图像尺寸信息后续转 YOLO、COCO 都方便annotation folderVOC2007/folder filename000001.jpg/filename size width1280/width height720/height depth3/depth /size object namecar/name bndbox xmin100/xmin ymin150/ymin xmax350/xmax ymax400/ymax /bndbox /object /annotation标注规范里必须写清楚 bndbox 贴多紧。我的约定是四边贴着车体外轮廓含后视镜但不含车牌突出部分被遮挡超过一半的目标不标路灯下阴影不算车体。LabelImg 导出后xml 散落在各个目录需要归拢到标准 VOC 结构。一个很简单的脚本就能完成import shutil import os from pathlib import Path def collect_to_voc(src_dirs, voc_root): jpg_dir Path(voc_root) / JPEGImages xml_dir Path(voc_root) / Annotations jpg_dir.mkdir(parentsTrue, exist_okTrue) xml_dir.mkdir(parentsTrue, exist_okTrue) for src in src_dirs: for img in Path(src).glob(*.jpg): xml img.with_suffix(.xml) if xml.exists(): shutil.copy(img, jpg_dir / img.name) shutil.copy(xml, xml_dir / xml.name) else: print(fmissing xml: {img})这段脚本做两件事把 jpg 和 xml 配对复制进标准目录发现缺 xml 的图直接打日志。missing xml 列表要人工确认很多时候是标注漏了而不是文件损坏。归拢之后强烈建议跑一次 xml 完整性校验解析每个文件、检查 bndbox 是否在图像范围内这能挡掉后面训练时最常见的崩溃来源。4. VOC 转 YOLO 与 COCO归一化参数、类别映射与防泄漏划分4.1 为什么训练前必须做格式转换VOC 标注能直接喂给 torchvision 自带的 Faster R-CNN但 YOLOv5/YOLOv8、MMDetection 都不吃 xml。YOLO 系列要求每个标注文件是 txt每行格式为“类别序号 中心点x 中心点y 宽 高”且 x、y、w、h 都是相对图像宽高的归一化数值MMDetection 的常用配置则要求 COCO json 格式。范围再广一点如果你用 MMRotate 做旋转框检测那需要的是 DOTA 格式VOC 的 axis-aligned 框转过去后基本要重新标。所以格式转换不是可选项而是从数据到训练闭环的第一道工序。转换脚本本身不难难在类别映射的一致性VOC 里 class 是字符串YOLO 里是整数COCO 里是 idname 的结构三套体系一旦没对齐模型训练得越久越难查错。这也是我强调 labelmap 单文件管理的原因。4.2 VOC 转 YOLO一个脚本吃透归一化和类别映射YOLO 训练目录通常是 images/ 和 labels/ 平级每张 jpg 对应一个同名的 txt。下面是核心转换逻辑import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_dir, class_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue 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) # 计算中心点坐标和宽高全部除以图像宽高做归一化 xc (xmin xmax) / 2 / w yc (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{class_map[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) out_path Path(out_dir) / (Path(xml_path).stem .txt) out_path.write_text(\n.join(lines)) return len(lines)class_map 是从类别名到整数编号的字典比如{car: 0, bus: 1, truck: 2, motorcycle: 3, bicycle: 4, van: 5}。这里有个细节xmin、ymin 是 int 类型但读出来一定要转 float 再运算否则除以宽高后直接舍零小目标会全部变成面积为 0 的框。out_path只写了 txt没写 jpg因为 YOLO 训练时是从图片目录反向找同名 txt不是从 txt 找图片。批量处理时我习惯每个 xml 做完后把结果追加到一个 all_labels.txt 统计表方便后面数各类别数量。4.3 VOC 转 COCOjson 结构与 seg 字段的补零技巧如果走 MMDetection 或 Detectron2需要 COCO json。VOC 转 COCO 的关键是 categories、images、annotations 三项的组织方式。可以直接把 COCO 的 segmentation 字段写成矩形轮廓[xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax]COCO API 能正常读只是没有精细分割而已。核心代码如下def voc_to_coco(xml_dir, class_map, output_json): coco {images: [], annotations: [], categories: []} for name, cid in class_map.items(): coco[categories].append({id: cid, name: name}) ann_id 1 for idx, xml_path in enumerate(Path(xml_dir).glob(*.xml)): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) coco[images].append({ id: idx, file_name: root.find(filename).text, width: w, height: h }) for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue 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) coco[annotations].append({ id: ann_id, image_id: idx, category_id: class_map[name], bbox: [xmin, ymin, xmax - xmin, ymax - ymin], area: (xmax - xmin) * (ymax - ymin), iscrowd: 0, segmentation: [[xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax]] }) ann_id 1 with open(output_json, w) as f: json.dump(coco, f)bbox 用的是xmin, ymin, width, height不是 xmin、ymin、xmax、ymax这个顺序写错最常见的表现是训练时 loss 不降、mAP 始终在 0.1 以下。categories 的 id 要严格按 class_map 来而不是直接沿用 COCO 原生的 80 类 id——很多现成脚本会默认 car 是 2但你的模型只检测车辆时类别下标是从 0 重排的。4.4 划分 train/val/test防信息泄漏的按片段拆分划分是最容易把数据做脏的一步。常见操作是random.shuffle所有文件名后按比例切这对独立图片数据集问题不大但对视频抽帧数据是灾难同一辆车出现在连续帧里可能一帧在 train、一帧在 valmAP 虚高到离谱部署时直接崩。正确做法是按视频片段划分import random from collections import defaultdict def split_by_video(file_list, train_ratio0.7, val_ratio0.2, seed42): random.seed(seed) video_groups defaultdict(list) # 文件名格式为 {video_name}_{frame_idx}.jpg for f in file_list: video_name f.split(_)[0] video_groups[video_name].append(f) videos list(video_groups.keys()) random.shuffle(videos) n_train int(len(videos) * train_ratio) n_val int(len(videos) * val_ratio) train_files [] val_files [] test_files [] for i, video in enumerate(videos): if i n_train: train_files.extend(video_groups[video]) elif i n_train n_val: val_files.extend(video_groups[video]) else: test_files.extend(video_groups[video]) return train_files, val_files, test_filesseed 固定下来保证每次划分结果一致。比例上我一般用 7:2:1验证集至少覆盖每个类别 20 张以上类别本身就少的 bus 如果验证集里只有 3 张评估结果方差会大到没有任何参考价值。划分完成后把 list 写入 txt 文件同时打印各类别在 train/val 中的数量分布不均衡的就在这里发现别等训练完才追。5. 车辆检测数据集的 5 个排查避坑点xml 解析、类别失衡与验证集泄漏5.1 ET.parse 报错的隐藏原因BOM 头和相对路径现象批量转换时ET.ParseError发生在某个 xml但用浏览器打开一切正常。 原因一是 xml 被人用带 BOM 的编码保存过解析器不认识开头的不可见字符二是 xml 里 filename 写的是「./images/000001.jpg」这种相对路径转换脚本直接拿来拼接目录就找不到文件。 解决转换前先做一次全线校验。用xmllint --noout Annotations/*.xml快速暴露解析问题对 filename 字段统一Path(x).name只取文件名。遇到 BOM 问题在代码里用utf-8-sig读文件再转存一次即可。这个坑在你自己写脚本合并多批标注时极容易出现LabelImg 导出的原始文件没问题问题都出在后处理阶段。5.2 类别失衡bus 只有 117 张car 有一万八千张模型直接摆烂现象训练后 val 集上 car 的 AP 有 0.85bus 只有 0.12整体 mAP 被平均得很难看。 原因数据分布天然倾斜街景里轿车数量碾压客车。模型学不到 bus 的判别特征不是它笨是它见的 bus 样本太少。 解决先统计每个类别的数量做公示如果最少的类别和最多的差一个数量级就要处理而不是硬训。简单有效的手段是类别加权采样dataloader 里给样本分配权重bus 的采样概率调到 car 的 5 到 10 倍。注意这不是让模型重复看同一张图而是降低 car 在每一轮 iteration 中的占比。对 bus 单独做增强也有用hsv 扰动、上下翻转、随机裁剪后粘贴到街景背景里。粘贴时要处理透视直接矩形贴上去会让模型学到边框特征。5.3 转换时没过滤 difficult 和截断目标现象训练时 loss 下降正常但验证集 AP 在 0.4 附近上不去。 原因VOC xml 里 object 下有difficult、truncated、occluded字段很多转换脚本直接忽略。结果是把被树挡了 90% 的车、训练集里只有半个车身的目标全当正样本模型被噪声标签反复拉扯。 解决转换前过滤。我一般把difficult等于 1 的目标直接跳过不输出truncated大于 0.5 的如果有其他完整样本兜底就删如果已经是稀缺类别就单独建一个 hard_examples.txt 备用。只对occluded做记录不做过滤因为遮挡目标在真实场景中很常见全删了会导致模型对遮挡不鲁棒。5.4 resize 图像后 xml 坐标没同步缩放现象训练开启imgsz640后一切正常但自己写预处理脚本时图像缩到 640×480坐标还是原图 1920×1080框全部错位。 原因xml 里的坐标是绝对像素值和具体图像一一绑定。任何对图像尺寸的改动都要同步更新这两个坐标。 解决统一在转换脚本里把图像和标注同时处理不要先 resize 图片、后改 xml。我自己踩过这个坑后把规则定为「resize 必须发生在格式转换之前」一旦进入 YOLO 的归一化坐标阶段image 的原始宽高就不该再改。若必须改用new_x x * new_w / old_w的公式重算四个值别手动估。5.5 验证集泄漏带来的假高分现象训练记录显示 mAP 0.85模型一出实验室就翻车现场视频里车都漏检。 原因train 和 val 里混入了同一视频片段的相似帧。车辆外观、背景、光照几乎一致模型实际上是在做记忆而不是做泛化。更隐蔽的泄漏是同一地点不同时间段的图像比如停车场固定车位上的同一辆车反复出现。 解决划分时严格按视频片段、时间戳、地理位置元数据做分组。正则表达式解析文件名把视频 ID 提取出来分组划分如果是网络爬的独立图片那就控制来源域名同一域名的图像默认放同一集合。划分完成后做一个检查随机抽 20 对 train/val 图像看是否有完全相同的车出现在两张图里。另外训练完成后对比 train mAP 和 val mAP如果差值超过 15 个点优先怀疑泄漏而不是模型过拟合。6. 用转换后的 VOC 车辆数据集跑通 YOLOv8data.yaml、训练命令与按类别看 AP转换完成后最后一个闭环是把数据喂进 YOLOv8 跑一遍训练并验证效果。项目根目录下面应该同时存在 images/ 和 labels/data.yaml 按这个方式写path: /path/to/your_dataset train: images/train val: images/val nc: 6 names: [car, bus, truck, motorcycle, bicycle, van]YOLOv8 会自动在同等路径下寻找 labels/train 下的 txt 文件。nc 必须和 names 长度一致这两个值由 labelmap 生成不要手写数字。训练用如下命令yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16一个几百到几千张规模的数据集s 模型在单张消费级显卡上两三个小时能跑完。训练结束后看runs/detect/train/下的 confusion_matrix.png 和 results.png先确认 loss 曲线没有发散再看混淆矩阵里 truck 和 bus 是否互混。验证阶段直接跑yolo detect val \ modelruns/detect/train/weights/best.pt \ datadata.yaml终端输出的 Per Class 表会列出每个类别的 AP50 和 AP50-95。我现在的习惯是数据集交付前先跑一版「完全默认参数」的 baseline不看调参效果只看数据和标注质量。如果 car 的 AP50 在 0.9 以上但 bus 只有 0.3先回去补 bus 样本不要动模型结构如果所有类别都低检查第 5 章里的遮挡过滤和划分泄漏。数据质量过关之前调任何超参都是自我安慰。希望帮到你。本文还有配套的精品资源点击获取
返回列表