简介面向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 两个子目录。这种摆法在公开数据集里很常见原图统一放 imagesYOLO 格式标签放 labels/txtVOC 格式标签放 labels/xml然后各自下面再按 train、val、test 分子目录。文件名的规律也简单比如 img_0308_4095.xml 对应的图像是 img_0308_4095.jpgtxt 也是同名后缀不同。刚开始用 YOLOv8 的人容易在这踩第一个坑很多训练框架的默认逻辑是从图像目录找同名 labels 文件而不是到另一个目录去读。你必须在训练前手动把 images/train 里面的图片和 labels/txt/train 里的 txt 摆成可被 dataloader 识别的结构不然训练一开始就会报 No labels found或者更隐蔽地loss 一直是 0 但没有任何报错。整理后的标准目录结构大致如下这份资源本身已经划分好集合你要做的是把 labels 这层关系理顺目录内容训练时用途images/train训练图像YOLO 训练输入images/val验证图像训练中评估images/test测试图像最终验证labels/txt/trainYOLO 格式标签训练标签labels/txt/val验证标签评估标签labels/xml/trainVOC 格式标签需要转格式时用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, encodingutf-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.yamlpath 改成数据集根目录的绝对路径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 \ modelyolov8n.pt \ epochs100 \ batch16 \ imgsz640 \ device0逐个说参数。data 指向我们刚改好的配置文件modelyolov8n.pt 表示用 nano 版本预训练权重作为起点继续在自己的数据集上 fine-tune通常比从零训练收敛快很多epochs100 是一次合理默认值不是越多越好训练资源有限时 50 轮也能看到可用结果batch16 是 8GB 显存左右比较稳的数值如果显存不足我会降到 8 或者把 imgsz 降到 416device0 指定用第一块 GPU没有 GPU 就写 devicecpu。训练开始后重点看两组指标box_loss 和 cls_loss 有没有稳定下降以及每轮结束时的 mAP50 是不是在往上走。如果前 20 轮 mAP50 一直是 0大概率不是参数问题而是标签没配对成功也就是 4.1 节要讲的 No labels found 问题。3.5 断点续训、早停与验证输出训练中途断电或显存被打爆是常态YOLOv8 支持断点续训不用从头再来yolo detect train resumeTrue设置早停也可以有效节省时间在 data.yaml 所在目录创建或修改超参数文件或者在训练命令里加参数patience20表示验证指标连续 20 轮没有改善就自动停止。这个选项对中小数据集尤其好用省去盲等。训练完成后用验证集评估 best.ptyolo detect val \ data/path/to/dataset/data.yaml \ modelruns/detect/train/weights/best.pt输出结果重点看三个指标mAP50、mAP50-95、每类别的 AP。mAP50 是 IoU 阈值 0.5 时的平均精度数值比较容易刷高mAP50-95 是 0.5 到 0.95 多个阈值的平均更能反映定位质量。验证时容易踩的坑是用了训练集来评估指标虚高得离谱所以 data.yaml 里 val 一定要指到真实的验证图像目录。指标含义关注度Precision预测框中正确的比例中等Recall真实目标中被召回的比例重要mAP50IoU0.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_weightTrue或者给 losses 里 cls_loss 加每个类别的权重让模型更重视小类二是做针对性增广水平翻转、轻微旋转、色彩抖动只加在包含摩托车的图像上。增广要小心阴影、雨天场景过度变化可能适得其反。4.4 标签坐标越界或出现负值现象校验脚本检查出某个 txt 里有 x_center 是 -0.02width 是 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(parentsTrue, exist_okTrue) # 类别映射务必与 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), encodingutf-8)逻辑说明用 root.iter(object) 而不是 findall 更稳能兼容 xml 里 object 嵌套在别处的情况。size 节点里读的是图像原始宽高这也是归一化分母。转换后保留六位小数足够YOLO 训练用 float 读入精度损失可忽略。参数方面class_map 是整个脚本的核心如果 xml 里有 car 而你的 mapping 里没有这一行会被跳过训练时对应目标就丢了所以要提前把 voc 类别名称和数据集的 names 对齐。5.3 水平翻转后标签同步训练前做数据增广时最常做也最安全的是水平翻转。翻转以后图像左右镜像框的 x_center 要变为 1 - x_centery_center 和宽高不变。写一个小脚本批量处理import random from pathlib import Path src_txt Path(labels/train) dst_txt Path(labels/train_flip) dst_txt.mkdir(parentsTrue, exist_okTrue) for txt in src_txt.glob(*.txt): lines [] for line in txt.read_text(encodingutf-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), encodingutf-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(encodingutf-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/目录改数据集后重新跑一遍把输出贴到笔记里当版本记录。从那以后我每次拿到带标签数据集都强制走一遍标签分布体检再决定要不要调损失权重。这个动作帮我省下的返工时间比训练代码本身多得多。希望帮到你。本文还有配套的精品资源点击获取