开车的朋友应该都有这种经历:早晚高峰堵在路口,眼睛盯着远处的信号灯,一边要防着旁车加塞,一边还得分辨这个圆灯到底能不能右转、左转箭头是不是绿了。这种对注意力的高度占用,恰好是自动驾驶和辅助驾驶系统最想替人类分担的场景之一。但想要让车自己“看懂”红绿灯,第一步不是写多聪明的算法,而是先喂给它足够多、足够干净的带标注数据。
我最近整理并实测了一个交通信号灯识别数据集,核心任务是识别红绿灯、黄灯,以及单独的左转箭头灯,模型实测识别率达到82.88%。这个数据集的一大好处是支持三种主流标注格式:YOLO的txt格式、COCO的JSON格式、PASCAL VOC的XML格式,不管你是用Ultralytics YOLO系、MMDetection还是自己写训练脚本,拿过去都能直接开跑,不需要费劲做格式转换。这篇文章就把这个数据集从标注结构、训练细节到踩坑实录完整拆开讲清楚,适合正在做目标检测入门、或者需要在项目中快速加入交通灯识别能力的朋友参考。
1. 项目整体设计与思路拆解
真实路口的信号灯识别,远没有很多人想象的那么简单。它跟识别猫狗、识别水杯这类通用目标检测任务有非常明显的区别,如果没先想清楚就闷头开训,最后很容易得到一套“demo很美好、上车就翻车”的模型。下面我从数据采集的难点和标注体系的设计逻辑两部分,聊聊这个数据集为什么要这样做。
1.1 真实场景下的信号灯识别难点
第一个难点是目标太小。站在路口看信号灯,一块屏幕可能只有几十个像素宽,尤其在远视角场景下,整张1080p的图里,灯体区域可能只占不到百分之一。这对检测器的感受野和特征提取能力都是很大的考验,不是随便拿个通用目标检测模型就能搞定的。
第二个难点是环境干扰。真实路口的背景极其复杂,有电线杆、广告牌、树木枝叶、桥体结构,这些东西里总有一些颜色和形状跟信号灯高度相似的东西。最常见的是红色广告牌被误检为红灯,或者远处车辆的红色尾灯被当成刹车灯信息。数据集的采集范围如果不够广,模型就很容易在这些干扰物上翻车。
第三个难点是灯组结构差异。国内路口的信号灯有横排、有竖排,有圆灯、有箭头灯,有些路口左转灯和直行灯是分开的,有些则是组合在一个大灯组里。再加上倒计时数字牌的亮度干扰,这对检测器的鲁棒性要求非常高。这个数据集在设计之初就考虑到了这些问题,采集样本时特意覆盖了不同的路口视角、不同天气时段和不同灯组排列。
第四个难点跟标注强相关:状态标注容易模糊。红灯亮的时候,同一组灯里的黄灯可能微微带亮,或者黄灯刚灭、红灯刚亮起的过渡瞬间,人眼都很难判断,标注员更容易产生分歧。这种标注不一致会直接拉低训练效果,所以这个数据集在标注规范里做了非常明确的状态判定标准,我在第2部分会详细展开。
1.2 为什么这么设计标注体系和格式
这个数据集一共包含三类核心目标状态:red(红色灯)、yellow(黄色灯)、green(绿色灯),外加单独的left(左转箭头灯)。初看会觉得“左转灯”不算颜色分类,但实际交通场景里,左转箭头灯往往是独立的灯组单元,它的亮灭状态是单独的语义,跟直行圆灯的逻辑解耦。把它单独作为一类,可以让模型对“是否有左转放行条件”这件事有更直接的感知,后续接下游逻辑也更方便。
不过这里有个细节值得展开:如果你把left和red、green放在同一层级,检测器输出的类别除了颜色还混合了灯型,这在某些业务场景里反而会引发行驶策略判断的歧义。例如,一个左转箭头红色灯,应该归类为“left_red”还是只归为“left”?或者只归为“red”?不同项目对这个问题的处理方式不一样。我实际测试下来,这个数据集的做法——把颜色和灯型适度拆分,让模型先定位灯体区域,再在决策层去结合位置信息做判断——在小尺寸目标场景下反而更稳。原因很简单:类别拆分越细,每个类别的样本数量就越少,小目标检测的难度本来就高,类别太多容易把分类器学糊涂。
格式方面,这个数据集的贴心之处是同时导出YOLO txt、COCO JSON、PASCAL VOC XML三个版本。做过目标检测的朋友都知道,不同框架对标注格式的要求千差万别:YOLO系列需要归一化的中心点坐标和宽高,COCO需要分割掩码或多边形点集,VOC则用XML保存左上角和右下角绝对坐标。自己写脚本转格式不是不行,但坐标系的转换、归一化、空标注文件的处理,每个环节都可能出BUG,而且消耗的时间远超预期。这个数据集直接帮你做好了三份,省掉的不仅是转换时间,还有转换过程中的心智负担。
2. 数据集结构与标注格式实操详解
拿到数据集之后,第一步不是急着开训,而是先把目录结构和标注内容吃透。很多初学者会在这里吃暗亏——标注内容跟框架期望的格式不完全一致,结果训练时报错或者loss曲线异常。下面我把这套数据集的目录组织方式和三种标注格式的要点说清楚。
2.1 数据集目录结构与样本分布
典型的目录结构是这样的:
traffic_light_dataset/ ├── images/ │ ├── train/ │ │ ├── img_001.jpg │ │ ├── img_002.jpg │ │ └── ... │ └── val/ │ ├── img_101.jpg │ └── ... ├── labels/ │ ├── yolo/ │ │ ├── train/ │ │ │ ├── img_001.txt │ │ │ └── ... │ │ └── val/ │ └── voc/ │ ├── img_001.xml │ └── ... └── coco.json图片统一采用JPG格式,分辨率集中在1920x1080附近。实际统计下来,训练集大概占据了总体样本的85%,验证集15%。分布上有几个特点值得注意:一是晴天场景占比过半,阴天和傍晚场景占比相对少一些,雨雾天气样本更少;二是近距离样本(灯体占比大于5%)和远距离样本(占比小于1%)都有覆盖,远距离样本是保识别率下限的关键。
这里插一句,如果你拿到手的样本分布跟实际使用场景差距较大,最直接的做法是补充采集或做针对性增强。例如你的车主要跑夜间高速,那训练集里夜间样本太少的话,一定要重点补夜间数据,否则就算整体识别率到了82.88%,落在你的场景上也可能大打折扣。
提示:拿到任何数据集,第一件事永远是用脚本统计一下每个类别的样本数量,确认是否存在类别严重不均衡。尤其在交通灯场景里,黄色灯往往比红灯和绿灯出现频次少很多,如果直接用原始分布训练,黄灯的召回率可能会很难看。
我写了个简单脚本,读YOLO格式标注并统计类别数量:
import os from collections import Counter label_dir = "labels/yolo/train" counter = Counter() class_names = {0: "red", 1: "yellow", 2: "green", 3: "left"} for fname in os.listdir(label_dir): with open(os.path.join(label_dir, fname), "r", encoding="utf-8") as f: for line in f: cls = int(line.split()[0]) counter[class_names[cls]] += 1 print(counter)如果你的分布里某个类别的数量严重偏少,建议后续做针对性增强或者过采样,不要指望模型自己“理解”类别重要性。
2.2 三种标注格式的读取与校验方法
YOLO格式是最常用的。每个txt文件对应一张图片,每一行代表一个目标,五个字段依次是:类别ID、归一化中心点x坐标、归一化中心点y坐标、归一化宽度、归一化高度。注意这个归一化是相对于原始图片宽高的,不是相对于边长较小的一侧。
0 0.5214 0.3829 0.0152 0.0331 2 0.4733 0.5017 0.0128 0.0289COCO JSON格式则是把所有图片和标注整合在一个文件里,包含images、annotations、categories三个核心字段。在annotations里,bbox的坐标形式是[x, y, width, height],全部是绝对像素值,和YOLO有本质区别。用pycocotools可以直接加载并评估指标,这套流程在MMDetection里用得非常多。
PASCAL VOC XML格式以XML标签形式存储,每个目标对应一个object节点,里面的bndbox子节点保存xmin、ymin、xmax、ymax的绝对坐标。这种格式可读性最好,用文本编辑器直接能看懂:
<annotation> <filename>img_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>red</name> <bndbox> <xmin>987</xmin> <ymin>398</ymin> <xmax>1016</xmax> <ymax>433</ymax> </bndbox> </object> </annotation>注意:VOC的坐标是包含目标边缘的绝对坐标,YOLO的坐标是归一化的中心点加宽高,COCO则用左上角坐标加宽高。三者的互相转换极易出错,建议转换后一定要抽样可视化校验,不要盲目使用。例如把YOLO框转回原图坐标时,需要乘回原始图片宽高,而不是直接使用标注里的归一化数值。
我常用的校验手段是把标注框直接画回原图,肉眼确认位置是否贴合。因为转换脚本出错有时候是静默的——不报错,但框就是偏移那么几个像素,肉眼很容易看出来。
2.3 标注规范里的几个关键细节
这个数据集的标注规范里有几个细节,我觉得特别值得说一下。
第一,对于多灯组场景,每个灯体独立标注。也就是说一张图里可能有五六个信号灯目标,每个都要单独画框,而不是把整个灯组画成一个框。这种细粒度标注对后续的策略层判断更有价值——如果你的检测框是整个灯组,那根本分不清到底是哪个灯在亮。
第二,对于被遮挡的灯体,标注规范要求框住可见部分,而不是去还原被遮挡的完整轮廓。这一点很多人会反对,“模型不应该学到被遮挡的目标吗?”,但实际工作中,标注不可见部分是给训练数据引入了噪声,因为检测器学到的可能是不存在的完整框形状。对于严重遮挡的目标,保守的做法是直接不标注,避免训练信号混乱。
第三,如果黄灯和红灯同时亮起,也就是切换过渡状态,标注规范里做了优先级约定:优先标注实际语义状态,而不是颜色状态。但是这里有个两难:人眼在过渡瞬间都难以判断,标注员更容易出错。所以规范的最终约定是,如果肉眼能清晰判断,就按实际状态标;如果判断不了,就跳过不标。这种取舍是对的,宁可少一个训练样本,也不要给模型一个错误的标签。
3. 模型训练与识别率优化实操记录
数据和标注是基础,但真正让识别率达到82.88%的,是训练环节里一系列细节的把控。这一部分我直接讲实操,包括整体配置、训练启动、优化踩坑和结果评估。这些内容全部来自我自己的实测过程,不是理论推演。
3.1 训练环境配置与超参数选择
先说环境。我用的是一张消费级显卡完成训练,具体配置是:
- PyTorch 2.x + CUDA 11.8
- Ultralytics YOLOv8(也可用YOLOv5或YOLO11,步骤类似)
- 输入尺寸640x640
- Batch Size 16
- 初始学习率0.01
- 训练轮次100轮
在同样的数据上,我自己还对比过YOLOv5s、YOLOv8s和YOLO11s三种模型,最终选了YOLOv8s作为主力。原因不是它在所有指标上碾压其他模型,而是它在速度和精度之间更均衡。YOLOv5s的推理速度稍快但精度略低,YOLO11s的精度略高但mAP提升幅度跟训练成本不成正比。
训练启动命令很简单,确保数据集按YOLO格式目录摆放好,修改一下data.yaml里的路径和类别名:
train: datasets/traffic_light/images/train val: datasets/traffic_light/images/val nc: 4 names: ['red', 'yellow', 'green', 'left']然后启动训练:
yolo train model=yolov8s.pt data=traffic_light.yaml epochs=100 imgsz=640 batch=16这里有个细节要注意:模型的预训练权重是在COCO数据集上学出来的,COCO里没有交通灯类别,所以模型前几个epoch其实是在快速适应新的类别分布。前10个epoch的loss下降会很快,这很正常,不用因为前期指标波动就焦虑。
3.2 数据增强策略与小目标专项优化
交通灯识别最典型的问题是目标太小,所以数据增强策略不能照搬通用目标检测那套。我自己实测下来,下面这几项增强是最有价值的:
第一,Mosaic增强。把四张图拼成一张,相当于变相扩大了batch size,对小目标的检测稳定性有明显帮助。YOLOv8默认开启这个增强,我建议保留,但mosaic概率不宜设置过高,否则在小数据集上容易过拟合,我实测0.8左右比较合适。
第二,随机缩放和随机裁剪。模拟不同距离下灯体尺寸的变化。交通灯在远处可能只有二三十个像素,在近处可能占满半个屏幕,尺度变化极大,这一点对模型的尺度泛化能力要求很高。
第三,HSV颜色增强。光照变化是交通灯场景最大的干扰项——同一个红灯,在正午阳光下和黄昏背光下拍出来的颜色差异巨大。适度增大饱和度、明度扰动,可以让模型不过度依赖颜色绝对数值,而对“相对颜色”更敏感。
有一个增强我反而建议关闭——随机翻转。因为交通灯在画面中的位置具有固定的交通语义(左转灯在左边,直行灯在中间,右转灯在右边),如果做随机的水平翻转,会破坏灯体位置和类别之间的弱相关性。虽然在纯目标检测任务里这不一定致命,但交通灯场景下,翻转训练可能会让模型学习到错误的方位先验,有害无益。
训练过程中我额外加入了两个小目标专项优化:一是开启SAHI切片推理的评测模式,在验证集上把小图切块放大后再检测,这样可以大幅提升小目标的召回率;二是在训练时把输入分辨率从640提高到768,对小目标较为友好,mAP还能再涨一点,但相应地显存占用和推理时间也会增加,这个取舍要看你的实际部署环境。
3.3 82.88%识别率的评估口径与指标复盘
训练结束后,模型在验证集上的综合识别率达到了82.88%。需要说明的是,这个数值不是单一mAP,而是综合考虑了mAP@0.5、mAP@0.5:0.95和分类准确率之后的一个加权评估结果。
评估脚本大致长这样:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") metrics = model.val(data="traffic_light.yaml") print(f"mAP@0.5: {metrics.box.map50:.4f}") print(f"mAP@0.5:0.95: {metrics.box.map:.4f}")最终结果分布大概是mAP@0.5在0.87左右,mAP@0.5:0.95在0.55左右,综合识别率82.88%。从类别维度看,红绿灯的AP值最高,因为样本量最充足;黄灯偏低,符合预期,因为黄灯本身在路口出现的频次就少。如果要在业务里把黄灯识别率做上去,我的建议是去采集更多黄灯状态的数据,或者做数据合成。
这里说一个很多人容易忽略的点:识别率不只看指标数字,还要看错误类型。交通灯场景里,漏检(False Negative)和误检(False Positive)的代价完全不对等。漏检会让自动驾驶系统“没看到红灯”,直接导致闯红灯的风险;而误检会让系统在绿灯时误刹车,体验不好但安全风险低很多。因此训练时的损失函数和推理时的置信度阈值设置,都应该偏向于降低漏检率。实际操作上,我会把推理置信度阈值从默认的0.25调低到0.15左右,然后通过非极大值抑制的后处理去滤除重叠框。这样一改,漏检会明显减少,代价是误检略增,但辅助驾驶场景下我更愿意接受这个取舍。
3.4 模型导出与部署验证
训练完模型之后,最后一步是转成部署格式并做实际验证。我用的是TensorRT FP16导出,在嵌入式设备上跑,推理延迟控制在10毫秒以内。导出命令如下:
yolo export model=runs/detect/train/weights/best.pt format=engine device=0 half=True如果用CPU部署,可以导出ONNX格式做通用推理。有一点要提醒:TensorRT的engine文件和显卡型号强绑定,在一台机器上导出的engine换到另一台显卡上不能直接用,需要重新导出。
经验之谈:验证部署效果时,不要只拿测试集截图看效果,建议接一个USB摄像头对着真实路口做一次全天候测试。我之前在测试中发现模型对傍晚逆光场景表现很差,后来在数据集里补充了更多逆光时段的标注样本,重新训练后才解决。数据集的最终效果不能只看离线指标,上车实测才是硬道理。
4. 高频踩坑与排查方法实录
数据集的坑不比模型结构少。下面这几个问题是我自己和身边朋友在类似项目中反复踩过的,直接列成速查表加解说的形式,方便你对照排查。
4.1 标注格式转换导致框偏移
自己在YOLO和COCO之间转换格式时出现坐标偏移,是最容易犯也最容易忽略的问题。特别是YOLO格式的归一化坐标,如果忘了乘回原始宽高,框会整体缩小几十倍,训练出的模型肯定废掉。还有COCO的坐标是float类型,转VOC时如果直接取整,遇上目标特别小(比如20像素宽的灯体),取整误差就可能让框偏移好几个像素——对交通灯这种小目标来说,这个误差足以让框完全偏离灯体。
解决方案:转换之后必须自动可视化抽样,把框画在图上确认贴合度。这一步不要省略,不要嫌麻烦。
4.2 小目标在降采样后消失
YOLO系列的输入尺寸是640x640,如果原图是1920x1080,下采样比例接近3倍,一个原本30像素的灯体会缩到10像素以内,很容易在特征提取过程中丢失。这是我一开始识别率上不去的最主要原因。
针对这个问题,我试过两种有效的办法。一种是把输入分辨率提升到768甚至896,直接缓解降采样压力;另一种是使用SAHI切片推理,把大图先切成小块再分别检测,最后合并结果。前者简单,后者效果好但部署会复杂一些。如果你用YOLO系列,建议先从提升输入分辨率入手试起,成本最低。
4.3 类别不均衡导致黄灯检测率低
黄灯在真实道路中出现的频次远比红灯绿灯少,造成训练集里黄灯样本天然稀缺。模型为了降低整体loss,会倾向于把不确定的目标预测为样本量更大的红灯或绿灯,导致黄灯识别率明显偏低。
类别的解决思路有几种:最简单的做法是重复采样,让黄灯样本复制几份参与训练;更精细的做法是做针对黄灯的增强,如颜色扰动、亮度变化,人工扩充黄灯状态下的样本;还有一种思路是降低黄灯类别的loss权重惩罚力度,让模型在训练时对黄灯误判更“宽容”。这三种我都试过,组合使用的效果最好,单靠某一种提升幅度有限。
4.4 标注噪声对训练效果的影响
交通灯状态在切换瞬间非常模糊,导致标注员之间的一致性难以保证。标注噪声带来的直接后果是loss曲线震荡不收敛,或者模型对某些灯体的预测置信度普遍偏低。
我的排查方法是做一轮标注清洗:把置信度低、模型反复预测错误的样本单独抽出来看,如果发现标注本身有问题,就直接修改标注重新训练。这个过程虽然费时间,但对最终精度提升的帮助非常明显,值得在项目初期就留出这部分时间预算。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| loss不下降或震荡明显 | 标注噪声过大 | 抽样可视化检查标注框 | 清洗标注、去除错误样本 |
| 小目标检测不到 | 输入分辨率太低 | 查看特征图尺寸与感受野 | 提高输入分辨率、使用SAHI |
| 黄灯识别率低 | 样本不均衡 | 统计各类别数量分布 | 过采样、针对性增强 |
| 夜晚误检增多 | 夜间样本不足 | 按拍摄时段分类统计测试结果 | 补充夜间数据或做夜间风格迁移 |
| 框偏移明显 | 格式转换脚本有误 | 可视化标注框检查 | 重写转换逻辑并自动校验 |
| 模型在路口沙盘上表现差 | 训练数据与部署场景差距大 | 采集真实路口测试视频 | 用真实场景数据做微调 |
5. 场景落地与后续扩展方向
很多人拿到这个数据集,第一反应是直接训练一个模型完事。但“模型跑通了”和“功能上线了”之间还有很长的路要走。这部分我讲一下跟业务场景对接、策略联动和后续扩展上的个人实践心得。
5.1 从识别到决策:如何把检测结果用起来
信号灯识别一般的应用场景分两类:一类是辅助驾驶系统,需要根据信号灯状态去控制车辆启停;另一类是车路协同或交通监控,需要统计路口状态和灯态切换记录。既然是辅助驾驶用途,检测结果就不能只是画框,必须转换成明确的状态信号,例如“当前直行是绿灯,可以通行”“当前左转为红灯,禁止左转”。
我在实际项目中会把模型输出的多个检测框做一层后处理:先按检测框的空间位置聚类,确定哪几个框属于同一个灯组;再把每个框对应的类别置信度汇总,按灯组维度做最高投票决策,得到一个灯组的状态信号。例如同一个灯组里同时检测到red和yellow两个框,经过聚类后,最终应该输出“红灯亮”或“过渡状态”,而不是简单地把两个结果都上报。
这一层后处理的实现,比模型本身更能决定业务系统的稳定性。纯模型输出会有抖动,偶尔一帧把红灯误判为绿灯,下游控制模块如果直接采信,后果是致命的。所以一定要在状态机里做帧间平滑和状态切换确认,连续多帧稳定判断才执行动作切换。
5.2 从单模型到多模型协同
用模型独立完成信号灯识别,在这个数据集上可以做到82.88%,但到了真实产品里,我强烈建议把信号灯检测和高精度地图信息做融合。高精度地图可以提供路口的拓扑关系、灯组位置、车道方向,视觉检测负责确认当前灯态。两者互为校验,比单独依赖视觉结果可靠得多。
举个实际例子,城市道路上有一种情况是高架桥下的阴影遮挡了信号灯,视觉检测器可能完全找不到灯体。但如果地图数据告诉你这个路口一定有灯组,系统就可以进入“地图兜底”模式,提前减速慢行,而不是傻乎乎往前冲。这种多传感器融合的思路,比追求单个模型的极限精度更能保障安全性。
5.3 数据集扩展与持续迭代建议
从数据集的角度,后续扩展的方向主要有两个:一是增加天气和光照覆盖,比如大雾、暴雨、夜间逆光、低照度等,这些是模型走向量产绕不开的关卡;二是增加不同地域的路口类型,不同城市的路口标线、灯组排列方式存在差异,模型的泛化能力需要在更广泛的数据上验证。
另外,还有一个完全不同的玩法:用合成数据增强。用游戏引擎或者仿真环境生成大量带有完美标注的交通灯场景,然后通过域适应方法迁移到真实场景。这个思路在学术界和工业界已经很成熟了,我自己也试过在仿真场景里渲染一批夜间样本加入训练集,对真实夜间场景的识别确实有帮助。如果你手头数据不足,可以考虑这个方向。
写在最后
这个数据集给我最大的感受是:数据质量往往比模型结构更能决定项目天花板。交通灯识别看着任务简单,真正做好却难在细节——标注的细粒度、小目标处理、类别平衡、状态判定标准,每一步都需要踏踏实实打磨。我也在尝试把它扩展到更多复杂的场景,比如传感融合和路径决策方向。
最后分享一个小习惯:每次训练完,不要只看mAP那一行数字,一定要去翻一翻验证集里分错的那几十张图。那些图才是模型真正学不好的地方,它们指向的问题往往比模型结构本身更值得关注。希望这篇内容能帮你少踩一些我踩过的坑,把信号灯识别这件事做得更稳。