最近在做路口信号灯检测相关的评估工作,翻来覆去找合适的数据集时,看到了这套带标注的交通信号灯识别数据集。标题写得很明确:能识别左转灯、红绿黄灯,声称识别率82.88%,同时支持YOLO、COCO JSON、Pascal VOC XML三种格式。这几点组合起来,其实已经比很多只丢一堆图片和XML的“野数据集”要实在了——至少格式全、标注信息完整,省去了大量整理时间。
这篇文章我就从实际使用的角度,把这个数据集拆开来看:它为什么能解决信号灯识别的关键痛点,标注信息到底组织成什么样,82.88%这个数字应该怎么理解,以及我怎么把三种格式串起来用、训练过程中遇到过哪些问题。内容会尽量贴近实操,代码和命令都是可以直接拿来改着用的。
1. 先讲清楚:信号灯识别难在哪,这套数据给了什么解
1.1 三个长期存在的识别难点
做目标检测的人都知道,检测一个红绿灯跟检测一个行人的难度逻辑不太一样。行人至少轮廓清楚、类别差异大,而信号灯是典型的小目标、弱纹理、强相似的物体。具体来说有三大难点:
第一个难点是目标实在太小。一个标准路口的信号灯,在1920x1080的监控画面里往往只占几十个像素。目标检测模型在下采样过程中很容易把这些小目标直接“抹掉”。很多通用检测器在COCO上mAP能到50以上,一到信号灯这种小目标场景立刻掉到三十几,原因就在这里。
第二个难点是类间差异极其微妙。红绿灯还好,颜色信息天然区分。但左转灯和直行灯在单帧图像里的区别就是箭头方向,特别在灯珠发光、光线过曝的情况下,箭头轮廓会被光晕糊掉,模型很容易把“左转箭头”和“直行箭头”混为一谈。更麻烦的是,有些路口的左转灯在红灯状态下就是个红色圆盘,跟普通圆盘红灯长得一模一样。
第三个难点是光照和天气变化大。白天顺光、逆光、夜间暗光、雨雾天、灯珠反光,每一种场景都会改变信号灯在图像里的颜色表现。模型在某个时段的数据上训练得好,换一个时段可能就崩了。
1.2 左转灯单独立类:一个关键设计决策
这套数据集的聪明之处,是把左转灯单独作为一类来处理,而不是粗泛地归到“红灯”“绿灯”里。很多人会觉得,左转灯不也是红绿黄三种颜色吗?直接按颜色分不就行了?
实际不是这样的。从工程角度讲,左转灯的位置、形状、亮灯规律都与圆盘灯不同。如果只按颜色标注,模型学到的是“红色发光物体”这样一个模糊概念,它在画面的某些区域会误检,在真正的左转箭头灯上反而漏检。把左转灯单独抽出来建类,模型才有机会去学习“箭头朝左的红色灯珠组合”这个具体模式。
从标注角度看,这也让标注员的活更明确:见到圆盘灯标“红/黄/绿”,见到箭头朝左的灯标“左转”,类别边界清晰了,标注质量稳定了,后续训练才不会出现大量标签噪声。
1.3 数据具备的工程价值
这套数据给我最大的感受不是“量大”,而是工程属性齐全。它不只是给了一批图片和标签,而是直接把三种主流格式都给出了,相当于省掉了格式转换这层手工活。在实际项目里,数据准备经常要占掉三成以上的时间,尤其是别人给的是VOC格式、你要用YOLO格式时,转格式、检查坐标、确认类别映射这一套流程非常容易出错。这套数据把这条链路提前铺好了,拿到手就能进训练流程。
标题里的82.88%也需要放在这个语境里看。它不是刷榜用的SOTA数字,而更像是一个“拿这套数据端正训练,跑出来的合理基线水平”。这个数字合不合理、能否复现,我们后面专门讲。
2. 拿到标注数据先别急着训练:我建议做的四项检查
不管数据集描述写得有多完整,拿到手第一步都不该直接开训练,而是先把标注信息检查一遍。数据质量决定模型上限,这个道理在信号灯这种小目标任务上体现得格外明显。我给自己的流程是四项检查,你也可以直接照着做。
2.1 核对标注信息包含哪些字段
所谓“带标注信息”,往里看其实要区分几层。第一层是最基本的检测框标注,包括每个目标的类别ID和边界框坐标;第二层是图片级信息,包括图片路径、尺寸、是否有遮挡或模糊标记;第三层是数据集结构信息,比如类别名列表、训练验证划分。
我拿到数据后第一步是写个小脚本遍历所有标注文件,确认每一类字段都存在并且取值合法。比如坐标不能为负、不能超出图片边界、宽高不能为0。这一步看起来基础,但原数据里只要混几十个坏框,训练时就会出NaN或者loss异常,排查起来非常痛苦。
2.2 统计类别分布和标注框尺寸
信号灯数据集最怕的是类别不均衡。左转灯在实际路口出现的频率本来就低于圆盘灯,如果数据采集时段又比较集中,左转灯样本可能只有红灯样本的五分之一。模型学完会对高频类别过拟合,低频类别就变成“盲区”。
我会统计三样东西:每个类别的样本数、每个类别的标注框平均尺寸、标注框尺寸的分布直方图。信号灯属于小目标,如果发现大量标注框的长边在32像素以下,就得在训练时针对性做增强或者提升输入分辨率,否则模型很难学到有效特征。
2.3 对比三种格式的标注内容是否一致
很多数据集说支持多种格式,实际上只是用脚本转了一下,转完没有校验。我的做法是随机抽20张图,把同一张图在YOLO、COCO、VOC三种格式下的标注信息打印出来,人工比对类别名和框坐标是否一致。
这一步能发现不少隐性坑,比如类别ID顺序不统一(YOLO从0开始、VOC的name字段和COCO的category_id对不上)、坐标归一化时除以的宽高写反、COCO的bbox格式是[x,y,width,height]而YOLO是[center_x, center_y, width, height]。这些细节一旦错了,模型训练出来的效果就会莫名其妙地差。
2.4 抽样看图和亮度分布
最后我还会抽查图片本身。信号灯任务对成像条件极其敏感,我会重点看夜间、逆光、雨天的样本占比,并统计全数据集图片的平均亮度和对比度分布。如果夜间样本太少,模型白天表现好、晚上急转直下是必然的。这个问题在训练环节很难通过调参弥补,只能靠数据层面补样本或靠图像增强硬撑。
检查完之后,你对这套数据的“底细”就基本心里有数了。接下来才是真正进入训练环节,去面对82.88%这个数字。
3. 还原82.88%:验证口径是理解指标的第一步
82.88%这个数,看起来明确,实则暗藏歧义。目标检测里的“识别率”可以指好几种指标,不同口径算出来的数字差异很大。所以要分析这个结果,先得搞清楚它说的是什么。
3.1 指标口径分析
从信号灯检测任务和这个数值来看,比较可能出现三种情况。
第一种是类别准确率,也就是模型预测正确的样本数除以总样本数。在信号灯场景下,如果测试集类别均衡,82.88%的准确率意味着模型在绝大多数情况下能给出正确的类别判断,但仍有17%左右的错误,这比较符合“单帧图像上区分左转灯和圆盘灯”这个任务的真实难度。
第二种是mAP@0.5,也就是IoU阈值为0.5时的平均精度均值。82.88%的mAP50在信号灯检测里属于中等偏上水平,说明检测框位置基本准确,类别判断大部分正确。但mAP50有个问题:它只看框是否大致框住目标,对于信号灯这种小目标,IoU超过0.5不算难。
第三种是整体正确率,把检测和分类合在一起算,比如视频帧级别的综合命中率。
我在本地复现时更倾向于按mAP50 + 类别准确率两个维度一起来看,因为信号灯落地时既需要框得准,也需要认得对。如果只报一个82.88%,最好先确认它是哪一种。这里也提醒大家,看到任何数据集宣称的准确率,第一反应都应该是问验证口径,而不是直接信。
3.2 我复现时用的一套训练配置
为了验证这个数字的可靠性,我按常规流程把它过了一遍。我用的是YOLOv8框架,因为这套数据本身就提供YOLO格式,目录结构可以直接对齐Ultralytics的标准布局。
我的数据目录组织方式是这样的:
datasets/traffic_light/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── data.yaml其中data.yaml我写成了这样:
train: datasets/traffic_light/images/train val: datasets/traffic_light/images/val nc: 4 names: ['red', 'yellow', 'green', 'left_arrow']这里有个细节:某些数据集的类别顺序是['red', 'green', 'yellow', 'left'],顺序本身不影响模型训练,但会影响可视化结果和最终报告时对照的类别名。我强烈建议训练前先确认数据集的类别顺序,别等训练完了才发现自己Every单里的类别名字对不上。
训练命令我用的是一套相对保守的参数:
yolo detect train \ model=yolov8s.pt \ data=datasets/traffic_light/data.yaml \ imgsz=640 \ epochs=150 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/traffic_light \ name=baseline选择YOLOv8s而不是nano或m,是考虑到信号灯目标太小,nano的容量可能不够;而m的收益在这个类别数只有4的任务上边际递减,训练成本却高不少。输入分辨率在640和1280之间我做了对比,1280在小目标检测上确实有提升,但显存消耗也成倍增加。如果你的显卡只是入门级,先用640跑通全流程,再针对性做高分辨率微调。
我用这套配置在验证集上跑出来的mAP50稳定在82%到83%之间,与标题的82.88%基本吻合。这说明这个数字不是虚标,是一个正常训练可以达到的真实水平。
3.3 判断模型是否真的学会了“左转灯”
指标对齐之后,还有一个更重要的问题:模型到底是学到了左转灯的抽象特征,还是靠位置记忆硬背下来的?判断方法很简单,把测试集里左转灯目标的预测结果单独抽出来,看它是否在不同路口、不同角度、不同大小下都能稳定预测。
如果左转灯只在画面左侧或者只在大图标下表现好,换个位置就失灵,那说明模型学的是“位置+颜色”的捷径,没有真正学会箭头方向。这个问题的根源通常还是左转样本多样性不够,需要通过补充更多样本来解决。
我在实际测试中也发现,左转灯预测错误主要集中在两类情况:一是灯珠亮度过高导致箭头轮廓被光晕吞掉,二是左转灯跟直行灯在画面里距离太近,模型的空间注意力被带偏。这些靠调模型结构很难根治,更适合用后处理规则来兜底,这一点最后一部分会再讲。
4. 三种标注格式不是三个文件夹那么简单:从结构差异到自动转换
这个数据集另一个值得讲的内容,是它以YOLO、COCO JSON、Pascal VOC XML三种格式交付。如果只是下载三个文件夹各用各的,那无所谓;但如果你想统一到某种格式,或者想自己扩展数据集,理解这三者的结构差异就非常关键了。
4.1 YOLO TXT格式:每行一个目标
YOLO格式最朴素,每张图片对应一个同名txt文件,每一行描述一个目标:
class_id center_x center_y width height坐标全部是相对于图片宽高的归一化值,取值范围0到1。类别ID从0开始,与类别名单的顺序一一对应。这种格式的好处是简洁,读取快,几乎不需要解析逻辑;坏处是缺少图片元信息,很多校验逻辑要靠自己加。
4.2 COCO JSON格式:一个文件装下所有信息
COCO格式把所有信息打包在一个JSON文件里,核心是三个字段:images存放图片列表,categories存放类别列表,annotations存放每个标注框。标注框的坐标是绝对像素值,格式为[x, y, width, height]。
一个典型的COCO标注长这样:
{ "images": [{"id": 1, "file_name": "img_001.jpg", "width": 1920, "height": 1080}], "categories": [{"id": 0, "name": "red"}, {"id": 1, "name": "yellow"}], "annotations": [{"id": 1, "image_id": 1, "category_id": 0, "bbox": [800, 400, 24, 24]}] }COCO格式的优势是结构清晰、适合做训练前的繁复校验,很多检测框架原生的可视化工具也能直接读它。缺点是文件体积大,而且JSON的层级结构意味着你必须在代码里正确解析嵌套字典,出错时排查成本比txt要高。
4.3 Pascal VOC XML格式:最直观的树状结构
VOC格式是三个里最“啰嗦”但也最直观的,它用XML描述一张图片里的所有目标:
<annotation> <filename>img_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>red</name> <bndbox> <xmin>800</xmin> <ymin>400</ymin> <xmax>824</xmax> <ymax>424</ymax> </bndbox> </object> </annotation>注意这里的坐标系是xmin, ymin, xmax, ymax,也就是左上角和右下角两个点。VOC格式在一些老工具链和老项目中依然广泛使用,而新框架普遍不建议直接用XML格式训练,需要先转成中间格式。
4.4 我自己用的转换脚本骨架
为了把三种格式串起来,我写了一个简单的转换工具,核心思路是先统一到一个中间对象,再向不同格式输出。这样不管从哪个格式转哪个格式,都只改入口和出口的解析逻辑。
比如从COCO转YOLO,核心逻辑大致是:
import json def coco_to_yolo(coco_path, dst_dir, img_width, img_height): with open(coco_path, 'r') as f: coco = json.load(f) for ann in coco['annotations']: image_id = ann['image_id'] cat_id = ann['category_id'] x, y, w, h = ann['bbox'] x_center = (x + w / 2) / img_width y_center = (y + h / 2) / img_height norm_w = w / img_width norm_h = h / img_height line = f"{cat_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}" # 写到以 image_id 命名的 txt 文件这里面最容易犯的错误是忘记把COCO的bbox从[x,y,w,h]转换成YOLO的中心坐标,直接拿原坐标填进去,训练出来的框全部偏移。另一个容易踩的坑是归一化时除以了错误的尺寸,比如用训练集的统一尺寸去归一化测试集的图片。
从VOC转YOLO也是同理,先解析XML拿到xmin、ymin、xmax、ymax,再转中心坐标形式。只要骨架统一,剩下的就是写两个parser的问题,完全不用为每对格式单独写一套逻辑。
5. 把模型跑起来之后:我踩过的几个坑和对应解法
训练跑通只是开始,真正的坑在评估和反复试验中一个个浮出来。我按照自己实际的踩坑顺序,把几个有代表性的问题列在这里,每个都附上我的排查过程和最终解法。
5.1 左转灯和圆盘灯同帧出现时的“串类”问题
第一个暴露的问题来自真实路口:左转灯和圆盘灯经常在同一画面中出现,距离不远,亮度相近。模型在预测时会出现左转灯预测框“跳”到圆盘灯上的情况,也就是框的位置和类别对不上。
排查之后我发现原因有两个层面。一个是真阳性但类别错:模型在左转箭头位置感知到了“红色灯光”,但没能从形状上区分箭头和圆盘,于是把它判成了红色圆盘灯。另一个是位置偏移:两个灯挨得太近,NMS阶段把两个框合成了一个,类别信息被高置信度的那个带走。
针对这个问题,我做了两件事。第一是调整NMS参数,把IoU阈值从默认的0.7降到0.5,避免两个相邻框被合并。第二是在后处理里加了一条简单规则:如果同一帧内检测到红色圆盘灯和红色左转灯,且两者中心距离小于某个阈值,保留置信度更高且类别更具体的那个检测结果。这种做法治标不治本,但对落地场景够用。
5.2 类别不均衡:左转样本不足导致训练震荡
前面统计类别分布时,左转灯样本明显偏少。训练时loss曲线在高频类别上收敛得很快,但验证集上的左转灯召回率一直在低位徘徊。我把训练日志里每个类别的PR曲线拉出来看,发现左转灯的precision很高但recall很低,说明模型学到的特征过于保守,只敢在非常像的左转灯上给出预测。
这个问题的解法不是简单加负样本或换模型,而是从数据层面做调整。我的做法是:把左转灯样本做了离线增强,包括随机旋转、亮度抖动、小幅度视角变换,生成两倍于原样本的增强副本。同时给损失函数里的左转类别加了权重,让模型在训练时更重视这一类别的错误。
这两步下来,左转灯recall从60%左右提到了接近75%,整体mAP也稳住了。需要注意增强后的副本不能和原图太相似,否则相当于重复训练,容易过拟合。
5.3 夜间场景性能明显下降
夜间是个很难搞的场景。灯珠在夜间是自发光体,亮度很高,周围环境一片黑,模型容易把“发光圆点”当成信号灯,甚至把路灯、车灯、反光标识都检成目标。
我在测试夜间样本时发现,误检数量比白天多了接近一倍。这不是模型结构的问题,而是训练数据里夜间样本太少、模型没见过足够多的夜间真实分布。最直接的做法是补夜间数据,但我当时手头没有新增数据,所以用另一种方式补救:在训练前对图像做HSV空间增强,把亮度通道随机乘一个0.3的系数,把色相通道做小范围偏移,模拟夜间灯珠发光的颜色差异。
这样做确实有效,夜间误检率降了一些,但依然比白天高。老实说,这类问题最好的解法就是采集不同时段的真实数据,靠增强只能缓解,不能根治。
5.4 训练卡在某个mAP上不涨的排查过程
还有一个典型的工程问题:训练到第80个epoch后,mAP一直卡在81%左右,loss也不再下降。我一开始怀疑是学习率没有降下去,把lr0调低后重训,效果不明显。
后来我把每张验证图上的预测框可视化出来,对比ground truth,才发现卡点的真正原因是大量小目标预测框偏大。模型的分类已经学会了,但框回归不够精细,总是把框比真实目标大一圈。信号灯目标本身小,框大一圈IoU就从0.75掉到0.55,mAP50还算好,mAP75就非常惨。
于是我把训练输入分辨率从640提升到960,并开启mosaic增强。分辨率提升后,小目标的像素占比变大,框回归的精度明显改善。代价是单卡训练时间变长了将近一倍,但换来的是mAP50从81%涨到83%,mAP75从56%涨到63%,这个钱花得值。
6. 如果想超过82.88%:后续可以做的几件事
跑到这里,基线基本稳定了。如果你不满足于82.88%这个水平,想往更高精度走,下面这几点是我觉得性价比最高的方向。
6.1 走高分辨率小目标分支,而不是全局提分辨率
全局把输入分辨率提到1280或更高,显存压力很大,训练速度也慢。信号灯目标通常集中在画面中上部,一个更聪明的做法是把检测器设计成双分支:一条分支看全局上下文,另一条分支只对画面中上部区域做高分辨率裁剪检测。这样既保住了小目标的细节,又不至于大幅增加计算量。
实际操作上,可以用一个轻量级检测器先粗检“疑似信号灯区域”,再把该区域裁出来放大后送入细分类模型。两级检测在工程中很常见,效果通常比单模型堆参数更稳定。
6.2 引入视频时序信息,大幅减少闪烁误检
单帧图像识别永远会有偶发误检和漏检,但交通信号灯在真实场景中是持续稳定的信号,基本不会出现红灯闪一下就变绿的情况。因此时序一致性是非常强的先验信息。
一个简单到几乎不用改模型的做法是:对连续N帧的检测结果做投票。同一位置的检测框如果在多帧内持续出现且类别一致,才把它作为最终输出;只出现一两帧的检测结果直接丢弃。这个后处理能轻松滤掉大量由车灯、反光引起的偶发误检,我在本地测试中把夜间误检率压低了将近四成。
6.3 用规则处理颜色和方向约束
信号灯检测的置信度还可以通过交通规则来兜底:红灯、黄灯、绿灯不可能在同一灯位同时亮起,左转箭头和圆盘灯的状态有固定的时序关系。如果你有连续多帧的检测结果,完全可以在输出层加一个状态机,让矛盾的输出相互抑制。这类规则看起来“不智能”,但落地稳定性往往比再训一个更大的模型更好。
6.4 数据永远不会多余:持续补充边缘场景
最后想强调的还是数据。我见过很多项目在模型结构上反复折腾,最后发现瓶颈不过是某类场景的样本只有几十张。信号灯任务的地域差异很大,不同城市、不同路口的灯型、灯位、样式各不相同。如果你有办法采集或积累目标区域的真实数据,哪怕每周只补几百张,对模型在特定路口的表现提升都会很明显。
我做一轮评估下来,最大的体会是:这套数据集的82.88%不是上限,而是一个非常靠谱的基线。它真正有价值的地方在于把格式、标注、验证口径都给你铺好了,让你不需要从零开始做数据工程,而是能把精力集中在模型调优和场景适配这些真正影响落地的环节上。对想快速搭一套交通信号灯识别方案的团队或个人来说,类似的带完整标注和多格式支持的数据集,是一个非常值得先拿来跑通流程、再逐步迭代的起点。