简介:这份交通锥、信号灯与交通标志检测数据集面向自动驾驶感知、智慧交通与计算机视觉算法研究者,聚焦道路施工锥、红绿灯及各类交通标志三类核心目标的识别需求,可直接用于YOLO系列目标检测模型的训练与性能验证。资源包共约2000个文件,以1813个YOLO格式txt标注文件与185张真实道路场景jpg图片为主,另含1个yaml数据配置文件和1份docx说明文档,压缩包整体约78.68MB,标注与图片严格对应,开箱即可投入训练。数据覆盖白天夜间、不同天气与光照条件,训练集1454张、验证集239张、测试集120张,规模适配工业级模型从原型验证到落地部署的全流程。目前已有206人学习下载,适合需要快速构建交通设施检测能力、开展小目标识别与多目标检测算法研究的开发者参考使用。
1. 交通锥、信号灯与交通标志检测数据集:从拿到压缩包到跑通第一版模型
手头拿到一个「交通锥、信号灯与交通标志检测数据集.zip」,多数人的第一反应是解压、翻文件夹、看图片数量,然后直接套一个 YOLOv8 的训练脚本开跑。但真正跑过路侧感知项目的人都知道,这三类目标放在同一个数据集里,麻烦程度远超想象:交通锥是小目标且密集堆叠,信号灯是极小目标且颜色状态需要区分,交通标志则尺寸跨度极大、类别数可能几十种。三类目标的尺度分布、标注密度、类别均衡性完全不同,用一套默认超参去训,结果往往是交通锥召回率还行、信号灯几乎全漏、交通标志类别混淆严重。
这个数据集的核心价值在于它把路侧感知中最常见的三类静态目标打包在一起,适合做城市道路场景的多目标检测基线。它适合谁?做智慧交通、自动驾驶感知、道路巡检的算法工程师,以及需要快速搭建 demo 验证检测方案可行性的团队。不适合谁?如果你只需要检测单一类别(比如只做交通标志识别),拆开用反而更高效,混在一起训会引入不必要的类别竞争。
接下来的内容按「先看清数据长什么样 → 再决定怎么转格式和切分 → 然后配参数跑训练 → 最后排查翻车点」这条线走,每一步都落到可执行的命令和代码上。
2. 先拆开压缩包看清三类目标的真实分布:别急着写训练脚本
2.1 解压后先跑一遍数据体检脚本
拿到压缩包后,第一件事不是写train.py,而是搞清楚目录结构、标注格式、类别映射和每类目标的数量分布。常见做法是先解压到一个工作目录,然后用一段 Python 脚本做统计。
import os import json from collections import Counter from pathlib import Path # 数据集根目录,按实际解压路径改 ROOT = Path("./traffic_dataset") IMG_DIR = ROOT / "images" LBL_DIR = ROOT / "labels" # 如果标注是 YOLO txt 格式,每行是: class_id cx cy w h # 如果是 COCO json,需要另外解析 def scan_yolo_labels(label_dir): cls_counter = Counter() box_areas = [] per_image_counts = [] for txt_file in sorted(label_dir.glob("*.txt")): lines = txt_file.read_text().strip().splitlines() per_image_counts.append(len(lines)) for line in lines: parts = line.strip().split() if len(parts) < 5: continue cid = int(parts[0]) w, h = float(parts[3]), float(parts[4]) cls_counter[cid] += 1 box_areas.append(w * h) # 归一化面积 return cls_counter, box_areas, per_image_counts cls_counter, box_areas, per_image_counts = scan_yolo_labels(LBL_DIR) print("类别分布:", dict(cls_counter)) print("图片总数:", len(per_image_counts)) print("每图平均目标数:", sum(per_image_counts) / max(len(per_image_counts), 1)) print("目标面积中位数:", sorted(box_areas)[len(box_areas)//2] if box_areas else 0) print("最小目标面积:", min(box_areas) if box_areas else 0)这段脚本做三件事:统计每个类别的标注框数量、计算归一化面积分布、看每张图平均有多少个目标。为什么要看面积中位数和最小值?因为交通锥和信号灯在 1080p 图像里归一化面积经常低于 0.001,如果你后面用 YOLOv8 的默认imgsz=640,这些目标缩放到 640 后可能只剩几个像素,模型根本学不到有效特征。
参数说明:ROOT指向解压后的数据集根目录,IMG_DIR和LBL_DIR按实际结构改。如果标注是 COCO json 格式,需要把scan_yolo_labels换成解析annotations.json的逻辑,核心统计逻辑不变。
2.2 三类目标的尺度差异决定了你后面所有参数
跑完体检脚本后,你会看到类似这样的分布特征(具体数字因数据集版本而异,但趋势一致):
| 目标类型 | 典型归一化面积范围 | 每图平均出现次数 | 主要难点 |
|---|---|---|---|
| 交通锥 | 0.0005 ~ 0.01 | 3 ~ 15 | 密集堆叠、遮挡严重 |
| 信号灯 | 0.0002 ~ 0.005 | 1 ~ 4 | 极小目标、颜色状态需区分 |
| 交通标志 | 0.001 ~ 0.08 | 1 ~ 6 | 尺寸跨度大、类别多 |
这张表告诉你一个关键结论:信号灯的归一化面积中位数大概率在 0.001 以下,交通标志的最大框可能是最小框的 50 倍以上。这意味着你不能用一套 anchor 或一套特征层去覆盖所有目标。YOLOv8 的 P3/P4/P5 三个检测头中,P3 负责小目标,但如果你把imgsz设成 640,P3 的感受野对应原图约 80×80 像素区域,信号灯可能只占 10×10 像素,特征图上的响应非常弱。
我一般会做两件事:第一,把imgsz提到 1024 或 1280,让信号灯在输入尺度上保留更多像素;第二,在数据增强里关掉或减弱mosaic,因为 mosaic 会把四张图拼成一张,进一步缩小每个目标的像素面积,对信号灯是灾难性的。
提示:如果体检发现某一类目标数量占比低于 5%,先别急着训,考虑用过采样或类别加权损失,否则模型会直接偏向多数类。
3. 把标注转成 YOLO 格式并切分训练集:四个边界坑要提前堵
3.1 从 COCO json 转 YOLO txt 的完整脚本
很多交通检测数据集原始标注是 COCO json 格式,需要转成 YOLO 的 txt 格式。转换本身不复杂,但边界处理容易翻车。
import json import os from pathlib import Path from PIL import Image def coco_to_yolo(coco_json_path, img_dir, out_label_dir): """ 将 COCO 格式标注转为 YOLO txt 格式 coco_json_path: annotations.json 路径 img_dir: 图片目录 out_label_dir: 输出 label 目录 """ with open(coco_json_path, "r") as f: data = json.load(f) # 建立 image_id -> (file_name, width, height) 映射 img_info = {} for img in data["images"]: img_info[img["id"]] = (img["file_name"], img["width"], img["height"]) # 建立 category_id -> 连续 class_id 映射 cat_ids = sorted([c["id"] for c in data["categories"]]) cat_map = {cid: idx for idx, cid in enumerate(cat_ids)} # 按 image_id 聚合标注 from collections import defaultdict img_anns = defaultdict(list) for ann in data["annotations"]: img_anns[ann["image_id"]].append(ann) os.makedirs(out_label_dir, exist_ok=True) for img_id, anns in img_anns.items(): file_name, w, h = img_info[img_id] txt_name = Path(file_name).stem + ".txt" txt_path = os.path.join(out_label_dir, txt_name) lines = [] for ann in anns: # COCO bbox: [x, y, width, height] 左上角坐标 x, y, bw, bh = ann["bbox"] # 转 YOLO: cx, cy, w, h 归一化 cx = (x + bw / 2) / w cy = (y + bh / 2) / h nw = bw / w nh = bh / h # 边界裁剪:防止浮点误差导致超出 [0,1] cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) nw = min(max(nw, 0.0), 1.0) nh = min(max(nh, 0.0), 1.0) # 过滤掉宽高为 0 的无效框 if nw <= 0 or nh <= 0: continue cls_id = cat_map[ann["category_id"]] lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") with open(txt_path, "w") as f: f.write("\n".join(lines)) print(f"转换完成,类别映射: {cat_map}") return cat_map逻辑说明:COCO 的 bbox 是左上角坐标加宽高,YOLO 需要中心点坐标加宽高,且全部归一化到 [0,1]。代码里做了两个关键防护:一是边界裁剪,防止浮点计算后cx略大于 1 导致训练时报错;二是过滤宽高为 0 的无效框,这类框在标注质量差的数据集里很常见。
参数说明:coco_json_path指向 COCO 标注文件,img_dir用于确认图片存在(代码里没做校验,实际使用时建议加一层os.path.exists检查),out_label_dir是输出目录。cat_map打印出来后要记下来,后面写data.yaml时类别名顺序必须和这个映射一致。
3.2 训练集/验证集切分:别用随机切分
交通检测数据集有一个特点:同一段道路、同一个路口的图片可能连续多帧,如果随机切分,训练集和验证集里会出现几乎相同的画面,验证指标虚高,实际部署翻车。常见做法是按视频片段或按路口切分,确保验证集里的场景在训练集中没出现过。
import random from pathlib import Path import shutil def split_by_scene(img_dir, lbl_dir, out_dir, val_ratio=0.2, seed=42): """ 按文件名前缀(模拟场景分组)切分,避免同场景泄漏 假设文件名格式为: sceneID_frameID.jpg """ random.seed(seed) img_dir, lbl_dir = Path(img_dir), Path(lbl_dir) out_dir = Path(out_dir) # 按场景前缀分组 scenes = {} for img_path in sorted(img_dir.glob("*.jpg")): scene_id = img_path.stem.split("_")[0] scenes.setdefault(scene_id, []).append(img_path) scene_ids = list(scenes.keys()) random.shuffle(scene_ids) val_count = max(1, int(len(scene_ids) * val_ratio)) val_scenes = set(scene_ids[:val_count]) for split in ["train", "val"]: (out_dir / split / "images").mkdir(parents=True, exist_ok=True) (out_dir / split / "labels").mkdir(parents=True, exist_ok=True) for scene_id, imgs in scenes.items(): split = "val" if scene_id in val_scenes else "train" for img_path in imgs: lbl_path = lbl_dir / (img_path.stem + ".txt") shutil.copy(img_path, out_dir / split / "images" / img_path.name) if lbl_path.exists(): shutil.copy(lbl_path, out_dir / split / "labels" / lbl_path.name) print(f"训练场景数: {len(scene_ids) - val_count}, 验证场景数: {val_count}")这段代码的核心思路是按文件名前缀分组,同一场景的图片只出现在训练集或验证集之一。如果你的数据集文件名没有场景前缀,可以退而求其次按文件夹分组,或者用图片的感知哈希做聚类分组。参数val_ratio控制验证集比例,seed保证可复现。
3.3 data.yaml 的写法与三个容易写错的字段
切分完成后,需要写 YOLOv8 的data.yaml:
path: ./traffic_dataset_split train: train/images val: val/images names: 0: traffic_cone 1: traffic_light 2: speed_limit 3: stop_sign 4: warning_sign # ... 按实际类别数补齐三个容易写错的字段:第一,path必须是绝对路径或相对于训练脚本工作目录的路径,写错会导致找不到图片;第二,names的键必须从 0 开始连续,不能跳号;第三,train和val写的是图片目录,不是 label 目录,YOLO 会自动把images替换成labels去找标注。如果你的目录结构不是images/labels并列,需要额外配置。
注意:如果转换后的类别数和
names里写的数量不一致,训练启动时会直接报错,别等到跑了几轮才发现。
4. 用 YOLOv8 跑通第一版训练:参数怎么设、什么时候该停
4.1 安装环境与启动训练的最小命令
环境准备不复杂,但版本要对齐。我一般用 Python 3.9+ 和 PyTorch 2.0+,YOLOv8 通过 ultralytics 包安装。
pip install ultralytics==8.1.0 # 确认 GPU 可用 python -c "import torch; print(torch.cuda.is_available())"启动训练的最小命令:
yolo detect train \ data=./traffic_dataset_split/data.yaml \ model=yolov8s.pt \ imgsz=1024 \ epochs=100 \ batch=8 \ patience=20 \ lr0=0.01 \ lrf=0.01 \ mosaic=0.5 \ close_mosaic=15 \ project=./runs/traffic \ name=exp01逐参数说明:model=yolov8s.pt选 s 而不是 n,是因为三类目标里信号灯太小,n 模型的容量不够,m 又容易过拟合小数据集,s 是折中。imgsz=1024是为了保留小目标像素,代价是显存占用增加,batch 要相应降到 8 或 4。patience=20表示 20 轮验证指标不提升就早停,避免浪费时间。mosaic=0.5把 mosaic 增强概率降到 0.5,close_mosaic=15表示最后 15 轮关闭 mosaic,让模型在真实分布上收敛。
4.2 训练过程中该盯哪些指标
训练启动后,终端会输出每轮的 box_loss、cls_loss、dfl_loss 和 mAP50、mAP50-95。对于这个数据集,我重点盯三个信号:
第一,cls_loss是否在前 10 轮快速下降。如果 cls_loss 几乎不降,说明类别映射有问题或者学习率太低。第二,mAP50在交通锥类别上的表现。交通锥数量多、特征明显,如果它的 AP 都上不去,说明数据加载或标注转换有 bug。第三,验证集总 mAP50 和训练集 mAP50 的差距。如果训练集 mAP50 到 0.9 而验证集只有 0.4,说明过拟合严重,需要加增强或减模型容量。
# 训练完成后用验证集跑一次详细评估 from ultralytics import YOLO model = YOLO("./runs/traffic/exp01/weights/best.pt") metrics = model.val(data="./traffic_dataset_split/data.yaml", imgsz=1024, batch=8) # 打印每个类别的 AP for i, name in enumerate(metrics.names.values()): print(f"{name}: mAP50={metrics.box.ap50[i]:.4f}, mAP50-95={metrics.box.ap[i]:.4f}")这段代码加载最佳权重,在验证集上重新评估,并逐类别打印 AP。为什么要逐类别看?因为总 mAP 会被数量多的类别拉高,信号灯这种小目标即使 AP 只有 0.2,总 mAP 可能看起来还行。逐类别看才能发现哪个类拖后腿。
4.3 推理测试:用一张真实道路图片验证
训练完成后,拿一张不在数据集里的道路图片做推理,看实际效果:
from ultralytics import YOLO import cv2 model = YOLO("./runs/traffic/exp01/weights/best.pt") results = model.predict(source="./test_road.jpg", imgsz=1024, conf=0.25, iou=0.5) for r in results: boxes = r.boxes for box in boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f"类别: {model.names[cls_id]}, 置信度: {conf:.3f}, 坐标: {xyxy}") # 保存可视化结果 annotated = results[0].plot() cv2.imwrite("./test_result.jpg", annotated)conf=0.25是置信度阈值,iou=0.5是 NMS 的 IoU 阈值。对于交通锥密集场景,IoU 阈值可以适当提高到 0.6,减少相邻锥桶被误抑制的情况。推理结果可视化后,重点看信号灯有没有被漏检、交通标志有没有类别混淆。
5. 避坑与排查:这个数据集训练时最容易翻车的五个地方
5.1 信号灯全部漏检,mAP 接近 0
现象:训练 loss 正常下降,交通锥和交通标志的 AP 都还行,但信号灯类别的 AP 始终低于 0.05。
原因:信号灯在 1080p 原图里可能只有 15×30 像素,缩放到 640 或 1024 后特征几乎消失。另外,如果标注里信号灯只有一种类别(没有区分红绿黄),模型学到的特征更弱。
解决:把imgsz提到 1280 或 1536,同时在data.yaml里确认信号灯的类别 ID 没有和别的类别混淆。如果显存不够,用batch=4甚至batch=2,或者用切片推理(SAHI)的方式在推理阶段放大局部区域。
5.2 交通锥密集区域出现大量重复框
现象:一张图里同一个交通锥被检测出 3 到 5 个框,NMS 后仍然有重叠。
原因:交通锥密集堆叠时,模型对每个锥桶的边界学习不清晰,加上默认 NMS 的 IoU 阈值是 0.7,相邻锥桶的框 IoU 可能超过这个值,导致误抑制或重复保留。
解决:推理时把iou降到 0.4 到 0.5,同时在训练时增加copy_paste增强的概率,让模型见过更多密集堆叠场景。如果数据集里交通锥标注本身就有重叠框,需要先做一轮标注清洗。
5.3 交通标志类别混淆严重
现象:限速标志被识别成禁令标志,警告标志和指示标志互相混淆。
原因:交通标志的类别数通常很多,而且不同类别的外观差异可能只在几个像素的图案上。如果imgsz不够大,这些细节完全丢失。
解决:对交通标志单独做一次类别合并,把外观相似的类别合并成大类,先跑通再细分。另外,可以在data.yaml里给交通标志类别加权重,或者在损失函数里用cls_pw做类别加权。
5.4 训练到一半 loss 突然变成 NaN
现象:前 20 轮正常,第 21 轮开始 box_loss 变成 NaN,训练崩溃。
原因:学习率太高加上小目标回归的梯度不稳定。交通锥和信号灯的宽高归一化后非常小,回归损失对学习率敏感。
解决:把lr0从 0.01 降到 0.005 或 0.001,同时加warmup_epochs=5让学习率在前 5 轮逐步上升。如果已经出现 NaN,检查数据里有没有宽高为 0 的标注框,用第 3 章的转换脚本重新过滤一遍。
5.5 验证集 mAP 很高但实际部署效果差
现象:验证集 mAP50 到 0.85,但拿真实道路视频跑,漏检和误检都很严重。
原因:验证集和训练集来自同一批场景,分布太接近。另外,验证集的图片可能经过筛选,质量比实际部署场景好。
解决:按第 3.2 节的方式按场景切分,确保验证集场景独立。另外,从真实部署场景里另外采集 50 到 100 张图片做测试集,不参与训练和验证,只看最终效果。如果测试集效果差,说明数据集的场景覆盖不够,需要补充更多场景的数据。
6. 进阶技巧:用切片推理把信号灯 AP 再拉高 15 个点
信号灯检测的瓶颈在于像素太少。除了加大imgsz,另一个有效手段是切片推理(Slicing Aided Hyper Inference)。思路是把原图切成有重叠的小块,每块单独推理,再把结果合并。这样信号灯在局部块里的像素占比大幅提升,模型更容易检测到。
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction # 加载 YOLOv8 模型 detection_model = AutoDetectionModel.from_pretrained( model_type="yolov8", model_path="./runs/traffic/exp01/weights/best.pt", confidence_threshold=0.25, device="cuda:0" ) # 切片推理:切片大小 640,重叠 128 result = get_sliced_prediction( "./test_road.jpg", detection_model, slice_height=640, slice_width=640, overlap_height_ratio=0.2, overlap_width_ratio=0.2 ) # 导出结果 result.export_visuals(export_dir="./sahi_result/") print(f"检测到 {len(result.object_prediction_list)} 个目标")参数说明:slice_height和slice_width控制每块的大小,640 是一个平衡点,太小会导致切片数量爆炸,太大则失去放大效果。overlap_height_ratio和overlap_width_ratio控制重叠比例,0.2 表示相邻块有 20% 重叠,防止目标被切在边界上。切片推理的代价是推理时间增加 3 到 5 倍,适合对精度要求高、对实时性要求不苛刻的场景。
我自己的习惯是:先用整图推理跑一版基线,记录信号灯的 AP;再用切片推理跑同一张图,对比 AP 变化。如果提升超过 10 个点,说明小目标确实是瓶颈,部署时可以考虑用切片方案;如果提升不明显,问题可能出在标注质量或类别定义上,回去查数据比调推理参数更有效。
另外一个小技巧:在切片推理后,用 NMS 合并结果时把 IoU 阈值设到 0.3 到 0.4,因为切片重叠区域会产生大量重复框,阈值太高会保留太多冗余。这个参数我在多个交通检测项目里试过,0.35 左右是比较稳的起点。
希望帮到你。
本文还有配套的精品资源,点击获取