简介:一份面向目标检测与车辆检测场景的数据集说明PDF,共1个文件,大小约7.19MB,内容介绍一套托管于百度网盘的真实车辆图片数据集及配套训练说明。该数据集包含城市道路、高速道路、农村道路及遮挡、严重遮挡等丰富场景的高质量车辆图像约1000张,划分Auto、Bus、Car、LCV、Motorcycle、Multi-Axle、Tractor、Truck共8个类别,可直接服务于交通道路监控场景下的车辆检测项目,也可作为监控场景通用车辆检测数据集的补充。配套标签均采用labelimg标注,提供VOC(xml)、COCO(json)、YOLO(txt)三种标准格式,便于直接导入YOLO等算法训练。PDF内录有标注示例截图与数据集获取方式,同时附赠YOLO11一键训练脚本说明,覆盖GPU(GPUs)、CPU、Mac(M芯片)三平台,并给出博主训练结果日志供参考,帮助使用者快速评估数据构成并接入现有训练流程。已有1333人学习下载,适合从事车辆检测、智慧交通或相关算法研究的技术人员。
1. 拿到“1000张车辆图 + 三种格式标签 + YOLO11训练脚本”,先别急着跑训练
车辆检测数据集是目标检测里最成熟的落地场景之一,停车场统计、路口车流分析、园区安防都会先要一个稳定的“车在哪”结果。标题这套东西,本质上把两个痛点一次补齐:VOC/COCO/YOLO 三种格式标签,以及支持 GPU/CPU/Mac 三平台的 YOLO11 一键训练脚本。适合两类人:0 基础想跑通第一个车辆识别模型的新手,和已经在做智慧交通项目、想用 1000 张小数据快速验证方案可行性的老手。我的经验是,这套方案能不能成,不取决于脚本写得多少,而取决于数据集和脚本衔接处的格式转换、数据划分、设备选择是否经得住推敲。
2. 动手前先拆数据:1000张车图里的分布,比“三种格式”更决定模型效果
很多新手拿到压缩包后,第一反应是把 1000 张图直接塞进训练脚本。这里有个隐性前提:训练脚本要能用一个 data.yaml 指到 train/val 两个目录,并且每张图都有一一对应的标签文件。如果图 1000 张、标签只有 980 份,或者 train 里混进了本来该用来做最终测试的图,模型效果会莫名其妙地差。我一般先把数据拆成 train/val/test 三个子集,再跑一遍标注统计,最后才进入格式转换。这一步看着琐碎,却是后面所有流程能不能复现的地基。
2.1 先做数据集划分:images 和 labels 同名同步,缺失标签直接剔除
如果你的标签已经统一成 YOLO txt,那第一步不是转格式,而是划分数据集。我用的脚本是复制而不是移动文件,因为移动原图一旦切错,后面想重新转 COCO 就得重下数据。
import random import shutil from pathlib import Path random.seed(7) src_img = Path("dataset/images") src_lbl = Path("dataset/labels") out_root = Path("yolo_split") ratio = {"train": 0.8, "val": 0.1, "test": 0.1} all_imgs = sorted(src_img.glob("*.jpg")) random.shuffle(all_imgs) pos = 0 for split, r in ratio.items(): count = int(len(all_imgs) * r) chosen = all_imgs[pos:pos + count] pos += count img_dir = out_root / split / "images" lbl_dir = out_root / split / "labels" img_dir.mkdir(parents=True, exist_ok=True) lbl_dir.mkdir(parents=True, exist_ok=True) for img_path in chosen: lbl_path = src_lbl / (img_path.stem + ".txt") if not lbl_path.exists(): print(f"[warning] 缺少标签,跳过: {img_path.name}") continue shutil.copyfile(img_path, img_dir / img_path.name) shutil.copyfile(lbl_path, lbl_dir / lbl_path.name) print(f"原图数量: {len(all_imgs)}")这个脚本的核心逻辑很简单:按 8:1:1 把图片随机打散,复制图片时同步复制同名 txt。random.seed(7) 固定随机种子,保证每次跑出来都是同一份划分,这对后续调参特别重要。两处值得按素材调整:如果某一类场景特别少,val 可以从 0.1 降到 0.05;如果后续要做超参数对比,test 建议至少留 50 张,否则最终 mAP 没有统计意义。
提示:YOLO 训练时,data.yaml 里 train 和 val 指向的都是 images 目录,标签必须放在同级 labels 目录下,图片和标签主文件名要完全一致,后缀分别是 .jpg/.txt。
我见过最隐蔽的坑,是视频抽帧得到的数据集,连续两帧被拆到了 train 和 test。这种随机拆分会让模型在测试时“作弊”,因为相邻帧太像。正确做法是按视频片段或时间段分组,先把连续帧作为一个 group,再从 group 层面划分。1000 张图如果只来自一段视频,宁可不用随机拆分,而是每隔多少帧抽一帧作为 test,否则最后实拍验证一定会翻车。
2.2 标注质量自检:统计框面积、长宽比和每张图的框数
标签格式正确不代表标注质量过关。1000 张车辆图的人工标注,最常见的两块暗雷是:框只框车头没框车尾,以及小目标图上的框被画得过大。单靠眼睛翻很难发现问题,我习惯先跑一个统计脚本,把归一化面积、宽高比、每张图框数全打印出来。
from pathlib import Path import numpy as np lbl_dir = Path("yolo_split/train/labels") area_list, ratio_list, count_list = [], [], [] for txt in sorted(lbl_dir.glob("*.txt")): lines = [ln.strip() for ln in txt.read_text().splitlines() if ln.strip()] count_list.append(len(lines)) for line in lines: cls, cx, cy, w, h = map(float, line.split()) if w <= 0 or h <= 0: print(f"[warning] 无效框: {txt.name} -> {line}") continue area_list.append(w * h) ratio_list.append(max(w, h) / max(min(w, h), 1e-6)) area_list = np.array(area_list) ratio_list = np.array(ratio_list) print("图像数:", len(count_list)) print("框数:", len(area_list)) print("平均每图框数:", np.mean(count_list)) print("框面积 min/p50/max:", area_list.min(), np.percentile(area_list, 50), area_list.max()) print("宽高比 p90:", np.percentile(ratio_list, 90))这里的 w 和 h 是 YOLO 归一化后的值。框面积等于 w*h,如果 p50 小于 0.01,说明一半以上的框只占图面的百分之一,训练时小目标分支会非常吃力,后续训练 imgsz 得加到 800 或 1024。宽高比 p90 超过 2.5,说明素材里横向大车多,单类检测影响不大,但要是和自行车、行人一起做多类检测,就需要注意特征提取的感受野匹配问题。每张图超过 15 个框的 txt,我基本都会打开原图看一眼,高密度框大概率是同一辆车被重复标注了。
关于类别数量,标题没说,但这直接影响脚本里的 nc。1000 张是中小规模数据集,我通常建议压缩成单类 vehicle,或者按车种拆成 car/truck/bus 三类。参考 BDD100K 这类开放车辆数据集的类别口径没问题,但别被 10 个类诱惑,1000 张图拆 10 类以后,很多类可能只有几十个框,类别不均衡会让训练过程像黑匣子一样难调。样本少就靠类别少来补,这是小数据集的第一原则。
3. VOC/COCO/YOLO 三种格式怎么转:坐标参考系和类别编号才是关键
标题说的三种格式,常见做法是分别给 VOC 的 XML、COCO 的 JSON、YOLO 的 txt。训练脚本主要认 YOLO txt,所以第一步要把另外两种格式转成 txt。但转换不只是改文件后缀,而是两个底层问题:坐标单位,以及类别 id 体系。VOC 和 COCO 用的是像素绝对坐标,YOLO 用的是归一化的中心点加宽高。类别体系上,COCO 经常带背景类 id 0,VOC 也有 background 约定,YOLO 自定义数据集则一般从 0 开始连续编号。哪个不对齐,模型都可能不报错但默默学歪。
3.1 VOC XML 转 YOLO txt:先做坐标归一化,再做类别 id 映射
VOC 的 XML 里每个目标是一个<object>,框坐标写在<bndbox>下,xmin/ymin 是左上角,xmax/ymax 是右下角。转成 YOLO 后,需要的是中心点坐标和归一化宽高。下面这段是常见的最小实现,带上越界裁剪和类别映射。
import xml.etree.ElementTree as ET from pathlib import Path # 这里把类名和 YOLO 类别 id 对应好,0 代表第一类 class_map = {"car": 0, "truck": 1, "bus": 2} def voc_to_yolo(xml_path: Path, out_path: Path) -> None: root = ET.parse(xml_path).getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: 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) xmax = min(xmax, img_w) ymax = min(ymax, img_h) x_c = (xmin + xmax) / 2.0 / img_w y_c = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_map[name]} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}") out_path.write_text("\n".join(lines) + "\n") for xml_path in Path("voc_annotations").glob("*.xml"): voc_to_yolo(xml_path, Path("labels") / (xml_path.stem + ".txt"))代码里两处值得强调。第一,xmax 和 ymax 要 clamp 到图片尺寸内。很多标注工具的框会超出边界几个像素,YOLO 训练时一旦读到 w/h 大于 1,损失函数会把这个框当成一个超大目标,轻则损失异常重则 loss 炸掉。第二,class_map 必须自己维护,不要依赖 XML 里的 name 直接转成数字,因为不同批次的 XML 可能一个叫 “car”,另一个叫 “Car”,大小写不一致就会产生两个类别。转换后我会随机挑几张图,用画框脚本叠加显示原图和 txt,验证坐标没有整体偏移。这个验证动作看似笨,但能省下后面训练失败后排查的时间。
如果你想从 YOLO txt 逆转到 VOC,公式反过来就行:xmin = (xc - w/2) * img_w,ymax = (yc + h/2) * img_h。但注意 YOLO txt 本身没有图片尺寸,必须另读原图宽高,这也是为什么我前面一直强调图片和标签要同名同目录,少一个都追不回来。
3.2 COCO JSON 转 YOLO txt:别直接拿 category_id 当训练类别
COCO 的 bbox 比 VOC 多一层陷阱:它是 [x, y, w, h],x、y 是左上角坐标,w、h 是宽高,不是右下角。另外,json 里的 category_id 往往是原始 id,可能从 1 开始,也可能中间有空号,直接把 category_id 当作 YOLO 类别编号,会让模型学到好几个“空类别”,背景也会被当成目标的一部分。
import json from pathlib import Path def coco_to_yolo(json_path: Path, label_out: Path) -> None: data = json.loads(json_path.read_text()) categories = data["categories"] ctg_map = {c["id"]: i for i, c in enumerate(categories)} img_id_to_meta = {} for img in data["images"]: img_id_to_meta[img["id"]] = img label_out.mkdir(parents=True, exist_ok=True) anns_by_img = {} for ann in data["annotations"]: anns_by_img.setdefault(ann["image_id"], []).append(ann) for img_id, anns in anns_by_img.items(): img_meta = img_id_to_meta[img_id] img_w, img_h = img_meta["width"], img_meta["height"] lines = [] for ann in anns: x, y, w, h = ann["bbox"] if w <= 0 or h <= 0: continue cx = (x + w / 2.0) / img_w cy = (y + h / 2.0) / img_h nw = min(w, img_w - x) / img_w nh = min(h, img_h - y) / img_h lines.append(f"{ctg_map[ann['category_id']]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") out_file = label_out / (img_meta["file_name"].rsplit(".", 1)[0] + ".txt") out_file.write_text("\n".join(lines) + "\n") coco_to_yolo(Path("annotations.json"), Path("labels"))关键在ctg_map = {c["id"]: i for i, c in enumerate(categories)}这一句。它把 json 里不连续的原始 id 重新映射成 0、1、2,避免类别空洞。如果转换后标签文件出现超过 10 的类别数字,那多半是这一步漏了。还要注意 COCO 的 file_name 可能是相对路径,比如images/train/0001.jpg,如果直接把 file_name 拼接到 output,目录会多出几层。这会让 YOLO 的 labels 目录和 images 目录对不上,训练时直接报错找不到标签。我会先Path(file_name).name,只取文件名,保证标签平铺到同一个 labels 目录。
三种格式都备齐的好处是用在验证和部署上:COCO 格式方便和公共指标对比,VOC 格式方便用传统工具做可视化,YOLO txt 是最直接的训练入口。标题说对应三种格式,我的落地建议是转换后保留一份转换脚本和一份“原格式文件名-新文件名”的映射表,否则下个月再拿到新版标注,你又得从零转一遍。
4. 用一键训练脚本在 GPU/CPU/Mac 三平台跑通 YOLO11:设备识别和参数设置
训练脚本要解决的第一个问题不是网络结构调得多花哨,而是“训练跑在哪一块硬件上”。GPU 走 CUDA,Mac 的 Apple Silicon 走 MPS,普通电脑退到 CPU。很多新手在自己电脑上能跑,换台 Mac 就报 torch.backends 没有 mps,换台 Windows 又看 CUDA not available。真正的三平台脚本必须像下面这样,先自动探测计算后端,再决定训练参数。
4.1 环境配置:先装 Ultralytics,再确认 yolo11 能感知到当前设备
YOLO11 的常用训练入口是 Ultralytics 的 Python 库,官方维护得比较勤,环境和模型是同一个体系,0 基础纯小白最稳妥的路径就是先建一个干净的虚拟环境。
python -m venv venv_vehicle source venv_vehicle/bin/activate # Windows 用 venv_vehicle\Scripts\activate pip install --upgrade pip pip install ultralytics安装完先做一次设备探测,不要直接开始训练:
python -c "import torch; print('cuda:', torch.cuda.is_available()); print('mps:', hasattr(torch.backends, 'mps') and torch.backends.mps.is_available())"如果输出里 cuda 为 False,先检查驱动和 torch 版本。Mac 上 cuda 为 False 是正常的,只要 mps 是 True 就能用 Apple Silicon 训练。CPU 平台的输出两项都是 False,脚本会自动落在 CPU 后端。这里的判断逻辑会直接复用到下面的一键训练脚本里,所以先跑通这一段,等于给后面的脚本铺路。YOLO11 的模型文件名一般是 yolo11n.pt、yolo11s.pt,n 是 nano,s 是小模型,首次运行会自动下载权重,需要联网。
4.2 一键训练脚本:设备自动选择、data.yaml 自动生成、最小参数入口
这三个平台环境差异大,我一般把脚本设计成“只传一个 data 目录就能开跑”,其他参数都给默认值。脚本会自动检查数据集结构,自动写 data.yaml,自动选设备。下面是能直接用的一段,注释里写了关键设计原因。
import argparse from pathlib import Path import torch from ultralytics import YOLO def pick_device(args_device: str | None) -> str: """GPU 优先,其次 Mac MPS,最后 CPU。也可以手动覆盖。""" if args_device: return args_device if torch.cuda.is_available(): return "cuda:0" if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): return "mps" return "cpu" def write_data_yaml(data_root: Path, class_names: list[str]) -> Path: """自动生成 YOLO 训练需要的 data.yaml""" names_str = str(class_names).replace("'", '"') yaml_file = data_root / "data.yaml" yaml_file.write_text( f"path: {data_root.as_posix()}\n" "train: train/images\n" "val: val/images\n" f"nc: {len(class_names)}\n" f"names: {names_str}\n" ) return yaml_file def main() -> None: parser = argparse.ArgumentParser(description="车辆检测 YOLO11 一键训练") parser.add_argument("--data", type=Path, required=True, help="数据集根目录,下含 train/val/images") parser.add_argument("--classes", nargs="+", default=["car", "truck", "bus"], help="类别名称,顺序对应标签 id") parser.add_argument("--weights", default="yolo11n.pt", help="初始化权重") parser.add_argument("--epochs", type=int, default=100) parser.add_argument("--batch", type=int, default=16) parser.add_argument("--imgsz", type=int, default=640) parser.add_argument("--lr0", type=float, default=0.005) parser.add_argument("--device", default=None, help="手动指定 cuda:0 / mps / cpu") args = parser.parse_args() data_root = args.data.resolve() if not (data_root / "train" / "images").exists(): raise ValueError("没找到 train/images 目录,请先完成第 2 章的数据划分") yaml_path = write_data_yaml(data_root, args.classes) device = pick_device(args.device) print(f"[vehicle-yolo11] device = {device}, dataset = {yaml_path}") model = YOLO(args.weights) model.train( data=str(yaml_path), epochs=args.epochs, batch=args.batch, imgsz=args.imgsz, lr0=args.lr0, device=device, cache=False, project="runs/vehicle_yolo11", name="vehicle", exist_ok=True, ) if __name__ == "__main__": main()这个脚本解决三平台兼容的关键在 pick_device。手动指定--device的优先级最高,适合明确要固定跑在 CPU 上做对比的场景;没有指定时,CUDA 优先于 MPS,MPS 优先于 CPU。用hasattr(torch.backends, "mps")而不是直接访问is_available(),是为了兼容比较老的 torch 版本,避免 Mac 上没有 mps 属性时直接报 AttributeError。
write_data_yaml 里,data_root 用 resolve 换成绝对路径,这是避免“在 A 目录启动终端,在 B 目录跑训练”时找不到图片的经典问题。classes 的参数顺序必须和标签里的数字对应:如果标签里 0 是 car,1 是 truck,那命令行里就要写--classes car truck。实际使用中,单类模型直接--classes vehicle最省心;要分车型就按权重从多到少排。
模型训练参数里,lr0 默认给 0.005 而不是 Ultralytics 默认的 0.01,对其他类似数据集更稳妥。小数据集和 YOLO11 这种深层网络,学习率稍微大一点就容易在首个 epoch 冲出去,出现 loss 瞬间飙高的情况。1000 张图、100 个 epoch、batch 16,普通 GPU 上一般十几分钟到一个小时能跑完一轮;CPU 上时间会增加几倍,所以脚本里没把 device 写死,而是让机器自己判断。
4.3 三平台运行时该改哪些参数:显存、内存和 MPS 的边界
同样的脚本在不同平台跑,最需要调整的是 batch。GPU 显存不足会直接 OOM,Mac 的 MPS 内存不足时不会立刻报错,而是频繁换页,让训练速度看起来像是在原地踏步。我常用的起点如下:
| 运行环境 | 推荐模型 | batch 建议 | 注意事项 |
|---|---|---|---|
| NVIDIA GPU 显存 6 GB 以上 | yolo11s.pt | 16 | 显存紧张时 batch 降到 8 |
| 普通笔记本 CPU | yolo11n.pt | 4~8 | imgsz 可降到 480 加速 |
| Mac Apple Silicon MPS | yolo11n.pt | 8~16 | 训练变慢时优先降 batch,不要直接上 yolo11s |
CPU 平台还有一个容易忽略的参数是 Ultralytics 的 workers,默认值在 Linux 上会拉起多个数据加载进程,Windows 上偶尔会卡住。真遇到这种情况,我一般直接不跑脚本而是先跑yolo detect train ... workers=0,把数据加载改成单线程,定位慢在训练还是慢在读图。Mac 上遇到 MPS 比 CPU 还慢的情况也正常,某些算子会回退到 CPU,多线程来回切换反而更耗时,干脆用--device cpu加上小 batch,稳定性优先。
5. YOLO11 三平台训练踩坑与排错:四个高频问题
训练脚本写完后,真正占时间的是排错。以下四个问题是我在 GPU、CPU、Mac 三平台都实际撞过墙的场景,每条按“现象、原因、解决”拆开讲。
5.1 现象:loss 一开始就是 NaN,或者第一个 epoch 结束就崩成无穷大
原因有两类。最常见的是学习率过高,0.01 的默认学习率在 1000 张这种小数据集上,首轮更新步长过大,梯度直接溢出。另一类是标签里有非法框,比如 w 或 h 为负、中心点坐标超过 1,这种框会让回归损失在一开始就不可导。解决:先用第 2 章的质量自检脚本过滤非法框,再把 lr0 手动调到 0.005 或 0.002,看看 loss 曲线是否回落。如果 lr0 调到 0.001 后 loss 还是炸,那就去检查 txt 标签里有没有空行、全 0 的类别 id。
5.2 现象:Mac 上选 mps 训练,一个 epoch 跑得比 CPU 还慢,而且内存占用一路飞涨
原因:YOLO11 的部分算子在 MPS 后端没有完整实现,会回退到 CPU,造成频繁的同步等待;加上 MPS 使用统一内存,batch 设太大时,显存和系统内存争抢,速度反而更差。解决:先把 batch 降到 8,再观察速度;如果还是慢,直接--device cpu,并且把imgsz降到 512 或 480。小数据集在 CPU 上跑 yolo11n,epoch 时间虽然长一点,但至少能稳定出结果。不要为了“用上 MPS”而死磕,训练产出才重要。
5.3 现象:验证集 mAP50 很高,但拿去拍一段真实路口视频,大量漏检
原因通常是数据划分泄漏。视频抽帧生成的数据集,随机拆分会把同一段连续帧分进 train 和 test,模型其实背下来了一部分画面。另外,1000 张图如果全来自同一个摄像头视角,模型学到的“车”会和背景纹理绑定。解决:回到第 2 章的划分逻辑,按视频片段或拍摄场景分组,test 只放没见过的场景。接下来看 val 输出图,把预测框和真实框叠在一起,如果框普遍偏小或偏左,再考虑 NMS 阈值和回归分支的权重。
5.4 现象:三种格式转换完,YOLO 训练时标签出现不存在的第 4 类,或者类别 id 对不上
原因:VOC/COCO 的类别 id 里带了背景类,或者原始 id 不连续。比如 COCO 的类别 id 是 1、3、5,如果转换脚本里直接用annotation["category_id"],训练脚本会认为共有 5 个类别,而标签里只出现 1、3、5,类别 0 和 2 就成了空类。解决:用第 3 章的ctg_map和class_map做一次重新编号,转换完抽 10 张图,把 txt 画回图片上,确认类别 id 和框位置都正常。这个检查一定要做,别指望训练过程替你发现问题,YOLO 对“类别编号空洞”这种事很包容,它只会默默多出一个错误类别。
6. 训练完之后先做一次最小验收:用验证指标和可视化决定是否部署
训练脚本跑完,最后的 best.pt 躺在runs/vehicle_yolo11/vehicle/weights/里。我不建议直接拿去写业务代码,至少做一轮最小验收:指标、可视化和导出。
6.1 不要只盯 mAP50:我的一步 val 验收顺序
先打开results.png,看 val loss 和 train loss 的间距,间距过大就是过拟合信号。再看控制台输出的 Precision、Recall、mAP50 和 mAP50-95。1000 张图的单类车辆检测,mAP50 在 0.9 左右算正常,mAP50-95 如果能到 0.6 以上,说明模型对尺度变化比较稳。紧接着看val_batch0_pred.jpg,图中会有预测框和真实框叠加,框如果整体偏小,通常是小目标分辨率不够;框如果抖动厉害,可能需要调高conf阈值,或者换更强的主干。
6.2 从 best.pt 导出 ONNX:跨平台部署前值得保留的习惯
我的习惯是验证完直接导出 ONNX,再拿一段没参与训练的实拍视频做一次推理确认。导出代码很简单:
from ultralytics import YOLO best = YOLO("runs/vehicle_yolo11/vehicle/weights/best.pt") best.export(format="onnx", imgsz=640)导出的 best.onnx 可以拿到支持 ONNX 的端侧设备或边缘硬件上继续转工具链。做这类导出时,我会顺手写一个小的 README,记下验证时的 conf 阈值、imgsz、类别顺序。因为隔一个月再回来用这个模型,第一个忘的就是类别 id 对应关系。希望这些流程和踩坑总结能帮你在同样的数据集和脚本上少走弯路,祝顺利跑出自己的第一个 YOLO11 车辆检测模型。
本文还有配套的精品资源,点击获取