简介:面向自动驾驶感知、智能交通监控与驾驶辅助训练,提供一套多类别交通物体与交通标志检测数据集,覆盖车辆、行人、路标、交通灯等同步检测需求。数据来自真实道路场景,训练集656张、验证集188张、测试集94张共938张图片,标注了限速、禁止通行、交叉路口等134种交通标志,以及轿车、公交车、卡车、摩托车、自行车等交通工具,并包含弯道、陡坡、红绿灯、斑马线、单行道、落石、动物出没等146个精细类别。资源共1878个文件,含938张JPG道路图片、938个对应TXT标注文件、1个YAML配置和1个DOCX说明文档,压缩包大小104.37MB。标注采用YOLO边界框格式,可直接用于YOLOv5/v7/v8等主流框架训练,支持多目标密集标注,覆盖城市道路、高速公路、山区路段及不同光照条件。目前已有89人学习,适合需要构建交通场景目标检测模型的研究者与开发者快速上手。
1. 自动驾驶多类别交通物体与交通标志检测数据集:一次把车辆、行人、骑手和标志牌凑齐
开箱一个名为“自动驾驶多类别交通物体与交通标志检测数据集”的 zip 包,等于拿到一批贴着边界框的交通场景素材:车辆、行人、骑手、公交车这类交通物体是一组,限速牌、禁令标志、指示标志、信号灯这类交通标志是另一组。落地路径很清晰——把压缩包解析成标准格式,喂给 YOLOv8 或 MMDetection 这类检测框架,训出一个多类别目标检测模型,再谈实车部署或决策链路接入。数据包能不能支撑自动驾驶感知,从来不取决于 zip 有多少 GB,而取决于场景覆盖、标注一致性和类别分布均衡程度。下面从解压开始,按一个完整训练项目的顺序,把每一步怎么落、坑在哪里讲透。
2. 解压之后先别急着训练:目录结构、标注格式与类别分布怎么核查
2.1 第一次解压:用这三条命令确认数据集完整性与目录骨架
拿到这个 zip,我一般不会直接在图形界面里双击解压,而是先看压缩包清单,避免解到一半发现文件损坏或目录结构不对。Linux 下习惯这么操作:
# 1. 先列压缩包里的顶层目录,确认是否带根目录 unzip -l 自动驾驶多类别交通物体与交通标志检测数据集.zip | head -40 # 2. 解压到指定目录,保留原有目录层级 unzip -q 自动驾驶多类别交通物体与交通标志检测数据集.zip -d ./datasets/traffic # 3. 解压后统计文件总数,和压缩包窗口里显示的数做对比 find ./datasets/traffic -type f | wc -l第一行unzip -l只列内容不解压,能看出 zip 里有没有打包顶层目录,这直接影响后面的路径配置。第二行把包解开到./datasets/traffic,-q让日志安静一点。第三步统计文件总数,和压缩包侧边栏或下载页给的文件数对比,数字对不上就说明解压过程有文件被跳过,多数是文件名编码问题或磁盘满了。
解压完成后,第一件事不是跑训练,而是把目录骨架完整看一遍。交通场景数据集最常见的布局是images/和labels/两层,各自下面再分train/、val/、test/;也有部分数据集把标注合并成一个annotations/文件夹,里面放一个大的 COCO JSON 文件。这两种布局对应完全不同的训练流程,先查清楚再动手:
tree -L 3 ./datasets/traffic按我经手过的同类数据集经验,images/train/xxx.jpg配labels/train/xxx.txt的是 YOLO 风格,images/配annotations/instances_train.json的是 COCO 风格。同一个 zip 里两套都出现的情况也有,但通常其中一套是附带工具生成的冗余文件,训练前先确认你要用哪一套,别让配置文件同时指向两个人。
2.2 标注格式对比:COCO 的 json 和 YOLO 的 txt 怎么识别、怎么转
多类别交通检测数据集里,标注格式几乎只有两大家族:COCO 的 JSON 和 YOLO 的纯文本。先看 COCO 格式长什么样。一个典型的instances_train.json里有images、annotations、categories三个数组,categories决定了类别 ID 排序:
{ "categories": [ {"id": 1, "name": "person"}, {"id": 2, "name": "bicycle"}, {"id": 3, "name": "car"}, {"id": 4, "name": "motorcycle"}, {"id": 5, "name": "bus"}, {"id": 6, "name": "traffic_light"}, {"id": 7, "name": "traffic_sign"} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 3, "bbox": [320, 240, 128, 96]}, {"id": 2, "image_id": 1, "category_id": 7, "bbox": [860, 120, 54, 38]} ] }注意 COCO 的bbox是[x, y, width, height],x、y 是框的左上角坐标。而 YOLO 的 txt 每行是class_id center_x center_y width height,坐标值是相对于图片宽高的 0 到 1 浮点数。两套格式最大的坑在坐标换算:COCO 转 YOLO 时每个值都要除以图宽图高,而且类别 ID 不一定从 0 开始连续,上面 COCO 里person是 1,YOLO 分类规定必须从 0 开始,所以转换时必须做一次重映射。
我一般用一段 Python 脚本做 COCO 到 YOLO 的转换,省得手算坐标:
import json import os coco_path = 'annotations/instances_train.json' out_dir = 'labels/train' os.makedirs(out_dir, exist_ok=True) with open(coco_path, 'r', encoding='utf-8') as f: coco = json.load(f) # 建 image_id -> 文件名的映射,必须和实际图片文件名一致 id2name = {im['id']: im['file_name'] for im in coco['images']} # 类别重映射:COCO 的 id 可能不连续,压成 0..N-1 的连续整数 old2new = {cat['id']: i for i, cat in enumerate(coco['categories'])} for ann in coco['annotations']: img_name = id2name[ann['image_id']] img_info = next(im for im in coco['images'] if im['id'] == ann['image_id']) w, h = img_info['width'], img_info['height'] x, y, bw, bh = ann['bbox'] # 左上角+宽高 转 中心点+宽高,并归一化到 0..1 cx, cy = x + bw / 2, y + bh / 2 cx, cy, bw, bh = cx / w, cy / h, bw / w, bh / h cls_id = old2new[ann['category_id']] txt_path = os.path.join(out_dir, os.path.splitext(img_name)[0] + '.txt') with open(txt_path, 'a') as f: f.write(f'{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n')这段逻辑有三个关键细节。其一,os.path.splitext把img_name的后缀换成.txt,要求图片名和标注名的前缀严格一致,哪怕图片是.jpeg、标注是.txt也一样。其二,类别重映射用enumerate把 COCO 的稀疏 ID 压成 0 开始的连续索引,避免训练时类别数比实际多或者索引越界。其三,坐标转换必须用同一张图的宽高归一化,如果原图被旋转过、裁剪过或启用过 EXIF 旋转,宽高就和标注基准不一致,转出来的 YOLO 框全是歪的。
提示:转换之后随机抽三五张图,用 OpenCV 把框叠上去看一眼,不要轻信脚本输出。画框验证这一步,比之后上 GPU 跑了 50 轮再发现问题省时得多。
import cv2 import os img_path = 'images/train/road01_000001.jpg' txt_path = 'labels/train/road01_000001.txt' img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cls, cx, cy, bw, bh = map(float, line.split()) x1, y1 = int((cx - bw / 2) * w), int((cy - bh / 2) * h) x2, y2 = int((cx + bw / 2) * w), int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) img_resized = cv2.resize(img, (1280, 720)) cv2.imshow('check', img_resized) cv2.waitKey(0) cv2.destroyAllWindows()这段可视化脚本本身没什么高深内容,但它能把标注框是整体偏移、宽高比不对、还是类别 ID 与目标物不符这些问题一次性暴露出来。我几乎在每一个数据集项目里都会跑一遍,宁可多花三分钟也不想让问题藏到训练结束。
2.3 类别统计与不平衡:交通标志偏少、行人被遮挡,训练前要有心理预期
多类别交通数据集的痛点是类别不均衡。车辆类(car、bus、truck)往往占一半以上的标注框,行人次之,交通标志和信号灯因为目标面积小、出现频率低,在整体标注框里占比通常低于 10%。这不奇怪——真实路面上车的数量本来就比标志牌多,采样时又经常从车载摄像头连续抽帧,长时间在同一条路、同一个路口,标志牌反复出现在同一批画面里,特征重复度高,多样性反而低。
在动手训练前用脚本统计一下每个类别的框数量:
python - <<'EOF' import os, glob label_dir = 'labels/train' cnt = {} for txt in glob.glob(os.path.join(label_dir, '*.txt')): with open(txt) as f: for line in f: if not line.strip(): continue cls = line.split()[0] cnt[cls] = cnt.get(cls, 0) + 1 for k, v in sorted(cnt.items(), key=lambda x: -x[1]): print(f'class {k}: {v}') EOF统计完你会直观看到哪些类别在拖后腿。如果交通标志这类占比很低,训练时要么给分类损失加权重,要么在数据增强时对含该类别的图像做上采样复制。更稳妥的做法是先看置信度阈值下的逐类召回率:如果行人好检测而标志牌召回率只有六成,那不是模型能力问题,是样本本身不够。
这套数据集的隐含价值在于它同时包含“交通物体”和“交通标志”两个语义层。前者是通用目标检测都覆盖的常见类别,后者对检测器的泛化要求更高——标志的颜色、亮度和形变在不同时段、逆光和雨雾条件下差异很大,正好是第 4 章避坑记录里要展开的内容。
3. 用这个数据集训练一版多类别检测模型:从划分到调参的完整流程
3.1 按场景划分数据集:别让同一条路的样本同时出现在训练集和验证集
拿到 zip 直接跑训练的人,通常第一步就栽在划分上。很多数据集包自带train/、val/目录,看起来能用,但划分方式未必合理。交通数据集最典型的问题是连续帧:同一辆车出现在第 100 帧和第 101 帧,如果第 100 帧在训练集、第 101 帧在验证集,验证 mAP 会虚高,因为模型其实“见过”这辆车了。
我一般不管它自带的划分,重新做一次场景级切分。所谓场景级,就是把图片按时间戳、拍摄路段或 GPS 轨迹分组,同一个场景下的所有帧要么全进训练集,要么全进验证集。一个简单可操作的做法是按文件名前缀分组:
import os, shutil, random from collections import defaultdict src_img = 'images/all' # 原始图片目录 src_lbl = 'labels/all' # 原始标签目录 out_img = 'data_reorg/images' out_lbl = 'data_reorg/labels' os.makedirs(out_img + '/train', exist_ok=True) os.makedirs(out_img + '/val', exist_ok=True) os.makedirs(out_lbl + '/train', exist_ok=True) os.makedirs(out_lbl + '/val', exist_ok=True) # 按场景标识分组;若文件名是 road01_000123.jpg,就取 road01 作为场景组 groups = defaultdict(list) for f in os.listdir(src_img): scene_id = f.rsplit('_', 1)[0] groups[scene_id].append(f) # 场景级切分,保证一个场景的所有帧只进一边 random.seed(42) all_scenes = list(groups.keys()) random.shuffle(all_scenes) val_scenes = set(all_scenes[: int(len(all_scenes) * 0.2)]) for scene, files in groups.items(): target_img = out_img + ('/val' if scene in val_scenes else '/train') target_lbl = out_lbl + ('/val' if scene in val_scenes else '/train') for f in files: shutil.copy(os.path.join(src_img, f), os.path.join(target_img, f)) lbl_name = f.rsplit('.', 1)[0] + '.txt' lbl_src = os.path.join(src_lbl, lbl_name) if os.path.exists(lbl_src): shutil.copy(lbl_src, os.path.join(target_lbl, lbl_name))这段代码里f.rsplit('_', 1)[0]假设文件名是road01_000123.jpg这样的格式,去掉最后一截帧号后把road01当作场景组。如果你的文件名没有清晰前缀,就得看有没有伴随的scene.csv或timestamp元数据;如果都没有,退而求其次按编号每间隔 N 帧抽一帧做验证。总之目标是让验证集和训练集在内容上解耦,而不是随机打散文件,这是交通视频类数据集和普通静态图片数据集最本质的区别。
3.2 最小可运行训练流程:Ultralytics YOLOv8 的 yaml 配置与启动命令
划分好之后进入训练环节。目标检测框架选型,我在交通数据场景下优先推荐 YOLOv8,理由是文档全、坑少、一套 CLI 能跑通全流程。先建一个数据集描述文件traffic.yaml:
# traffic.yaml — 放在数据集根目录下 path: ./data_reorg # 上一步 reorg 后的根目录 train: images/train val: images/val names: 0: person 1: bicycle 2: car 3: motorcycle 4: bus 5: traffic_light 6: traffic_sign这里的names字典必须和标注文件里的class_id一一对应,顺序从 0 开始不可随意调整。很多人在这一步把 COCO 的 id 直接搬过来,比如person写成 1,结果 YOLO 认为第 0 类是person但标注文件里根本没有 0,训练直接报class index out of range,属于最基础的低级错误。
写好后启动训练:
yolo detect train \ data=traffic.yaml \ model=yolov8s.pt \ epochs=80 \ imgsz=1280 \ batch=16 \ project=runs/traffic_bench \ name=exp1解释一下这几个参数在交通场景里的取舍。
model=yolov8s.pt:在精度和速度之间取平衡,用 s 版本做第一次 baseline,后面再切 m 或 l。imgsz=1280:交通标志是典型小目标,640 输入下远处一块 30x30 像素的牌子可能只占十几个像素,1280 能显著提升小目标召回,代价是显存和训练时间翻倍,显存不够就先从 960 开始。epochs=80:中小型数据集 80 轮足够收敛。前 20 轮观察 loss 趋势,如果降到 2.0 以下可以放心跑完,如果 20 轮还在 4 以上,先查学习率和数据加载是否有错。batch=16:按 1280 输入、单卡 4090 估算,这是稳妥值。batch 过大会让 BatchNorm 统计不稳定,验证 loss 振荡。
训练完成后,runs/traffic_bench/exp1/weights/best.pt就是最优权重。验证输出会给出每个类别的mAP50和mAP50-95,第一件事就是看交通标志这一类有没有明显掉队。
3.3 调参的三种常见策略:类别权重、多尺度训练、置信度阈值
如果跑出来的逐类 mAP 差距很大,需要针对性调参。我在交通检测项目里的调参顺序通常分三步。
第一步是类别加权。YOLOv8 没有直接的cls_loss权重参数,但可以通过采样变相实现:统计每个类别的框数,对稀有类别所在的图像复制几份,再写入新目录。脚本逻辑不复杂,就是把 labels 里含traffic_sign这类目标的 txt 找出来,把对应的 jpg 和 txt 复制多份。注意复制后图片不能重名,否则数据加载会串。
第二步是多尺度训练。把imgsz改成[960, 1280],让模型在小图和大图之间交替训练。本质是在模拟摄像头在不同距离下的观察——近处的标志大、远处的小,多尺度能缓解距离变化带来的尺度跨度问题。代价是训练时间增加约 20%,但对交通标志这类尺度跨度极大的目标,收益非常明显。
第三步是推理阈值。训练完的模型默认置信度阈值 0.25,在交通场景里不算最优。实车测试时如果发现远距离标志漏检,把conf降到 0.1 或 0.15;如果误检太多,提到 0.4 以上。这个动作不影响权重,只改推理参数,但在实车感知体验上的差异往往比换更大的模型更直接。
4. 避坑:标注噪声、小目标丢失与格式转换的五条踩坑记录
4.1 现象:验证 mAP 到 0.78,实车跑一段路却误检出一堆假标志牌
原因:训练集和验证集划分时把同一路口的连续帧拆开了,模型对场景过拟合,对没见过的路口泛化差。同时训练集缺少负样本——也就是完全没有交通标志的路面背景图。
解决:按 3.1 的场景划分重新切数据,再额外找一批不含任何目标物的背景图放进images/train,每张背景图配一个空 txt 文件。空标注文件不是 bug,是 YOLO 训练里重要的负样本机制,它能让模型学会“没有标志就是没有标志”,而不是靠场景记忆硬猜。
4.2 现象:mAP50 还不错,但 mAP50-95 一直上不去,小目标召回只有三成
原因:训练分辨率太低。远处交通标志在 640x640 输入下只占十几个像素,特征提取时几次下采样就把目标抹掉了。
解决:把imgsz从 640 提到 1280,必要时结合多尺度训练。另外检查数据增强里的mosaic参数——mosaic 拼接时,小目标容易被其他图块挤压甚至被裁掉,如果发现打开 mosaic 后小目标召回掉得很厉害,可以把mosaic概率降到 0.5 或直接关掉再对比一轮。
4.3 现象:把 COCO json 转成 YOLO txt 后,训练时报class index out of range
原因:COCO 的category_id不一定从 0 开始,很多数据集从 1 开始,有的在中间跳号。转换脚本里只搬了 bbox 坐标,没有做old2new重映射,导致标注里出现 7 或 14 这类超出类别数的 ID。
解决:转换时先读取categories数组,用enumerate生成新索引。转换完成后抽查几个 txt 文件,确认所有 class_id 都落在 0 到 N-1 之间。
# 抽查所有 txt 里的最大类别 ID,超过 N-1 就要回头查映射 awk '{print $1}' labels/train/*.txt | sort -n | tail -14.4 现象:解压后labels/train文件数比images/train少,训练时一直提示找不到标签
原因:部分标注文件在打包时遇到非法文件名(如中文冒号、首尾空格),没有正确写入 zip,或者在解压时被安全软件拦截。这是最麻烦的一类问题。
解决:解压后立刻用命令对比两个目录的文件名集合,找出缺失列表。如果缺失数量少,直接删除对应图片(前提是剩余图片量足够训练);如果缺失超过 5%,建议重新下载原包。
diff \ <(ls images/train | sed 's/.jpg//' | sort) \ <(ls labels/train | sed 's/.txt//' | sort) | head -20diff的输出会列出只在某一侧存在的文件名,能精确定位哪张图缺标签、哪个标签缺图片。
4.5 现象:训练 Loss 正常下降,但验证集上每一轮的准确率忽高忽低
原因:这不是数据集质量问题,而是划分方式的问题。纯随机切分会让验证集里某一类目标偶尔整段消失或翻倍,验证集图片数量少、场景集中时,两个 epoch 之间落在验证集上的目标分布差异大,指标自然震荡。
解决:把验证集锁死,改成按场景分组的固定文件列表,并且验证集中每个类别至少要有 150 个标注框。如果数据量实在少,可以用 K 折交叉验证,但每一折也要按场景切,不能随机切。
5. 从数据集到自动驾驶决策:评估、延迟下调和结构化输出
5.1 分类型评估:只看总 mAP 会让交通标志问题被平均掉
总 mAP 高不等于能上车,交通场景里每个类别的失效模式不一样。用下面的命令跑一次带 JSON 输出的验证:
yolo detect val \ data=traffic.yaml \ model=runs/traffic_bench/exp1/weights/best.pt \ conf=0.25 \ save_json=True输出会生成predictions.json,里面每个检测框带category_id和score。按类别汇总,重点看traffic_light和traffic_sign在 0.25 置信度下的召回率。这两类低于 0.5,基本没有上车资格,先回去补样本,而不是换模型。
5.2 决策延迟32.8毫秒需要什么条件
“决策延迟32.8毫秒”换算过来接近 30 FPS 的推理节奏,想在嵌入式设备或车载工控机上达到,通常要做三件事:模型导出为 FP16 或 INT8 精度、用 TensorRT 做优化、把推理输入分辨率锁在 960 而不是 1280。32.8 毫秒指的是从图像进入推理引擎到输出检测结果的时间,包含 NMS 后处理,不是纯 GPU 前向时间。实测时我会用trtexec接口交替测多次取中位数,而不是看训练框架里的日志,因为后者会吞掉不少预处理开销。
5.3 结构化输出:检测框如何变成自动驾驶决策消息
检测结果最终要交给融合模块或决策模块,格式要标准化。我惯用一个简单的 JSON 结构作为中间消息:
{ "timestamp": 1712372948123, "objects": [ {"class": "car", "box": [320, 240, 640, 480], "score": 0.92, "distance_m": 18.5}, {"class": "traffic_sign", "box": [800, 100, 860, 180], "score": 0.74, "distance_m": 42.0} ] }distance_m由相机内参和检测框底边像素位置换算,行业里通常接一个基于平面假设的单目测距模块,精度不追厘米级,0 到 60 米范围误差控制在 10% 以内,就能满足大部分辅助决策场景。检测框坐标用像素值还是归一化值,要和你下游模块对齐,别在集成时才发现单位不一致。
最后说一个老兵的习惯:数据集的 zip 不只是训练素材。我会给每个版本的 zip 建一个校验和清单,连同标注修改变动记录存进 git。将来模型在某个地区大规模误检时,能回头定位是数据变了还是代码变了。这个习惯帮我省掉过不止一次“玄学翻车”的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取