简介:这份交通道路目标检测数据集面向计算机视觉学习者、自动驾驶感知方向研究者及模型训练工程师,用于车辆、行人、自行车与摩托车四类目标的检测与识别任务。数据采用PASCAL VOC标准格式组织,每张原始图片均配有对应的XML标注文件,可直接接入YOLO、Faster R-CNN等主流检测框架进行训练与验证。压缩包内共2000个文件,全部为xml标注文件,整体约129.73MB,标注内容涵盖目标类别与边界框坐标,便于快速构建训练集与测试集。资源原始图片规模为2998张,覆盖道路场景下的多类交通参与者,适合做类别均衡分析、数据增强与模型调优实验。目前已有590人学习下载,可作为课程设计、毕业项目或算法预研的现成数据基础,帮助读者省去从零标注的时间成本,把精力集中在模型结构设计与精度提升上。
1. 交通道路目标检测数据集:2998 张 VOC 标注到底能训出什么
手头拿到一个 zip,解压后是 2998 张原始道路图片,标注格式是 VOC,类别覆盖车辆、行人、自行车、摩托车。这类数据集在目标检测圈子里属于典型的「小而全」——场景是真实交通道路,目标类别是四类高频交通参与者,标注格式是 Pascal VOC XML。它解决的核心问题很直接:让你在不采集、不标注的前提下,快速跑通一个交通场景目标检测模型的完整训练链路。适合谁?适合想验证自研检测头、做数据增强策略对比、或者给边缘设备做交通目标识别原型的工程师。VOC 格式虽然老,但它的 XML 结构清晰、解析工具链成熟,从 VOC 转 YOLO 或 COCO 都有现成脚本,这也是为什么大量标注数据集仍然以 VOC 作为交付格式。2998 张不算多,但四类目标的分布如果均衡,足够支撑一个轻量模型的微调实验。
2. VOC 标注格式拆解与数据体检:先看清手里有什么
2.1 VOC XML 的字段含义与解析逻辑
VOC 格式每张图对应一个 XML 文件,文件名与图片名一致,放在 Annotations 目录下。核心字段包括:folder、filename、size(宽高和通道数)、object(每个目标一个节点,含name、pose、truncated、difficult、bndbox)。bndbox里是xmin、ymin、xmax、ymax,坐标原点在左上角,单位是像素。difficult标记为 1 的目标在评估时通常忽略,truncated表示目标是否被截断。解析时最容易翻车的地方是坐标越界——有些标注框的xmax会等于图片宽度,某些训练框架会因此报错,需要做 clamp 处理。
import xml.etree.ElementTree as ET import os def parse_voc_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) objects = [] for obj in root.findall('object'): name = obj.find('name').text difficult = int(obj.find('difficult').text) bndbox = obj.find('bndbox') xmin = max(0, int(float(bndbox.find('xmin').text))) ymin = max(0, int(float(bndbox.find('ymin').text))) xmax = min(img_w, int(float(bndbox.find('xmax').text))) ymax = min(img_h, int(float(bndbox.find('ymax').text))) if xmax <= xmin or ymax <= ymin: continue # 跳过无效框 objects.append({ 'name': name, 'difficult': difficult, 'bbox': [xmin, ymin, xmax, ymax] }) return img_w, img_h, objects这段代码做了三件事:读取图片尺寸、遍历所有 object 节点、对坐标做边界裁剪。max(0, ...)和min(img_w, ...)是防止标注框超出图片范围,xmax <= xmin的判断是过滤掉零面积或负面积的无效框。difficult字段保留下来,后续做评估时可以决定是否忽略。
2.2 用统计脚本给数据集做一次体检
拿到数据集第一件事不是直接开训,而是统计类别分布、框的尺寸分布、每张图的平均目标数。这一步能帮你判断数据集是否存在严重的长尾问题。比如行人目标如果只占 5%,训出来的模型对行人召回率会明显偏低。
import glob from collections import Counter xml_files = glob.glob('Annotations/*.xml') class_counter = Counter() boxes_per_image = [] widths, heights = [], [] for f in xml_files: _, _, objs = parse_voc_xml(f) boxes_per_image.append(len(objs)) for o in objs: class_counter[o['name']] += 1 w = o['bbox'][2] - o['bbox'][0] h = o['bbox'][3] - o['bbox'][1] widths.append(w) heights.append(h) print('类别分布:', class_counter) print('每图平均目标数:', sum(boxes_per_image) / len(boxes_per_image)) print('框宽中位数:', sorted(widths)[len(widths)//2]) print('框高中位数:', sorted(heights)[len(heights)//2])跑完这个脚本,你会得到四个关键数字。类别分布告诉你有没有类别需要过采样;每图平均目标数决定你 batch size 能开多大;框宽高中位数帮你选 anchor 尺寸或者判断是否需要开启多尺度训练。如果中位数小于 32 像素,说明小目标占比高,特征金字塔的层数要相应增加。
2.3 可视化抽查:别让脏标注混进训练集
统计只能看宏观,脏标注得靠可视化。随机抽 20 张图,把 XML 里的框画到图上,肉眼过一遍。常见问题包括:框只框了半个目标、类别标错(自行车标成摩托车)、一个目标多个框、框完全偏离目标。这些问题不查出来,模型会学歪。
import cv2 import random sample = random.sample(xml_files, 20) for f in sample: img_name = os.path.basename(f).replace('.xml', '.jpg') img = cv2.imread(os.path.join('JPEGImages', img_name)) _, _, objs = parse_voc_xml(f) for o in objs: x1, y1, x2, y2 = o['bbox'] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, o['name'], (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(f'vis_{img_name}', img)画完框后重点看三类问题:框是否贴合目标边缘、类别标签是否与目标一致、有没有漏标的目标。漏标比错标更隐蔽,因为统计脚本查不出来,但模型会把漏标区域当背景学,导致召回率上不去。如果发现漏标比例超过 5%,建议重新过一遍标注。
3. 从 VOC 转 YOLO 格式:转换脚本与四个边界坑
3.1 坐标归一化与目录结构重组
YOLO 格式要求每张图对应一个 txt 文件,每行是class_id x_center y_center width height,全部归一化到 0~1。转换时类别名到 id 的映射要固定,建议按字母序或按统计数量降序排列,并且存成classes.txt供训练和推理共用。
import os CLASSES = ['bicycle', 'car', 'motorbike', 'person'] CLASS_TO_ID = {c: i for i, c in enumerate(CLASSES)} os.makedirs('labels/train', exist_ok=True) os.makedirs('images/train', exist_ok=True) for f in xml_files: img_w, img_h, objs = parse_voc_xml(f) img_name = os.path.basename(f).replace('.xml', '.jpg') lines = [] for o in objs: if o['name'] not in CLASS_TO_ID: continue x1, y1, x2, y2 = o['bbox'] xc = (x1 + x2) / 2 / img_w yc = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{CLASS_TO_ID[o['name']]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(f'labels/train/{img_name.replace(".jpg", ".txt")}', 'w') as fp: fp.write('\n'.join(lines))CLASSES列表的顺序就是类别 id,必须和后续训练配置文件里的names完全一致。归一化时除法用浮点,保留 6 位小数足够。如果某个类别在CLASSES里没有对应项,直接跳过而不是报错,这样能兼容标注里混入的意外类别。
3.2 四个容易翻车的边界情况
第一个坑:图片格式不统一。VOC 数据集里图片可能是 jpg、png、jpeg 混用,转换脚本里硬编码.jpg会漏掉一部分。正确做法是用glob匹配所有图片扩展名,或者先统一转成 jpg。
第二个坑:XML 里size字段与实际图片尺寸不一致。有些标注工具在图片被裁剪后没有更新size,导致归一化分母错误。解决办法是用cv2.imread读实际尺寸,而不是信 XML 里的size。
第三个坑:类别名大小写不一致。比如Car和car同时出现,映射时会当成两个类。转换前先做一次name.lower()统一。
第四个坑:空标注文件。有些图没有任何目标,对应的 txt 是空文件。YOLO 训练时空 txt 是合法的负样本,但某些数据加载器会报错,需要确认框架是否支持。
3.3 划分训练集与验证集:别用随机划分
随机划分在交通场景里有个隐患:同一段路的连续帧可能被分到训练集和验证集,导致验证指标虚高。如果数据集里的图片是按时间或路段排列的,建议按顺序切分,比如前 80% 做训练,后 20% 做验证。如果图片已经完全打乱,随机划分也可以接受,但要固定随机种子。
import random random.seed(42) all_imgs = sorted(os.listdir('images/train')) random.shuffle(all_imgs) split = int(len(all_imgs) * 0.8) val_imgs = all_imgs[split:] os.makedirs('images/val', exist_ok=True) os.makedirs('labels/val', exist_ok=True) for img in val_imgs: os.rename(f'images/train/{img}', f'images/val/{img}') txt = img.replace('.jpg', '.txt') os.rename(f'labels/train/{txt}', f'labels/val/{txt}')固定seed=42是为了可复现。验证集比例 20% 是常规做法,如果数据量少于 2000 张,可以降到 15%。移动文件用os.rename比复制再删除快,但要注意跨磁盘分区时会失败,跨分区得用shutil.move。
4. 训练配置与参数调优:2998 张图怎么设 batch 和学习率
4.1 轻量模型选型与输入分辨率
2998 张图属于小数据集,直接上大模型容易过拟合。常见做法是选 YOLOv8n 或 YOLOv8s 这类轻量骨干,参数量在 3M 到 11M 之间。输入分辨率建议 640×640,这是 YOLO 系列的默认值,预训练权重也是在这个尺度上训的。如果小目标多,可以提到 960 或 1280,但显存占用会翻倍,batch size 要相应降下来。
| 模型 | 参数量 | 640 分辨率显存 | 建议 batch |
|---|---|---|---|
| YOLOv8n | 3.2M | ~4GB | 16 |
| YOLOv8s | 11.2M | ~6GB | 8 |
| YOLOv8m | 25.9M | ~10GB | 4 |
显存数字是训练时的粗略估计,实际取决于框架实现和是否开启混合精度。如果显存不够,优先降 batch 而不是降分辨率,因为分辨率对精度影响更大。
4.2 学习率与 warmup 设置
小数据集微调时,初始学习率建议设 0.001 到 0.01 之间。如果加载了 COCO 预训练权重,学习率可以低一些,0.001 起步;如果从头训,0.01 起步。warmup 轮数设 3 到 5 个 epoch,让模型先适应新数据分布再进入正常学习率。余弦退火调度比阶梯下降更平滑,最终学习率降到初始值的 1%。
# ultralytics YOLOv8 训练配置示例 from ultralytics import YOLO model = YOLO('yolov8s.pt') model.train( data='traffic.yaml', epochs=100, imgsz=640, batch=8, lr0=0.001, lrf=0.01, warmup_epochs=3, cos_lr=True, patience=20, augment=True, mosaic=1.0, mixup=0.1, degrees=10.0, translate=0.1, scale=0.5, fliplr=0.5, )lr0是初始学习率,lrf是最终学习率比例,cos_lr=True开启余弦退火。patience=20表示验证指标 20 个 epoch 不提升就早停。mosaic=1.0是 YOLO 的招牌增强,把四张图拼成一张,对小数据集特别有效。mixup=0.1做图像混合,比例别太高,0.1 到 0.2 足够。degrees=10.0是随机旋转角度,交通场景里车辆倾斜角度不会太大,10 度合理。
4.3 数据增强的边界:哪些增强会帮倒忙
翻转增强fliplr=0.5对交通场景安全,因为左右翻转后车辆和行人仍然合理。但上下翻转flipud要关掉,倒过来的车不真实。旋转角度超过 15 度后,标注框会变得很松,模型学到的定位精度下降。色彩抖动hsv_h、hsv_s、hsv_v可以开,但幅度别太大,否则夜间和白天场景会混淆。马赛克增强在训练后期建议关掉,最后 10 个 epoch 用原始图微调,能提升最终精度。
提示:如果验证集 loss 震荡厉害,先把 mosaic 关掉看是否稳定,再逐步加回来。
5. 避坑与排查:训练不收敛、指标虚高、推理漏检
5.1 现象:loss 从第一轮就不降
原因通常是类别 id 映射错位。YOLO 的names列表顺序和 txt 里的 class_id 必须严格对应,差一位就全错。排查方法:打印traffic.yaml里的names,再随机抽一个 txt 文件看第一列数字,确认映射一致。另一个原因是图片路径配置错误,数据加载器读不到图,只能读到空张量。
5.2 现象:验证集 mAP 很高但实际推理漏检严重
这是典型的验证集泄漏。如果训练集和验证集里有同一段路的连续帧,模型相当于见过验证集的「近亲」,指标虚高。解决办法是按路段或时间切分,而不是随机切分。另一个可能是验证集太小,2998 张图如果验证集只有 100 张,指标波动会很大,建议至少 300 张。
5.3 现象:小目标召回率极低
交通场景里远处的行人和自行车是小目标,640 分辨率下可能只有 10 几个像素。解决办法有三个:提高输入分辨率到 960 或 1280;在模型里加 P2 特征层(YOLOv8 默认从 P3 开始,P2 是更高分辨率的浅层特征);用切图推理,把大图切成小块分别检测再合并。切图推理会增加后处理复杂度,但小目标召回提升明显。
5.4 现象:训练到一半 loss 突然变 NaN
最常见原因是学习率太大,梯度爆炸。先把lr0降一个数量级试试。如果还不行,检查数据里有没有坐标全为 0 的脏标注,这种框在计算 loss 时会产生异常值。另外,混合精度训练amp=True在某些显卡上会溢出,关掉 amp 用 FP32 训练能排除这个因素。
5.5 现象:推理时同一目标出多个框
NMS 的 IoU 阈值设太高了。默认 0.7 对密集交通场景偏松,两辆车挨得近时容易保留重复框。把 IoU 阈值降到 0.5 到 0.6 之间,或者换用 DIoU-NMS,它对重叠目标的区分更好。如果目标确实密集,还可以调低置信度阈值后做一次软 NMS。
6. 用 2998 张图榨出更高精度:三个我反复验证过的技巧
第一个技巧是「预训练权重 + 分层学习率」。骨干网络加载 COCO 预训练权重后,前几层学的是通用特征,不需要大改,学习率设小一点;检测头是随机初始化的,学习率设大一点。在 ultralytics 框架里可以通过参数组实现,把骨干的学习率设为lr0 * 0.1,检测头用lr0。这个操作在 2998 张这种小数据集上能明显减少过拟合,我试过在同一个数据集上,分层学习率比统一学习率 mAP 高 2 到 3 个点。
第二个技巧是「难例挖掘 + 二次微调」。第一轮训练完后,用模型在训练集上推理一遍,把漏检和误检的图挑出来,人工检查标注是否有问题。如果标注没问题但模型就是学不会,把这些图复制一份加权采样,做第二轮微调。加权采样可以通过在数据集配置文件里重复列出这些图实现,或者用WeightedRandomSampler。这个做法相当于让模型多看几遍难例,对长尾类别提升明显。
第三个技巧是「测试时增强」。推理时把图片水平翻转一次,两次推理结果做 NMS 融合。这个操作不增加训练成本,推理时间翻倍,但 mAP 通常能涨 1 个点左右。如果对推理速度不敏感,还可以加多尺度测试,把图片缩放到 640、800、960 三个尺度分别推理再融合,涨点更多但耗时也更多。
# 测试时增强的简化实现 import cv2 import numpy as np def tta_predict(model, img_path, conf=0.25, iou=0.5): img = cv2.imread(img_path) results = [] # 原始图推理 r1 = model(img, conf=conf, iou=iou)[0] results.append(r1.boxes.data.cpu().numpy()) # 水平翻转推理 img_flip = cv2.flip(img, 1) r2 = model(img_flip, conf=conf, iou=iou)[0] boxes2 = r2.boxes.data.cpu().numpy() if len(boxes2) > 0: boxes2[:, 0] = img.shape[1] - boxes2[:, 2] # x1 翻转 boxes2[:, 2] = img.shape[1] - boxes2[:, 0] # x2 翻转 results.append(boxes2) # 合并后做 NMS all_boxes = np.concatenate(results, axis=0) # 此处省略 NMS 实现,可用 torchvision.ops.nms return all_boxes翻转后坐标要还原,x1和x2的翻转公式容易写反,写完拿一张图验证一下框的位置对不对。TTA 的收益在分类任务上更明显,检测任务上因为 NMS 的存在,涨点幅度有限,但胜在实现简单。
最后说一个我踩过的坑:不要用验证集调参。验证集只能用来选模型,不能用来调学习率、增强参数。调参要用训练集里再切出来的一小部分做验证,或者用交叉验证。我见过太多人反复在验证集上试参数,最后指标好看但上线就崩。希望帮到你。
本文还有配套的精品资源,点击获取