简介这份交通锥、信号灯与交通标志检测数据集面向自动驾驶感知、智慧交通与计算机视觉算法研究者聚焦道路施工锥、红绿灯及各类交通标志三类核心目标的识别需求可直接用于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 的默认imgsz640这些目标缩放到 640 后可能只剩几个像素模型根本学不到有效特征。参数说明ROOT指向解压后的数据集根目录IMG_DIR和LBL_DIR按实际结构改。如果标注是 COCO json 格式需要把scan_yolo_labels换成解析annotations.json的逻辑核心统计逻辑不变。2.2 三类目标的尺度差异决定了你后面所有参数跑完体检脚本后你会看到类似这样的分布特征具体数字因数据集版本而异但趋势一致目标类型典型归一化面积范围每图平均出现次数主要难点交通锥0.0005 ~ 0.013 ~ 15密集堆叠、遮挡严重信号灯0.0002 ~ 0.0051 ~ 4极小目标、颜色状态需区分交通标志0.001 ~ 0.081 ~ 6尺寸跨度大、类别多这张表告诉你一个关键结论信号灯的归一化面积中位数大概率在 0.001 以下交通标志的最大框可能是最小框的 50 倍以上。这意味着你不能用一套 anchor 或一套特征层去覆盖所有目标。YOLOv8 的 P3/P4/P5 三个检测头中P3 负责小目标但如果你把imgsz设成 640P3 的感受野对应原图约 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_okTrue) 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_ratio0.2, seed42): 按文件名前缀模拟场景分组切分避免同场景泄漏 假设文件名格式为: 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(parentsTrue, exist_okTrue) (out_dir / split / labels).mkdir(parentsTrue, exist_okTrue) 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.yamlpath: ./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.0YOLOv8 通过 ultralytics 包安装。pip install ultralytics8.1.0 # 确认 GPU 可用 python -c import torch; print(torch.cuda.is_available())启动训练的最小命令yolo detect train \ data./traffic_dataset_split/data.yaml \ modelyolov8s.pt \ imgsz1024 \ epochs100 \ batch8 \ patience20 \ lr00.01 \ lrf0.01 \ mosaic0.5 \ close_mosaic15 \ project./runs/traffic \ nameexp01逐参数说明modelyolov8s.pt选 s 而不是 n是因为三类目标里信号灯太小n 模型的容量不够m 又容易过拟合小数据集s 是折中。imgsz1024是为了保留小目标像素代价是显存占用增加batch 要相应降到 8 或 4。patience20表示 20 轮验证指标不提升就早停避免浪费时间。mosaic0.5把 mosaic 增强概率降到 0.5close_mosaic15表示最后 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, imgsz1024, batch8) # 打印每个类别的 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, imgsz1024, conf0.25, iou0.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)conf0.25是置信度阈值iou0.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 没有和别的类别混淆。如果显存不够用batch4甚至batch2或者用切片推理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_epochs5让学习率在前 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_typeyolov8, model_path./runs/traffic/exp01/weights/best.pt, confidence_threshold0.25, devicecuda:0 ) # 切片推理切片大小 640重叠 128 result get_sliced_prediction( ./test_road.jpg, detection_model, slice_height640, slice_width640, overlap_height_ratio0.2, overlap_width_ratio0.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 左右是比较稳的起点。希望帮到你。本文还有配套的精品资源点击获取
