行人车辆检测数据集实战:4500张训练图与500张验证集训练YOLOv8避坑指南

发布时间:2026/10/26 21:37:53
行人车辆检测数据集实战:4500张训练图与500张验证集训练YOLOv8避坑指南

简介:这份行人车辆检测数据集面向计算机视觉学习者与目标检测开发者,用于训练和评估行人与车辆检测算法,尤其适合搭配YOLOv5等实时检测模型进行实践。压缩包共16821个文件,约499.51MB,其中5607张jpg图像、5607个xml标注文件与5607个txt标签文件一一对应,xml记录边界框坐标,txt适配YOLO训练格式,可直接投入模型训练与验证流程。数据集包含4500张训练图像与500张验证图像,覆盖不同光照、视角与遮挡条件下的行人和车辆实例,有助于模型学习鲁棒特征。已有2889人学习下载,说明其在同类资源中具备一定参考价值。借助完整标注与清晰目录结构,读者可快速复现训练流程、评估mAP等指标,并以此为基础开展自动驾驶、交通监控等场景的算法实验与对比研究。

1. 4500 张训练图 + 500 张验证图:行人车辆检测数据集到底该怎么用

手上拿到一个行人车辆检测数据集,一共 4500 张验证集 500 张,很多人第一反应是「先跑起来再说」。但真正跑过几个项目就知道,这个量级的数据集,坑不在模型结构,而在数据本身——标注格式对不对、类别是否平衡、验证集有没有混进训练集、小目标占比多少。行人车辆检测是典型的双类目标检测任务,行人属于高宽比极端、遮挡严重的类别,车辆则尺度跨度大、密集排列常见。4500 张训练图如果分布合理,用 YOLOv8n 或 YOLOv8s 从预训练权重微调,mAP@0.5 做到 0.75 以上是现实的;但如果标注质量差、验证集和训练集同源同场景,指标虚高到 0.9 也不代表能落地。这篇文章面向的是拿到类似规模数据集、准备做行人车辆检测的工程师,从数据检查、格式转换、训练配置到验证集评估,把每一步的参数和踩坑点讲清楚,让你少走弯路。

2. 先搞清楚这 4500+500 张图能不能直接用

2.1 行人车辆检测数据集的四个必查项

拿到数据集别急着写 dataloader,先做四件事。第一,看标注格式:是 VOC XML、COCO JSON 还是 YOLO txt?不同格式的坐标定义完全不同,VOC 是左上角加右下角的绝对像素,YOLO 是归一化的中心点加宽高,COCO 是左上角加宽高的绝对像素。第二,看类别定义:有些数据集把「人」和「行人」分成两类,有些把「轿车」「卡车」「公交车」都归到「车辆」,你得确认自己的任务需要几类。第三,看图片和标注是否一一对应:常见问题是图片有 4500 张但标注只有 4300 个,或者标注文件名和图片文件名大小写不一致。第四,看验证集 500 张的来源:如果验证集是从训练集里随机切出来的,那指标只能反映拟合程度,不能反映泛化能力。

import os from pathlib import Path img_dir = Path("dataset/images/train") label_dir = Path("dataset/labels/train") imgs = {p.stem for p in img_dir.glob("*.jpg")} labels = {p.stem for p in label_dir.glob("*.txt")} print(f"图片数: {len(imgs)}, 标注数: {len(labels)}") print(f"有图无标注: {imgs - labels}") print(f"有标注无图: {labels - imgs}")

这段脚本做的是最基础的完整性校验。imgs - labels输出有图片但没有对应标注文件的样本,这些图在训练时会被当成负样本或者直接报错;labels - imgs输出有标注但找不到图片的孤儿标注,通常是文件名后缀不一致导致的。参数上只需要改img_dir和label_dir两个路径,注意 YOLO 格式的标注是.txt,VOC 是.xml,COCO 是单个.json,按实际情况调整 glob 的后缀。

2.2 用统计脚本看穿类别分布和小目标占比

行人车辆检测最怕两件事:一是某一类样本极少,比如 4500 张里只有 200 张有行人;二是小目标太多,行人远距离成像可能只有 20×40 像素,YOLOv8 默认的 P3 特征图 stride 是 8,太小的目标在深层特征图上几乎消失。跑一个统计脚本,把每个类别的框数量、宽高分布、面积分布都算出来。

import numpy as np from pathlib import Path from collections import Counter label_dir = Path("dataset/labels/train") cls_counter = Counter() wh_list = [] for txt in label_dir.glob("*.txt"): for line in txt.read_text().strip().splitlines(): parts = line.split() cls_id = int(parts[0]) w, h = float(parts[3]), float(parts[4]) cls_counter[cls_id] += 1 wh_list.append((w, h)) wh_arr = np.array(wh_list) areas = wh_arr[:, 0] * wh_arr[:, 1] print("类别框数:", dict(cls_counter)) print("宽高比中位数:", np.median(wh_arr[:, 0] / wh_arr[:, 1])) print("面积小于 0.01 的框占比:", (areas < 0.01).mean())

cls_counter告诉你每个类别的实例数,如果行人类别占比低于 10%,训练时需要考虑类别加权或者过采样。areas < 0.01是归一化面积,0.01 大约对应 640 输入下 64×64 像素的区域,低于这个值的框属于小目标,如果占比超过 30%,建议把输入尺寸从 640 提到 960 或者 1280,同时开启 YOLOv8 的close_mosaic参数在最后几轮关闭马赛克增强,让小目标获得更稳定的梯度。

2.3 验证集 500 张的划分陷阱

500 张验证集听起来不少,但如果这 500 张和 4500 张训练集来自同一段视频的连续帧,那验证集里的场景、光照、目标外观和训练集高度相似,评估出来的 mAP 会明显偏高。判断方法很简单:看图片文件名是否连续编号,或者用感知哈希做相似度聚类。

import imagehash from PIL import Image from pathlib import Path def compute_hashes(img_dir, limit=200): hashes = [] for p in list(Path(img_dir).glob("*.jpg"))[:limit]: h = imagehash.phash(Image.open(p)) hashes.append((p.name, h)) return hashes train_h = compute_hashes("dataset/images/train") val_h = compute_hashes("dataset/images/val") dups = 0 for vname, vh in val_h: for tname, th in train_h: if vh - th <= 5: dups += 1 break print(f"验证集中与训练集高度相似的图片数: {dups}/{len(val_h)}")

imagehash.phash计算感知哈希,汉明距离小于等于 5 通常意味着两张图视觉上几乎一样。如果验证集里超过 20% 的图和训练集高度相似,建议重新划分:按视频来源或者时间戳切分,确保验证集的场景在训练时没见过。这一步不做,后面调参全是玄学。

3. 把标注转成 YOLOv8 能吃的格式

3.1 VOC 转 YOLO:坐标归一化的四个边界坑

很多公开的行人车辆数据集是 VOC 格式,转 YOLO 时最容易翻车的地方有四个。第一,VOC 的xmin, ymin, xmax, ymax是绝对像素,YOLO 需要(x_center/w, y_center/h, bw/w, bh/h),除的是图片实际宽高而不是 640。第二,有些 VOC 标注里xmax等于图片宽度,归一化后中心点加半宽会超过 1.0,YOLOv8 虽然会截断但最好自己 clamp。第三,图片尺寸要从 XML 里的size字段读,不能假设所有图都是 640×480。第四,类别名到类别 ID 的映射要固定,训练和推理必须一致。

import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image classes = ["person", "vehicle"] cls2id = {c: i for i, c in enumerate(classes)} def voc_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in cls2id: continue bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) xc = (xmin + xmax) / 2 / img_w yc = (ymin + ymax) / 2 / img_h bw = (xmax - xmin) / img_w bh = (ymax - ymin) / img_h xc, yc = min(max(xc, 0), 1), min(max(yc, 0), 1) bw, bh = min(bw, 1), min(bh, 1) lines.append(f"{cls2id[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") out_path = Path(out_dir) / (Path(xml_path).stem + ".txt") out_path.write_text("\n".join(lines)) for xml in Path("dataset/annotations").glob("*.xml"): voc_to_yolo(xml, "dataset/labels/train")

cls2id决定了类别顺序,YOLOv8 的data.yaml里names列表必须和这个顺序完全一致,否则训练出来的模型会把行人和车辆搞反。min(max(xc, 0), 1)是保险措施,防止个别标注越界导致训练时 loss 异常。输出保留 6 位小数足够,YOLOv8 内部会再做一次解析。

3.2 COCO 转 YOLO:处理 iscrowd 和空标注

COCO 格式的数据集转 YOLO 时,iscrowd=1的标注要特别处理。这类标注表示「这是一群人/车,不单独标注每个实例」,YOLOv8 训练时如果把它当普通框,模型会学到一个巨大的、内部包含多个目标的框,推理时 NMS 很难处理。常见做法是直接跳过iscrowd=1的标注,或者把它们转成多个小框。另外 COCO 的bbox是[x, y, w, h]左上角加宽高,转 YOLO 时中心点是x + w/2。

import json from pathlib import Path coco = json.load(open("dataset/annotations/instances_train.json")) img_id2info = {img["id"]: img for img in coco["images"]} cat_id2name = {c["id"]: c["name"] for c in coco["categories"]} cls2id = {"person": 0, "vehicle": 1} out_dir = Path("dataset/labels/train") out_dir.mkdir(parents=True, exist_ok=True) for ann in coco["annotations"]: if ann.get("iscrowd", 0) == 1: continue img = img_id2info[ann["image_id"]] w, h = img["width"], img["height"] x, y, bw, bh = ann["bbox"] xc, yc = (x + bw / 2) / w, (y + bh / 2) / h bw, bh = bw / w, bh / h name = cat_id2name[ann["category_id"]] if name not in cls2id: continue line = f"{cls2id[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}\n" with open(out_dir / f"{img['file_name'].rsplit('.',1)[0]}.txt", "a") as f: f.write(line)

用追加模式写文件是因为一张图可能有多个标注,每次追加一行。iscrowd跳过是保守做法,如果数据集中 crowd 标注很多,可以考虑用pycocotools的mask转成多个独立框,但实现复杂度高,一般行人车辆检测任务里 crowd 占比不大,跳过影响有限。

3.3 data.yaml 的路径和类别配置

YOLOv8 训练依赖data.yaml,里面三个关键字段:path是数据集根目录,train和val是相对路径,names是类别名列表。路径写错是新手最常见的翻车点,YOLOv8 不会报「文件不存在」,而是直接报「No labels found」,让人误以为是标注问题。

path: /home/user/dataset train: images/train val: images/val names: 0: person 1: vehicle

path用绝对路径最稳,train和val相对于path。YOLOv8 会自动把images替换成labels去找标注,所以目录结构必须是images/train和labels/train平行。如果标注目录名不是labels,需要在data.yaml里显式指定train_labels和val_labels。

4. 用 YOLOv8 在 4500 张图上跑通训练

4.1 从预训练权重微调的最小命令

4500 张图不算大,从 COCO 预训练权重微调是最稳的选择。YOLOv8n 参数量 3.2M,YOLOv8s 是 11.2M,行人车辆检测这种双类任务,YOLOv8s 通常比 n 高 3 到 5 个点 mAP,但推理速度慢一倍。如果目标是边缘部署,选 n;如果追求精度,选 s 或 m。

yolo detect train \ model=yolov8s.pt \ data=/home/user/dataset/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=3 \ close_mosaic=10 \ patience=20 \ device=0 \ project=runs/person_vehicle \ name=exp1

lr0=0.01是初始学习率,微调任务比从头训练要小,0.01 是 YOLOv8 官方推荐的微调值。lrf=0.01是最终学习率因子,100 轮后学习率降到 0.0001。warmup_epochs=3让学习率在前 3 轮从 0 线性升到 0.01,避免一开始就大梯度破坏预训练权重。close_mosaic=10在最后 10 轮关闭马赛克增强,让模型在真实分布上收敛。patience=20是早停,20 轮验证指标不升就停,省时间。

4.2 行人车辆检测的四个关键超参

除了上面的基础参数,行人车辆检测有几个针对性调整。第一,imgsz如果小目标多,提到 960 或 1280,但 batch 要相应降到 8 或 4,否则显存爆。第二,cls损失权重,如果行人类别样本少,可以在data.yaml里加cls_pw或者用focal_loss,YOLOv8 默认的BCEWithLogitsLoss对类别不平衡有一定鲁棒性,但极端情况需要手动加权。第三,box损失用默认的 CIoU 就行,行人高宽比极端,CIoU 对宽高比敏感,比 GIoU 收敛快。第四,augment参数,行人车辆场景里hsv_h=0.015, hsv_s=0.7, hsv_v=0.4是默认值,如果数据光照变化大可以适当提高hsv_v。

from ultralytics import YOLO model = YOLO("yolov8s.pt") results = model.train( data="/home/user/dataset/data.yaml", epochs=100, imgsz=960, batch=8, lr0=0.01, close_mosaic=10, mixup=0.1, copy_paste=0.1, degrees=5.0, translate=0.1, scale=0.5, )

mixup=0.1和copy_paste=0.1是额外增强,mixup 把两张图按比例混合,copy_paste 把目标复制粘贴到其他图上,对行人车辆这种实例密集的场景有帮助,但比例不能太高,0.1 到 0.2 之间比较安全。degrees=5.0限制旋转角度,行人车辆通常是水平拍摄,大角度旋转会引入不真实的样本。scale=0.5允许 0.5 到 1.5 倍缩放,增加尺度多样性。

4.3 训练日志里该盯哪几个数

YOLOv8 训练时输出一堆指标,重点看四个:box_loss、cls_loss、dfl_loss、mAP50。box_loss下降说明定位在改善,cls_loss下降说明分类在改善,dfl_loss是分布焦点损失,YOLOv8 特有,下降说明边界框回归的分布估计在收敛。mAP50是 IoU 0.5 时的平均精度,行人车辆检测里这个数到 0.8 以上算不错。如果box_loss降但mAP50不升,通常是过拟合,看验证集的val/box_loss是不是在升。如果cls_loss震荡,检查学习率是不是太大,或者类别标注有没有错。

tensorboard --logdir runs/person_vehicle/exp1

TensorBoard 能看到 loss 曲线和 mAP 曲线,比看终端输出直观。重点看val/box_loss和train/box_loss的差距,差距超过 0.5 说明过拟合明显,需要加数据增强或者减模型容量。

5. 验证集 500 张怎么用才不白费

5.1 验证集评估的三个层次

500 张验证集不能只跑一个model.val()看 mAP 就完事。第一层是整体指标:mAP50、mAP50-95、precision、recall。第二层是分类别指标:行人的 AP 和车辆的 AP 分开看,如果行人 AP 明显低,说明行人样本少或者小目标多。第三层是分尺度指标:YOLOv8 的val()会输出metrics/mAP50但不直接分尺度,需要用pycocotools按面积区间统计。

from ultralytics import YOLO import json model = YOLO("runs/person_vehicle/exp1/weights/best.pt") metrics = model.val(data="/home/user/dataset/data.yaml", split="val") print(f"mAP50: {metrics.box.map50:.4f}") print(f"mAP50-95: {metrics.box.map:.4f}") print(f"每类 AP50: {metrics.box.ap50}") print(f"每类 precision: {metrics.box.p}") print(f"每类 recall: {metrics.box.r}")

metrics.box.ap50是一个数组,按类别顺序输出,对应data.yaml里的names。如果行人的 AP50 比车辆低 0.15 以上,优先补行人样本或者对行人做专门增强。metrics.box.p和metrics.box.r看精确率和召回率的平衡,如果精确率高召回低,说明模型保守,可以降置信度阈值;反过来则提高阈值。

5.2 用混淆矩阵定位类别混淆

YOLOv8 的val()会生成混淆矩阵,存在runs/person_vehicle/exp1/confusion_matrix.png。行人车辆检测里最常见的混淆是「远处行人被当成车辆」或者「车辆里的乘客被当成行人」。混淆矩阵能直接看到哪两类在互相误判。如果行人被大量误判为背景,说明行人的特征学习不足,需要增加行人样本或者提高行人的损失权重。

import matplotlib.pyplot as plt from ultralytics.utils.plotting import plot_confusion_matrix # 假设已经通过 model.val() 得到了 confusion_matrix cm = metrics.confusion_matrix.matrix plot_confusion_matrix(cm, names=["person", "vehicle", "background"], save_dir="runs/cm")

confusion_matrix.matrix是归一化后的矩阵,行是真实类别,列是预测类别。最后一行是背景,如果背景列的值高,说明漏检多。这个图比数字直观,调参时对着看。

5.3 验证集上的失败案例怎么归因

从验证集里挑出 mAP 最低的 20 张图,逐张看。常见失败模式有四种:一是小目标漏检,行人在远处只有十几个像素;二是遮挡,车辆挡住行人或者行人互相遮挡;三是运动模糊,快速移动的车辆拖影;四是极端光照,夜间或者逆光。每种模式的解法不同:小目标提输入尺寸,遮挡加copy_paste增强,运动模糊加运动模糊增强,光照问题加 HSV 增强。归因做对了,调参才有方向,否则就是瞎试。

import cv2 from pathlib import Path from ultralytics import YOLO model = YOLO("runs/person_vehicle/exp1/weights/best.pt") val_imgs = list(Path("dataset/images/val").glob("*.jpg")) # 找出检测框数量最少的图,通常是漏检严重的 results = model(val_imgs, conf=0.25, iou=0.5, verbose=False) counts = [(r.path, len(r.boxes)) for r in results] counts.sort(key=lambda x: x[1]) for path, n in counts[:10]: print(f"{path}: {n} 个检测框")

conf=0.25是推理置信度阈值,iou=0.5是 NMS 的 IoU 阈值。检测框数量少的图不一定是失败案例,但如果人工看确实有目标却没检测到,那就是漏检。把这些图单独拿出来,用model.predict可视化,看模型到底输出了什么。

6. 避坑与排查:行人车辆检测数据集训练的五个血泪教训

6.1 验证集 mAP 0.95 但实际部署全漏检

现象:训练完在 500 张验证集上 mAP50 跑到 0.95,信心满满部署到实际场景,结果行人车辆几乎全漏。原因:验证集和训练集同源,场景、光照、拍摄角度高度相似,模型只是记住了这个分布,没有泛化能力。解决:重新按场景或时间划分验证集,确保验证集包含训练时没见过的场景;如果数据本身来源单一,考虑引入外部数据集做交叉验证,比如用 BDD100 的验证集测一下泛化。

6.2 训练 loss 正常但 mAP 一直是 0

现象:训练日志里 box_loss 和 cls_loss 都在降,但 mAP50 始终是 0。原因:data.yaml里的names顺序和标注文件里的类别 ID 不一致,或者标注文件里的类别 ID 从 1 开始而不是从 0 开始。YOLO 格式要求类别 ID 从 0 开始连续编号,如果标注里是 1 和 2,模型会把 1 当成第 1 类(索引 1),但names里只有 0 和 1,导致类别错位。解决:检查标注文件第一列,确保是 0 和 1;检查data.yaml的names键值对,确保 0 对应 person,1 对应 vehicle。

6.3 小目标行人检测不出来

现象:近处行人检测正常,远处小目标行人全部漏检。原因:YOLOv8 默认输入 640,P3 特征图 stride 8,对应 80×80 的特征图,一个 16×32 像素的行人在 P3 上只有 2×4 个格子,特征太弱。解决:把imgsz提到 960 或 1280,P3 特征图变成 120×120 或 160×160,小目标有更多像素;同时开启close_mosaic,马赛克增强会把四张图拼成一张,小目标变得更小,最后几轮关掉让模型专注真实尺度。

6.4 训练到一半显存爆了

现象:前几十轮正常,突然报 CUDA out of memory。原因:YOLOv8 的close_mosaic在最后几轮关闭马赛克增强,关闭后每张图的实际像素量增加,显存占用上升;或者batch设得太大,前期靠马赛克拼图降低了单图分辨率,后期恢复后爆显存。解决:把batch降到 8 或 4,或者把imgsz从 960 降到 640;如果必须用大尺寸,开启amp混合精度训练,YOLOv8 默认开启,但某些显卡需要手动确认。

6.5 验证集评估结果每次跑都不一样

现象:同一个best.pt,每次model.val()出来的 mAP 差 1 到 2 个点。原因:验证时的数据增强没关,YOLOv8 的val()默认不做增强,但如果data.yaml里配置了val的增强参数,或者用了augment=True,每次评估的输入不同。解决:model.val()时显式设置augment=False,确保评估用的是原始图片;另外检查val的batch是否一致,batch 大小会影响 BN 层的统计量,虽然推理时用 running mean,但某些实现会有细微差异。

7. 用 500 张验证集做阈值搜索和模型集成

训练完拿到best.pt只是开始,真正落地前还有两件事值得做。第一件是置信度和 NMS 阈值的联合搜索。YOLOv8 默认conf=0.25, iou=0.7,但行人车辆检测里,行人的最优置信度通常比车辆低,因为行人特征变化大。可以在验证集上跑一个网格搜索,找出使 F1 最大的阈值组合。

import numpy as np from ultralytics import YOLO model = YOLO("runs/person_vehicle/exp1/weights/best.pt") best_f1, best_conf, best_iou = 0, 0.25, 0.7 for conf in np.arange(0.1, 0.6, 0.05): for iou in np.arange(0.4, 0.8, 0.05): m = model.val(data="/home/user/dataset/data.yaml", conf=conf, iou=iou, verbose=False) f1 = 2 * m.box.p * m.box.r / (m.box.p + m.box.r + 1e-6) if f1.mean() > best_f1: best_f1 = f1.mean() best_conf, best_iou = conf, iou print(f"最优 conf={best_conf:.2f}, iou={best_iou:.2f}, F1={best_f1:.4f}")

这段搜索在 500 张验证集上跑,每个组合大约几秒到十几秒,总共几十个组合,几分钟能跑完。f1.mean()是所有类别 F1 的平均,如果更关注行人,可以只取行人类别的 F1。找到最优阈值后,部署时用这组参数,比默认值通常能提升 2 到 5 个点的 F1。

第二件事是模型集成。4500 张图训一个模型可能不够稳,可以训三个不同 seed 或者不同增强强度的模型,推理时用 WBF(加权框融合)合并结果。WBF 比 NMS 更能保留密集场景下的框,行人车辆检测里车辆密集排列时 NMS 容易误删。

from ensemble_boxes import weighted_boxes_fusion import numpy as np # 假设三个模型的输出已经转成 [x1, y1, x2, y2, score, label] 格式 boxes_list = [model1_boxes, model2_boxes, model3_boxes] scores_list = [model1_scores, model2_scores, model3_scores] labels_list = [model1_labels, model2_labels, model3_labels] fused_boxes, fused_scores, fused_labels = weighted_boxes_fusion( boxes_list, scores_list, labels_list, iou_thr=0.55, skip_box_thr=0.1, weights=[1, 1, 1] )

iou_thr=0.55是融合时的 IoU 阈值,比 NMS 的 0.7 低,因为 WBF 会加权平均框坐标,阈值低一点能合并更多框。skip_box_thr=0.1过滤掉低置信度的框,避免噪声参与融合。三个模型权重相同,如果某个模型在验证集上明显好,可以调高它的权重。

我自己做行人车辆检测的习惯是:训练前一定跑一遍数据统计脚本,确认类别分布和小目标占比;训练时开 TensorBoard 盯val/box_loss和train/box_loss的差距;训练后在验证集上做阈值搜索,不直接用默认值。这套流程走下来,4500 张图训出的模型在真实场景里的漏检率能控制在可接受范围内。希望帮到你。

本文还有配套的精品资源,点击获取