YOLO车辆检测数据集:VOC与YOLO标签格式转换及训练避坑指南
简介面向YOLO系列目标检测训练场景这份车辆识别数据集覆盖公交车、卡车、摩托车、行人、自行车、小型车等多类别适合需要规范标注数据来训练与验证模型的算法工程师、竞赛选手及相关研究人员。据标题信息图像样本规模为9767张压缩包共2000个文件以XML标注文件为主包含YOLO格式TXT与VOC格式XML两套标签标签采用归一化中心坐标与宽高比例存储配合内置的data.yaml数据集配置训练集、验证集、测试集已预先划分可直接供yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等算法调用。资源以zip压缩包形式发布整体约205.35MB目录按标签格式分区便于按需取用。目前已有97人学习该数据集对车辆检测研究者而言带标签数据、双格式标注与现成配置的组合能显著缩短数据准备周期免去自行标注、格式转换与数据集划分环节快速投入模型训练与效果验证。1. 车辆检测数据集9767张图、六类目标两种标签格式直接开工做车辆检测项目时最怕的往往不是模型结构选不出来而是数据集标签格式和训练框架对不上光转格式就能耗掉半天。这份 YOLO 车辆检测数据集一共 9767 张图像全部带标签覆盖公交车、卡车、摩托车、行人、自行车、小型车六类路面常见目标YOLO 格式和 VOC 格式各存一份data.yaml 也已经划好训练集与验证集。换句话说下载解压后从 yolov5 到 yolo11 系列都能直接接训练脚本不需要再手写格式转换、不需要重新划分数据集。适合做交通监控、辅助驾驶训练集验证或是课程设计里需要真实道路场景的车辆检测复现。2. 标签格式解析从 VOC 的 XML 到 YOLO 的归一化 TXT拿到压缩包先别急着训练。我习惯第一步确认标签结构因为很多报错其实是格式理解出错导致的。这个包里的图像和标注是刚才提到的这种命名img_0308_4095.xml、img_0308_4098.xml。每个 xml 对应一张同名图片例如 img_0308_4095.jpg。标签同时提供了两套YOLO 格式是 txt 文件每行一个目标内容为class x_center y_center width heightVOC 格式是 xml 文件包含目标的名称和边界框像素坐标。2.1 目录结构与文件名对应关系解压后我看到的目录结构通常是类似下面这样的布局两个标签文件夹和图片文件夹分开存放vehicle_det ├── images │ ├── img_0308_4095.jpg │ └── img_0308_4098.jpg ├── labels_yolo │ ├── img_0308_4095.txt │ └── img_0308_4098.txt └── labels_voc ├── img_0308_4095.xml └── img_0308_4098.xml如果解压后的子目录名称不同不要紧关键是同名文件对应同一张图片。YOLO 训练时images 和 labels 两个目录名要能够在 data.yaml 里配对yolov8 默认取与 image 同名的 txt 作为标签所以去掉扩展名后的主文件名必须完全一致。我一般会跑一个 Python 检查把两个集合取差集避免出现一张图有 xml 没 txt 这种“文件配对缺失”的情况。# 用 set 差集检查图片和标签是否一一对应 from pathlib import Path img_stem {p.stem for p in Path(images).glob(*.jpg)} yolo_stem {p.stem for p in Path(labels_yolo).glob(*.txt)} voc_stem {p.stem for p in Path(labels_voc).glob(*.xml)} print(缺 yolo 标签的图片, img_stem - yolo_stem) print(缺 voc 标签的图片, img_stem - voc_stem)这段脚本先用 glob 拿到图片、txt、xml 的文件主名再求差集输出结果是 set。如果输出为空说明配对完整如果两套标签各自都有缺失就需要把缺失文件删掉或重新导出否则训练中会报 “Label not found”。2.2 坐标换算XML 的像素坐标怎么变成 0~1 比例值VOC 的 xml 里bndbox 记录的是 xmin、ymin、xmax、ymax这些值是像素坐标。YOLO 的 TXT 里存的是中心点坐标和宽高而且全部归一化到 0~1比例是相对图像宽高的。虽然这个包已经提供了转换后的 txt我还是建议你把公式背熟因为后续修改类别或调整图片尺寸时大概率要重写脚本转换。# 读取 VOC xml 并输出 YOLO 归一化 tx import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, img_path, out_path, class_names): tree ET.parse(xml_path) root tree.getroot() img_w, img_h Image.open(img_path).size with open(out_path, w, encodingutf-8) as f: for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(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 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n)这段脚本核心是把像素坐标换算成比例坐标中心 x 等于左右边界平均值除以图宽宽等于右边界减左边界除以图宽y 方向同理。class_names 的顺序必须和后面 data.yaml 里的 names 完全一致比如[bus, truck, motorcycle, person, bicycle, car]因为 txt 每行第一个数字就是类别索引索引错了等于标注全错。如果你只需要处理某几类可以在脚本里加个白名单过滤不想参与训练的类别直接跳过。2.3 画框验证标签到底落在图上什么位置文本格式检查通过不代表坐标没问题我印象最深的一次是某批标签 x_center 全部大于 1看起来是有人把归一化坐标写成了像素坐标。绘图脚本能一眼暴露这种问题。下面的脚本读一张图和对应的 txt把每个目标的框和类别名叠加到图上。# 可视化 YOLO 标签检查框是否落在正确目标上 import cv2 img_path images/img_0308_4095.jpg txt_path labels_yolo/img_0308_4095.txt classes [bus, truck, motorcycle, person, bicycle, car] colors [(0, 0, 255), (0, 255, 0), (255, 0, 0), (0, 255, 255), (255, 0, 255), (255, 255, 0)] img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls_id int(parts[0]) xc, yc, bw, bh map(float, parts[1:5]) # 从归一化坐标还原像素坐标 x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), colors[cls_id], 2) cv2.putText(img, classes[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[cls_id], 2) cv2.imwrite(check_visualized.jpg, img)注意这里 xc、bw 都是归一化比例所以还原像素坐标时要乘以图像的宽和高。如果画出来的框明显偏出图像边界或者框内根本不是对应类别的物体就说明标签有问题不能直接进训练。cv2.putText 里的y1-5是为了让文字显示在框上方避免遮挡目标本体。colors 列表长度必须和类别数一致类别索引超出时脚本会直接报 IndexError这也是一种快速排查。3. data.yaml 与数据集划分训练前十分钟先做完的三个检查yolov8 训练命令看起来很简单但翻车点往往在数据侧。这一章我们把 data.yaml、目录划分和类别分布三个地方过一遍确保训练是从一个干净的数据集开始的。3.1 data.yaml 的字段解读data.yaml 是 YOLO 系列框架读取数据配置的入口。没有它训练脚本不知道去哪里找图片和标签也不知道类别有多少种。这个包里提供的 data.yaml 内容大致如下train: ./images/train val: ./images/val nc: 6 names: [bus, truck, motorcycle, person, bicycle, car]train 和 val 指向的是训练集、验证集的图片目录yolov8 会默认在图片目录的同级 labels 目录下寻找同名 txt。如果你的目录不是 images/labels 这种经典布局就在 yaml 里显式写 labels 路径。nc 是类别总数names 是类别名列表列表顺序必须和 txt 文件里的类别索引一致。我看到很多人把 nc 写错比如实际上有 6 类却只写 5训练过程会直接报 “nc number does not match names length”。提示yolov8 的 data.yaml 里 train/val 路径建议使用相对路径或绝对路径不要写带空格的中文路径部分版本的 OpenCV 处理这类路径会直接闪退。这里的 names 顺序是我按标题中六类目标的常见顺序推的但不要直接信任。你要打开一个 txt 看第一行第一个数字再打开同一个图片确认框里的目标是不是 names 对应位置写的那种。一个数字对不上后面整个训练和评估都会错位。3.2 划分比例与文件摆放的验证这个包已经划分好训练集和验证集训练时直接指向 data.yaml 即可。如果你拿到的是未划分版或者想重新划分常见做法是 85% 训练、15% 验证。车辆目标分布通常不均衡随机按文件名划分容易让某一类在一个集合里特别少我更建议按图片维度随机划分后再统计两个集合的类别数量。# 按比例随机划分图片与标签固定随机种子便于复现 import random from pathlib import Path import shutil random.seed(42) img_dir Path(images_all) train_dir Path(images/train) val_dir Path(images/val) train_dir.mkdir(parentsTrue, exist_okTrue) val_dir.mkdir(parentsTrue, exist_okTrue) all_imgs list(img_dir.glob(*.jpg)) random.shuffle(all_imgs) val_ratio 0.15 val_count int(len(all_imgs) * val_ratio) for img in all_imgs[:val_count]: shutil.copy(img, val_dir / img.name) # 同步复制标签到对应验证集目录 shutil.copy(Path(labels_all) / (img.stem .txt), Path(labels/val) / (img.stem .txt)) for img in all_imgs[val_count:]: shutil.copy(img, train_dir / img.name) shutil.copy(Path(labels_all) / (img.stem .txt), Path(labels/train) / (img.stem .txt))这段脚本先生成全部图片列表再用固定随机种子打乱按比例取出验证集。shutil.copy 直接复制图片到对应目录标签也同步复制过去。固定random.seed(42)是为了保证多次执行得到相同划分方便复现实验。如果图片量很大建议用 os.link 做硬链接而不是 copy避免浪费磁盘空间。3.3 类别分布统计确定训练样本有没有偏科类别不均衡会直接影响 mAP尤其是行人、自行车这类小目标天然比卡车、公交车难检测。训练前统计每个标签里的目标数量能提前知道模型要面对的数据长什么样。# 统计每个 txt 中出现的类别索引次数 from collections import Counter from pathlib import Path def count_classes(txt_dir): counter Counter() for txt_path in Path(txt_dir).glob(*.txt): with open(txt_path) as f: for line in f: parts line.strip().split() if parts: counter[int(parts[0])] 1 return counter train_counter count_classes(labels/train) val_counter count_classes(labels/val) print(训练集类别统计, train_counter) print(验证集类别统计, val_counter)Counter 会把每个 txt 的每一行第一个数字累加起来输出的结果形如Counter({5: 8000, 0: 1200, ...})。数字对应 data.yaml 中的索引比如 0 表示 bus5 表示 car。如果某个类别训练集只有几百个目标另一个类别有上万后续训练就要考虑加大这个类别的权重或者做类别级采样不然模型对少数类的召回会很差。这一步不修改文件只是确认数据状态花不了几分钟但能省下训练完才发现的痛苦返工。4. 用 YOLOv8 训练车辆检测命令参数与显存适配数据确认没问题后进入训练环节。我以当前主流的 YOLOv8 为例同时给 yolov5 的对应命令。两者都直接支持这个包里的 txt 标签不需要额外转换。4.1 安装与数据路径确认先装 ultralytics 库一行命令就带出 yolo 命令行工具# 安装 ultralytics会附带 yolo 命令 pip install ultralytics安装完成后在包含 data.yaml 的目录下执行训练命令。注意当前目录里最好有 images 和 labels 文件夹或者 data.yaml 里的路径能正确解析。如果路径不对最先报错的是 FileNotFoundError而不是训练相关错误。4.2 训练命令与关键参数说明用 COCO 预训练权重做微调收敛速度远比从零训练快。下面是 YOLOv8 的训练命令# 使用 yolov8n 预训练模型在车辆数据集上微调 yolo detect train datadata.yaml modelyolov8n.pt epochs100 batch16 imgsz640 patience20 projectvehicle_detect nameexp1参数含义data 指向 data.yamlmodel 是预训练权重或模型结构yolov8n.pt 是最轻量级模型显存占用小适合快速验证epochs 是完整训练轮数batch16 是单卡一次迭代喂入的图片数imgsz640 是输入图片缩放后的尺寸patience20 表示连续 20 轮验证集 mAP 没有提升就早停可以防止无效等待project 和 name 决定训练结果保存路径。如果你的显卡只有 8GB 显存把 batch 降到 8imgsz 降到 512 一般能跑起来如果还不行就换更小的模型结构或开梯度累积。yolov5 用户用另一套写法但参数对应关系差不多# yolov5 训练命令参数与 yolov8 类似 python train.py --data data.yaml --weights yolov5s.pt --batch-size 16 --img 640 --epochs 100 --patience 20yolov5 的 --weights 指定预训练权重--img 是输入尺寸--batch-size 是批大小。注意 yolov5 默认读取 labels 目录与 images 目录平级如果你的标签实际在别的文件夹里需要修改 data.yaml 中的 labels 字段或在代码里调整路径。显存不够时的第二套方案是使用--cache参数把图片提前加载到内存减少每轮从磁盘读取的开销但内存占用会升高适合已经缓存过一轮的数据。yolov8 中在训练命令后加cacheTrue可以开启yolov5 中则用--cache。如果你只是想先验证流程通不通把 epochs 设成 5训练完看 results.png 里 loss 有没有下降趋势即可没必要一上来就跑 100 轮。4.3 验证与模型导出训练结束后best.pt 保存在 project/name/weights 下。先跑验证拿到不带主观判断的精度值# 用最佳权重跑验证集 yolo detect val datadata.yaml modelvehicle_detect/exp1/weights/best.pt验证输出包含 mAP50、mAP50-95、precision、recall。mAP50 是 IOU 阈值为 0.5 时的平均精度车辆这种大目标一般训练到位后能到 0.7 以上mAP50-95 更严格会更低一些。训练过程中生成的 results.png 会实时画 loss 曲线和精度曲线打开看比盯终端输出高效得多。如果模型要接到其他框架导成 ONNX 很方便# 导出 ONNX 格式便于部署 yolo export modelvehicle_detect/exp1/weights/best.pt formatonnx imgsz640导出后可以用 onnxruntime 或 TensorRT 做推理。导出的前提是确保验证无误不然等于把一个错误模型打包了。我有个小习惯导出的 onnx 最好用 netron 打开看一眼输入输出形状避免动态批尺寸和固定尺寸混用导致部署时报维度错误。5. 避坑指南类别索引、归一化坐标、显存与分布不均的四个翻车点这一章写我实际踩过的坑每条按现象、原因、解决三步拆开。看完之后你大概率能避开这个包里最容易出现的四类问题。5.1 类别索引错位训练起来了预测框全乱现象训练正常跑loss 下降也很漂亮但推理时把卡车识别成行人或者一个小型车框里出现两个标签混淆矩阵对角线看不出明显优势。原因data.yaml 的 names 顺序和 txt 里的 class id 不一致。比如数据集中 class id2 实际是摩托车但 names 第 3 位写的是行人模型学到的是错位的映射关系。解决训练前随机取三五个 txt把行首 class id 对应的实体和实际图片画框核对。同时写一个脚本检查最大类别索引是否小于 nc# 检查所有 yolo 标签中是否出现越界类别索引 from pathlib import Path max_id 0 for txt in Path(labels_yolo).glob(*.txt): for line in txt.open(): if line.strip(): cls_id int(line.split()[0]) max_id max(max_id, cls_id) print(最大类别索引, max_id) # 最大索引应小于 nc否则一定有标签错位如果最大索引大于等于 nc说明标签里有非法类别编号。即使小于 nc也不能保证 names 顺序正确因为这是一种相对映射必须抽图人眼确认。5.2 归一化坐标写成像素坐标可视化全乱训练却不报错现象用绘图脚本画标签框跑到图片外面或者画出来比目标大一截。原因txt 里存的是相对于图像宽高的比例值但被看成像素坐标使用画框时忘了乘上 img_w 和 img_h。解决标注可视化脚本里显式做还原x1 int((xc - bw/2) * w)。另外YOLO 训练时如果标签坐标不在 0~1 内部分版本不会报错而是直接丢弃该标注导致 mAP 偏低所以更要在训练前用脚本扫描所有 txt检查是否有小于 0 或大于 1 的值。# 找出所有越界的归一化坐标 from pathlib import Path bad [] for txt in Path(labels_yolo).glob(*.txt): for i, line in enumerate(txt.open(), 1): parts line.split() if len(parts) 5: continue vals list(map(float, parts[1:5])) if any(v 0 or v 1 for v in vals): bad.append((txt.name, i)) print(越界标签, bad)这段脚本遍历所有标签文件把每一行的四个归一化值都检查一遍。任何值超出 0~1 都会被标记。如果有输出就说明标签有问题需要回退到 xml 重新生成 txt。5.3 显存不足batch 与 imgsz 的调法现象训练没跑几步终端直接报 CUDA out of memory程序退出。原因batch64、imgsz1280 的设置超过显卡显存常见于直接抄大模型训练参数。解决先降 batch一次降一半再降 imgsz。车辆检测不一定非要大分辨率640 已经能覆盖大多数场景。比如在 12GB 显卡上yolov8n 配 batch16、imgsz640 通常能跑如果还不行就降到 batch8、imgsz512。另外不要几轮就调一次参一次改一个变量不然不知道是哪个参数造成的。5.4 类别不均衡人少车多少数类 mAP 崩了现象验证时 bus、truck 的 mAP 不错但 person、bicycle 的 recall 很低。原因每类样本数相差大模型倾向于把目标判成数量多的类别。车辆检测数据里通常小车数量远多于行人和自行车训练时过采样大类别少数类很容易被淹没。解决训练前看一下 3.3 的统计结果如果 person 目标数只有 car 的十分之一可以对包含 person 的图片做复制增强让每类在训练中出现次数更接近。另一种做法是单独看验证集里每一类的 PR 曲线确定少数类是漏检还是误检再对这部分数据补充难例。标准 yolo 没有直接类别权重参数所以常见思路是在数据层面做平衡而不要指望调 loss 万能。6. 验证与进阶用混淆矩阵和随机抽检决定模型是否能用训练完成后不要只盯着 mAP先打开 project/name/confusion_matrix.png。这个图横轴是真实类别纵轴是预测类别对角线越亮说明分类越准。如果看到 person 和 bicycle 互相混淆说明两个类别在图片上确实常常相邻出现或者训练数据里小目标背景太复杂。这时别急着加模型结构先从数据侧看问题很多“模型不行”其实是数据质量或类别分布的问题。我常用的一个验证技巧是从验证集随机抽 20 张图把模型预测结果和真实标签画在一起预测框用红色真实框用绿色一眼能看出是漏检还是误检。下面这个脚本用训练好的 best.pt 对一张验证图做预测再叠加真实框# 红绿框对比红色为预测绿色为真实标签 from ultralytics import YOLO import cv2 model YOLO(vehicle_detect/exp1/weights/best.pt) img cv2.imread(images/val/img_0308_4095.jpg) results model(img, conf0.25, iou0.45) # 画预测框 for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) cls int(box.cls[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, f{model.names[cls]} {float(box.conf[0]):.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) # 画真实框 with open(labels_yolo/img_0308_4095.txt) as f: h, w img.shape[:2] for line in f: parts line.split() cls_id int(parts[0]) xc, yc, bw, bh map(float, parts[1:5]) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(pred_vs_truth.jpg, img)conf0.25 是置信度阈值只保留 25% 以上把握的检测结果iou0.45 是 NMS 的 IoU 阈值对密集目标可以适当调高但车辆场景 0.4~0.5 比较常见。如果红色和绿色大量重合说明检测结果可靠如果绿色框周围完全没有红色说明漏检如果红色框框住的是背景说明误检。从那以后我每次训练完新数据集都会强制走一遍这个流程先看混淆矩阵再抽几张验证图做红绿框对比。这个习惯救过我至少两次一次是数据里混入了大量重复截图模型把背景纹理当成了小型车一次是标签类别顺序错位最后在红绿框对比里发现预测结果总比真实框偏一类。先花半小时验证比训练完后做两周调参更有把握。希望帮到你。本文还有配套的精品资源点击获取