简介:面向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> <height>;VOC 格式是 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', encoding='utf-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(parents=True, exist_ok=True) val_dir.mkdir(parents=True, exist_ok=True) 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 表示 bus,5 表示 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 data=data.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640 patience=20 project=vehicle_detect name=exp1参数含义:data 指向 data.yaml;model 是预训练权重或模型结构,yolov8n.pt 是最轻量级模型,显存占用小,适合快速验证;epochs 是完整训练轮数;batch=16 是单卡一次迭代喂入的图片数;imgsz=640 是输入图片缩放后的尺寸;patience=20 表示连续 20 轮验证集 mAP 没有提升就早停,可以防止无效等待;project 和 name 决定训练结果保存路径。如果你的显卡只有 8GB 显存,把 batch 降到 8,imgsz 降到 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 中在训练命令后加cache=True可以开启,yolov5 中则用--cache。如果你只是想先验证流程通不通,把 epochs 设成 5,训练完看 results.png 里 loss 有没有下降趋势即可,没必要一上来就跑 100 轮。
4.3 验证与模型导出
训练结束后,best.pt 保存在 project/name/weights 下。先跑验证,拿到不带主观判断的精度值:
# 用最佳权重跑验证集 yolo detect val data=data.yaml model=vehicle_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 model=vehicle_detect/exp1/weights/best.pt format=onnx imgsz=640导出后可以用 onnxruntime 或 TensorRT 做推理。导出的前提是确保验证无误,不然等于把一个错误模型打包了。我有个小习惯:导出的 onnx 最好用 netron 打开看一眼输入输出形状,避免动态批尺寸和固定尺寸混用导致部署时报维度错误。
5. 避坑指南:类别索引、归一化坐标、显存与分布不均的四个翻车点
这一章写我实际踩过的坑,每条按现象、原因、解决三步拆开。看完之后,你大概率能避开这个包里最容易出现的四类问题。
5.1 类别索引错位:训练起来了,预测框全乱
现象:训练正常跑,loss 下降也很漂亮,但推理时把卡车识别成行人,或者一个小型车框里出现两个标签,混淆矩阵对角线看不出明显优势。
原因:data.yaml 的 names 顺序和 txt 里的 class id 不一致。比如数据集中 class id=2 实际是摩托车,但 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,程序退出。
原因:batch=64、imgsz=1280 的设置超过显卡显存,常见于直接抄大模型训练参数。
解决:先降 batch,一次降一半;再降 imgsz。车辆检测不一定非要大分辨率,640 已经能覆盖大多数场景。比如在 12GB 显卡上,yolov8n 配 batch=16、imgsz=640 通常能跑;如果还不行,就降到 batch=8、imgsz=512。另外,不要几轮就调一次参,一次改一个变量,不然不知道是哪个参数造成的。
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, conf=0.25, iou=0.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)conf=0.25 是置信度阈值,只保留 25% 以上把握的检测结果;iou=0.45 是 NMS 的 IoU 阈值,对密集目标可以适当调高,但车辆场景 0.4~0.5 比较常见。如果红色和绿色大量重合,说明检测结果可靠;如果绿色框周围完全没有红色,说明漏检;如果红色框框住的是背景,说明误检。
从那以后,我每次训练完新数据集,都会强制走一遍这个流程:先看混淆矩阵,再抽几张验证图做红绿框对比。这个习惯救过我至少两次:一次是数据里混入了大量重复截图,模型把背景纹理当成了小型车;一次是标签类别顺序错位,最后在红绿框对比里发现预测结果总比真实框偏一类。先花半小时验证,比训练完后做两周调参更有把握。希望帮到你。
本文还有配套的精品资源,点击获取