开头写一段场景引入,得让读者有代入感。我去年做某个辅助驾驶视觉项目时,发现最难的不是模型结构,而是数据本身。项目需求很明确:在城市路口识别红绿灯、箭头、倒计时数字,让系统在复杂路况下能提前判断通行状态。我拿来现成的国外开源数据集试了一圈,效果相当尴尬——远处的小灯体漏检严重,倒计时数字被当成杂碎目标,最离谱的是路口的红色广告牌经常被认成红灯。折腾了两周后,我决定放弃"拿来主义",老老实实搭一套面向国内交通场景的YOLO数据集。从采集、清洗、标注、转换到训练调优全流程走完,模型表现才真正稳定下来。这篇文章就把我在这个过程中的关键步骤和踩坑记录整理出来,给正在做同类交通信号检测项目的朋友一个可复用的参考。
1. 为什么"国内交通信号检测"不能直接套用现成数据集
1.1 国内外路口的信号灯形态差异远比想象中大
很多做目标检测的朋友一开始都会图省事,直接下载国外机构公开的交通信号数据集来训练,比如某些基于欧美路况采集的数据集。但国内路口的信号灯形态和国外差别非常大,这种差异直接决定了模型的泛化能力。
国外数据集里的信号灯大多是横排安装,灯体本身较小,拍摄视角多为路口全景,远处的灯在画面中只占很小面积。而国内常见的是竖排信号灯,有些带倒计时数字显示屏,有些是复合箭头灯组,有些安装在悬臂杆上横跨车道,有些在路侧低矮立柱上。再加上国内路口经常存在"满屏红灯+倒计时"的组合,一个灯体内既有圆形灯也有数字区域,模型如果不经过针对性训练,很容易把同一个灯体拆成两个目标,或者把倒计时数字区域误认为独立的信号灯。
另一个问题是颜色呈现的差异。国内一些路口使用LED点阵式倒计时屏,颜色偏红偏亮,和信号灯本身的颜色非常接近;国外数据集中这类样本极少,模型学到了"圆形红灯"的特征,却学不会"红灯旁边还有一块更亮的红色数字区域"这种组合关系。所以用国外数据集训练的模型,到国内路况上性能下降是必然的。
1.2 真实路口场景的复杂度决定了数据覆盖面
除了灯体形态差异,真实路口的复杂场景是数据集的第二个关键维度。我在搭建数据集时专门统计了一个普通城市路口会出现的干扰情况:
- 早晚高峰时段,路口车辆密集,前车刹车灯、尾灯和信号灯同时出现在画面中,颜色高度相似。
- 正午逆光条件下,信号灯整体偏暗,红色灯体的饱和度下降,模型容易漏检。
- 夜间场景中,LED灯体发光强烈,周围存在光晕和反射,灯体边缘模糊。
- 雨雾天气下,灯体清晰度下降,玻璃外壳上的水渍会产生反光条纹。
这些场景如果数据集中覆盖不足,模型在某个特定时段或天气下的表现就会崩塌。比如我最初只用白天正常光照的数据训练,到了傍晚低照度时,模型的召回率直接降了20个百分点。后来补了足够多的黄昏、夜间样本才恢复回来。
所以做国内交通信号检测的第一步不是急着下载数据集或调模型,而是先想清楚:我的模型要部署在什么场景,什么路况,什么时间段,然后围绕这些场景去构建数据。数据覆盖度是模型性能的天花板,模型结构只是逼近这个天花板的手段。
2. 采集方案与类别体系设计:先把边界定清楚
2.1 数据采集的几种可行路径
自建数据集的第一步是采集原始素材。我试过几种方式,各有利弊:
行车记录仪视频抽帧:这是最接近真实部署场景的方式。行车记录仪固定在挡风玻璃上,视角、高度、运动状态都和实际使用场景一致。我采集了多个路口的行车视频,覆盖不同时段,然后用抽帧工具每隔5-10帧抽取一张。这个方式成本低,素材量大,但要注意画面中红灯时前车遮挡的情况很多,需要后期筛选。
手机手持拍摄:灵活性高,可以站在行人视角、天桥视角、路侧视角拍摄,补充车载视角之外的多样性。但手机拍摄容易产生手持抖动模糊,且视角和实际部署差异较大,只能作为补充数据。
网络公开视频片段:可以获取到更多样的天色、天气、路口结构,但来源复杂、清晰度参差不齐,而且需要仔细筛选和确认授权合规。我的做法是只用于补充极端天气素材,比如暴雨、雾天,正样本仍然以自采为主。
采集中一个容易被忽略的问题是"路线单一化"。如果每次都走同一条路,同一个路口,模型会严重过拟合到这条路的灯体外形和视角上。我后来调整的策略是规划了至少10条不同走向的路线,覆盖高架桥下、新建路口、老式路口、多车道宽路口等不同类型,样本多样性明显提升。
2.2 类别定义:少而精,避免类别间混淆
这是整个数据集中最关键也最容易出错的一步。交通信号灯的检测类别看起来简单,无非就是红灯、绿灯、黄灯再加几个方向箭头。但实际标注时你会发现,类别之间存在着严重的视觉相似性,需要提前划清边界。
我在项目中先定义了以下基础类别:
| 类别ID | 名称 | 说明 |
|---|---|---|
| 0 | red_circle | 红色圆形灯体(含满屏红灯) |
| 1 | green_circle | 绿色圆形灯体(含满屏绿灯) |
| 2 | yellow_circle | 黄色圆形灯体 |
| 3 | red_left | 红色左转箭头 |
| 4 | red_right | 红色右转箭头 |
| 5 | red_straight | 红色直行箭头 |
| 6 | green_left | 绿色左转箭头 |
| 7 | green_right | 绿色右转箭头 |
| 8 | green_straight | 绿色直行箭头 |
| 9 | countdown | 倒计时数字显示区域 |
这个设计有两条原则:
第一,灯体类和箭头类分开,而不是统一标成"red_light"。因为箭头灯的语义和圆形灯完全不同,如果合并成一类,模型虽然能检测到灯,却无法区分能否转向,这对实际决策系统是致命的。
第二,倒计时单独作为一类,而不是拆成0-9十个数字类。拆成数字类虽然可以做数字识别,但会引入大量标注成本,而且倒计时数字区域在画面中通常很小,十个类别的样本量会被严重稀释。我的做法是先把倒计时区域整体检测出来,后续如果需要识别具体数字,再单独训练一个OCR模块或分类网络去处理。这样职责清晰,模型也更容易收敛。
2.3 样本数量与场景分布怎么规划
类别定义好之后,接下来是样本规模。以我的项目为例,总共标注了约2.2万张图片,包含约4.5万个信号灯实例。分布上大致遵循以下比例:
| 场景 | 占比 |
|---|---|
| 白天正常光照 | 40% |
| 清晨/黄昏低照度 | 20% |
| 夜间 | 25% |
| 雨雾/逆光等特殊天气 | 15% |
我的一个体会是:夜间数据不要贪多。一开始我以为夜间越多越好,结果夜间数据占到40%后,模型在白天正常光照下的表现反而有点下降,因为夜间灯体亮度过高,模型学到的是"发光的红色圆形区域",对白天低饱和度的红色灯体反而判断犹豫。后来把夜间数据压到25%左右,并适当增加白天逆光样本,平衡之后两边表现都恢复了。
每类样本的具体数量,我的底线是每个箭头类至少3000个实例,主灯类5000个以上,倒计时区域至少4000个。低于这个量,训练出来的模型在对应类别上的精度会明显波动,尤其是右转箭头这种在部分路口很少出现的类别,需要特意去有此类信号灯的路口补采。这里也分享一个经验,如果某些类别实在难以采到足够样本,可以考虑在数据增强阶段做针对性合成,但合成样本的比例最好控制在15%以内,否则模型容易对合成纹理过拟合。
3. 清洗与标注实操:数据质量是训练效果的隐形天花板
3.1 初步筛选:哪些帧可以直接扔掉
从视频里抽出来的原始帧质量参差不齐,直接拿去标注就是在浪费人力。我第一次做数据集时图快,忽略了清洗环节,结果标注团队在模糊帧和过曝帧上花了大量力气,标注出来的框位置也不准,训练时模型反复震荡。后来总结了四条筛选标准,凡是命中任意一条的帧直接删除:
- 运动模糊:车辆转弯、颠簸导致的画面拖影,灯体边缘已经无法分辨。这种帧即使标注了,框的边界也是错的,会向模型传递错误信息。
- 严重过曝或欠曝:灯体发光区域一片纯白,或者整个画面黑到完全看不到灯体轮廓。颜色信息都丢失了,没法标注。
- 目标遮挡超过50%:比如前车正好挡在信号灯前面,只露出一小部分。这种样本价值极低,除非是为了专门训练遮挡鲁棒性,否则建议删掉。
- 目标过小:灯体在画面中宽度小于15像素时,人眼都很难确认这个灯到底是红是绿,标注出来的类别大概率也是错的,导入训练只会变成噪声。我实测下来,小于15x15像素的目标即使标注了,YOLO也很难学到有效特征,直接放弃更划算。
筛选这一步看似简单,但对后续所有环节的影响非常大。干净的数据集,标注效率至少能提升三成,训练收敛速度也更快。
3.2 标注工具与框规范的选择
清洗完之后就可以进入标注环节。我试过两个主流开源标注工具:单机快速标注首选LabelImg,轻量直接,适合个人和小团队;多人协同或者需要做复杂标签管理时用Label Studio,支持在线标注和多人审核,效率更高。工具本身不是重点,重点在于边界框的标注规范。
交通信号灯的边界框怎么画,是个很容易被忽视但影响很大的问题。信号灯有外壳和发光区域两部分,我的建议是:圆形灯体标注时包含整个黑色外壳,而箭头类只标注箭头发光区域本身。
原因很简单:圆形灯体在点亮时发光区域边界不清晰,会出现光晕,如果只标发光部分,不同帧里框的大小会跟着光晕变化,导致标注不一致,模型学到的框回归目标就是乱的。包含外壳后,框的位置稳定,模型容易收敛。箭头灯则相反,箭头的发光区域轮廓清晰,而外壳内还有数字、装饰等其他元素,如果把外壳整体框进去,模型会误把其他元素也当作箭头特征,反而干扰分类。
至于框与框重叠的情况,比如同一个灯组内红灯和倒计时区域靠得很近甚至部分重叠,我的处理原则是:重叠面积不超过50%时两个目标都标注,超过50%时优先标注主要目标(通常是灯体),被遮挡的小目标就不标了。这样做可以避免模型中两个锚点抢同一个区域的情况。
3.3 标注质量检查:别省这一步
标注完不等于结束,我专门留出时间做质量审核,方法很简单但非常有效:
- 抽检制度:随机抽10%的已标注图片,由另一位标注员重新审核,重点检查类别是否正确、框是否贴合、有没有漏标。红灯被标成绿灯这种错误,如果不及时发现,对模型来说是致命的。
- 可视化检查:把标注框画回原图,用拼接图的方式快速扫一遍。我能直观看到哪些框偏离目标、哪些框大小异常。
- 规则校验:写一个脚本统计所有标注框的宽高比、面积分布。正常信号灯框的宽高比集中在1:1到1:3之间,如果出现大量极端宽高比,说明标注可能有误,比如把远处的路灯灯柱也框进来了。
我踩过的坑是有一批夜间数据没有做抽检,结果发现标注员把红色LED广告屏的局部区域当成了红灯框进去了,一共有一百多张。这些噪声数据混入训练集后,模型在夜间场景对广告屏的误检率明显上升,后来把这批数据清洗掉并补充了正确标注才恢复。所以质检看似费时间,其实是给训练省时间。
4. 从原始标注到YOLO格式:转换与增强的关键细节
4.1 YOLO标注格式的核心规则
清洗好的数据如果用LabelImg标注,默认导出的是VOC格式的XML文件,而YOLO训练需要的是txt格式,所以必须做一次格式转换。理解YOLO标注格式的原理是这一步的基础。
YOLO的txt标注文件里,每一行对应一个目标,五个数值分别是:
class_id center_x center_y width height其中center_x、center_y、width、height都是归一化后的浮点数,范围0到1,计算公式是像素坐标除以图片的宽或高。class_id是整数,对应你的类别编号。
举个例子,一张1280x720的图片里,某个红灯目标的边界框像素坐标是x_min=400,y_min=300,x_max=600,y_max=450,那么:
- center_x = (400 + 600) / 2 / 1280 = 0.390625
- center_y = (300 + 450) / 2 / 720 = 0.520833
- width = (600 - 400) / 1280 = 0.15625
- height = (450 - 300) / 720 = 0.208333
转换脚本的逻辑很简单,遍历所有标注文件,读入每个目标的边界框坐标和类别名,按上述公式计算归一化值,再写入对应的txt文件。这个环节最需要注意的是类别名称到ID的映射关系,确保XML里的"red_circle"对应txt里的"0",不能错位,否则会在训练时造成类别混乱,而且这种错误很难通过loss曲线发现。
4.2 目录组织与数据集划分
转换完成后,文件目录建议按YOLO标准格式组织:
dataset/ ├── train/ │ ├── images/ │ │ ├── img_0001.jpg │ │ └── ... │ └── labels/ │ ├── img_0001.txt │ └── ... └── val/ ├── images/ └── labels/图片和标注txt必须放在同名的images和labels子目录下。这样做的好处是,训练框架可以直接从路径推断对应的标注文件,不用额外管理映射关系。
关于训练集和验证集的划分,有一个大家容易忽略的坑:按视频文件划分,而不是按单张图片随机划分。同一个视频连续帧之间高度相似,如果这些相似帧一部分进了训练集,一部分进了验证集,验证集的指标会虚高,因为模型相当于"见过"了验证图片的邻居帧。我的做法是先把每个视频的所有帧归为一个整体,然后按视频维度随机划分,80%的视频帧进训练集,20%进验证集。这样才能真实反映模型在未见场景上的泛化能力。
4.3 数据增强策略:哪些该用,哪些不该用
YOLO系列模型内置了丰富的数据增强策略,但交通信号检测这个场景需要谨慎选择。我这里直接给结论:
该开的增强:
- mosaic:四张图拼接成一张,能有效提升模型对小目标的检测能力,因为拼接后目标相对占比更小,模型被迫学习更紧凑的特征表达。
- 亮度对比度扰动:模拟黄昏、逆光、夜间等光照变化,增强模型对光照的鲁棒性。
- 轻微模糊和噪声:模拟雨雾、镜头污渍场景,提升模型在低画质条件下的表现。
该关的增强:
- 水平翻转:这个要非常注意。圆形灯体水平翻转后类别不变,没问题,但箭头灯水平翻转后,左转箭头就变成了右转箭头,类别完全变了。如果打开随机水平翻转而不同步修改标签,模型会学到完全混乱的箭头方向。我在项目中采取的做法是:开启水平翻转,但在增强代码里自动将箭头类的左右标签互换,具体可以这样处理:如果原标签是red_left,翻转后改为red_right,同时修改归一化坐标的x值为1-x。如果你的数据处理流程不支持这种自动标签调整,那就干脆关闭水平翻转,避免得不偿失。
- 极端色彩扰动:信号灯颜色是核心识别依据,过度的色调偏移会把红灯变成橙色甚至紫色,破坏颜色特征,影响分类精度。所以色彩增强幅度要控制在能够保持颜色语义的范围内。
样本不均衡的问题也在这个阶段处理。右转箭头在部分路口很少出现,训练时模型对该类别的特征学习不充分,导致精度偏低。我的处理办法有两个,一是对少数类别的样本进行过采样,复制多份参与训练;二是借助mosaic增强把少数类别样本拼接进更多训练图片,提高它们的出现频率。实测下来,过采样结合mosaic之后,右转箭头的Recall从78%提到了89%,效果还是比较明显的。
5. 训练配置与调优实测:从模型选型到参数验证
5.1 模型选型:轻量还是高精度
数据准备到位后,终于进入模型训练环节。YOLO系列发展到现在有多个版本,我在项目中主要对比了轻量级和中等规模两种配置:轻量级模型参数量小、训练和推理速度快,适合部署在嵌入式设备;中等规模模型精度更高,但对算力要求也更高。
以实际场景来说,交通信号检测是一个典型的小目标检测任务,信号灯在画面中通常占比很小,尤其是在远处路口场景中可能只有二三十个像素。中等规模模型的特征表达能力更强,小目标检测精度明显优于轻量级模型。所以我的建议是:如果你的部署设备算力允许,优先选择中等规模模型;只有在设备算力严格受限时才考虑轻量级模型,但要做好精度损失的心理准备。
输入分辨率同样关键。我用640x640分辨率训练时,远处信号的漏检率偏高;提高到896x896后,小目标的Recall提升了约5个百分点。代价是训练时间变长、推理速度变慢,但这个trade-off在交通信号这种安全关键场景中是值得的。如果你的设备实在跑不动高分辨率推理,后续可以再用小分辨率模型做迁移学习压缩,但初始训练仍然建议用高分辨率,让模型先学习到更精细的特征。
5.2 关键训练超参数的经验值
以下是我经过多轮实验后认为比较合理的超参数配置,可以直接作为起点:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| img size | 896 | 小目标多时提高分辨率收益明显 |
| batch size | 16-32 | 根据显存调整 |
| epochs | 200 | 配合早停策略 |
| optimizer | SGD或AdamW | 都行,SGD稳定,AdamW收敛快 |
| 初始学习率 | 0.01 | 前50个epoch用warmup |
| 预训练权重 | COCO预训练 | 迁移学习,收敛更快 |
训练过程我习惯开着早停机制,如果连续30个epoch验证集loss不再下降,就自动停止并保存最佳权重。实测下来大部分模型在120到160个epoch之间就已经收敛,没必要死磕到200个epoch。
5.3 训练过程中的常见信号与判断方法
训练中的loss曲线和验证指标需要结合起来看。我总结过几个判断模型状态的实用信号:
train loss持续下降,val loss也持续下降:模型还在学习,继续训练。
train loss下降但val loss反弹上升:过拟合了,需要增加数据增强强度,或者降低模型复杂度,或者提前停止。
train loss和val loss都停在某个值不动:可能是学习率太低或者模型容量不够,试着调高学习率或换更大的模型。
类别loss单独偏高:某个类别(比如倒计时区域)的loss明显高于其他类别,说明这个类别的特征不够区分,需要补充该类别的样本,或者单独调整该类别的损失权重。
除了loss曲线,验证集的指标同样重要。交通信号检测场景中,我特别关注Recall而不是Precision。原因很简单:漏检一个红灯可能导致车辆闯红灯,这是安全风险;而误检的情况可以通过后处理逻辑(比如结合路况上下文)来过滤。所以在调优时,如果Precision和Recall出现冲突,我通常优先保证Recall。
我还习惯在验证集上输出混淆矩阵,专门检查类别之间的混淆情况。有一次发现红灯和倒计时区域混淆严重,模型经常把倒计时里的红色数字区域当成红灯。后来分析发现是这两类样本中,倒计时区域的框经常和红灯区域的框重合,导致特征重叠。解决办法是扩大了倒计时区域的标注范围,同时将它的类别定义从"数字"调整为"包含数字的显示屏区域",与红灯的边界区别开,混淆率明显下降。
6. 部署环节的误检漏检与应对方法
6.1 误检的来源比想象中复杂
模型训练完成后,部署到实车或边缘设备上,新的问题才真正暴露出来。我在实测中遇到的最典型误检,是模型把一些"看起来像信号灯"的物体识别成了交通信号。
红色广告屏:路口商场外墙的大型LED屏,当画面中出现大面积的红色圆形象素区域时,模型会给出高置信度的红灯预测。
前车尾灯组:夜间排队等红灯时,前车的两个红色尾灯在YOLO的特征层面和红灯非常相似,尤其是当尾灯形成对称圆形时,误检概率很高。
红色交通标志牌:某些禁令标志是红圈加斜杠,红圈部分和红色圆形灯在颜色和形状上有一定相似度。
这些误检单纯靠模型很难彻底消除,我的处理思路是从数据和后处理两个方向同时入手:
数据层面,收集这些典型误检场景的负样本,加入训练集并标注为背景类别以外的干扰对象。这里有一个比较取巧的做法:单独增加一个"distractor"类别,把广告屏、红色标志牌、尾灯等标注为这个类别,让模型学会把它们和真正的信号灯区分开。相比单纯让模型把它们当背景,这种做法能强制模型学到"不是信号灯"的特征边界,效果更直接。
6.2 后处理与跟踪策略:结合时序信息消除跳变
后处理方面,一个低成本但很有效的方法是区域约束。信号灯在画面中的位置不是随意的,通常集中在路口区域的上方或路侧。部署时可以根据摄像头安装角度和位置,在图像上预先定义信号灯可能出现的ROI区域,推理时只保留ROI内的检测结果,ROI外的检测框直接丢弃。这个方法能过滤掉很大一部分广告屏误检,因为广告屏通常位于画面两侧或远处。
另一个方法是时间序列平滑。信号灯的颜色变化是有逻辑的,不会在一帧内从红跳到绿再跳回红。我在部署代码里加了一个轻量级的跟踪器,对每个信号灯目标做跨帧关联,记录其在最近N帧中的颜色状态。如果某个目标在连续帧中频繁变化颜色且变化顺序不符合交通信号逻辑,就判定为误检,降低其置信度。实测下去,这个策略让夜间尾灯误检率降低了约60%。
6.3 小目标漏检与推理性能的平衡
小目标漏检是另一个部署痛点。远处路口的信号灯在画面中确实很小,即便训练时用了896分辨率,部署推理时如果为了帧率把输入分辨率降到640,小目标检测能力还是会打折扣。
这里我给出几种实测有效的方案:
方案一:推理时保持高分辨率输入。如果设备帧率允许,推理分辨率直接沿用训练时的896,不做压缩,这是最简单直接的办法。
方案二:切片辅助推理。把高分辨率图像切成多个重叠切片,分别送入模型检测,再合并结果。思路类似"放大镜",专门解决小目标问题。缺点是推理时间翻倍,但对远处信号灯漏检改善非常明显。
方案三:模型轻量化与量化。如果最终部署设备算力紧张,可以使用通道剪枝或量化方式压缩模型。量化时需要注意校准数据的选择,我通常从训练集中挑出包含各种光照条件的代表性图片作为校准集,量化后的模型精度损失控制在2%以内。
关于帧率,我的经验是:在通用边缘设备上,中等规模模型加高分辨率输入大概能跑到20到30毫秒一帧,对信号灯检测这种实时性要求不极高的场景来说完全够用。如果追求更高的并发处理能力,可以再考虑批次推理和异步流水线,但那是部署细节里的优化方向了。
最后说点个人体会。这个项目让我印象最深的,不是YOLO模型本身有多强大,而是数据在目标检测任务中的决定性作用。同样的模型结构、同样的超参数,使用自建的高质量国内交通信号数据集训练,和使用网上随便下的开源数据集训练,验证集mAP能差出十个百分点以上。交通信号检测是一个安全相关的场景,模型的每一次漏检和误检都可能影响实际决策,这种场景下更值得在数据上下笨功夫。如果你也在做类似的项目,我的建议是:不要急着换更大的模型,先回头审视你的数据集是否真实覆盖了你的部署场景,数据的场景多样性、标注一致性和类别边界清晰度,往往比模型结构更能决定最终效果。