简介:面向城市街道车辆检测的YOLO目标检测数据集,包含359张实景图像,标注了公交车、轿车、卡车、面包车四类车辆,适配YOLOv5/v7/v8/v11等系列模型。压缩包共1088个文件,其中367张jpg原图、359个xml格式标注、359个txt格式标签,另含data.yaml配置文件及说明文档,总大小约114MB。数据集已按YOLO规范划分训练集与验证集,yaml中可直接设置路径开始训练,txt与xml标注并存也便于转换VOC或COCO格式,用于其他检测框架对比实验。目前已有8人学习,适合开发者和研究人员快速获取标准车辆检测数据,用于交通监控、车流统计、自动驾驶感知等场景的模型训练与精度评估。
1. 拿到“YOLO算法城市街道车辆目标检测数据集”先别解压:359张图到底能拿来做什么
手头这份“YOLO算法城市街道车辆目标检测数据集”,359张图、四个类别,公交车、轿车、卡车、面包车。先给结论:对跑通 YOLO 训练流程绰绰有余,对训练出生产级精度的模型远远不够。这个定位不先立住,后面每一步都容易走偏。
它能解决的是流程问题:验证标注格式、跑通迁移学习、排查环境配置,还能给车辆检测项目当冷启动预热样本。多数团队拿到它当演练数据用,不当生产数据。
适合刚接触目标检测、想在本地用真实街道场景练手的人;手头有业务数据但不知道“处理数据集用于 YOLOv8 训练”那套步骤的人,也能拿它验证工具链。接下去从解压体检开始,讲完整落地路径。
2. 拿到 zip 先做体检:目录结构、标注格式与类别分布怎么核查
训练之前先体检。压缩包解压很简单,但解压后别急着把数据拖进训练脚本。先回答三个问题:标注是什么格式?类别 ID 和名称的对应关系是什么?四类目标的框数量分布是否极端?这三件事决定了后续转换脚本怎么写,也决定了你对模型精度的预期怎么定。
2.1 解压之后先看什么:目录结构就是数据集的说明书
先把压缩包解开,用树形命令看一遍整体布局:
mkdir -p datasets/vehicle unzip YOLO算法城市街道车辆目标检测数据集-359张.zip -d datasets/vehicle cd datasets/vehicle && tree -L 2 | head -40tree 的输出会告诉你这是两种经典布局中的哪一种:images/ 与 labels/ 分目录,对应 YOLO 默认约定,图片和同名 txt 标签各归各的目录;JPEGImages/ 与 Annotations/ 并存,标签是 XML,对应 VOC 系数据集的经典结构。两种布局在训练前的准备工作完全不同,所以这一步不是可有可无。看到 XML 时不要慌,转换脚本后面会专门处理;看到 txt 时也要抽查几行,确认是 class_id、center_x、center_y、width、height 的五段格式,而不是 labelimg 导出的四段格式或 COCO 的 JSON。
数据集的“标注格式”决定了后面所有脚本的写法,三种常见格式对比如下:
| 格式 | 典型扩展名 | 坐标含义 | YOLO 可直接训练? |
|---|---|---|---|
| YOLO txt | .txt | 类别ID + 相对中心点 + 相对宽高 | 是 |
| PASCAL VOC XML | .xml | 绝对像素坐标 xmin,ymin,xmax,ymax | 需转换 |
| COCO JSON | .json | 绝对像素坐标 + 分割掩码 | 需转换 |
判断完格式,顺手检查图片扩展名是否统一。359 张图里如果混着 .jpg 和 .jpeg,后续类别映射会一半成功一半失败,报错还很隐蔽,只在“找不到对应标签”时出现。我的做法是在树形输出后跑一条 find 命令统计扩展名种类:find . -type f ( -name ".jpg" -o -name ".jpeg" -o -name ".png" ) | sed 's/..//' | sort | uniq -c。如果出现多种扩展名,先统一成 .jpg 再往下走。
2.2 用一个统计脚本摸清四类目标的分布,别让类别不平衡在后期爆发
四类车辆里,轿车通常占大头,公交车和卡车常常不到轿车的五分之一。这种不平衡如果不量化,训练中途只会以“某个类 AP 为 0”的方式爆发。所以解压之后先写个临时统计脚本,把每张图的类别、框数、框宽高比跑一遍:
# 统计标签文件类别分布与框尺寸,兼容YOLO txt与VOC xml import glob, os from collections import Counter import xml.etree.ElementTree as ET label_dir = "datasets/vehicle/labels" # 按实际路径改 xml_dir = "datasets/vehicle/Annotations" class_names = ["bus", "car", "truck", "van"] # 与数据集说明核对 distribution = Counter() box_sizes = [] # 分支1: 统计YOLO txt for txt in glob.glob(os.path.join(label_dir, "*.txt")): with open(txt, encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue # 跳过空行和解析异常行 class_id = int(parts[0]) if class_id >= len(class_names): print(f"越界ID: {txt}: {line}") # 类别ID越界会直接报错 continue distribution[class_names[class_id]] += 1 box_sizes.append(float(parts[3]) * float(parts[4])) # 分支2: 统计VOC xml for xml in glob.glob(os.path.join(xml_dir, "*.xml")): root = ET.parse(xml).getroot() for obj in root.findall("object"): name = obj.find("name").text box = obj.find("bndbox") w = int(box.find("xmax").text) - int(box.find("xmin").text) h = int(box.find("ymax").text) - int(box.find("ymin").text) distribution[name] += 1 box_sizes.append(w * h) print("类别分布:", dict(distribution)) print("平均框面积(像素):", sum(box_sizes) / len(box_sizes))这段脚本两个分支只跑一个,别在两种格式混存时重复统计。输出的信息很有用:类别分布如果显示“卡车”只有几十个框,说明该类别样本严重不足,训练时要给它更高权重或做针对性增强;平均框面积如果远小于 32×32,说明场景里小目标居多,第 4 章的 imgsz 就不要死守 640。
我还会顺带统计图片与标签的成对率:图片有多少张、标签有多少个,差的那些图在后续训练里会被当成纯背景样本,如果缺失量超过 10%,召回率会明显往下掉。这个统计结果记在项目 README 里,训练完对照着往回看,很多“模型为什么漏检公交车”的疑问其实在数据体检这一步就写了答案。
3. 整理成 YOLO 可训练的格式:转换脚本与四个边界坑
体检完,下一步是把数据整理成 YOLO 系工具能直接吃得下的结构。这一步最花时间的不是写脚本,而是处理那些“标注里看起来合法、训练时直接炸”的边缘数据。
3.1 先按 YOLO 约定搭好目录骨架:路径与类别名都写在明面上
无论原始格式是什么,整理好之后统一成三层结构,YOLOv8 的 data yaml 里写起来最省事:
datasets/vehicle/ ├── images/ │ ├── train/ # 训练图 │ └── val/ # 验证图 ├── labels/ │ ├── train/ # 与images同名的基础名+ .txt │ └── val/ └── classes.txt我习惯把 train/val 按 8:2 划分,划分前先按“同一个场景的多帧放进同一侧”的原则去重。城市街道数据通常是一段视频连续抽帧而来,相邻帧几乎一样,如果不按场景切分而是随机打乱,验证集里会出现大量与训练集几乎相同的画面,mAP 虚高到 0.9 以上,这个指标没有参考价值。切分时我一般先把图片文件名提取出来,按前缀或时间戳分组,保证同组文件只进训练侧或只进验证侧。
类别 ID 在这张数据里是 bus=0、car=1、truck=2、van=3,对应关系必须和后续 names 列表严格一致。训练 yaml 里 names 写错一个位置,混淆矩阵会整体错位,表现就是“卡车全被识别成公交车”。
3.2 VOC XML 转 YOLO txt:归一化计算、目标保存与异常拦截
如果你的数据是 VOC 结构,这一步是必经之路。核心是坐标归一化,把绝对像素坐标转成相对图片宽高的中心点坐标:
# 将VOC XML转换为YOLO txt,坐标归一化到[0,1] import os, glob import xml.etree.ElementTree as ET label_map = {"bus": 0, "car": 1, "truck": 2, "van": 3} xml_dir = "datasets/vehicle/Annotations" img_dir = "datasets/vehicle/JPEGImages" out_label_dir = "datasets/vehicle/labels/all" os.makedirs(out_label_dir, exist_ok=True) for xml_path in glob.glob(os.path.join(xml_dir, "*.xml")): root = ET.parse(xml_path).getroot() img_name = root.find("filename").text img_width = int(root.find("size").find("width").text) img_height = int(root.find("size").find("height").text) out_lines = [] for obj in root.findall("object"): name = obj.find("name").text class_id = label_map.get(name) if class_id is None: print(f"跳过未知类别: {xml_path} -> {name}") continue box = obj.find("bndbox") xmin = max(int(box.find("xmin").text), 0) ymin = max(int(box.find("ymin").text), 0) xmax = min(int(box.find("xmax").text), img_width) ymax = min(int(box.find("ymax").text), img_height) if xmax <= xmin or ymax <= ymin: print(f"异常宽高=0: {xml_path} {name}") continue # YOLO格式: 类别ID 中心点x 中心点y 宽度 高度,全部除以图片尺寸 center_x = (xmin + xmax) / 2.0 / img_width center_y = (ymin + ymax) / 2.0 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height out_lines.append(f"{class_id} {center_x:.6f} {center_y:.6f} {w:.6f} {h:.6f}") # 保存为与图片同名的基础名+ .txt base, _ = os.path.splitext(img_name) out_path = os.path.join(out_label_dir, base + ".txt") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(out_lines))逻辑说明:VOC 的 bndbox 是绝对坐标,xmin、xmax 经常越界到负值或超出图片宽度,直接换算会产生负数坐标或宽高为 0 的框,YOLO 加载时这类样本会被丢弃,整体表现为训练集数量莫名少一截。脚本先 clamp 再计算中心点,把异常情况打印出来而不是静默跳过,这样转换完你能清楚知道哪些框被过滤了。
参数说明:label_map 的顺序决定类别 ID,如果数据集说明的标注顺序不同,改这个字典即可。center_x、center_y 先求像素中心再除以宽高,避免先归一化再相加带来的额外误差。保留 6 位小数足够,4 位小数在 1920 宽图片上会引入约 0.2 像素误差,没必要用更多位。img_dir 在脚本里定义了但没用上,是因为有些数据集 XML 的 filename 字段与实际文件名不一致,实际使用时用 glob 图片目录来生成映射表,可靠性远高于信任 filename 字段。
除了转换本身,有四个边界坑在转换时就要处理掉。
第一个坑是同一目标被重复标注。同一块区域标了两遍,训练时会出现重叠框被 NMS 压掉、该类的置信度被重复梯度拉高的副作用。转换脚本可以加一步重叠 IoU 大于 0.6 就去重的过滤,阈值按场景密度调节,城市街道这种密集场景可以放宽到 0.7。
第二个坑是类别名大小写不统一。“Car”和“car”在 label_map.get() 中会被跳过,导致该框直接丢失,建议先对 name.strip().lower() 做一次归一化,再进映射表查询。
第三个坑是文件名带空格或中文。YOLO 读取时会断词,转换出来的标签路径对不上图片路径。转换时把 base 名清洗成只含字母数字下划线,并同步重命名图片文件,这一点对后续训练稳定性影响很大。
第四个坑是图片是 .jpg、标签却存成 .jpeg,或反过来。训练校验时会报“找不到对应标签”,但报错文件名又不完全一致,肉眼难查。以图片实际存在路径为准生成标签文件名,不要相信 XML 里的 filename 扩展名。
4. 用 YOLOv8 在本地把最小训练跑通:data.yaml、模型选择与损失曲线判读
数据管线通了,进入训练环节。我选 YOLOv8 做演示,因为它把权重初始化、数据加载、loss 计算打包得足够简洁,社区里“YOLOv8 训练自己的数据集”这套路径最完整,出了问题搜得到答案。
4.1 data.yaml 与预训练权重:先把配置写对,再谈调参
在项目根目录新建 vehicle.yaml:
# vehicle.yaml —— yolov8 训练数据配置 path: C:/datasets/vehicle # 数据集根目录,用绝对路径最稳 train: images/train # 相对根目录的训练图路径 val: images/val # 相对根目录的验证图路径 nc: 4 # 类别总数 names: ["bus", "car", "truck", "van"] # 类别ID必须和标签txt对应path 用绝对路径能避开 YOLOv8 在不同工作目录下找不到数据的问题。Windows 下写反斜杠容易转义出错,所以代码块里全部用正斜杠。nc 和 names 必须和转换脚本里的 label_map 完全一致,bus=0 的位置对不上的话,前面所有数据整理都白做。
预训练权重用 yolov8n.pt 就好。n 是 nano 系列,推理成本最低、训练最快,适合 359 张图这种规模。首次训练时默认会自动下载权重文件,这一步如果你有网络环境受限的情况,常见做法是先在有网机器上下好 yolov8n.pt,传到项目根目录,再把 model 参数改为本地路径。别在小数据集上直接挑战 l 或 x 系列,参数越多越容易过拟合,损失曲线会变成一边下降一边抖的锯齿形。
4.2 训练启动与关键参数:epochs、imgsz、batch 怎么在一张小数据上定
数据量小,参数不能照搬教程里的“epochs=300 保底”。城市街道场景的目标普遍偏小,我一般这样起:
yolo detect train \ model=yolov8n.pt \ data=vehicle.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20 \ device=0 \ project=runs/vehicle \ name=exp_001参数说明:patience 是早停阈值,验证集 mAP 连续 20 个 epoch 没有提升就自动停止,避免 359 张图硬跑 100 个 epoch 浪费机器。imgsz 保持 640 默认值;如果第 2 章统计框面积发现大量框小于 32×32,可以降为 512 或 416,但注意图太小会丢失小目标,图太大在小数据集上增益有限,这个值需要试跑两三组才能定。batch 16 在单卡上通常够用,显存不够先降 batch 而不是降 imgsz,除非你确认过框面积分布偏大。
训练过程里最值得盯的两张图是 train/loss 曲线和验证混淆矩阵。loss 曲线里,box_loss 和 cls_loss 在前 10 个 epoch 快速下降、60 个 epoch 后进入平台期,这是小数据集的正常节奏;如果 cls_loss 持续上升或出现 nan,多半是标签越界或学习率设定问题,下一章的避坑清单里会展开。
验证阶段生成的 confusion_matrix.png 在小数据集上比 mAP 更可信。mAP 是把所有类别平均后的单一数值,255 张训练图的数据很容易让某个优势类别把整体指标拉高;而混淆矩阵能直接看到“bus 被识别成 truck”这类具体的类别串扰,这种问题靠调参解决不了,只能回头改标注或增强该类别数据。
训练结束后,runs/vehicle/exp_001/ 下会生成 weights/best.pt、last.pt 以及一批图表。后续所有验证都去调 best.pt,不要碰 last.pt,last 很可能是早停前最后一个保存点、不一定是最佳状态。命名上建议每次改动都换 name,别复用同一目录,避免可视化结果被覆盖之后没法回看。
5. 车辆检测训练避坑:4 个高频翻车点与修复记录
这一章把训练中最高频的翻车点按“现象到原因到解决”整理出来,每一条都是带过真实踩坑经验的记录,不是理论推演。
5.1 训练 loss 出现 nan 或持续增大:先查标签,别急着抱怨模型
现象:第一个 epoch 打印的 box_loss 直接是 nan,或者前几个 epoch 正常、到中途某个 epoch loss 突然变为 inf。
原因:大概率是标签里有非法值。转换脚本里除以零、越界裁剪出错,都会在 txt 中产生负数坐标或 NaN 坐标,YOLO 训练读入时计算 IoU 直接失败。其次才是学习率过大引起梯度爆炸,小数据集上这种情况反而少见。
解决:训练前先跑一遍标签合法性检查,逐行校验 txt 中五个数值都是有限非负值、且 class_id 小于 nc。我通常会写一个 20 行的校验脚本,在训练前扫一遍,扫描通过之前不动学习率。把这段校验放在训练脚本前面,能省掉一整晚的 debug 时间。脚本很简单:读每一行,split 后 float 转换,检查 math.isnan 和是否小于 0,一旦发现异常立即打印文件名和行号。
5.2 训练一切正常但验证 mAP 为 0,混淆矩阵总行不唯一
现象:loss 曲线正常下降,但 epoch 结束后看 val 混淆矩阵,发现严重错位,比如实际是 car、预测结果却全落在 bus 上;或者混淆矩阵的行列总和与实际标签数对不上。
原因:最常见根因是 names 列表顺序和标签 txt 的 class_id 对不上。数据整理时 label_map 的顺序是 bus、car、truck、van,yaml 里 names 却写成了 car、bus、truck、van,整个类别轴就错位了。另一个容易被忽略的原因是没有按场景切分 train/val,导致验证集里出现与训练集几乎相同的图像,mAP 高得离谱,混淆矩阵看起来又“总合不唯一”。
解决:逐类跑一次验证集的预测结果,比对预测 class_id 与真实 class_id 的分布,错位规律会很清晰;再把 confusion_matrix.png 的行列标签打印出来核对。修复 names 顺序后,重跑验证,指标才会正常。
5.3 BN 层崩溃导致 loss 抖动:小数据集最容易被忽略的细节
现象:训练前中期 loss 开始周期性抖动,某个 epoch 后 running_mean 和 running_var 突然变为 nan,后续输出全部异常。
原因:BN 层的统计量在 batch 过小时不稳定。有些人为了让 359 张图多跑几个 epoch,强行把 batch 调到 4 甚至 2,BN 的滑动统计方差变大,在数据分布不均匀时直接数值溢出。另一个触发条件是不同显卡间切换训练断点,batch 从 16 降到 4 之后继续训练,BN 统计量会剧烈跳变。
解决:把 batch 加回到 16 以上,如果显存不足则同步调小 imgsz;同时加阶段性预热,前 5 个 epoch 冻结 BN 的 running 统计量更新,等骨干网络稳定后再放开。这一步在 batch 小于 8 时几乎是必需的。显存只有 8G 的机器上,我习惯的做法是 imgsz=512、batch=16,配合 grad-accum=2,效果比 imgsz=640、batch=4 稳定得多。
5.4 测试集表现与验证指标落差大:场景泄露与过拟合的证据
现象:验证 mAP 到了 0.85,但拿一张陌生街道图片测试时,召回率明显下降;尤其在不同光线、不同城市街景下,漏检率成倍上升。
原因:359 张图如果来自同一段场景序列,模型学到的多是画面纹理而不是“车”的概念。验证集和训练集同源,模型相当于“记住了场景”而不是学会了检测,这就是过拟合。
解决:回到第 2 章检查场景数,如果只有一个采集时段,需要补充不同时段、不同路口的样本。如果暂时补不了数据,把 mosaic、hsv_h、hsv_s、degrees 等增强项加大,degrees 给到 10 到 30 之间的随机旋转,能提升模型对视角变化的容忍度。训练后做一次跨场景冒烟测试,选择与训练集完全不同的拍摄角度,用这个结果判定模型是否真能可用,而不是只看验证集数字。
6. 把这份数据当作冷启动样本:迁移到自有数据的三个进阶操作
这份数据的真正价值,在于帮你形成一套可复用的数据整理与训练管线,而不是那四个类别的权重。三个进阶操作能把它迁移到真实项目里。
第一个操作是把它当“预训练预热集”。先用这份数据训 30 个 epoch,得到一套城市交通场景的初始化权重,再用自己的真实数据微调。相比直接使用 COCO 预训练权重,这种预热在城市街道这种聚焦场景下,能明显减少早期 loss 波动;业务数据量少的时候,这个技巧的收益比调任何超参数都大。
第二个操作是用它验证标注工具和团队标注质量。无论是 LabelImg、CVAT 还是 X-AnyLabeling,先让标注人员在 50 张图上过一遍,对比自动生成的边界框与原始标注的差异,能快速验证标注规范、快捷键效率和导出格式是否正确。避免团队标完 1000 张才发现坐标格式不对的返工。
第三个操作是建立一个离线验证脚本,对陌生场景输出做一致性校验。把 val 分成“同源”与“异域”两个子集,异域子集放拍摄时段、路口结构完全不同的图,用两者 mAP 的差值作为过拟合程度的量化指标。这个指标我后续项目里一直沿用,比单看验证集 loss 可靠得多。
我自己早期踩过一个典型的坑:拿到一份小样本数据集后直接调高 epochs 与增强强度,希望在 mAP 上限号调参硬碰高分,结果过拟合更严重,最后还被误判成预训练权重的问题。后来才明白,小数据集的正确用法是训练流程与数据校验,而不是在单一指标上死磕。
如果你也准备拿这份 359 张的数据起步,先把第 2 章的统计脚本跑出来,再走转换和训练流程,每一步的统计结果都存下来。这套从数据体检到训练验证的路径,比任何一个单独的数字都更值得投入,希望帮到你。
本文还有配套的精品资源,点击获取