1. 为什么交通信号灯检测值得单独做一个数据集
交通信号灯检测这个方向,看起来只是目标检测里一个很窄的细分,但真正做过自动驾驶感知或者智慧交通项目的人都知道,它跟通用目标检测完全不是一个难度层级。行人、车辆、道路这些目标通常占据画面较大面积,特征明显,模型很容易学到稳定的纹理和轮廓。而交通信号灯在车载摄像头画面里往往只有几十个像素,远距离下甚至不到二十个像素,同时还要区分红、黄、绿、方向箭头、倒计时数字等多种状态,背景又经常是天空、树荫、广告牌、霓虹灯这些强干扰源。我见过不少团队拿COCO预训练模型直接跑交通信号灯,结果mAP连0.3都上不去,原因就在这里。
这次整理的这份数据集,规模是3200张,采用YOLO格式标注,面向的就是自动驾驶场景下的交通信号灯检测任务。3200张这个量级不算特别大,但对于单一类别的检测任务来说,如果场景覆盖足够合理,已经能够训练出一个可用的基线模型。它解决的核心问题是:让做自动驾驶感知、ADAS前视系统、路口违章抓拍、智能交通分析的人,有一个开箱即用的起点,不用从零开始标注几千张图。适合的人群包括目标检测初学者、自动驾驶算法工程师、做课程设计或竞赛的学生,以及需要快速验证信号灯检测方案的产品团队。
我拿到这个数据集之后,第一件事不是直接丢进YOLO训练,而是先做了一轮完整的数据体检。这一步很多人会跳过,但恰恰是决定最终模型上限的关键。下面我把整个思路、实操细节、踩过的坑,按我实际做项目的顺序拆开讲。
2. 数据集整体设计与标注格式拆解
2.1 3200张的规模到底够不够用
先回答一个最常被问到的问题:3200张训练交通信号灯够吗。我的实测结论是,如果只做红黄绿三类的粗分类检测,3200张在YOLOv8n或YOLOv8s这个量级的模型上,配合合理的增强策略,是能跑出可用效果的。但如果要区分方向箭头、倒计时、圆形灯与方形灯,那这个量级就偏紧了,需要靠数据增强和迁移学习来补。
这里有个经验公式可以参考:单一类别目标检测,每类至少需要1500到2000个有效实例才能让模型稳定收敛。3200张图里,如果平均每张有2到3个信号灯目标,总实例数大概在6000到9000之间,分摊到红黄绿三类,每类2000到3000个实例,刚好踩在及格线上。黄灯因为出现频率天然低,往往只有红灯的十分之一,这是这类数据集普遍存在的长尾问题,后面我会讲怎么处理。
2.2 YOLO格式标注的目录结构
YOLO格式的标注跟COCO、VOC最大的区别在于,它用归一化的中心点坐标加宽高,每个标注文件是一个txt,跟图片一一对应。标准结构是这样的:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml每个txt文件里每行代表一个目标,格式为:
class_id center_x center_y width height所有数值都是相对于图片宽高的归一化值,范围0到1。这里有个新手极易踩的坑:中心点坐标是相对于整张图归一化的,不是相对于某个网格。我见过有人把坐标写成像素值直接除以640,结果图片不是640×640时就全乱了。正确做法是除以图片自身的实际宽和高。
data.yaml的写法:
path: ./dataset train: images/train val: images/val test: images/test nc: 3 names: ['red', 'yellow', 'green']如果你的数据集把箭头灯也单独标了类,那nc和names要相应调整。我建议初期先做三类,把问题简化,等基线跑通再考虑细分。
2.3 类别定义与合并策略
交通信号灯的状态定义直接决定标注难度和模型上限。常见的有几种粒度:
| 粒度级别 | 类别定义 | 优点 | 缺点 |
|---|---|---|---|
| 粗粒度 | 红、黄、绿 | 标注快,样本充足 | 无法区分箭头和圆形 |
| 中粒度 | 红圆、黄圆、绿圆、红箭头、绿箭头 | 信息更全 | 箭头样本少,长尾严重 |
| 细粒度 | 加上倒计时、闪烁状态 | 信息最全 | 标注成本极高,样本极不平衡 |
我个人的建议是,如果你的应用只是判断当前能不能通行,粗粒度完全够用。如果是做车道级导航或者复杂路口决策,才需要中粒度以上。这份3200张的数据集,我建议先按粗粒度使用,把箭头灯统一归到对应颜色类里,这样能最大化利用样本。
3. 数据体检:训练前必须做的四件事
3.1 类别分布统计与长尾处理
拿到数据集第一件事,写个脚本统计每个类别的实例数和图片数。我用的是下面这段Python:
import os from collections import Counter label_dir = 'dataset/labels/train' counter = Counter() img_with_class = Counter() for txt in os.listdir(label_dir): if not txt.endswith('.txt'): continue classes_in_img = set() with open(os.path.join(label_dir, txt)) as f: for line in f: cid = int(line.split()[0]) counter[cid] += 1 classes_in_img.add(cid) for c in classes_in_img: img_with_class[c] += 1 print('实例数:', counter) print('图片数:', img_with_class)跑完之后你大概率会发现黄灯实例数远低于红绿。我处理过的类似数据集里,红灯和绿灯各占40%左右,黄灯只有10%到15%。这种不平衡如果不处理,模型会倾向于把黄灯预测成红或绿。处理手段有三个:一是过采样含黄灯的图片,二是用YOLO的类别权重参数,三是在数据增强时对黄灯目标做额外的复制粘贴增强。我一般先用过采样,简单有效。
3.2 标注质量抽查
3200张里一定存在漏标、错标、框不准的情况。我的做法是随机抽100张,用脚本把标注框画到图上,肉眼过一遍。画框脚本:
import cv2 import os img_dir = 'dataset/images/train' label_dir = 'dataset/labels/train' for txt in os.listdir(label_dir)[:100]: img_path = os.path.join(img_dir, txt.replace('.txt', '.jpg')) img = cv2.imread(img_path) h, w = img.shape[:2] with open(os.path.join(label_dir, txt)) as f: for line in f: cid, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw/2) * w) y1 = int((cy - bh/2) * h) x2 = int((cx + bw/2) * w) y2 = int((cy + bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(f'check_{txt}.jpg', img)重点看三类问题:远距离小目标有没有漏标、被遮挡的信号灯有没有标、多个信号灯重叠时框有没有串。漏标比错标危害更大,因为模型会把有目标的地方学成背景。
3.3 图片尺寸与分辨率分布
交通信号灯检测对分辨率极其敏感。我统计过,同一批数据里,信号灯目标框的像素面积中位数往往只有30×30左右,远距离的甚至10×10。如果你的训练分辨率是640,那这些小目标经过下采样后可能只剩几个像素,特征几乎消失。
我的建议是:训练分辨率至少640,如果显存允许,用960或1280效果会明显更好。但分辨率提高会带来显存和速度的代价,需要权衡。实测下来,YOLOv8s在640分辨率下,小目标召回率大概比1280低15到20个百分点。如果你的应用场景以中远距离为主,别省这点显存。
3.4 重复图片与近似图片去重
3200张里如果有大量连续帧截取的近似图片,会导致训练集和验证集信息泄漏,验证指标虚高。我用感知哈希做去重:
import imagehash from PIL import Image import os hashes = {} img_dir = 'dataset/images' for root, _, files in os.walk(img_dir): for f in files: if f.endswith('.jpg'): path = os.path.join(root, f) h = imagehash.phash(Image.open(path)) hashes[path] = h # 找出汉明距离小于5的近似图 paths = list(hashes.keys()) for i in range(len(paths)): for j in range(i+1, len(paths)): if hashes[paths[i]] - hashes[paths[j]] < 5: print('近似:', paths[i], paths[j])发现近似图后,要确保它们不会同时出现在训练集和验证集里,否则验证结果不可信。
4. 训练配置与关键参数实操
4.1 模型选型:从YOLOv8n到YOLOv8m怎么选
交通信号灯检测的模型选型,核心矛盾是速度和小目标精度。我列一个实测对比:
| 模型 | 输入分辨率 | mAP@0.5 | 单帧耗时(T4) | 显存占用 |
|---|---|---|---|---|
| YOLOv8n | 640 | 0.72 | 3ms | 1.2G |
| YOLOv8s | 640 | 0.79 | 5ms | 2.1G |
| YOLOv8m | 640 | 0.83 | 11ms | 4.3G |
| YOLOv8s | 1280 | 0.86 | 18ms | 6.8G |
| YOLOv8m | 1280 | 0.89 | 35ms | 11.2G |
如果你的部署平台是T4或者类似算力的边缘设备,YOLOv8s加640分辨率是性价比最高的选择,能跑到100帧以上,足够支持多路视频。如果追求精度且算力充足,YOLOv8m加1280是更好的方案。注意这里的mAP是我在类似数据集上的经验值,实际会因数据质量浮动。
4.2 训练命令与超参数设置
我用的是Ultralytics的YOLOv8框架,训练命令:
yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=32 \ lr0=0.01 \ lrf=0.01 \ momentum=0.937 \ weight_decay=0.0005 \ warmup_epochs=3 \ cos_lr=True \ close_mosaic=10 \ patience=30 \ device=0几个关键参数的解释:
- lr0=0.01:初始学习率。YOLOv8默认是0.01,如果从预训练模型微调,可以降到0.001到0.005,避免破坏预训练特征。
- close_mosaic=10:最后10个epoch关闭mosaic增强。mosaic会把四张图拼成一张,对小目标检测有帮助,但训练末期关闭能让模型适应真实分布,通常能涨1到2个点。
- cos_lr=True:余弦退火学习率,比阶梯下降更平滑,收敛更稳。
- patience=30:30个epoch验证指标不提升就早停,防止过拟合。
4.3 数据增强策略针对小目标的调整
YOLOv8默认的增强参数对交通信号灯不一定最优。我调整过的配置:
hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 degrees: 0.0 translate: 0.1 scale: 0.5 shear: 0.0 perspective: 0.0 flipud: 0.0 fliplr: 0.5 mosaic: 1.0 mixup: 0.1 copy_paste: 0.1重点说几个:
- degrees=0.0:不做旋转。交通信号灯在真实场景中基本是正立的,旋转会引入不真实的样本,反而有害。
- scale=0.5:缩放范围大一些,模拟远近距离变化,对小目标检测有帮助。
- flipud=0.0:不做上下翻转。信号灯翻转后红绿位置颠倒,语义错误。
- copy_paste=0.1:复制粘贴增强,把信号灯目标复制到其他位置,能有效增加小目标样本。这个对黄灯长尾特别有用。
4.4 损失函数与正负样本分配
YOLOv8用的是TaskAlignedAssigner做正负样本分配,分类损失是BCE,回归损失是CIoU加DFL。交通信号灯检测里,正样本极少,负样本极多,这是典型的前景背景不平衡。YOLOv8的分配器已经处理得不错,但如果你的数据里信号灯特别小,可以适当调大align参数里的topk,让更多高质量anchor参与正样本。
我遇到过一种情况:模型训练loss正常下降,但验证mAP卡在0.5上不去。排查后发现是正样本太少,模型学不到有效特征。解决办法是检查标注框有没有过小被过滤掉。YOLOv8默认会过滤掉宽高小于2像素的目标,如果你的数据里有大量极小目标,需要调整这个阈值。
5. 训练过程监控与常见问题排查
5.1 看什么指标判断训练是否健康
训练启动后,我重点盯四个指标:
- box_loss和cls_loss:应该平稳下降,如果cls_loss震荡剧烈,通常是学习率太大或batch太小。
- mAP@0.5和mAP@0.5:0.95:前者看整体检测能力,后者看定位精度。如果前者高后者低,说明框的位置不够准。
- precision和recall:precision高recall低,说明模型保守,漏检多;反过来说明误检多。
- 混淆矩阵:重点看红黄绿之间有没有互相混淆。黄灯被大量预测成红灯,就是长尾问题的典型表现。
5.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| mAP长期低于0.5 | 标注质量差或类别不平衡 | 可视化标注框,统计类别分布 | 清洗标注,过采样少数类 |
| 小目标大量漏检 | 训练分辨率太低 | 统计目标框像素面积分布 | 提高imgsz到960或1280 |
| 黄灯识别率极低 | 黄灯样本太少 | 看混淆矩阵黄灯行 | 过采样+copy_paste增强 |
| 验证loss上升训练loss下降 | 过拟合 | 对比训练验证曲线 | 增加增强,加dropout,早停 |
| 训练中途loss变nan | 学习率过大或数据有脏标注 | 检查lr和标注文件 | 降lr,清洗异常标注 |
| 推理速度远低于预期 | 模型太大或没做量化 | 测单帧耗时 | 换小模型,导出TensorRT |
5.3 黄灯长尾问题的专项处理
黄灯是这类数据集的通病。我的处理流程是三步:
第一步,统计含黄灯的图片,单独复制一份到过采样目录,在训练时通过sampler提高采样概率。第二步,用copy_paste增强,把黄灯目标抠出来粘贴到其他图片的合理位置,注意粘贴位置要在天空或路口区域,不能贴到车上。第三步,如果黄灯实在少,考虑用focal loss加大难样本权重,但YOLOv8默认没有暴露这个参数,需要改源码。
实测下来,这三步做完,黄灯召回率能从0.4左右提到0.7以上。
5.4 小目标检测的专项优化
除了提高分辨率,还有几个技巧:
- 加P2检测层:YOLOv8默认从P3开始,P2层是160×160的特征图,对极小目标更友好。改模型配置文件加一个P2头,mAP能涨2到3个点,代价是速度慢20%左右。
- 用SAHI切片推理:训练时正常,推理时把大图切成小块分别检测再合并,对小目标效果显著。适合离线分析场景,实时性要求高的慎用。
- 调整anchor:虽然YOLOv8是anchor-free,但DFL的回归范围可以调,让模型更关注小尺度。
6. 模型评估与部署落地要点
6.1 评估指标的正确解读
交通信号灯检测不能只看mAP。实际业务里,漏检一个红灯的代价远大于误检。所以评估时要分开看:
- 红灯召回率:必须尽可能高,建议目标0.95以上。
- 误检率:误检会导致误刹车,也要控制。
- 黄灯召回率:虽然样本少,但黄灯决策时间短,漏检影响大。
- 推理延迟:实时系统里,延迟超过50ms就可能影响决策。
我一般会画PR曲线,看不同置信度阈值下的表现,然后根据业务需求选阈值。比如红灯可以把阈值调低到0.3,宁可误检不可漏检。
6.2 导出与部署
训练完导出ONNX:
yolo export model=best.pt format=onnx opset=12 simplify=True导出TensorRT:
yolo export model=best.pt format=engine half=True device=0部署时的几个注意点:
- 预处理要对齐:训练时的letterbox填充方式,推理时必须一致,否则框会偏。
- NMS阈值:交通信号灯通常不会密集重叠,NMS的iou阈值可以设0.5到0.6,比默认的0.7更严格,减少重复框。
- 类别阈值分开设:红灯低阈值,绿灯可以稍高,黄灯中等。
6.3 多路视频支持的算力估算
有人问T4在1080p25帧下用TensorRT跑YOLO 640分辨率能支持多少路。我实测的数据是:YOLOv8s TensorRT FP16,单帧推理约5ms,加上预处理和后处理约8ms,理论上一路25帧需要200ms,T4可以并行跑4到5路。但实际还要留余量给解码和其他任务,稳妥起见按3到4路规划。如果换成YOLOv8n,能到6到8路。
7. 我踩过的坑和几条实在建议
第一个坑是标注格式转换。很多人从VOC或COCO转YOLO时,坐标归一化除错了对象,导致训练时框全偏。记住是除以图片自身的宽高,不是除以网络输入尺寸。
第二个坑是验证集泄漏。如果3200张里有连续帧,随机划分会让近似图同时进训练和验证,mAP虚高十几个点。一定要按场景或视频来源划分,而不是随机划分。
第三个坑是过度依赖预训练模型。COCO预训练模型里交通信号灯类别很少,直接微调效果有限。建议先用这份数据训练一个基线,再用它去初始化其他相关任务。
第四个坑是忽略推理时的色彩空间。训练用RGB,推理如果喂BGR,红灯绿灯会识别反。OpenCV读图默认BGR,记得转换。
最后分享一个实用技巧:训练完成后,用模型在验证集上跑一遍,把置信度0.1到0.3之间的预测框可视化出来,这些是模型的“犹豫区”。分析这些框能发现标注问题和模型弱点,比单纯看mAP有用得多。我靠这个方法揪出过一批漏标的远距离信号灯,补标之后mAP直接涨了4个点。