
简介一套基于VOC格式的车辆检测数据集面向计算机视觉与自动驾驶方向的入门开发者解决车辆目标检测任务中标注数据不足的问题。数据涵盖bus、car、suv、taxi、truck五类常见车型标签同时提供txt与xml两种格式分别存放于独立文件夹中可无缝衔接YOLO系列与VOC格式训练流程。资源包共2000个文件主要包括700个txt、699个xml和699个jpg其中jpg为原始车辆图像txt为YOLO格式标注xml为VOC格式标注图像与标签一一对应。整体压缩包大小466.3MB便于下载后直接开展数据划分和模型训练。目前已有961人学习下载适合需要标准车辆检测数据集的个人开发者或小型团队使用。数据集的目录结构清晰标注信息完整既可用于模型训练与验证也可作为目标检测算法测评的基准数据。对于正在从事智能交通、辅助驾驶或车辆识别项目的研究者这份数据能省去手动标注的时间快速启动实验。1. VOC 各种类型车辆检测数据集先解决标签统一再谈训练VOC 各种类型车辆检测数据集是我处理车辆检测任务时最常拿来做模型验证的一类数据。它覆盖 bus、car、suv、taxi、truck 五个常见车辆类别每张图片同时给出 txt 和 xml 两种格式的标注文件分别存在两个文件夹里解决了刚上手跑检测时最头痛的标签格式混用问题。很多人拿到这类数据集第一反应是找模型结果却在读取标注和类别映射上先卡了几天。这份数据集更适合用 YOLO 系列训练自定义车辆检测模型的读者比如停车场出入统计、道路流量监测。要把这份数据集真正用起来需要做两件事统一类别顺序、按 train/val 合理划分下面按完整流程走一遍。2. 双格式标签对应的三个要点txt 里是归一化坐标xml 里是像素坐标2.1 目录结构与文件命名约定先看目录结构。解压后一般是三个平级目录images、labels_txt、labels_xml。vehicle_dataset/ ├── images/ # 原图 │ ├── car_125.jpg │ ├── SUV_77.jpg │ └── ... ├── labels_txt/ # YOLO 格式标注 │ ├── car_125.txt │ ├── SUV_77.txt │ └── ... └── labels_xml/ # Pascal VOC 格式标注 ├── car_125.xml ├── SUV_77.xml └── ...命名规则是图片名.jpg对应图片名.txt和图片名.xml三个目录里同名文件才是同一张图的完整标注。文件名的前缀如car_、SUV_是人工整理的样本类别标识但训练时不能靠文件名猜类别一切以标注文件内容为准。同时保留两份标注常见原因是标注工具直接导出 VOC xml再通过脚本转出 YOLO txt 给训练用。xml 保留绝对像素坐标方便画框复查txt 提供归一化坐标YOLO 训练时可以直接读取。训练阶段我一般只看 txt检查漏标或画可视化时才回头用 xml因为 xml 的 bndbox 可以直接叠在图上不需要反归一化。2.2 txt 标签五列class、cx、cy、w、h用一段小脚本读 txt 标注先确认内容结构。def read_yolo_label(txt_path): with open(txt_path, r, encodingutf-8) as f: lines f.readlines() for line in lines: line line.strip() if not line: continue cls, cx, cy, w, h line.split() cls int(cls) cx, cy, w, h map(float, (cx, cy, w, h)) print(fclass_id{cls}, center({cx:.4f}, {cy:.4f}), size({w:.4f}, {h:.4f})) read_yolo_label(labels_txt/car_125.txt)每行代表一个目标框第一列是类别 id后面四列依次是归一化后的中心点 x、中心点 y、框宽 w、框高 h。归一化指的是相对图片真实宽高的比例取值应该在 0 到 1 之间。打印结果全部落在这个区间说明标注坐标没有异常如果出现大于 1 的值多半是转换脚本除错了图片尺寸后面避坑章节会专门处理。2.3 xml 标签绝对坐标与 txt 互换xml 是 Pascal VOC 标准结构看一个car_125.xml的典型内容。annotation folderimages/folder filenamecar_125.jpg/filename size width1920/width height1080/height depth3/depth /size object namecar/name bndbox xmin387/xmin ymin321/ymin xmax782/xmax ymax540/ymax /bndbox /object /annotation解析 xml 时读取 size 节点下的宽高和每个 object 的 bndbox 坐标。import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) for obj in root.iter(object): name obj.find(name).text bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) print(f{name}: ({xmin}, {ymin}, {xmax}, {ymax})) parse_voc_xml(labels_xml/car_125.xml)xml 里是绝对像素坐标和 txt 的归一化坐标转换关系很简单cx (xmin xmax) / 2 / width cy (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / heightwidth 和 height 必须是 xml size 节点里的值。如果某张图 xml 里的尺寸和图片实际宽高不一致所有坐标都会错位。这个坑在数据集中偶尔出现训练前最好统一校验。3. 从原始目录到 YOLO 训练结构类别映射与数据划分3.1 类别映射表与 names 配置这份数据集的五个类别在 txt 和 xml 里都是直接用名称或 id 标识的。训练 YOLO 时模型只认数字编号所以需要先建立一张固定的类别映射表。类别名class_id说明bus0公交车car1小轿车suv2城市 SUVtaxi3出租车truck4货车映射表一旦定下来整个训练周期都不要改。如果后续想增加难例挖掘或 ignore 区域可以在这个表后面继续扩展但已有 id 不要重新排序。训练前把映射关系写进 data.yamlpath: /absolute/path/to/vehicle_dataset train: images/train val: images/val nc: 5 names: 0: bus 1: car 2: suv 3: taxi 4: truckpath 尽量用绝对路径避免相对路径带来的目录解析问题。names 顺序必须和 txt 里的 class_id 严格对齐这里写反了训练不会报错但预测结果会全部错位。3.2 train/val 划分脚本拿到数据后不能直接拿全部图片训练需要划出一部分做验证。我习惯用脚本随机划分并固定随机种子。import os import random import shutil random.seed(42) # 固定随机种子保证结果可复现 image_src images label_src labels_txt train_img_dir images/train val_img_dir images/val train_label_dir labels_txt/train val_label_dir labels_txt/val for d in [train_img_dir, val_img_dir, train_label_dir, val_label_dir]: os.makedirs(d, exist_okTrue) imgs [f for f in os.listdir(image_src) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(imgs) val_ratio 0.1 val_count max(1, int(len(imgs) * val_ratio)) val_imgs imgs[:val_count] train_imgs imgs[val_count:] def move_files(img_list, img_dst, label_dst): for img_name in img_list: shutil.copy2(os.path.join(image_src, img_name), os.path.join(img_dst, img_name)) stem os.path.splitext(img_name)[0] txt_src os.path.join(label_src, stem .txt) if os.path.exists(txt_src): shutil.copy2(txt_src, os.path.join(label_dst, stem .txt)) move_files(val_imgs, val_img_dir, val_label_dir) move_files(train_imgs, train_img_dir, train_label_dir) print(ftrain images: {len(train_imgs)}, val images: {len(val_imgs)})这里用的是 copy2 而不是 move保留原始目录后面跑标签体检还会用到。划分依据是图片文件名随机打乱不是顺序切分否则监控视频连续帧会被分到同一个集合里导致验证集和训练集分布过于相似。val_ratio 取 0.1如果数据量小可以放宽到 0.2优先保证验证集每类都有样本。3.3 划分后类别统计验证划分完不要直接训练先看一眼每个类别在训练集和验证集里的数量分布。from collections import Counter def count_labels(label_dir): counter Counter() for txt_name in os.listdir(label_dir): with open(os.path.join(label_dir, txt_name)) as f: for line in f: line line.strip() if not line: continue cls int(line.split()[0]) counter[cls] 1 return counter train_cnt count_labels(train_label_dir) val_cnt count_labels(val_label_dir) for cls, name in [(0, bus), (1, car), (2, suv), (3, taxi), (4, truck)]: print(f{name}: train{train_cnt.get(cls, 0)}, val{val_cnt.get(cls, 0)})打印结果能直观看到每类在 train 和 val 中的占比。如果验证集里某个类别数量为 0这个类的指标就没有参考价值。truck 在真实街景里占比偏低是常见现象。如果差距过大不要用简单随机划分改成按类别分层的抽样脚本。这一步虽然多花两分钟但能避免训练完才发现某个类完全没被验证。4. 避坑记录路径错位、类别错乱、小目标漏检4.1 训练日志一直显示 0 images现象运行训练命令后日志显示Scanning ... 0 imagesloss 完全不变。原因data.yaml 里的 train 和 val 路径写的是相对路径比如train: images/train但当前执行命令的目录不在数据集根目录下再一个常见原因是图片目录和标签目录不在同一个父目录YOLO 默认图片和标签同级才能自动匹配。解决把 data.yaml 里的 path 改成绝对路径并确认目录结构已经整理成vehicle_dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels_txt/ ├── train/ └── val/同时注意图片目录叫images/train标签目录也要叫labels_txt/train名字不同会找不到对应标签。我一般配置完直接用 ls 检查一次路径确认有图片再启动训练。4.2 txt 中出现大于 1 或小于 0 的坐标现象训练 loss 一直不降val mAP 长时间为 0打印某条标注发现 cx1.37。原因txt 里存的不是归一化坐标可能是脚本误把绝对像素值直接写进了 txt也可能转换时用了错误的图片尺寸比如 xml 里是 1920x1080但原图实际只有 800x600坐标全部超界。解决写一个清洗脚本把异常字段过滤掉。import os bad 0 for txt_name in os.listdir(label_src): path os.path.join(label_src, txt_name) kept_lines [] with open(path) as f: for line in f: t line.strip() if not t: continue parts list(map(float, t.split())) if len(parts) ! 5: bad 1 continue cls, cx, cy, w, h parts if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): bad 1 continue kept_lines.append(t) with open(path, w) as f: f.write(\n.join(kept_lines)) print(ffiltered {bad} bad lines)脚本检查两点字段数量必须是 5 个坐标范围必须在 [0,1] 之间。过滤后同步做一次可视化不要直接训练。这类脏数据在车辆数据集里经常出现尤其是多目标图片一张图里有 5、6 辆车其中一两个框写反就会拉低整体指标。4.3 names 顺序和 txt class_id 对不上现象模型把 car 预测成 truck混淆矩阵里 bus 和 suv 混在一起但训练过程没有报错。原因txt 第一列的 class_id 是数字YOLO 只认这个数字不认 names 字符串。如果 data.yaml 里 names 列表顺序和 txt 的 id 顺序不一致模型训练时标签就已经错位了。解决回到 3.1 的映射表把 names 严格按 0-4 顺序写成 bus、car、suv、taxi、truck。如果已经训练了一版需要重新处理 txt 中的类别 id。这里有个血泪经验拿到任何数据集都不要直接信任 data.yaml 里的 names先随机抽几张图把标注框画出来确认类别对得上再开始训练。类别顺序错了mAP 是正常的但预测出来的名字全是乱的。4.4 小目标漏检和样本不均衡现象近处车辆检测效果很好远处车辆漏检mAP50 到后期不再提升。原因车辆检测场景里远处的车往往只有几十个像素高属于小目标而 bus、truck 这类大车框数量多模型容易偏向学习大目标。如果训练参数还按默认 imgsz640小目标特征在降采样后会丢失。解决把 imgsz 从 640 提到 960 或 1280同时开启 mosaic 和多尺度训练。显存不够时降低 batch搭配梯度累积。判断标准看验证集结果如果 mAP50 提升但 mAP50:95 没变化说明框的位置精度不够优先检查 dfl_loss 是否下降如果 mAP50 也在原地说明目标特征没有进到模型里大概率是输入分辨率不够。这个场景下用 YOLOv8s 起步比 n 版本效果好不少。这四条坑在实际项目里经常连着出现。我处理车辆数据集时固定顺序是路径检查 → 坐标清洗 → 可视化验证 → 类别映射 → 再训练。顺序不要反路径错了训练起不来坐标错了 loss 先降后震荡类别映射错了模型训得再好预测结果也全是张冠李戴。5. YOLOv8 训练自己的车辆数据集参数设置与验证评估5.1 准备训练配置与启动命令划分好数据后写一份完整的 data.yaml这是训练前的最后一步。path: /abs/path/vehicle_dataset train: images/train val: images/val nc: 5 names: [bus, car, suv, taxi, truck]启动训练yolo detect train datavehicle_dataset.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0常用参数参考参数推荐值适用场景modelyolov8n.pt / yolov8s.pt样本量不大时先用 n 跑通流程epochs50 起步100 看收敛先跑 50 个 epoch 做基线imgsz640 / 960 / 1280视频监控小目标场景用 960 以上batch8 / 16 / 32按显存调整显存小就降 batchdevice0 或 0,1单卡、多卡训练patience20-30早停防止无效训练浪费时间model 的选择原则是先用小模型把流程跑通确认数据没问题再换大模型提精度。不要上来直接跑 yolov8x回归训练只跑了一半就发现标注有问题返工成本高。5.2 训练过程监控与早停训练日志里核心看几个指标box_loss、cls_loss、dfl_loss、metrics/mAP50、metrics/mAP50-95。box_loss 和 cls_loss 应该在前 20 个 epoch 内明显下降。mAP50 上升后进入平台期mAP50-95 继续缓慢上升说明框的定位精度在改善。训练 loss 还在降但验证 loss 开始回升典型的过拟合信号。车辆数据集样本量不大时这种情况很容易出现。早停参数可以直接写进命令yolo detect train datavehicle_dataset.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0 patience20设置 patience20 的作用是验证指标连续 20 个 epoch 不提升就自动结束训练避免挂机训练回来后模型已经过拟合。5.3 混淆矩阵和分类别 mAP 验证训练结束后在runs/detect/train/下会生成confusion_matrix.png、results.csv用验证集评估最佳权重yolo detect val datavehicle_dataset.yaml modelruns/detect/train/weights/best.pt imgsz640 device0车辆检测数据集的验收重点看三个地方car 和 truck 是否互相混小轿车和货车在侧视角下轮廓确实接近。suv 和 taxi 的混淆程度城市道路场景里这两类经常被误判。bus 和 truck 的 mAP50 一般较高但 mAP50-95 才能反映边界框精度。从混淆矩阵里能定位到具体是哪两个类别互相干扰。比如 taxi 大量被预测成 car可以检查是不是出租车外观本身和 car 差异太小后续可以补充更多 taxi 训练样本或者单独增加 taxi 的权重。位深做类别不均衡的损失函数混合方式比如cls1.5调整分类损失权重。6. 标签体检训练前用一键脚本筛出问题样本6.1 自动体检脚本拿任何车辆数据集我都会先跑一轮体检检查三件事缺失标签、空标签、越界框。from PIL import Image import os img_dir images label_dir labels_txt for img_name in sorted(os.listdir(img_dir)): stem os.path.splitext(img_name)[0] txt_path os.path.join(label_dir, stem .txt) if not os.path.exists(txt_path): print(fmissing label: {img_name}) continue with open(txt_path) as f: lines [line.strip() for line in f if line.strip()] if len(lines) 0: print(fempty label: {img_name}) continue with Image.open(os.path.join(img_dir, img_name)) as im: width, height im.size for line in lines: parts list(map(float, line.split())) if len(parts) ! 5: print(fbad field count: {img_name}, {line}) continue cls, cx, cy, w, h parts x1 int((cx - w / 2) * width) y1 int((cy - h / 2) * height) x2 int((cx w / 2) * width) y2 int((cy h / 2) * height) if x1 0 or y1 0 or x2 width or y2 height: print(fout of range: {img_name}, {line})脚本逻辑是对每张图检查同名 txt 是否存在、是否有目标、五个字段是否齐全、还原成像素坐标后是否超过图片边界。输出的问题样本列表逐个手工确认有问题的删掉或重新标注。6.2 可视化抽查体检脚本只能发现数值问题类别标错、框位置偏了这类语义错误必须靠人眼看。import cv2 img cv2.imread(images/car_125.jpg) h, w img.shape[:2] with open(labels_txt/car_125.txt) as f: for line in f: parts list(map(float, line.split())) cls, cx, cy, bw, bh parts x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(check_car_125.jpg, img)画框时按类别用不同颜色比如 car 用绿色、truck 用红色能快速看出类别是否标错。随机抽几十张图扫一遍比训练完再回头查标签要省时间得多。那次我偷懒跳过体检脚本直接开训结果到第 40 个 epoch 发现部分框越界loss 和 mAP 反复震荡最后浪费了两次完整训练。从那以后我每次拿到车辆数据集都会强制走一遍体检脚本和可视化抽查再开始调模型。这套流程对 VOC 格式的车辆检测数据集尤其适用希望帮到你。本文还有配套的精品资源点击获取