简介:这套汽车目标检测数据集采用YOLOV5标准目录结构,聚焦单类别car的车辆检测任务。训练集包含1000张图片及对应的1000个txt标签,测试集包含208张图片及208个txt标签,标签为YOLO格式的边界框坐标,图片以jpg为主、png为辅,覆盖道路、停车场、园区等多样化生活场景。数据划分清晰,压缩包共2000个文件,总大小约243MB,主要由1200余个txt标注文件、近800张jpg/png图片、1个类别字典txt和1个可视化py脚本组成。其中可视化脚本无需修改即可运行,随机传入一张图片就能绘制边界框并保存到当前目录,便于快速核对标注效果。这份数据集可直接接入YOLOV5等主流目标检测框架,省去数据收集与格式转换环节,适合目标检测初学者、计算机视觉开发者以及需要车辆识别能力的项目使用,也可作为模型训练、算法对比或教学演示的优质示例。目前已有187人学习下载,是开展车辆检测实践的高性价比选择。
1. 不是有图就能训:YOLOV5 目录格式才是汽车目标检测数据集能跑通的关键
拿到一个「目标检测数据集(YOLOV5目录格式):汽车目标检测数据集」,多数人的第一反应是"图片在哪、有多少张",但我负责任地讲,图片反而是这件事里最不值钱的部分。真正决定你能不能在一个晚上把训练跑通、把 mAP 刷到能看的水平的,是目录结构是否严格对齐 YOLOV5 的读取逻辑:images 和 labels 怎么分 train/val、同名 txt 放哪、data.yaml 写什么、类别编号是否错位。这套东西错一个字符,训练不会报错,但指标就是全 0。下文按我平时自建车辆数据集的路子,从公开数据选型、目录搭建、质量体检到踩坑清单,把这条链路完整走一遍。
2. 汽车数据集的选型逻辑:BDD100K、CCPD、UA-DETRAC 与自建怎么选
2.1 先分清四类车载数据的「性格差异」
做汽车目标检测,最常碰到的公开数据是 BDD100K、UA-DETRAC、CCPD 和 KITTI。很多人以为它们只是标注格式不同,换一下脚本就行,实际上它们的差异暴露在三个维度:场景构成、目标尺度、标注噪声。
BDD100K 的优势是全是真实道路场景,包含晴天、雨天、夜晚、遮挡、稀疏和拥挤交通,共 10 万帧,标注了 car、bus、truck、traffic light 等类别。它适合做泛化性要求高的模型,缺点是早期版本标注有小幅错位,边界框偶尔松紧不一。
UA-DETRAC 是纯车辆检测数据集,场景集中在城市道路和收费站,带 8250 个视频的逐帧框标注,单帧车辆数多、遮挡频繁,适合做密集车流检测。它的成像多为监控视角,和行车记录仪视角有一定差异。
CCPD 专攻中国车牌场景,是从停车场监控里截出来的车辆图,类别只有"车"这一个语义,但它对大规模车辆检测预训练非常友好,因为样本量超过 20 万张。它的局限是视角单一,全是停车场斜俯视。
CrowdHuman 不是汽车专用数据,但如果你要做"行人+车辆"的混合场景,它的密集行人标注可以当作难例补充。它的标注框质量很高,类别只有 person,不适合单独训练车辆检测器。
还有一大类思路是直接用 COCO2017 的 person、car、truck、bus 四个类做裁剪。COCO 的边界框标注相对干净,类别名标准化,转换脚本成熟。下表是我常用来给同事说明选型的参考:
| 数据集 | 适合场景 | 标注格式 | 转换成本 | 主要坑 |
|---|---|---|---|---|
| BDD100K | 自动驾驶多场景泛化 | JSON | 中 | 夜间/雨雾质量参差 |
| UA-DETRAC | 密集车流、监控视角 | MAT/XML | 中高 | 视频帧去重难 |
| CCPD | 大规模车辆预训练 | 文件名自带框 | 低 | 单类别、视角单一 |
| COCO2017 | 多类别迁移 | JSON | 低 | 类别多需过滤 |
| 自建小样本 | 特定场地/特定光照 | 任意 | 高 | 标注一致性靠人工 |
我一般建议:先确定你的部署场景,如果是园区门口的车牌识别、小区入口的车辆计数,CCPD 加自建补充;如果是路侧感知或无人车,BDD100K 起步;如果只做车流量统计,UA-DETRAC 的密集帧更划算。
2.2 把 BDD100K 转成 YOLOV5 目录格式:转换脚本与过滤逻辑
选定数据后,最硬的活是把 JSON 转成 YOLO 的 txt。这一步的常见翻车点不是 JSON 解析,而是坐标归一化和类别索引两个地方。下面这个脚本处理 BDD100K 的训练标注,输出结构和 YOLOV5 完全对齐。
import json import os from pathlib import Path # 只保留车辆相关类别,按目标检测数据集(YOLOV5目录格式)的要求输出 txt CATEGORY_MAP = {"car": 0, "truck": 1, "bus": 2} def bdd100k_to_yolo(json_path, img_dir, out_label_dir): with open(json_path, "r", encoding="utf-8") as f: annos = json.load(f) os.makedirs(out_label_dir, exist_ok=True) for item in annos: name = Path(item["name"]).stem # 去掉 .jpg 后缀 img_w = item["width"] img_h = item["height"] lines = [] for obj in item["labels"]: cat = obj["category"] if cat not in CATEGORY_MAP: continue # BDD100K 的 box2d 是 [x1, y1, x2, y2],注意是整数像素 x1, y1, x2, y2 = obj["box2d"].values() bw = max(x2 - x1, 1) bh = max(y2 - y1, 1) cx = (x1 + x2 / 2) / img_w cy = (y1 + y2 / 2) / img_h w = bw / img_w h = bh / img_h # 做一次夹紧,防止坐标越界导致训练时 OpenCV 读取异常 cx, cy, w, h = [max(0.0, min(v, 1.0)) for v in (cx, cy, w, h)] lines.append(f"{CATEGORY_MAP[cat]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_txt = Path(out_label_dir) / f"{name}.txt" out_txt.write_text("\n".join(lines), encoding="utf-8") # 调用示例:请按实际路径修改 bdd100k_to_yolo("bdd100k_labels_train.json", "images/train", "labels/train")这段代码的关键点有三个。第一,box2d是像素坐标,不除以img_w和img_h就会得到大于 1 的标签,YOLOV5 会在 loss 计算时产生极不稳定的梯度。第二,类别映射必须从 0 开始连续排列,如果你把bus写成 3 而 data.yaml 里只有 3 个类,训练不会报错,但推理时类别名全错位。第三,max(bw, 1)是为了防住某些标注框宽度为 0 的脏数据,这类数据在公开集里不少。
3. 从零搭 YOLOV5 目录格式:目录结构、data.yaml 与训练集划分
3.1 标准目录树:一个空目录引发的连锁报错
YOLOV5 的数据加载逻辑非常「死板」:它只认dataset/ images/train和dataset/ labels/train这种固定路径。你要是多建一层car_images/train但没匹配上,运行时会直接显示train: No labels found,但训练进程不退出,只是每轮都在空跑。这是新手最容易误判成"数据集有问题"的点,实际上是目录映射没写对。
我惯用的目录格式如下,你可以直接照着建:
datasets/vehicle/ ├── vehicle.yaml ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ ├── 000002.jpg │ └── val/ │ ├── 000100.jpg └── labels/ ├── train/ │ ├── 000001.txt │ ├── 000002.txt └── val/ ├── 000100.txt注意三个硬性规则。第一,图片和 txt 必须同名,只允许后缀不同;如果你有一张000001.png,标签就必须是000001.txt。第二,val 是必建目录,即使你暂时不验证,也要留一个空目录加占位文件,否则train.py --data会因找不到 val 而中断。第三,文件名不要用中文和空格,YOLOV5 的多线程 DataLoader 对非 ASCII 字符的兼容性很差,会出现间歇性报错。
用下面这段脚本可以一键建好目录,顺带把已有图片按比例搬进去。
from pathlib import Path import random root = Path("datasets/vehicle") for split in ["train", "val"]: (root / "images" / split).mkdir(parents=True, exist_ok=True) (root / "labels" / split).mkdir(parents=True, exist_ok=True) # 假设原始图片都在 datasets/raw_images 下 raw_imgs = list(Path("datasets/raw_images").glob("*.jpg")) random.seed(42) random.shuffle(raw_imgs) val_cnt = int(len(raw_imgs) * 0.2) for idx, img in enumerate(raw_imgs): if idx < val_cnt: img.rename(root / "images" / "val" / img.name) else: img.rename(root / "images" / "train" / img.name) print(f"train: {len(raw_imgs) - val_cnt} imgs, val: {val_cnt} imgs")这里random.seed(42)保证每次划分结果一致,这是定位问题的基础。如果你发现某个 epoch 的 validation 指标忽高忽低,先检查是不是划分时没有固定随机种子,导致每次加载的数据分布不一致。
3.2 labels 文件里那 5 个数字,边界条件比你想得多
一个 YOLO 格式 txt 的每一行是class_id cx cy w h,cx cy是归一化中心点,w h是归一化宽高,四者都必须是 0 到 1 之间的小数。很多从 LabelImg 转出来的标签是像素绝对坐标,直接写进 txt 会让模型把 anchor 学成"全图一个大框"。
写一个轻量校验函数,可以在训练前把脏标签找出来:
import numpy as np def check_yolo_label(txt_path): bad_lines = [] with open(txt_path, "r") as f: for ln, line in enumerate(f, 1): parts = line.strip().split() if len(parts) != 5: bad_lines.append((ln, "field count != 5")) continue cls, cx, cy, w, h = map(float, parts) if not (0.0 <= cls <= 2): # 假设只有 0/1/2 三类 bad_lines.append((ln, "class id out of range")) if not all(0.0 < v <= 1.0 for v in (cx, cy, w, h)): bad_lines.append((ln, f"coord out of range: {parts[1:]}")) return bad_lines # 检查整个 labels/train 目录 from pathlib import Path for p in Path("datasets/vehicle/labels/train").glob("*.txt"): issues = check_yolo_label(p) if issues: print(f"{p.name}: {issues[:3]}")这个脚本在数据量大时尤其值钱。公开数据集经过压缩、切图、二次标注后,很容易产生小数位过长或负值的标签;如果没做这步,训练出来的模型在推理时会把背景当车,因为 loss 在引导它"全图都给框"。
3.3 data.yaml 的路径、类别名与下载路径三个易错点
YOLOV5 的 data.yaml 是唯一牵动训练、验证、推理三端配置的文件,它写错了不会像 Python 那样抛异常,而是悄悄加载空数据集。我的最小模板是:
# datasets/vehicle/vehicle.yaml path: ../datasets/vehicle # 相对于运行 train.py 的目录 train: images/train val: images/val nc: 3 names: 0: car 1: truck 2: bus三个易错点。第一,path最好不要写绝对路径。绝对路径在你自己机器上没问题,但数据集打包给别人或挪到服务器后,直接就是"找不到路径"的报错,而且排查时会让你误以为是文件被移动了。用相对路径,让 YOLOV5 按照path拼上train和val去读,是最稳的方式。
第二,nc必须和 labels 里实际出现的class_id最大值加一相等。比如你只有 car 和 truck 两个类别,truck 的 id 是 1,那么nc必须大于等于 2;写成 1 不会报错,但 truck 样本全部变成背景。
第三,很多公开数据集附带的 data.yaml 里带download:字段,指向官方下载脚本。如果是在内网或离线环境用自己的数据,请务必删掉这一行,否则训练前 YOLOV5 会尝试访问外网下载数据,卡在等待超时上,看起来像训练进程挂起。
4. 训练前的数据集体检:可视化标注、类别分布与最小训练试跑
4.1 用 OpenCV 直接画框,先肉眼确认 50 张图
训练前我从不直接跑 train.py,而是先做一轮可视化抽检。画框脚本很简单,但能暴露两类问题:一是标签和图片不对应(框画在空地上),二是地物太小或太大导致标注张数失衡。代码逻辑是把 txt 里的归一化坐标乘回图片宽高,再用矩形画出来,存成_check.jpg供人工过目。
import cv2 from pathlib import Path def visualize_label(img_path, txt_path, out_dir): img = cv2.imread(str(img_path)) h, w = img.shape[:2] with open(txt_path, "r") as f: for line in f: cls, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) color = (0, 255, 0) if cls == 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, f"cls{int(cls)}", (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) out_path = Path(out_dir) / f"{Path(img_path).stem}_check.jpg" cv2.imwrite(str(out_path), img) # 抽查 val 里的前 50 张 val_imgs = sorted(Path("datasets/vehicle/images/val").glob("*.jpg"))[:50] for img_p in val_imgs: txt_p = Path("datasets/vehicle/labels/val") / (img_p.stem + ".txt") if txt_p.exists(): visualize_label(img_p, txt_p, "check_output")抽检时重点看三类图:含有公交车的大尺寸目标、夜晚车辆的弱光目标、多车重叠的目标。如果这三类里有两类的框明显偏移,就说明标注质量不稳定,不值得直接进训练。人工标注的话,我一般要求框要紧贴车身边缘,留 2 到 3 个像素的余量,不要为了"框得全"而放大很多。
4.2 类别分布统计:别让 99% 都是小车样本
车辆数据集的另外一个暗坑是类别分布极度不均衡。停车场数据里轿车可能占 95%,卡车和公交车各占 2% 和 3%。YOLOV5 的 loss 本身对不均衡有一定容忍度,但极端情况下会学成"只输出 car"的退化模型。写一个类别统计脚本,用每张图的 txt 累计各 class 的出现次数和框面积,可以快速量化不均衡程度。
import numpy as np from collections import Counter from pathlib import Path dist = Counter() area_sum = {0: [], 1: [], 2: []} # car/truck/bus for txt in Path("datasets/vehicle/labels/train").glob("*.txt"): with open(txt) as f: for line in f: cls, cx, cy, w, h = map(float, line.split()) dist[int(cls)] += 1 area_sum[int(cls)].append(w * h) print("class count:", dict(dist)) for cls_id, areas in area_sum.items(): if areas: print(f"cls {cls_id}: mean area={np.mean(areas):.4f}, " f"median={np.median(areas):.4f}, max={np.max(areas):.4f}")如果某个类别的mean area比其他类小一个数量级,比如 car 的框占图面积只有 0.02 而 truck 有 0.2,则模型对远处小车天然不敏感。缓解方式有两种:一种是后续提到的高分辨率输入或者多尺度训练;另一种是直接按框面积筛选掉过于极端的数据,比方说删掉占图面积小于 0.005 的框,因为它们本身也不可标注。统计脚本输出的数字,就是你决定要不要动数据、怎么动的依据。
4.3 用一个 epoch 试跑,验证数据和代码链路通不通
体检完图片和标签,最后一步是用极小配置跑一次完整训练链路。这一步很多人嫌浪费时间,但我认为它是性价比最高的操作:能同时验证数据集路径、DataLoader 缓存、loss 是否收敛、GPU 显存是否够用。我惯用的命令是:
python train.py \ --data datasets/vehicle/vehicle.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 3 \ --cache disk \ --noval--weights yolov5s.pt用官方预训练权重,不要用随机初始化,否则 3 个 epoch 什么都看不出来;--cache disk先把图片缓存到磁盘,避免首次训练大量读取小文件时的 I/O 等待;--noval是跳过验证,因为这次只检查链路,不追求验证指标。跑完 3 个 epoch 后看两点:第一,train loss 是否在下降;第二,日志里labels这一行是否为 0,如果labels为 0,说明标签没被加载到,后面跑 100 个 epoch 也是白费。
5. 避坑清单:汽车目标检测数据集制作与训练最容易翻车的 5 个环节
5.1 loss 不降、mAP 全程为 0:标签坐标没归一化
现象:训练日志显示 loss 从 0.09 降到 0.07 就不再动了,所有类别的 mAP 全部是 0;手动推理时框要么全图一个,要么完全落在背景。
原因:这是最经典的坐标翻车。我在 2.2 节强调过,YOLO 格式的cx cy w h必须是 0 到 1 的归一化值。很多人从 LabelImg 导出 XML 后只做了x2 - x1的宽度计算,忘了几img_width和img_height,导致标签里全是 1300、780 这种像素值。YOLOV5 不会对这类数据报错,因为 loss 计算时对超过 1 的宽高也有反应,但网络无法学到稳定的目标尺度。
解决:写一个全量遍历脚本,读回每个 txt,发现任意坐标大于 1 就用对应图片的宽高重新归一化。注意要先读图片尺寸再重写 txt,不能只把坐标截断到 0~1,那样会改变框的位置。修复后重新跑 4.3 节的单 epoch 试跑,看labels是否正常、loss 是否显著下降。
5.2 验证结果中所有车全变成了卡车:类别编号错位
现象:训练的指标看起来不错,mAP50 有 0.85,但推理时车全部被标成 truck,而手动标注明明写的是 car。
原因:类别编号从标注阶段就串了。比如你在第 2.2 节的CATEGORY_MAP里定义"car": 1, "truck": 0,而 data.yaml 的names里写的是0: car, 1: truck,两边的索引没有对齐。YOLOV5 的训练代码只消费标签里的数字,不会反向校验类别名,所以这种错位可以完整地传递到推理端。
解决:做一次一票否決式的抽查脚本,随机取 200 张图片,同时打印 txt 里的类别编号和图像上肉眼看到的车型,人工核对。更省事的方式是在第 4.1 节的可视化代码里,把cls直接显示成类别名而不是编号,这样一旦错位,字面上就穿帮。这个检查建议在数据集首次交付时做,不要拖到模型训完再查。
5.3 夜间车辆基本漏检:数据里只有白天样本
现象:train 数据在白天路况下 val 指标都超过 0.9,一换到夜间监控画面,召回率直接跌到 0.3。
原因:这是数据分布和部署场景不一致的问题,而不是模型问题。很多车辆数据集偏重晴天白天的停车场或高速场景,夜间样本要么少,要么因为低照度被标注人员删掉了。YOLOV5 在训练时虽然有 mosaic 和 HSV 扰动,但色彩空间的变化幅度有限,不能凭空造出"灯光明亮的夜晚车灯特征"。
解决:向 BDD100K 的 night 子集或自建夜间样本补齐训练集的场景比例。我在实际操作中会让夜间样本占训练集 30% 左右,并且额外对夜间图片做一次亮度均衡预处理,但不要在训练和验证时用不同的预处理函数,否则指标失真。补数据后重新统计各类别占比,夜间车灯和反光物体要单独看它们的标注是否紧贴车身,很多标注工具在半暗环境下会把光晕也框进去。
5.4 val 指标高但现场全崩:训练图片分辨率远低于部署分辨率
现象:训练用--img 416,val 指标看着还行,部署时摄像头有 1080 分辨率,结果小目标车辆完全检测不到。
原因:输入分辨率直接决定了模型能看到多小的目标。--img 416时一张 1080p 的图被压缩到 416,远处一辆 60 像素宽的车在压缩后只剩 23 像素,这已经接近 YOLOV5 small 的检测极限。很多车辆数据集里大量远距离目标,它们的归一化框非常小,低分辨率训练会直接忽略。
解决:训练时把输入提到--img 640或--img 1280。注意这会让显存占用翻倍甚至更多,我是先跑 5 个 epoch 的--img 640观察显存,再决定是否使用--img 1280。同时检查目标尺寸分布,如果小于 0.03 的目标占比很高,考虑对图片做切片训练,但切片的边界框要重新计算,不能直接沿用原图的归一化坐标。
5.5 数据集拷贝到新环境,训练报「No labels found」:相对路径和绝对路径混用
现象:在自己电脑上训练一切正常,把datasets/vehicle压缩包发给同事,他解压后运行,训练直接报train: No labels found in .../vehicle/images/train,但目录里明明有文件。
原因:data.yaml 里的path写的是C:\Users\me\datasets\vehicle这类绝对路径,或者压缩时把目录结构裁剪过,导致path指向的位置不存在。YOLOV5 对数据路径的解析是先读 yaml 里的path,再拼接train: images/train,所以只要path无效,后面全废。这不是代码问题,是数据包的可移植性问题。
解决:按 3.3 节的规范,path一律写相对路径,并且打包时把 data.yaml 和 images、labels 放在同一级。如果你经常要把数据集发给别人,建议再写一个verify_dataset.py,在训练前自动检查 images 和 labels 的完整性,列出缺失标签的图片数量和空标签的比例,让接收方一眼看出数据包是否有问题。这也是我最常用的交付习惯。
6. 进阶技巧:用 cutout 和难例回灌,把现有车辆数据扩出 3 倍有效样本
当你的目标检测数据集(YOLOV5目录格式)目录已经规范、训练能跑通后,下一步的核心不再是收集更多图片,而是怎么让每张图贡献更多有效监督信号。我常用的三个手段,按性价比从高到低排列。
第一个是 YOLOV5 自带的copy_paste增强。在data/hyps/hyp.scratch-low.yaml里打开copy_paste: 0.1,训练时每个 batch 有 10% 的概率把一张图的车辆目标复制到另一张图上。这对小目标尤其有效,因为车辆与车辆的相对位置被强行多样化,模型不会再把"车必须出现在路面中央"这种场景 bias 学进去。注意copy_paste对显存和训练时间的影响有限,初学者可以直接开。
第二个是难例回灌。训练完第一个版本后,拿那个模型去跑一批你没有标注过的实拍图,把置信度在 0.2 到 0.5 之间的预测框全部导出,人工快速筛选出真正的车,补标后加入labels/train。这一步能显著提升模型在复杂场景下的召回率,比单纯增加白天样本更管用。我一般把这批补标数据单独放在一个新目录,比如datasets/vehicle_hard/,训练时在 data.yaml 的 train 项里写上[images/train, ../vehicle_hard/images/train],这样原始数据不会被污染,随时可以回退。
第三个是输入分辨率的分阶段训练。先用--img 640训 200 个 epoch 让模型收敛到大致的特征表达,再用--img 1280接着训 50 个 epoch 微调。这个做法的效果接近换更大的模型,但显存压力小很多。注意切换分辨率后学习率要调低一半,否则前面学到的精确定位信息会被大梯度冲掉。
最后说一个血泪经验:每次在数据集上做增删改之前,我会把当前完整目录压缩归档成vehicle_v1.0_date.zip,训练实验记录也存成同名 yaml。这样模型翻车时,我能快速对比是数据改了还是代码改了,而不是在几百 GB 的目录里做黑匣子排查。这套习惯帮我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取