简介面向自动驾驶与智能交通研究的中国交通数据集基于PASCAL VOC标准构建收录4000张涵盖城市道路、高速路与乡村路等多种场景的交通图像标注了车辆、行人、交通标志、道路等关键元素可用于目标检测、实例分割、语义理解及迁移学习等视觉任务。压缩包内共7200个文件以3783个XML标注文件与3413张JPG原图一一对应为主体另有4个TXT文档用于训练、验证与测试划分整体约707.68MB目录结构遵循VOC2007的ImageSets、JPEGImages、Annotations等规范便于使用PyTorch或TensorFlow直接读取。已有2126人学习下载这套数据。它能提供标准化的边界框与类别标注可直接配合Faster R-CNN、YOLO或Mask R-CNN等模型训练与微调由于包含部分中国特色交通工具和交通标志也为算法在真实本土场景中的泛化能力提供了有价值的测试样本。无论是学术实验还是工程原型验证这套数据都能帮助使用者减少数据采集与标注的时间成本更快聚焦模型设计与调优。 第一次拿到一份“中国交通数据集VOC格式4000张图片”时我的第一反应是总算不用自己顶着太阳蹲在路口拍素材了。但紧跟着第二个问题就冒出来了——这4000张图到底能不能直接扔进训练脚本开跑答案是能但别急着跑。VOC格式本身很成熟可数据集的“VOC格式”只代表标注文件合规不代表图像质量、类别分布、场景覆盖都合规。尤其是中国交通场景车辆类型、交通标志、非机动车混行、信号灯小目标这些都比通用目标检测数据集复杂。这篇直接把我的处理流程、踩过的坑、以及从VOC转到YOLOv8训练管线的完整方法写出来给正在做交通目标检测、或者刚拿到类似数据集的你一个可以照着抄的作业。1. 为什么“4000张”这个数字值得较真中国交通数据的特殊性1.1 中国交通场景到底要识别什么很多人的第一个误区是把交通场景等同于COCO数据集的“person、car、bus、truck”。实际上一旦上路你会发现真正让算法崩溃的往往是“中国特产”两轮车大军电动自行车、摩托车、自行车混在一起从外观上看差别极大但俯拍或逆光时根本分不清。非机动车道和机动车道的边界模糊外卖骑手斜穿车道、三轮车混入汽车流这些对自动驾驶感知和安防监控都是硬需求。交通信号灯普遍比欧洲、北美的更小而且很多城市是组合灯、倒计时灯、左转待转灯。中文交通标志汉字内容本身有语义但检测阶段只需要把标志框出来分类阶段才会用到OCR所以和公开数据集里的Stop、Yield标志并不一样。所以拿着通用数据集预训练模型在真实高速路或城市路口做微调输入分布差异非常大。一份“中国交通数据集VOC格式4000张图片”的价值不在于数量而在于它是否覆盖了上述真实路况。1.2 类别分布和场景覆盖才是第一道筛子我拿到4000张图之后第一件事不是训练而是画类别分布。假设你打算做8类检测person、car、bus、truck、motorcycle、bicycle、traffic_light、traffic_sign那么你会看到类似这样的统计类别图片中出现次数估算说明person3500行人非常多但大量是小尺寸car3000正样本充裕bus600数量少且外观单一truck500数量少混乱常发生在bus/truck之间motorcycle1200和非机动车混行困难样本多bicycle1600骑行者姿态多变traffic_light900目标小容易漏检traffic_sign1200目标小密集区域多这只是个示意但它能立刻暴露两个问题一是truck、bus这类少样本类别容易欠拟合二是traffic_light这种目标小、样本少、背景复杂的类型会成为整个模型mAP的拖油瓶。场景覆盖也要查。4000张图如果全是大晴天中午拍的那模型到了傍晚、雨天、夜间会直接崩掉。很多数据集从公开渠道抓取时天然偏多白天所以训练前先看一下图像亮度直方图如果全是亮调后面得靠数据增强补齐暗光分布。2. VOC格式的家底目录结构和XML标注容易被忽略的字段2.1 VOC目录长什么样Pascal VOC系列格式是目标检测界最经典的格式之一。你手头这份数据集如果完整目录通常长这样traffic_dataset/ ├── JPEGImages/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── Annotations/ │ ├── 000001.xml │ ├── 000002.xml │ └── ... ├── ImageSets/ │ ├── Main/ │ │ ├── train.txt │ │ ├── val.txt │ │ └── test.txt └── label_list.txtJPEGImages放原始图像Annotations放同名XML标注ImageSets/Main下是划分文件。train.txt里每一行是不带后缀的文件名不是图片路径。很多人卡在第一步是把“000001.jpg”整行写进txt结果训练脚本读不到文件。2.2 XML标注的关键字段VOC的XML核心结构如下annotation folderJPEGImages/folder filename000001.jpg/filename size width1920/width height1080/height depth3/depth /size object namecar/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin102/xmin ymin84/ymin xmax512/xmax ymax303/ymax /bndbox /object /annotationbndbox里的坐标是绝对像素值左上角为原点x向右增大y向下增大。这里有两个容易被忽略的坑difficult字段很多工具导出的VOC格式会把难例标成1训练时默认该物体不参与loss计算。如果你把含有大量difficult1的XML直接转成YOLO格式没有过滤difficult样本就可能把标注噪声带进训练。文件名一致性有些数据集的JPEGImages里是IMG_20200101_123456.jpg但Annotations里的XML filename字段写的是000001.jpg或者两侧大小写不一致。这种错位不会在解压时报错只会让你在划分数据时莫名其妙丢掉几百张图。3. 训练前先做体检统计类别、清除坏样本3.1 用脚本扫清标注硬伤我处理任何数据集第一步是写一个扫描脚本把这个问题一次性暴露出来XML和图片数量是否对得上、坐标是否超出图像边界、有没有宽高为0的框、有没有空标注。下面是用来处理这批数据的核心脚本框架import os import glob from xml.etree import ElementTree as ET from PIL import Image jpeg_dir JPEGImages ann_dir Annotations jpgs glob.glob(os.path.join(jpeg_dir, *.jpg)) xmls glob.glob(os.path.join(ann_dir, *.xml)) print(fJPEG数量: {len(jpgs)}, XML数量: {len(xmls)}) # 忽略扩展名对比文件名 jpg_names {os.path.splitext(os.path.basename(p))[0] for p in jpgs} xml_names {os.path.splitext(os.path.basename(p))[0] for p in xmls} print(有XML无图片:, xml_names - jpg_names) print(有图片无XML:, jpg_names - xml_names) # 检查坐标边界 invalid [] for xml_path in xmls: tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text img_path os.path.join(jpeg_dir, img_name) if not os.path.exists(img_path): invalid.append((img_name, 图片缺失)) continue with Image.open(img_path) as im: w, h im.size for obj in root.findall(object): box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) if xmax xmin or ymax ymin: invalid.append((img_name, 框面积为0)) elif xmin 0 or ymin 0 or xmax w or ymax h: invalid.append((img_name, 坐标越界)) print(invalid[:20])这段代码在遇到坐标越界时我建议优先检查该XML是否因为原图被裁剪过导致尺寸不匹配。如果是小范围越界比如xmax比宽度多出2个像素可以当成标注工具手滑直接按边界截断但如果越界值超过图像宽度的一半就要考虑这个文件是不是被错误配对了。3.2 类别不平衡怎么处理体检完坏样本接下来是类别统计。如果bus只有300个框car有9000个框模型很容易把所有长方形大车都归成car。处理办法按优先级排序先合并易混类别如果不能保证标注规范就把bus和truck合并成motor_vehicle或者把motorcycle和bicycle合并成two_wheeler。少两个类比多两个错类更安全。再做过采样把包含少见类别的图片在训练列表里多放几轮让模型每个epoch多看到几次。最后用增强对稀有类别的对象做局部随机裁剪、旋转、复制粘贴但复制粘贴要小心透视关系否则会让模型学到“悬空框”。4. 把VOC喂给YOLOv8格式转换和划分训练集4.1 为什么不直接训练XMLYOLO系列官方仓库不支持直接读VOC XML训练前必须转成YOLO txt格式。YOLO格式每一行对应一个目标class_id center_x center_y width height注意这里的中心坐标和宽高都是相对图像宽度和高度归一化到0~1之间的小数和VOC的绝对像素坐标完全相反。转换公式很简单center_x (xmin xmax) / 2 / img_width center_y (ymin ymax) / 2 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height4.2 转换脚本我通常会把类别清单先写死在脚本里再逐个XML转换。以下是一个可直接复用的转换脚本片段import os import glob from xml.etree import ElementTree as ET from PIL import Image classes [person, car, bus, truck, motorcycle, bicycle, traffic_light, traffic_sign] def convert_annotation(xml_path, out_dir, jpeg_dir): tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text img_path os.path.join(jpeg_dir, img_name) with Image.open(img_path) as im: w, h im.size out_lines [] for obj in root.findall(object): difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue cls_name obj.find(name).text if cls_name not in classes: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 防止越界 xmin max(0, min(xmin, w - 1)) xmax max(0, min(xmax, w - 1)) ymin max(0, min(ymin, h - 1)) ymax max(0, min(ymax, h - 1)) if xmax xmin or ymax ymin: continue cls_id classes.index(cls_name) cx (xmin xmax) / 2 / w cy (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h out_lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) base os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base .txt), w) as f: f.write(\n.join(out_lines))转完之后记得对比一下输出txt目录和JPEGImages目录的文件数。我遇到过转换到一半脚本崩掉、后面几百张图没有生成的场景这步对账能省下一堆莫名其妙的训练报错。4.3 划分和检查泄漏再强调一次划分策略把4000张图按7:2:1拆成train/val/test也就是train 2800张、val 800张、test 400张。这个比例对4000张的小数据集比较稳val集太小会让早停判断失真。划分时需要特别小心数据泄漏。如果原始数据集的图片来自连续视频抽帧那同一路口的相邻帧很容易被同时分进train和val造成验证集虚高。实际处理时我会先看文件名是否带时间戳或摄像头编号尽量按拍摄时间段分组再随机分配。如果文件名完全无规律也要每轮训练后对比train和val的loss曲线——val loss不降、train loss狂降优先怀疑数据泄漏或标注噪声。5. 4000张图片的训练策略迁移学习、增强和评估5.1 从COCO预训练权重开始4000张图对目标检测来说是小规模数据从头训练不现实。我直接用YOLOv8在COCO上的预训练权重做初始化然后把类别分类头从80类替换成你的8类。YOLOv8的backbone已经学到了通用视觉特征比如边缘、纹理、局部形状这些特征对中国交通场景同样有效。训练时前面backbone的权重可以先用较小学习率或者前10个epoch冻结只让head层快速收敛。5.2 增强组合怎么配数据增强是这个小数据集的胜负手。我的常用组合是Mosaic增强把4张图拼成一张变相增加每张训练图里的目标个数。但对小目标交通灯Mosaic拼接后尺寸更小反而容易学不到特征所以我在最后10个epoch会关闭Mosaic让模型在原图比例上精调。随机仿射变换包括水平翻转、小角度旋转、缩放。交通标志是正圆形旋转90度会破坏语义吗不会检测目标不需要区分字是否正着。HSV抖动在真实交通场景中强烈推荐因为一天中色温变化很大正午偏白、黄昏偏黄、夜间偏蓝。把饱和度和亮度做随机扰动能模拟不同天气条件。随机遮挡和模糊模拟树木、电线杆、前车尾气遮挡。要注意的是增强强度不能开太大否则验证集loss和真实场景mAP会劈叉。像Mosaic这种强增强如果训练集本身已经包含了很多密集目标可以降为0.5的概率。5.3 评估指标只看mAP吗交通目标检测里mAP50和mAP50-95都要看。mAP50只关心框和真实框IoU超过0.5对小目标检测能力不敏感mAP50-95对框的定位精度要求更高信号灯这种30x30像素的小目标mAP50-95往往惨不忍睹。如果你的项目重点是远距离车辆检测建议额外关注小目标子集的召回率而不是只看总mAP。6. 实测中反复出现的三类坑6.1 信号灯小目标为什么这么难traffic_light是这类数据集中最典型的难题。一张1920x1080的路口图信号灯区域可能只有20x30像素占总面积不到百分之三。常见特征金字塔在高层特征图上已经下采样到很小尺寸信号灯信息几乎丢失。我的处理方案是把信号灯的检测阈值调低同时用高分辨率图像训练。如果原图全是1080p可以切patch训练把图像按路口中心区域切出640x640局部图让信号灯在patch里占据更大比例。这样会改变目标尺寸分布但能显著提升小目标召回。6.2 标注不一致究竟叫它“骑手”还是“人”中国交通场景最常见的标注矛盾是骑车的人到底算person还是算rider。VOC格式里如果你新建了一个rider类别但标注规范没有统一一会儿把骑电动车的人标成person一会儿标成rider模型就会无所适从。我后来直接合并成person不管走路还是骑车只要是人体轮廓都标person两轮车单独标一个two_wheeler。这样损失了细分能力但换来的是更稳定的训练结果。类别设计永远要服从标注一致性。6.3 场景偏差白天训练、晚上崩盘有一轮训练白天场景mAP能到0.82但夜间测试集mAP直接掉到0.41。查了训练图后发现4000张里真正常曝光、带车灯光晕的图非常少。单纯靠HSV抖动解决不了夜间的光影结构问题因为夜间的对比度分布和白天完全不同。补数据最直接其次是加入合成低光增强降低亮度、增加噪声、模拟大光圈光晕。但合成效果有限最实用的还是拍一批夜间视频抽帧补进数据集后重新训练。我在处理这个数据集的时候最大的体会是拿到手后别急着开训先把每一个坏样本、每一处标注硬伤都摊在桌面上看一遍。4000张图说多不多说少也不少足够支撑做出一版能跑通 demo 的交通目标检测模型也足够让那些数据质量问题一五一十地暴露出来。用这份数据跑完第一轮你真正收获的其实不是模型 weight而是对“数据质量决定模型上限”这句话的切肤认知。本文还有配套的精品资源点击获取