简介:面向YOLO系列算法训练与评估的车辆检测数据集,覆盖公交车、卡车、摩托车、行人、自行车、小型车六类目标,适合目标检测初学者及算法工程师快速开展模型训练、验证与测试,并可用于实际项目部署前的算法验证。数据集已按训练/验证划分,并附带data.yaml配置文件,可直接适配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等主流版本;同时提供VOC格式XML标注,便于在其他检测框架中迁移使用。压缩包共2000个XML标注文件,整体约205.35MB,标注内容包含目标类别与边界框坐标信息,其中XML文件保留VOC标准结构,并配套YOLO格式TXT标签及data.yaml配置,可大幅减少数据准备时间。目前已有98人浏览学习,资源结构清晰,适合需要规范车辆检测标注数据并直接训练模型的用户,省去自行标注与格式转换的精力,聚焦模型迭代与算法调优。整个数据集类别均衡、标注准确,方便快速搭建车辆检测基准。
1. YOLO系列算法目标检测数据集:9767张带标签车辆图,先搞清楚它能不能直接训练
这份 YOLO系列算法目标检测数据集解决了训练车辆检测模型时最不想碰的脏活:9767张道路场景图像,公交车、卡车、摩托车、行人、自行车、小型车六类目标全部带好标签,同时给 YOLO 格式的 txt 文件和 VOC 格式的 xml 文件,数据集内部还按 train/val/test 划好了目录,data.yaml 已经躺在里面。对急着复现 YOLOv5/YOLOv8/YOLOv9 甚至 YOLO11 流程的人来说,这是可以直接丢给训练脚本的输入。但别高兴太早,数据格式对不等于能直接训练,我拆这类资源时最常见的翻车点,全藏在目录结构、类别顺序和路径写法里。
2. 数据集结构与标签格式:先把 yolo 和 voc 两种标注分别读明白
2.1 目录结构:图像、txt、xml 和 data.yaml 的默认摆法
解压这个包之后,第一眼会看到 images 和 labels 两个大目录,labels 下又拆成 txt 和 xml 两个子目录。这种摆法在公开数据集里很常见:原图统一放 images,YOLO 格式标签放 labels/txt,VOC 格式标签放 labels/xml,然后各自下面再按 train、val、test 分子目录。文件名的规律也简单,比如 img_0308_4095.xml 对应的图像是 img_0308_4095.jpg,txt 也是同名,后缀不同。
刚开始用 YOLOv8 的人容易在这踩第一个坑:很多训练框架的默认逻辑是从图像目录找同名 labels 文件,而不是到另一个目录去读。你必须在训练前手动把 images/train 里面的图片和 labels/txt/train 里的 txt 摆成可被 dataloader 识别的结构,不然训练一开始就会报 "No labels found",或者更隐蔽地,loss 一直是 0 但没有任何报错。
整理后的标准目录结构大致如下,这份资源本身已经划分好集合,你要做的是把 labels 这层关系理顺:
| 目录 | 内容 | 训练时用途 |
|---|---|---|
| images/train | 训练图像 | YOLO 训练输入 |
| images/val | 验证图像 | 训练中评估 |
| images/test | 测试图像 | 最终验证 |
| labels/txt/train | YOLO 格式标签 | 训练标签 |
| labels/txt/val | 验证标签 | 评估标签 |
| labels/xml/train | VOC 格式标签 | 需要转格式时用 |
| data.yaml | 类别与路径配置 | 训练核心配置 |
把标签和图像放在同一级是常见做法。我更推荐在数据集根目录下新建 labels 总目录,把 txt 按 train/val/test 复制进去,保持 images 不动,然后训练时让 data.yaml 里的 train 指向 images/train,框架会自动去同级的 labels/train 找同名 txt。这个动作看起来简单,但能避免后面排错一小时也查不出原因的路径问题。
2.2 标签格式对照:一行 txt 坐标为什么是归一化的
YOLO 格式的每一行是五个数字,顺序是 class、x_center、y_center、width、height,后四个值全都除以了图像宽高,归一化到 0 到 1 之间。VOC 的 xml 则是绝对像素坐标,记录 bndbox 的 xmin、ymin、xmax、ymax。这是两种标注体系最核心的差别,也是很多人转换时算错的源头。
举例:图像宽度 1920,高度 1080,某辆小型车的框左上角是(960, 540),右下角是(1440, 810)。中心点横坐标是 (960 + 1440) / 2 = 1200,除以 1920 得 0.625;中心点纵坐标 (540 + 810) / 2 = 675,除以 1080 得 0.625;宽度是 1440 - 960 = 480,除以 1920 得 0.25;高度是 810 - 540 = 270,除以 1080 得 0.25。这一行就写:
0 0.625 0.625 0.25 0.25归一化的好处是不同分辨率的图像可以混在一起训练,不需要提前统一尺寸。坏处是人眼不容易判断坐标是否合理,越界问题往往要写脚本才能查出来。xml 里的 bndbox 如果是从标注工具直接导出的,大概率没归一化,所以很多工具链是先转成 YOLO 格式再训练。这个转换脚本我放到第五章讲。
2.3 data.yaml 文件的内容与修改点
data.yaml 是 YOLOv5 到 YOLO11 都认的配置文件,字段不多,但错一位类别就要出大事。典型内容长这样:
path: /absolute/path/to/dataset train: images/train val: images/val test: images/test nc: 6 names: 0: bus 1: truck 2: motorcycle 3: pedestrian 4: bicycle 5: small_carpath 表示数据集根目录,train、val、test 是相对路径。names 列表的索引顺序就是标签文件里的 class id,这个顺序必须和你 txt 里第一个数字完全对应。资源本身带的 data.yaml 大概率已经写好了类别,但你解压后的实际路径变了,path 这一行几乎一定要改。我一般建议把 path 改成绝对路径,不要依赖当前工作目录,避免从不同位置启动训练时出现找不到图像的怪问题。
2.4 快速校验:一张图一张图地查标签
在训练之前花五分钟跑一个校验脚本,能拦下大部分低级错误。下面的脚本只依赖标准库,遍历 labels/txt/train 目录,检查字段数、类别索引和坐标范围:
import os from pathlib import Path label_dir = Path("labels/txt/train") num_classes = 6 # 以 data.yaml 里的 nc 为准 bad = [] for txt in label_dir.glob("*.txt"): with open(txt, encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue parts = line.split() if len(parts) != 5: bad.append((txt.name, "field_count", line)) continue cls = int(parts[0]) values = [float(x) for x in parts[1:]] if cls < 0 or cls >= num_classes: bad.append((txt.name, "class_index", line)) if not all(0.0 <= x <= 1.0001 for x in values): bad.append((txt.name, "coord_range", line)) print(f"校验文件数: {len(list(label_dir.glob('*.txt')))}") print(f"异常记录数: {len(bad)}") for item in bad[:20]: print(item)脚本逻辑是按空格拆出五个字段,先看数量是不是 5,再判断 class 是否落在 0 到 nc-1,最后确认四个坐标是否在归一化范围内。参数 1.0001 不是随便写的,浮点计算有时会给出 1.0000000002 这种值,严格等于 1 的判断会把正常数据误杀。如果你跑出来有异常,先不要急着训练,找对应 xml 对一下原始标注,多半是转换时没除对图像宽高。
3. 用 YOLOv8 把训练跑起来:环境、目录归位和一组不翻车的参数
3.1 环境安装:虚拟环境别偷懒
拆过几个数据集之后我的习惯是,任何资源解压后第一步永远是建虚拟环境。YOLOv8 训练的依赖主要是 torch、torchvision、ultralytics,一个环境里同时装太多版本的深度学习库,最后往往会因为 torch 版本和 CUDA 对不上而返工。先干活后装环境,翻车了再排查,那是在给自己设坑。
python -m venv venv source venv/bin/activate pip install --upgrade pip pip install ultralytics如果有 N 卡 GPU,再补 torch 相关。常见做法是先确认显卡驱动支持的 CUDA 版本,然后通过对应 index 安装匹配的 torch,避免 pip 默认拉的是 CPU 版本。没有 GPU 也别慌,YOLOv8n 用 CPU 也能训,只是慢,epoch 数调少一点也能看到完整流程跑通。
3.2 目录归位:让图片和标签躺在同一层
资源里的图片在 images/train,标签在 labels/txt/train,这种两级隔开的结构不直接符合 ultralytics 的默认查找逻辑。最省事的做法是重新组织一下,把训练集和验证集的标签复制到与图片平级的 labels 目录里:
# 在数据集根目录执行 mkdir -p labels/train labels/val cp labels/txt/train/*.txt labels/train/ cp labels/txt/val/*.txt labels/val/然后确认 images/train 里放的是所有训练图像,images/val 里是所有验证图像。框架读 data.yaml 里 train 指向的图片目录,然后自动在同一级目录下找 labels 目录里的同名 txt。如果你不想复制原文件,用 ln -s 做软链接也可以,但要小心 Windows 上软链接权限的坑,复制更省心。
3.3 修改 data.yaml:路径和 names 一个都不能错
回到 data.yaml,path 改成数据集根目录的绝对路径,train 和 val 保持相对路径,names 列表确认是从 0 开始。这份资源的类别是公交车、卡车、摩托车、行人、自行车、小型车,但注意,我强烈建议你不要根据标题里的顺序凭空猜 class id 的排列,要打开任意一个 txt 文件,看第一列数字代表哪类目标,再回 xml 里对一下 bndbox 对应的名称。class 索引根本没有标准顺序这回事,完全取决于生成标签时用的类别表。
path: /home/user/datasets/vehicle_dataset train: images/train val: images/val nc: 6 names: 0: bus 1: truck 2: motorcycle 3: pedestrian 4: bicycle 5: small_car如果你发现标签里小型车占大多数,摩托车很少,要把摩托车排在 0 还是 5 对训练结果没有影响,但对 loss 曲线的解读有影响。类别顺序本身就是一种信息,保持稳定比追求顺序美观更重要。
3.4 训练命令与关键参数
环境就绪、目录归位、data.yaml 改完,就可以跑训练了。基于 YOLOv8 的最简命令:
yolo detect train \ data=/path/to/dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ batch=16 \ imgsz=640 \ device=0逐个说参数。data 指向我们刚改好的配置文件;model=yolov8n.pt 表示用 nano 版本预训练权重作为起点,继续在自己的数据集上 fine-tune,通常比从零训练收敛快很多;epochs=100 是一次合理默认值,不是越多越好,训练资源有限时 50 轮也能看到可用结果;batch=16 是 8GB 显存左右比较稳的数值,如果显存不足,我会降到 8 或者把 imgsz 降到 416;device=0 指定用第一块 GPU,没有 GPU 就写 device=cpu。
训练开始后重点看两组指标:box_loss 和 cls_loss 有没有稳定下降,以及每轮结束时的 mAP50 是不是在往上走。如果前 20 轮 mAP50 一直是 0,大概率不是参数问题,而是标签没配对成功,也就是 4.1 节要讲的 No labels found 问题。
3.5 断点续训、早停与验证输出
训练中途断电或显存被打爆是常态,YOLOv8 支持断点续训,不用从头再来:
yolo detect train resume=True设置早停也可以有效节省时间,在 data.yaml 所在目录创建或修改超参数文件,或者在训练命令里加参数:patience=20,表示验证指标连续 20 轮没有改善就自动停止。这个选项对中小数据集尤其好用,省去盲等。
训练完成后,用验证集评估 best.pt:
yolo detect val \ data=/path/to/dataset/data.yaml \ model=runs/detect/train/weights/best.pt输出结果重点看三个指标:mAP50、mAP50-95、每类别的 AP。mAP50 是 IoU 阈值 0.5 时的平均精度,数值比较容易刷高;mAP50-95 是 0.5 到 0.95 多个阈值的平均,更能反映定位质量。验证时容易踩的坑是用了训练集来评估,指标虚高得离谱,所以 data.yaml 里 val 一定要指到真实的验证图像目录。
| 指标 | 含义 | 关注度 |
|---|---|---|
| Precision | 预测框中正确的比例 | 中等 |
| Recall | 真实目标中被召回的比例 | 重要 |
| mAP50 | IoU=0.5 时的平均 AP | 基本参考 |
| mAP50-95 | 多阈值平均 AP | 核心参考 |
4. 避坑与踩坑记录:五个车辆检测训练的高频翻车现场
4.1 训练直接报 No labels found 或 loss 恒为零
现象:命令敲下去之后,数据加载阶段报错 "No labels found",或者不报错但前几轮 loss 一直是 0.0000,验证集 mAP 也为 0。原因:最常见的是 labels 目录和 images 目录没有摆成 YOLO 默认的结构,训练框架按图片路径去找同名标签文件时找不到任何 txt。另一个隐蔽原因是标签文件扩展名不对,比如被存成了 .txt.txt 或者 .text。解决:先执行find . -name "*.txt" | head -5确认标签文件真实路径,再把标签复制或移动到 images 同级的 labels 目录下,并保证文件名和图片完全同名。如果你发现文件后缀不对,批量重命名:
for f in *.txt.txt; do mv "$f" "${f%.txt.txt}.txt"; done4.2 类别索引错位导致召回率极低
现象:训练过程看起来正常,loss 在下降,但验证集上摩托车全部被识别成小型车,行人和自行车互相错认,召回率惨不忍睹。原因:转换标签时用的类别映射表和 data.yaml 里的 names 顺序不一致。举个例子,xml 里写的是 motorcycle,但生成 txt 时把第 2 类定义成了 truck,那么训练时摩托车图像上的框都带着错标签,模型当然学错。解决:从标签文件的第一行开始,找到一张只有单个目标的样本,解析出 class id,然后打开对应 xml 看真实名称,建立一个干净的类别对照表。这条规则我建议写进自己的项目模板里,而不是靠记忆。
4.3 摩托车类别 AP 低到离谱
现象:样本量较少的类别,比如摩托车或公交车,训练结束时 AP 只有 0.1 上下,其他类别 0.7 以上。原因:类别不平衡。车辆检测数据集里小型车往往占一半以上,摩托车数量可能只有几百张,模型把有限的表示能力全用在拟合大类上。解决:先别急着删数据,看看每一类的正样本框数量,如果最少类不足总框数的 5%,考虑两种方案:一是用class_weight=True或者给 losses 里 cls_loss 加每个类别的权重,让模型更重视小类;二是做针对性增广,水平翻转、轻微旋转、色彩抖动只加在包含摩托车的图像上。增广要小心,阴影、雨天场景过度变化可能适得其反。
4.4 标签坐标越界或出现负值
现象:校验脚本检查出某个 txt 里有 x_center 是 -0.02,width 是 1.2,个别框中心点跑到图像外面。原因:原始 VOC 标注里 bndbox 的坐标精度不够,或者图像经过裁剪后标签没有同步更新,更常见的是转换脚本里分子写反了。比如把 xmin - xmax 除以图像宽度,得到负宽度。解决:校验脚本不能只打印,得直接修复。越界的值 clamp 到合理范围,负宽度用绝对值重算:
x_center = min(max(x, 0.0), 1.0) w = abs(w)但注意,如果一个框 width 小于 0.001,说明原始标注基本失效,强行保留会让训练过程抖动,直接删掉这一行更稳。
4.5 行人自行车紧邻导致漏检和多检
现象:验证时看到行人旁边有自行车,模型要么漏掉行人,要么把人行道上推车的人识别成自行车,表面看 mAP 还行,实际部署到街拍场景就翻车。原因:这类目标在图像中的比例往往很小,且高度重叠,标签中的遮挡和被截断目标没有单独区分。解决:先看 xml 里有没有 truncated 或 occluded 属性,如果有,训练时可以选择保留但控制权重,或者干脆过滤掉遮挡比例过高的目标框。另一个经验是不要盲目调低置信度阈值,0.25 的 conf 与 0.1 的 conf 在小目标上差距很大,先保持默认,分析结果再决定。
5. 从 VOC XML 到 YOLO TXT 的转换脚本:包括增广的标签同步
5.1 为什么要保留两套标签
一份资源同时给 yolo 和 voc 两种格式,很多人觉得浪费容量。实际不是,两种格式各有用途。YOLO 系列的训练直接吃 txt,读取效率高、坐标归一化;VOC 格式则是与标注工具、可视化工具和旧模型兼容的桥梁。要用 LabelImg 复查某个标签,打开 xml 才是最方便的;要跑需要绝对像素框的检测框架,xml 也能直接解析。训练数据在增广之后,txt 需要同步重算,xml 反而不需要一直维护,所以转换脚本值得保留在项目里,你自己改数据时也用得上。
5.2 不依赖第三方库的 XML 转 TXT 脚本
下面这个脚本用标准库的 xml.etree.ElementTree 解析,零外部依赖,直接遍历某个存 xml 的目录,输出同名 txt 到指定目录。前提是 xml 里 bndbox 的标签结构是标准的 VOC 风格。
import xml.etree.ElementTree as ET from pathlib import Path xml_dir = Path("labels/xml/train") txt_dir = Path("labels/txt_from_xml/train") txt_dir.mkdir(parents=True, exist_ok=True) # 类别映射,务必与 data.yaml 的 names 顺序一致 class_map = { "bus": 0, "truck": 1, "motorcycle": 2, "pedestrian": 3, "bicycle": 4, "small_car": 5, } for xml_file in xml_dir.glob("*.xml"): tree = ET.parse(xml_file) root = tree.getroot() filename = root.findtext("filename") or xml_file.stem size = root.find("size") img_w = int(size.findtext("width")) img_h = int(size.findtext("height")) lines = [] for obj in root.iter("object"): name = obj.findtext("name") if name not in class_map: continue bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("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 # 过滤无效框:宽或高小于等于0的不写进 txt if w <= 0 or h <= 0: continue lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_txt = txt_dir / f"{xml_file.stem}.txt" out_txt.write_text("\n".join(lines), encoding="utf-8")逻辑说明:用 root.iter("object") 而不是 findall 更稳,能兼容 xml 里 object 嵌套在别处的情况。size 节点里读的是图像原始宽高,这也是归一化分母。转换后保留六位小数足够,YOLO 训练用 float 读入,精度损失可忽略。参数方面,class_map 是整个脚本的核心,如果 xml 里有 "car" 而你的 mapping 里没有,这一行会被跳过,训练时对应目标就丢了,所以要提前把 voc 类别名称和数据集的 names 对齐。
5.3 水平翻转后标签同步
训练前做数据增广时,最常做也最安全的是水平翻转。翻转以后图像左右镜像,框的 x_center 要变为 1 - x_center,y_center 和宽高不变。写一个小脚本批量处理:
import random from pathlib import Path src_txt = Path("labels/train") dst_txt = Path("labels/train_flip") dst_txt.mkdir(parents=True, exist_ok=True) for txt in src_txt.glob("*.txt"): lines = [] for line in txt.read_text(encoding="utf-8").splitlines(): parts = line.split() if len(parts) != 5: continue cls, x, y, w, h = parts[0], float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) lines.append(f"{cls} {1 - x:.6f} {y:.6f} {w:.6f} {h:.6f}") dst_txt.joinpath(txt.name).write_text("\n".join(lines), encoding="utf-8")翻转后的 txt 不能直接覆盖原来的文件,除非你确定原始 txt 已经备份。我通常会生成一个 train_flip 目录,和原标签目录一起参与训练,相当于把训练集数量翻了一倍。翻转对这种对称性强的车辆目标很友好,行人、自行车方向各异时效果稍差,但一般不会让模型变笨。
5.4 类别映射表维护:一个坑一张表
维护车辆检测项目的类别映射表时,我吃过一次大亏。当时新版本数据里多了一个 "van" 类,我图省事把它插在列表当中,没注意到它后面的类别 id 全部后移了一位,结果已经生成的旧标签全错位。从那以后,所有涉及类别表变动的操作,都强制走两步:第一步备份旧的 class_map;第二步写一个小脚本把新旧映射表做 diff,输出每个类别 id 的对应关系,确认没有位移再继续生成标签。
6. 训练前的最后把关:写一个标签分布体检脚本
训练流程跑顺之后,我反而更愿意把时间花在训练前 30 分钟。这一步用脚本统计标签分布,能提前发现类别不平衡、空标签图像、标签框数量异常这三个最影响结果的问题。
from pathlib import Path from collections import Counter label_train = Path("labels/train") image_train = Path("images/train") cls_counter = Counter() bbox_counter = [] empty_images = 0 total_boxes = 0 for txt in label_train.glob("*.txt"): boxes = 0 for line in txt.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue parts = line.split() if len(parts) != 5: continue cls = int(parts[0]) cls_counter[cls] += 1 boxes += 1 total_boxes += 1 bbox_counter.append(boxes) if boxes == 0: empty_images += 1 names = ["bus", "truck", "motorcycle", "pedestrian", "bicycle", "small_car"] for i in range(len(names)): print(f"类别 {i} ({names[i]}): {cls_counter.get(i, 0)} 个框") print(f"总框数: {total_boxes}") print(f"空标签图: {empty_images}") print(f"单图最大框数: {max(bbox_counter)} 最小框数: {min(bbox_counter)} 平均: {total_boxes / max(len(bbox_counter), 1):.1f}")统计结果怎么用?如果摩托车框数不足总框数的 3%,训练前就要准备做增广或加类别权重,别等训练完再看 tensorboard 后悔。如果空标签图像超过 50 张,先查是不是漏标,正常带标签数据集的空标签比例很低。如果某一张图有超过 40 个框,场景可能过于拥挤,训练时可以考虑把它降采样或者裁切。这个体检脚本我一般会放进项目的scripts/目录,改数据集后重新跑一遍,把输出贴到笔记里当版本记录。
从那以后,我每次拿到带标签数据集,都强制走一遍标签分布体检,再决定要不要调损失权重。这个动作帮我省下的返工时间,比训练代码本身多得多。希望帮到你。
本文还有配套的精品资源,点击获取