简介:俯拍视角下的道路车辆与行人目标检测数据集,面向算法工程师与计算机视觉学习者,适用于智能交通和辅助驾驶项目,可直接用于主流YOLO系列网络训练。数据集包含超过三千张图片及对应标签,划分为训练集与验证集,提供类别文件;整体采用YOLO标注格式,并已进行旋转、缩放、裁剪等数据增强处理,可增强模型鲁棒性。类别共有十个,覆盖汽车、摩托车、行人、卡车等常见城市交通目标,可用于交通监控与辅助驾驶研究。压缩包内共两千个文件,其中一千九百九十九个为文本标签文件,另有一个辅助脚本,整体大小四百六十三点一一兆字节。已有超过一千六百人学习,对需要高质量标注数据的开发者来说,可有效节省采集与标注时间,也可直接作为算法验证模板与项目素材,帮助快速完成目标检测任务。
1. 俯拍道路目标检测数据集:为什么 3000 张图比三万张平视图更考验模型
无人机巡检、高架路口、智慧园区安防,这些场景里摄像头视野是俯拍的:车辆是一个个倾斜的小矩形,行人往往只有十几个像素,道路标线比车身更容易被模型记住。3000 张俯拍道路图,带车辆和行人标签,真正的价值不是“又多了一份数据集”,而是把目标检测在小目标、密集、遮挡条件下的极限摆到桌面上。平视场景里跑得飞快的 YOLO,换到俯拍图往往 mAP 掉十几个点,差距不在数量,在视角与尺度。这篇文章按“数据 → 训练 → 评估 → 清洗”的顺序,把这套数据落地成可复现流程,适合交通感知工程师和用 YOLOv8 训练自己数据集的算法同学。
2. 数据集结构与标注格式:从目录布局看清一张俯拍图被标注成了什么
拿到目标检测数据集,第一个动作不是看图片,而是摸清目录结构和标签口径。俯拍道路数据最常见的组织方式是图片与标签文件同名同层级分布,训练、验证、测试三个子集分开。标签格式决定了后续接训练脚本时要不要写转换器,也决定了类别顺序会不会在某个环节悄悄错位。
2.1 俯拍道路数据集的目录与命名约定
标准布局通常是这样:
data/ ├── images/ │ ├── train/ # 约 2100 张 │ ├── val/ # 600 张 │ └── test/ # 400 张 └── labels/ ├── train/ ├── val/ └── test/图片img_0042.jpg对应的标签就是同目录级别下的img_0042.txt。这种一一对应的命名约束是所有后续流程的基石,如果原始数据集给你的是 JSON 或 XML,第一步先转成同名文件再谈训练。俯拍道路场景里,同一条路段往往有大量连续帧,划分 train/val/test 时不能随机洗牌,否则同一地点的图片会同时出现在训练集和验证集,评估指标虚高。按拍摄批次或路段 ID 划分,也就是让同一个场景只出现在一个子集里,是这类数据集首先要遵守的纪律。
有一个前提必须确认:3000 张图片里是不是存在“连拍重复”。俯拍无人机录像抽帧出来的数据,相邻帧车辆位置只差几个像素,如果直接全量进训练,模型会把背景纹理当特征。检查方法很简单,对图片算感知哈希,把相似度超过 0.95 的帧做去重或分到同一个批次。
2.2 标签格式怎么选:VOC xml、YOLO txt 还是 COCO json
常见的目标检测标注格式有三种,俯拍道路数据集里概率最高的是前两种。
| 格式 | 坐标口径 | 适合链路 | 常见坑 |
|---|---|---|---|
| Pascal VOC xml | 绝对像素 xmin/ymin/xmax/ymax | 老训练库、mmdet 系列 | 解析慢,需自己改造读入 |
| YOLO txt | 归一化 cx/cy/w/h | YOLOv5/v8、SAHI | 类别 id 漂移、宽高为 0 |
| COCO json | 绝对像素 x/y/w/h | Detectron2、mmdet | 大 json 文件难增量修改,容易漏类别 |
如果这份数据同时带了 xml 和 txt,优先以 txt 为主做训练,同时保留 xml 作为可读源。拿 YOLOv8 训练的话,标签文件每一行就是class_id cx cy w h,四个坐标值全部除以图片宽高,是 0 到 1 之间的小数。俯拍图里一辆车的框可能只有 0.05 宽,写错成像素值会导致 loss 直接发散。
2.3 读懂一条车辆标注:边界框坐标的两种口径
以一张 3840×2160 的俯拍路口图为例,VOC 里一条车辆标注长这样:
<annotation> <folder>train</folder> <filename>img_0042.jpg</filename> <size> <width>3840</width> <height>2160</height> <depth>3</depth> </size> <object> <name>car</name> <difficult>0</difficult> <bndbox> <xmin>1204</xmin> <ymin>860</ymin> <xmax>1390</xmax> <ymax>968</ymax> </bndbox> </object> </annotation>对应到 YOLO txt,同样是这个框:
0 0.3378 0.4231 0.0484 0.0500先从 xml 读出目标框坐标,然后按公式换算中心点:cx=(xmin+xmax)/2/w,cy=(ymin+ymax)/2/h,w=(xmax-xmin)/w,h=(ymax-ymin)/h。这里最容易错的是把中心点当成了角点坐标,或者忘了归一化直接写像素,这两种错误在俯拍小目标上表现得尤其隐蔽,因为数值很小,loss 未必爆炸,但精度会非常差。
遇到像这类垂直视角的采集,一个框里出现半截车身的概率很大。建议核对标签里有没有大量与图像边缘相交的框,如果标注时没有把边缘截断的部分标进去,训练时模型会学到“车身不完整也可以算正样本”的错误规律。
2.4 用脚本统计标签分布与小目标占比
拿到标签后先跑一轮统计,比直接训练重要得多。以下脚本扫描 YOLO 格式标签,输出每类目标数量、小目标占比,并检查类别 id 是否越界:
from pathlib import Path import random label_dirs = [Path("data/labels/train")] class_names = ["car", "person"] stats = {c: {"count": 0, "small": 0} for c in class_names} image_width = 3840 # 改成实际图的宽 for d in label_dirs: for txt in sorted(d.glob("*.txt")): for line in txt.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: print("异常行:", txt, line) continue cid = int(parts[0]) if cid >= len(class_names): print("类别 id 越界:", txt, cid) continue w_norm = float(parts[3]) area = w_norm * image_width stats[class_names[cid]]["count"] += 1 if area < 48: stats[class_names[cid]]["small"] += 1 for c in class_names: s = stats[c] ratio = s["small"] / max(s["count"], 1) print(f"{c}: 总数 {s['count']}, 小目标占比 {ratio:.2%}")这段脚本用 width 判断的是归一化宽度小于 48 像素的目标,也就是 MS COCO 定义里 small object 的横向尺寸口径,阈值可以根据俯拍高度调整到 32 或 64。如果小目标占比超过一半,直接喂给默认的模型配置几乎必然在 mAP-small 上惨败,后续训练、切片推理都要按这个统计结果做决策。同时输出异常行也保护了后面 YOLO 训练脚本不会因为一个脏标签中途崩掉。这个统计结果会成为第 4 章切片方案的输入参数。
3. 用 YOLOv8 在俯拍道路数据集上跑通训练:从数据划分到第一个 mAP
YOLO 目标检测流程走到自定义数据这一步,卡住人的地方往往不是模型结构,而是数据文件的对齐。YOLOv8 训练自己的数据集,本质上只做三件事:把图片和标签放到对应目录,写一个 dataset.yaml 描述类别,然后跑训练命令。每一步都有容易踩空的地方,下面拆开说。
3.1 先按路段划分数据,别用随机洗牌
俯拍道路数据里同一路段的光照、地面纹理、车道线高度相似。如果随机划分,验证集里可能藏着与训练集几乎相同的背景,mAP 虚高到 0.8,部署到新路段直接掉到 0.3。按采集批次或路段前缀划分是最稳妥的方案,假设文件名里有路段标识,可以这样切:
python - <<'PY' from pathlib import Path import random imgs = sorted(Path("data/images").glob("*_*.jpg")) site_groups = {} for p in imgs: site = p.name.split("_")[0] site_groups.setdefault(site, []).append(p) sites = sorted(site_groups) random.Random(42).shuffle(sites) # 假设有 20 个路段,按 15:3:2 切分 train_sites = set(sites[:15]) val_sites = set(sites[15:18]) test_sites = set(sites[18:]) for site in train_sites: for p in site_groups[site]: p.rename(Path("data/images/train") / p.name) PY这段脚本的关键在设计逻辑里:以路段为最小单位洗牌,而不是以图片为最小单位。代码里的站点拆分方式要根据文件名规则调整,但原则不变——验证集和测试集必须包含训练集没见过的路段,才能测出模型在真实场景里的泛化能力。俯拍数据里时间因素也很重要,白天和夜晚尽量同时出现在三个子集里,避免模型没见过夜间灯光下的车辆轮廓。
3.2 写 dataset.yaml 时的类别顺序陷阱
YOLOv8 训练自己的数据集离不开一个 yaml 配置文件。下面的写法兼容官方训练流程:
path: /path/to/data # 数据根目录的绝对路径 train: images/train # 相对 path val: images/val test: images/test nc: 2 names: 0: car 1: person训练时报错说 “found 2 classes with same name” 或者 mAP 一直为 0,十有八九是这里类别顺序和标签 txt 里的 class_id 对不上。标签里1代表行人,yaml 里却写成了0: person,模型训练出来会把行人当车。如果数据集自带 class_list 说明文件,以它为唯一事实来源;如果没有,按第 2 章的统计脚本输出 order 列表作为 names 顺序。宁可在 yaml 里多写注释,也不要凭文件名猜顺序。
还有一种情况是 names 数量比标签里实际类别多,导致某些类别完全没有正样本,训练日志会看到某一类 AP 为 0。检查方法是在训练前跑一条验证命令,看类别 id 是否在合法范围内。
3.3 训练命令与必调参数
数据就绪后,跑训练的命令如下:
yolo detect train \ data=dataset.yaml \ model=yolov8s.pt \ imgsz=960 \ batch=16 \ epochs=100 \ optimizer=AdamW \ cos_lr=True \ patience=15 \ project=runs/detect \ name=birdview_960先说 imgsz:俯拍图如果原始是 3840 宽,直接缩到 640 会让车辆框变得只有 8×8 像素,特征提取器根本分不清车和人。建议先尝试 960,显存不够再降 800,而不是一上来就 640。batch 按显存调,单卡 24G 用 16 就差不多,多卡可以翻倍。epochs 配 patience=15 做早停,俯拍数据集 3000 张的情况下,通常第 60 到 80 轮就收敛了,没必要硬跑满。
optimizer 很多人默认用 SGD,但俯拍小目标场景 AdamW 收敛更稳,特别是标签里少量脏样本存在时,AdamW 对梯度噪声的容忍度更高。cos_lr=True 会让学习率按余弦曲线衰减,减少后期在小目标框上反复震荡。几个关键参数的作用和适用场景汇总如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| imgsz | 960 或 800 | 比原始分辨率小 3 到 4 倍即可,过小会丢小目标 |
| batch | 16(24G 单卡) | 显存不足时优先降 imgsz,其次降 batch |
| epochs | 100 + patience=15 | 小数据集早停收益大,没必要追求固定轮数 |
| optimizer | AdamW | 对噪声标签和有遮挡的小目标更友好 |
| lr0 | 0.001(AdamW) | SGD 场景用 0.01,混用会震荡 |
| warmup_epochs | 3 | 前几轮先稳定梯度,适合新数据集冷启动 |
命令跑完后看runs/detect/birdview_960/下的results.csv和weights/best.pt。这里说的几个参数不是定死的,如果你的图本身只有 1080p,那 imgsz=800 就够了,960 反而浪费显存。训练结束后先别急着测 test 集,先跑一轮yolo detect val看验证集 mAP-50 是否明显高于 0.5,如果 mAP 很低,回到第 2 章检查小目标占比和脏标签,模型结构的问题排在数据问题之后。
4. 俯拍小目标检测的两个狠招:切片推理与针对性数据增强
模型在验证集上 mAP 到 0.6 不算完,俯拍道路上真正让人头疼的是小目标漏检。车辆只有二三十像素时,直接推理很容易漏,此时常规增强手段帮不上忙,切片推理才是立竿见影的方案。这一章讲怎么判断要不要切片,以及切片参数怎么给。
4.1 先看评估指标再决定要不要切图:mAP-small 和 mAP-50 的差别
训练结束后,跑一次带详细指标的验证:
yolo detect val \ model=runs/detect/birdview_960/weights/best.pt \ data=dataset.yaml \ imgsz=960输出里除了 mAP50 和 mAP50-95,还有按目标面积分组的指标:small、medium、large。COCO 口径下小于 32×32 像素的框算 small,对俯拍道路图来说这几乎就是最常见的车辆尺寸。如果 mAP50 有 0.7,但 mAP50 small 只有 0.3,说明模型在大目标上工作正常、小目标上漏检严重,切片推理立刻安排。如果 small 和 medium 差距不大,那问题出在类别不平衡或标签噪声,切片解决不了,先回第 2 章做数据清洗。
有一个习惯值得保留:把每次评估的 small/medium 指标和 imgsz、增强参数一起记录。红外小目标检测里讨论的那些面积阈值和评价参数,在可见光俯拍道路上同样适用,唯一区别是阈值要按你的图片宽度等比放大。
4.2 SAHI 式切片:把一张大图切成 640 子图再拼回框
SAHI 是切片推理的常用开源方案,做法是把大图切成若干个重叠子图,分别推理后再把重叠区域的框用 NMS 合并,避免同一个目标被重复计数。安装 sahi 后,用训练好的模型做切片预测:
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type="yolov8", model_path="runs/detect/birdview_960/weights/best.pt", confidence_threshold=0.25, image_size=960, device="cuda:0", ) result = get_sliced_prediction( image_path="data/images/test/site03_018.jpg", detection_model=detection_model, slice_height=640, slice_width=640, overlap_height_ratio=0.2, overlap_width_ratio=0.2, ) result.export_visuals(export_dir="runs/sliced_vis/")模型推理时会把每个 640×640 子图缩放到模型输入尺寸,小目标在子图里被放大了好几倍,原本只有 20 像素的车轿在子图里变成 60 像素以上,特征提取自然更准确。切片尺寸和重叠比例的设置逻辑如下:
| 参数 | 建议值 | 效果与注意点 |
|---|---|---|
| slice_width/height | 640 或 512 | 子图越大,切块越少、速度越快,但如果目标太小还是要小切片 |
| overlap_width_ratio | 0.2~0.4 | 目标恰好被切片边界切断时需要重叠来兜底 |
| overlap_height_ratio | 与宽度一致 | 非方形输入可分开设置,俯拍一般对称即可 |
| confidence_threshold | 0.25~0.3 | 切片推理因为目标更清晰,阈值可以比整图推理略高 |
切片推理最大的注意点是速度:一张 4K 图切成 48 个子图,推理耗时可能比整图贵 10 倍。如果项目只有离线分析需求,代价可接受;如果要做实时视频流,就得换思路,比如只在检测到小目标候选区切片,或者用滑窗加缩放采样。另外切片推理结束后拼接回原图时会遇到 NMS 合并阈值,sahi 内部已处理,不需要额外写。
4.3 YOLOv8 的数据增强参数怎么调得不把行人切碎
默认的 mosaic 增强会把四张图拼成一张,对于平视大目标非常有效,但俯拍小目标场景里,mosaic 缩放后行人和车辆会被压缩到只有几个像素,反而成了噪声。这里建议保留但“降量”,在训练命令里加上:
augment: true hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 degrees: 30.0 translate: 0.1 scale: 0.5 fliplr: 0.5 flipud: 0.5 mosaic: 0.5 mixup: 0.1degrees=30 是俯拍场景下比较关键的参数,因为车辆朝向在画面里是任意角度的,默认 0 度旋转会限制模型对旋转车辆的适应能力;俯拍下左右翻转和上下翻转同样安全,但平视数据里 flipud 不该开。mosaic 和 mixup 调低到 0.5 和 0.1 左右,小目标就不会被拼图操作稀释成噪声。scale 保持 0.5 而不是默认的更大值,避免把小目标缩得完全不可见。
如果数据集本身已经很大,每张图都做这么多变换会拖慢训练速度,可以用cache=True把图片缓存进内存,加速读取。增强策略没有绝对答案,判断标准是:验证集 mAP 变化趋势稳定、训练 Loss 和验证 Loss 同降不背离。一旦发现训练 Loss 持续下降而验证 Loss 开始回升,先把 mosaic 关掉再试。
4.4 错误的做法:把 imgsz 直接拉到 1920
有人觉得小目标检测差,最简单的办法就是把推理分辨率调大,从 960 一路涨到 1920。这样做训练显存暴涨,而收益递减很快——小目标确实变清晰了,但模型在小图训练时见过的尺度分布和 1920 输入不一致,推理时反而产生大量误检。常见可用方案是保持训练 960,推理时做一个二阶段策略:整图粗检,对高密度区域裁块细检;或者把第 2.1 节里的路段去重做得更狠,集中批次去补齐低置信度的困难样本。如果你的数据里目标框本身是任意朝向的窄长条,水平框会把大量背景也框进去,此时可以考虑 mmrotate 这类旋转框检测方案,把 DOTA 风格的 OBB 标注转成旋转框格式再训练。
5. 训练后别急着部署:用可视化校验把脏标签和漏检一起挑出来
模型训练结束后,最常见的心态是直接看 test 集 mAP 数字。俯拍道路场景里,mAP 高不等于能上线,因为 3000 张图片里只要存在个别标签错位或漏标,评估结果就会被污染。收货前至少做一遍可视化校验,把预测框和真实标签叠在同一张图上,人工扫一遍,胜过任何曲线分析。
5.1 用 YOLOv8 自带的预测可视化快速建立第一印象
把验证集图片跑一遍预测,保存输出:
yolo detect predict \ model=runs/detect/birdview_960/weights/best.pt \ source=data/images/val \ imgsz=960 \ conf=0.25 \ save=True \ project=runs/val_vis跑完打开runs/val_vis里的图片,重点看两类现象:一是同一辆车上有两个重叠框,说明 NMS 阈值需要调低;二是行人被反复漏检,说明增强或切片参数不合适,回第 4 章调整。预测可视化需要和标签叠加对比,可以在代码里读取 YOLO txt 并画矩形,也可以用 LabelImg 打开原图加载标签,做法哪个顺手用哪个。
5.2 低置信度样本里藏着真问题
把置信度阈值从 0.25 降到 0.1,重新预测,专门保存置信度在 0.1 到 0.25 之间的预测框。这些“犹豫框”往往是标签漂移、遮挡严重、目标极小的三类样本。如果这批低置信度框里有大量真实车辆被标成 person,问题不在模型而在标签;如果一半以上都落在非机动车道上的人身上,模型可能把路面纹理或阴影学成了特征。这类样本直接补进数据集比反复调参更有效。
5.3 用硬负样本回填数据集,继续下一轮迭代
人工看过几百张可视化结果之后,把最常被漏检的场景截图回填到训练集中,建议按“候选池”方式管理:先攒 200 到 500 张困难样本,重新训练一轮,对比 mAP-small 的变化。硬负样本不只是“错图”,还包括大量正确的空背景图,模型需要学会在无人无车的路面上输出零检测。俯拍道路的背景变化很大,阴影、匝道护栏、路面积水反光都可能被误检成汽车,空背景图能有效压低误检率。把这一步并入迭代流程:切一批新路段图,做一轮可视化审查,补一批候选框,再训一轮,直到 mAP-small 的变化不再明显为止。验证集评估时保持同一批图片不变,这样前后指标才可比。
本文还有配套的精品资源,点击获取