ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

真实道路车辆检测数据集:1000张图与VOC/COCO/YOLO标签及训练教程

2026/10/2 21:45:01 拓冰建站 浏览量
真实道路车辆检测数据集:1000张图与VOC/COCO/YOLO标签及训练教程 简介本资源为面向目标检测初学者与算法工程师的真实道路车辆检测数据集聚焦YOLO系列模型训练场景可解决车辆目标检测任务中数据获取难、标注格式不统一的问题。压缩包共2000个文件约115.18MB包含1000张真实道路场景图片以及对应的VOCxml、COCOjson和YOLOtxt三种格式标签分别存放于不同文件夹可直接用于YOLO系列训练另附数据集划分脚本、YOLO环境搭建与训练教程等py、html、yaml文件方便按需划分训练集、验证集与测试集。目前已有491人学习下载。读者可获得一套开箱即用的车辆检测数据与配套工具快速完成环境配置、数据划分与模型训练并通过教程掌握Windows与Linux下的实操流程适合课程设计、毕业设计及入门级检测项目使用。1. 真实道路车辆检测数据集1000 张图、三套标签、一条能跑通的训练链路手上有一批真实道路场景的行车图片想拿来训练 YOLO 做车辆检测最卡人的从来不是模型结构而是数据这一环图片从哪来、框怎么标、VOC 和 COCO 的 XML/JSON 怎么转成 YOLO 的 txt、训练集验证集怎么切、切完类别对不对得上。这套「1000 张真实道路车辆图片 VOC/COCO/YOLO 三种格式标签 划分脚本 训练教程」的组合解决的正是这条从原始图到可训练数据集的完整链路。它适合两类人一类是刚接触 YOLO 目标检测、想拿一份现成数据把yolov8训练自己的数据集流程跑通的新手另一类是手里有自采道路图、需要一套可靠的格式转换与划分脚本做模板的工程师。下面按「数据长什么样 → 三种格式怎么互转 → 划分脚本怎么写 → 训练怎么配 → 坑在哪」的顺序讲透。2. 先看清数据1000 张真实道路图与三种标签格式的对应关系拿到一个压缩包第一件事不是急着解压训练而是把目录结构和标签格式摸清楚。真实道路车辆检测的数据组织方式直接决定了后面转换脚本和训练配置怎么写。这一章先把「数据是什么」讲明白再讲「为什么同一批标注要存三种格式」。2.1 目录结构与文件命名约定常见做法是把图片和标签分开放图片统一放images/标签按格式分目录。一个能直接对接 YOLO 训练的典型结构是这样road_vehicle_dataset/ ├── images/ # 1000 张真实道路图片jpg 为主 │ ├── 000001.jpg │ └── ... ├── annotations_voc/ # VOC 格式每张图一个同名 xml │ ├── 000001.xml │ └── ... ├── annotations_coco/ # COCO 格式整份一个 json │ └── instances.json ├── labels_yolo/ # YOLO 格式每张图一个同名 txt │ ├── 000001.txt │ └── ... ├── split_dataset.py # 划分脚本 └── train_yolo.py # 训练入口命名上最关键的一条图片名、VOC 的 xml 名、YOLO 的 txt 名必须严格同名只差扩展名。很多转换脚本翻车就是因为图片叫000001.jpg标签却叫1.xml脚本按 stem 匹配时直接对不上最后训练时一堆图没有标签模型学了个寂寞。VOC 的 xml 里车辆框存在object节点下包含name类别名、bndbox里的xmin/ymin/xmax/ymax左上右下像素坐标。COCO 的 json 里框是[x, y, width, height]的绝对像素值类别通过categories数组的 id 映射。YOLO 的 txt 每行是class_id cx cy w h全部归一化到 0~1。这三种格式的坐标语义完全不同转换时最容易错的就是「绝对像素」和「归一化」的混用。2.2 为什么同一批标注要同时保留 VOC、COCO、YOLO有人会问既然要训 YOLO直接留 YOLO 格式不就行了为什么还要 VOC 和 COCO血泪经验是——格式决定了你以后能复用什么。VOC 格式是很多经典检测框架和标注工具LabelImg 默认就导出 VOC的通用交换格式标注阶段产出的往往就是 xml。COCO 格式则是另一大批模型和评测脚本的入口coco2017数据集结构那套images/annotations/categories三段式被广泛沿用很多预训练权重和评估工具只认 COCO json。YOLO 格式是训练时真正喂给 dataloader 的。所以合理的工作流是标注产出 VOC → VOC 作为中间母格式 → 转 COCO 供评估/复用 → 转 YOLO 供训练。三种格式同时保留等于给自己留了后悔药换框架、换评估指标、换预训练模型时不用重新标一遍。1000 张图的标注成本不低格式齐全比省那点磁盘空间重要得多。2.3 类别体系与车辆检测的常见类别划分真实道路车辆检测的类别划分直接决定class_id的映射。常见做法有两种粒度粒度典型类别适用场景粗粒度vehicle所有车一类只做有无车辆、计数、简单避障细粒度car / bus / truck / motorcycle / bicycle需要区分车型的交通分析粗粒度标注快、类别少、训练容易收敛适合 1000 张这种中小规模数据。细粒度对标注质量要求高1000 张里如果某些类别样本只有几十个很容易出现类别不平衡训练时小类别几乎学不到。我的建议是先按粗粒度跑通全流程确认 mAP 正常后再考虑细分。类别映射必须三处一致VOC 的name、COCO 的categories、YOLO 的class_id。常见翻车是 VOC 里写的是中文「轿车」COCO 里映射成carYOLO 里 class_id 又是 0三处对不上评估时混淆矩阵全乱。定好一份classes.txt所有转换脚本都读它是唯一稳妥的做法car bus truck motorcycle bicycle行号即 class_id从 0 开始。这份文件是整条链路的「单一事实来源」改类别只改这里。3. 三种格式互转VOC 转 YOLO、VOC 转 COCO 的脚本与参数格式转换是这套数据集里最容易出 bug 的环节因为坐标语义、坐标系原点、归一化基准三处都可能出错。这一章给出可直接抄的转换脚本并逐段说明参数含义。3.1 VOC 转 YOLO归一化坐标与 class_id 映射VOC 的框是绝对像素xmin/ymin/xmax/ymaxYOLO 要的是归一化的中心点加宽高。转换公式是cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h对应脚本import os import xml.etree.ElementTree as ET from PIL import Image CLASSES [car, bus, truck, motorcycle, bicycle] CLASS_TO_ID {c: i for i, c in enumerate(CLASSES)} def voc_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] img_path os.path.join(img_dir, stem .jpg) # 用真实图片尺寸做归一化基准不能用 xml 里的 size 盲信 img_w, img_h Image.open(img_path).size tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASS_TO_ID: continue # 未定义类别直接跳过避免 class_id 越界 bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 裁剪到图像边界防止标注越界导致归一化后 1 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{CLASS_TO_ID[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_dir, stem .txt), w) as f: f.write(\n.join(lines))逻辑说明CLASS_TO_ID保证类别顺序和classes.txt一致归一化基准用PIL读真实图片尺寸而不是 xml 里size节点——标注工具的 size 字段经常和实际图片对不上这是隐蔽的坑。坐标裁剪到边界是为了防止个别越界标注让归一化值超过 1YOLO 训练时遇到 1 的框会直接报错或静默丢弃。参数说明xml_dir、img_dir、out_dir三个路径按实际目录传CLASSES必须和classes.txt完全一致顺序不能变否则 class_id 全错位。3.2 VOC 转 COCOjson 结构与 categories 映射COCO 格式是一份大 json包含images、annotations、categories三个核心数组。转换时最容易错的是id的唯一性和category_id的起始值——COCO 官方约定category_id从 1 开始0 通常留给背景而 YOLO 从 0 开始两者不能混。import os import json import xml.etree.ElementTree as ET from PIL import Image CLASSES [car, bus, truck, motorcycle, bicycle] def voc_to_coco(xml_dir, img_dir, out_json): coco {images: [], annotations: [], categories: []} # COCO 约定 category_id 从 1 开始 for i, name in enumerate(CLASSES, start1): coco[categories].append({id: i, name: name, supercategory: vehicle}) ann_id 1 img_id 1 for xml_file in sorted(os.listdir(xml_dir)): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] img_path os.path.join(img_dir, stem .jpg) img_w, img_h Image.open(img_path).size coco[images].append({ id: img_id, file_name: stem .jpg, width: img_w, height: img_h }) root ET.parse(os.path.join(xml_dir, xml_file)).getroot() for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASSES: continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) w xmax - xmin h ymax - ymin if w 0 or h 0: continue # 丢弃退化框 coco[annotations].append({ id: ann_id, image_id: img_id, category_id: CLASSES.index(name) 1, bbox: [xmin, ymin, w, h], # COCO 是 [x,y,w,h] 绝对像素 area: w * h, iscrowd: 0 }) ann_id 1 img_id 1 with open(out_json, w) as f: json.dump(coco, f)逻辑说明bbox用[x, y, w, h]绝对像素和 YOLO 的归一化中心点格式完全不同别混。area字段是 COCO 评估脚本要用的必须填。iscrowd标 0 表示普通实例。category_id用CLASSES.index(name) 1保证从 1 开始且和categories对应。参数说明out_json输出单文件img_id和ann_id必须全局唯一且递增重复 id 会让评估脚本读取出错。3.3 转换后的自检三行命令验证标签一致性转完不要直接开训先做一致性自检。下面三行能挡掉大部分低级错误# 1. 图片数和标签数是否一致 ls images/ | wc -l ls labels_yolo/ | wc -l # 2. 有没有空标签文件空文件说明该图没框或转换失败 find labels_yolo/ -name *.txt -empty | head # 3. 抽查一行确认 class_id 在合法范围、坐标都在 0~1 head -n 3 labels_yolo/000001.txt第一行数量对不上说明有图没标签或有标签没图训练时 dataloader 会报错或静默跳过。第二行空标签要区分是「图里真没车」还是「转换失败」——前者可以留后者必须查。第三行看坐标是否都在 0~1出现大于 1 或负数回去查 3.1 里的边界裁剪。这三步花不了一分钟能省掉后面几小时的排查。4. 划分脚本训练集/验证集切分与类别分布检查数据集划分看着简单实际是训练翻车的重灾区。随机切分如果没控制类别分布验证集里可能某个类别一个样本都没有训练日志里那个类别的 mAP 直接是 0你还以为是模型不行。4.1 按 8:2 切分并保证类别覆盖常见做法是 8:2 或 7:3 切分。关键是切分后要检查每个类别在训练集和验证集里都有足够样本。下面脚本先按图片切分再统计类别分布import os import random import shutil from collections import Counter def split_dataset(img_dir, label_dir, out_root, val_ratio0.2, seed42): random.seed(seed) # 固定种子保证可复现 stems [os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png, .jpeg))] random.shuffle(stems) n_val int(len(stems) * val_ratio) val_stems set(stems[:n_val]) train_stems set(stems[n_val:]) for split, subset in [(train, train_stems), (val, val_stems)]: img_out os.path.join(out_root, images, split) lbl_out os.path.join(out_root, labels, split) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for stem in subset: for ext in (.jpg, .png, .jpeg): src os.path.join(img_dir, stem ext) if os.path.exists(src): shutil.copy(src, os.path.join(img_out, stem ext)) break lbl_src os.path.join(label_dir, stem .txt) if os.path.exists(lbl_src): shutil.copy(lbl_src, os.path.join(lbl_out, stem .txt)) # 统计两个 split 的类别分布 for split in (train, val): counter Counter() lbl_dir os.path.join(out_root, labels, split) for f in os.listdir(lbl_dir): with open(os.path.join(lbl_dir, f)) as fh: for line in fh: if line.strip(): counter[int(line.split()[0])] 1 print(split, dict(sorted(counter.items())))逻辑说明seed42固定随机种子保证每次切分结果一致方便复现和对比实验。先 shuffle 再切片避免按文件名顺序切导致分布偏差比如按时间拍的图前 800 张是白天、后 200 张是夜晚顺序切会让验证集全是夜晚。最后打印两个 split 的类别计数一眼看出有没有类别缺失。参数说明val_ratio默认 0.21000 张图对应 200 张验证集够评估用数据量更小时可以调到 0.3。seed换值会得到不同切分做交叉验证时可以循环换 seed。4.2 类别不平衡时怎么处理打印出来的分布如果发现某个类别在验证集里只有个位数或者训练集里某类占比不到 5%就要处理。三种常见做法一是分层切分按类别把样本分组后再切保证每个类别在两个 split 里比例接近。二是过采样小类别训练时对小类别图片重复采样。三是调损失权重YOLO 训练配置里可以给不同类别加权但多数实现不直接暴露这个参数改起来麻烦。对 1000 张这种规模最实用的是分层切分。如果某类别总样本就几十个再怎么切验证集也就十几个这时候更该考虑的是合并类别——把样本太少的细分类别并进粗类比硬训一个学不动的类别划算。4.3 生成 YOLO 训练用的 data.yaml切分完要生成 YOLO 训练配置。以 Ultralytics 系 YOLO 为例data.yaml长这样path: /abs/path/to/road_vehicle_dataset train: images/train val: images/val nc: 5 names: 0: car 1: bus 2: truck 3: motorcycle 4: bicyclepath用绝对路径最稳相对路径在不同工作目录下启动训练时经常找不到。train/val指向图片目录YOLO 会自动去找同级的labels/目录把路径里的images换成labels。nc是类别数必须和names长度一致写错会直接报维度不匹配。names的顺序必须和classes.txt、转换脚本里的CLASSES完全一致这是整条链路一致性的最后一环。5. 训练教程从环境配置到跑通第一个 epoch数据准备好接下来是训练。这一章给出一条能跑通的最小训练链路重点讲参数怎么设、日志怎么看。5.1 环境配置与依赖版本YOLO 训练环境的核心依赖是 PyTorch 和对应 CUDA。常见做法是用 conda 建独立环境避免和系统 Python 冲突conda create -n yolo python3.10 -y conda activate yolo # 按本机 CUDA 版本装 PyTorch下面以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python pillow pyyaml逻辑说明PyTorch 版本必须和 CUDA 驱动匹配装错会出现torch.cuda.is_available()返回 False训练自动掉到 CPU 上1000 张图一个 epoch 能跑几十分钟。装完先验证python -c import torch; print(torch.__version__, torch.cuda.is_available())输出里True才说明 GPU 可用。这一步别省我见过太多人训了半天发现用的是 CPU。5.2 训练命令与关键参数以 Ultralytics YOLO 为例最小训练命令yolo detect train \ data/abs/path/to/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/train \ nameroad_vehicle参数说明model用预训练权重如yolov8n.pt比从零训收敛快得多小数据集强烈建议用预训练。epochs100对 1000 张图通常够配合patience20早停验证指标 20 轮不涨就停省时间。imgsz640是通用尺寸显存不够降到 416 或 320。batch16按显存调8G 显存跑 640 尺寸大概能到 16爆显存就减半。lr00.01是初始学习率用预训练权重时这个值合适从零训可以调到 0.001。5.3 训练日志与指标怎么看训练启动后重点盯几个指标。box_loss和cls_loss应该整体下降如果震荡剧烈或发散多半是学习率太大或 batch 太小。mAP50是 IoU 阈值 0.5 下的平均精度车辆检测这种目标清晰的任务1000 张图跑到 0.7 以上算正常。mAP50-95更严格通常比mAP50低 0.2 左右。如果mAP50一直上不去先别怀疑模型回去查数据标签是不是有大量空文件、类别是不是标错、验证集是不是和训练集分布差太多。yolo混淆矩阵总合不唯一这类问题往往就是类别映射错位或标签格式不对导致的根子在数据不在模型。训练完在runs/train/road_vehicle/weights/下会有best.pt和last.ptbest.pt是验证指标最好的权重推理用它。验证一下yolo detect predict modelruns/train/road_vehicle/weights/best.pt \ source/abs/path/to/test_images saveTrue输出图会画上检测框肉眼扫一遍看有没有系统性漏检或误检比只看数字更直观。6. 避坑与排查标签、划分、训练里最容易翻车的五件事这一章全是踩过的坑每条按「现象 → 原因 → 解决」写遇到问题直接对号入座。现象一训练启动就报No labels found或大量图片被跳过。原因图片和标签不同名或标签目录结构和data.yaml不匹配。YOLO 默认按「图片路径里images换成labels、扩展名换成.txt」去找标签任何一环对不上就找不到。 解决跑 3.3 的自检命令确认图片数和标签数一致、同名对应。目录结构严格按images/train配labels/train。现象二训练不报错但 mAP 一直是 0 或极低。原因class_id 越界或类别映射错位。比如data.yaml里nc5但标签里出现了 class_id5 甚至更大YOLO 会忽略这些框或者 VOC 里类别名和classes.txt顺序不一致class_id 全错。 解决检查classes.txt、转换脚本CLASSES、data.yaml的names三处顺序完全一致用awk {print $1} labels/*.txt | sort -u看 class_id 范围是否在0 ~ nc-1。现象三验证集某个类别 mAP 为 0其他类别正常。原因该类别在验证集里样本太少甚至没有随机切分没控制分布。 解决用 4.1 脚本打印类别分布确认每个类别在 val 里都有样本样本实在太少的类别考虑合并或过采样。现象四训练中途 loss 变 NaN 或突然飙升。原因学习率过大、batch 太小导致梯度不稳或标签里有退化框宽高为 0 或负。 解决降lr0到 0.001增大 batch转换脚本里加if w 0 or h 0: continue过滤退化框3.2 已含。现象五推理时框位置整体偏移或大小不对。原因训练和推理的imgsz不一致或转换时归一化基准用错用了 xml 里的 size 而非真实图片尺寸。 解决训练和推理保持同一imgsz转换时用PIL读真实尺寸做归一化别信 xml 的size。提示这五条里前两条占了实际问题的七成以上遇到异常先查标签一致性和 class_id 范围再怀疑模型和超参。7. 进阶技巧用预训练权重和冻结策略把 1000 张小数据集榨出更高精度1000 张图在目标检测里属于小数据集从零训很难收敛到理想精度。真正拉开差距的是预训练权重的用法和训练策略的微调。我一般会分两阶段训先冻结主干只训检测头再解冻全网络微调。第一阶段冻结主干让检测头先适配你的类别yolo detect train \ data/abs/path/to/data.yaml \ modelyolov8n.pt \ epochs30 \ freeze10 \ lr00.01 \ imgsz640 batch16freeze10表示冻结前 10 层主干部分只更新后面的检测头。这样收敛快、不容易过拟合30 轮就能让检测头初步适配车辆类别。第二阶段解冻全网络用小学习率微调yolo detect train \ data/abs/path/to/data.yaml \ modelruns/train/road_vehicle/weights/best.pt \ epochs100 \ lr00.001 \ imgsz640 batch16 patience20注意第二阶段model指向第一阶段产出的best.ptlr0降到 0.001避免大学习率把预训练学到的特征冲掉。这套两阶段策略在 1000 张量级上通常比直接训能高出几个点 mAP。数据增强也是小数据集的救命稻草。YOLO 默认开了 mosaic、HSV 抖动、随机翻转对道路场景够用。如果发现模型对夜间或雨天泛化差可以针对性补充这类样本而不是盲目加大增强强度——增强过头反而让模型学不到真实分布。验证阶段除了看 mAP建议把best.pt在几十张没参与训练的图上跑一遍肉眼核对。数字好看但框飘的情况不少见尤其是小目标和遮挡目标。我自己的习惯是每次训完必抽 20 张图看预测结果比盯日志更能发现问题。这套数据集和脚本的价值不在于省了标注那点事而在于它把「格式转换 → 划分 → 训练 → 排查」这条链路完整串起来了你照着走一遍以后换任何自采数据都能复用这套流程。希望帮到你。本文还有配套的精品资源点击获取