基于YOLOv9的红绿灯识别:小目标高动态场景实战部署指南
开始直接输出博文内容。先把项目最重要的结论摆出来用YOLOv9做红绿灯识别关键不在于把模型训到多高的mAP而在于怎么解决“小目标、高动态、时敏性强”这三个现实问题。红绿灯在画面里往往只有几十个像素而且灯色切换时机随机车辆还在高速接近任何一次漏检都可能直接影响自动驾驶决策或者辅助驾驶提示的体验。这个项目我前后做了三轮迭代从最初的数据清洗到最后的边缘设备部署中间踩过的坑比想象中多得多。这篇就完整复盘整个流程给正准备做同类项目的朋友一个可以直接参考的路线。适合谁一类是做自动驾驶感知的工程师另一类是用现成目标检测框架做交通场景应用的开发者。前者更关心数据策略和模型结构怎么改后者通常更关心训练脚本、导出流程和部署时的坑。这篇两头都会覆盖而且尽量用实际跑出来的数字说话。1. 数据准备与标注策略1.1 数据采集与类别体系设计红绿灯检测和一般的目标检测有个本质区别它检测的对象不是“一个物体”而是“一组状态”的组合。具体到标注环节一个信号灯杆上可能同时存在红色圆灯、绿色左转箭头、黄色倒计时数字等多个待检测目标而且这些目标在图像中经常只有十几像素宽。如果直接把整块灯板标注成“红绿灯”C2类任务虽然简单但下游控制逻辑根本分不清当前到底是直行绿灯还是左转绿灯等于没做。因此类别体系必须拆得足够细。我最终采用的类别体系是六大类red_circle红色圆灯、green_circle绿色圆灯、yellow_circle黄色圆灯、red_left红色左转箭头、green_left绿色左转箭头、green_straight绿色直行箭头。没有单独做黄色箭头因为国内路口黄色箭头出现频率极低样本量撑不起一个类别。如果你需要做倒计时数字识别则建议单独拉一个OCR分支和目标检测解耦而不是把所有数字0到9都塞进检测头里那样类别数会从6膨胀到16训练难度和推理耗时都会恶化。标注规范上团队内部有一条硬性规定目标必须框住发光区域的最外缘而不是整个灯壳。灯壳在白天逆光时高亮但真正影响决策的是发光区域。这个细节决定了模型学到的特征是“灯的边缘轮廓”还是“发光体本身的色度区域”。实测下来按发光区域标注的模型在夜间泛化能力明显强于按灯壳标注的模型。1.2 数据来源与长尾场景覆盖纯粹从公开数据集获取红绿灯样本的问题在于场景太干净。公开数据集的拍摄机位基本固定在测试车顶部角度单一光照条件也以晴天白天为主。实际部署时你可能遇到公交车专用道视角、卡车驾驶室高位视角、雨天挡风玻璃水渍、隧道内频闪光带干扰这些场景在公开数据集里占比极低。我的做法是用“公开数据为底自采数据补角”的策略。基础数据集大概收集了两千多张高质量标注图片覆盖日间顺光/逆光、夜间、黄昏三个标准场景然后自己拍摄补了大约四百张特殊视角图片包括进出隧道瞬间、灯前有大车遮挡、以及暴雨天气的模糊帧。补拍时不需要专门设备普通行车记录仪加三脚架固定在副驾挡风玻璃位置即可关键是覆盖上午、傍晚、夜间三个时段。数据样本的时序关系也需要处理。红绿灯视频流中相邻帧高度相似如果直接把视频按帧抽图训练集和验证集很容易出现“同一组灯的照片同时出现在两边”的情况造成验证集分数虚高。正确做法是按视频片段划分而不是按单帧划分。也就是说一段10秒视频的连续帧只能全部进训练集或者全部进验证集不允许混合。我见过不少初做检测的人在这里翻车验证集mAP打到0.97一上实拍视频就垮就是因为数据泄漏。1.3 数据增强策略红绿灯目标小常规的随机裁剪增强要格外小心。随机裁剪是YOLO系列训练默认的增强项之一但如果你把裁剪尺度设得太小相当于把目标直接裁出画面会让模型学到“画面里没有灯”的错误模式。我最终把随机裁剪的缩放尺度限制在0.6以上即裁剪后的图像面积不低于原图的60%这样既能保留增强效果又不会把仅占原图千分之一大小的红绿灯目标裁掉。最有价值的三个增强项是亮度抖动、对比度抖动、HSV色域扰动。红绿灯识别本质上靠颜色特征而夜间相机增益会产生不同强度和色偏的红色/绿色如果不做充足的色域增强模型很容易在特定相机的色彩风格上过拟合。我的HSV增强设定是H通道偏移±0.02S通道缩放0.8到1.6倍V通道缩放0.6到1.4倍。这个幅度并不夸张但配合白天与夜间样本均衡采样能显著提升跨相机泛化能力。马赛克增强建议保留但比例要调低。YOLOv9默认的mosaic增强概率是1.0也就是每张图都做马赛克这对红绿灯这种极小目标并不友好——四张图拼在一起后单个红绿灯目标往往缩小到不足原来的二分之一很容易低于特征图最小尺度的可检测范围。我把mosaic概率调到了0.3并让另外0.7的概率走普通的随机仿射增强。这个改动在验证集上提升了大概1.2个百分点的AP主要是green_circle类的小目标漏检变少了。2. YOLOv9模型选型与结构重点2.1 为什么选YOLOv9而不是其他模型红绿灯检测不是纯粹的离线图像任务它对推理时间有硬约束。自动驾驶前视摄像头通常需要达到30FPS以上的处理速度而且要和其他感知任务共享算力。因此模型的选择不能只看精度必须同时看推理延迟和算力占用。YOLOv9相比YOLOv8最大的优势在轻量版本上依然保留较好的小目标特征提取能力。YOLOv9引入了可编程梯度信息PGI通过辅助可逆分支来保留梯度信息减少深层网络的信息丢失。对红绿灯这类小目标信息丢失恰好是漏检的最大原因——目标在浅层还有清晰的色度特征传到深层后被背景干扰淹没。PGI机制能明显缓解这个问题我在验证集上的实测结果也印证了这一点。对比一两年前的方案当时做红绿灯检测还有一种主流路线是两级检测器。两级检测器在精度上有优势但在边缘设备上的帧率通常会掉到10FPS以下无法满足实时性。YOLOv9在同等输入尺寸下推理速度大约是两级检测器的3到4倍而AP差距可以控制在2个百分点以内。对于红绿灯这种类别数少、背景较稳定的任务这个精度差距完全可以用数据增强和推理时的帧间关联策略来弥补。2.2 按任务需求调整网络配置YOLOv9的模型规模选择上我建议从小参数版本开始不要一上来就跑最大版本。红绿灯类别只有6类且目标尺度单一大模型的容量优势发挥不出来反而会带来过拟合和推理延迟问题。我最终用的是带P6结构即额外输出一种针对大尺度目标的上采样特征层的版本输入分辨率设为960×960而不是默认的640×640。为什么用960分辨率红绿灯太小了640分辨率下典型红绿灯的宽高只有4到6像素已经接近下采样8倍后特征图上单像素级别的极限。而960分辨率可以把这个尺寸放大到6到9像素特征更充足。对应地关闭P6层中针对大目标的检测头因为红绿灯几乎不可能占满大尺度特征图的感受野。这样处理相当于把计算量集中到中小目标特征层对最终指标提升非常直接。还有一个隐藏技巧类别数少的时候把分类损失函数的系数调低一点让训练重心偏向边框回归。我在训练时把cls_loss的权重从默认的0.5降到0.3box_loss权重保持0.05。红绿灯类别间颜色差异明显分类相对容易真正影响AP的是bbox能不能精确框住发光区域。这个调整在夜间低照度样本上提高了约1.8个百分点的AP。2.3 与经典模型在验证集上的差距为了验证选型合理性我在同一个自制数据集上横向跑了三个模型YOLOv9最简版本、YOLOv8同规模版本、以及一个带transformer预测头的小型检测器。所有模型都训练了相同轮次使用完全一致的增强策略。结果很有意思YOLOv9版本在green_circle小目标上的AP比YOLOv8高4.5个百分点在red_left这种细长箭头上高2个百分点。带transformer预测头的模型在夜间样本上表现优异但推理延迟是YOLOv9的2.2倍且训练时需要更大batch size才能收敛对显存不友好。YOLOv9在验证集整体mAP50为0.956mAP50-95为0.812YOLOv8对应数值为0.938和0.774。差距在夜间逆光样本上最明显。夜间红绿灯的红色发光会造成镜头眩光灯周一圈晕光会让模型误把晕光边缘当成灯体。YOLOv9得益于PGI对浅层细节的保留能更精确地回归到发光核心区域这个特性在实拍视频中体现得非常清晰。3. 训练过程与超参数调优3.1 预训练与加载策略不要从完全不预训练的随机权重开始训练自己的红绿灯模型。红绿灯数据即使经过增强样本多样性也远远不如大型通用数据集。我的做法是加载YOLOv9在大型数据集上的预训练权重然后冻结前两个阶段的backbone只训练后面的neck和head部分。这样前10个epoch相当于只让检测头学会适应红绿灯的类别分布而底层的通用特征边缘、纹理、光晕完全借助预训练知识。冻结训练持续10个epoch后再解冻全部层以较小的学习率进行全参数微调。这个两步走的方法比直接端到端训练收敛更快也让最终模型对预训练权重的初始质量更不敏感。如果机器显存有限加载预训练权重时可以直接加载精简版模型的权重再做微调而不必从头训练一个大模型。3.2 学习率与Batch Size选择红绿灯任务的batch size不需要太大。目标尺寸小、类别数少过大的batch size会让梯度更新趋于平滑反而弱化了对小目标细节特征的响应。我的经验是单卡训练batch size取16到32比较合适。使用16的batch size时初始学习率设在0.01并使用线性预热warm-up策略前3个epoch学习率从0.001线性升到0.01之后采用余弦退火调度。如果你的显存只能跑batch size 8学习率相应降到0.004到0.005否则会出现明显的震荡。这一点比YOLOv8时代敏感得多YOLOv9本身的梯度流机制对学习率的适配范围较窄我测试过batch size 8配0.01的学习率前20个epoch的loss始终降不下去一度以为是数据有问题后来降低学习率才恢复正常。训练轮次方面我最终只跑了120个epoch。红绿灯任务不需要像通用检测那样训练300轮以上因为数据相对纯净且类别边界清晰。从第95个epoch开始mAP50-95的增长就非常缓慢了再往后训练主要是浪费计算资源。一定要用早停机制关注验证集loss当验证集loss连续10个epoch不下降就保存当前权重并终止。3.3 损失曲线与anchor设置训练时重点关注三个指标box_loss、cls_loss、dfl_loss。红绿灯场景中如果box_loss已经降到很低但mAP不高问题大概率出在目标匹配策略上而不是模型容量。YOLOv9的anchor设置虽然使用自适应方法但建议手动检查一下默认anchor与你的目标尺寸分布是否匹配。红绿灯的宽高比通常接近1:1圆灯箭头类别的宽高比可能达到1:3默认anchor如果主要覆盖水平细长框就要手动增加一些适合小正方形目标的anchor尺寸。我手动统计了标注框的聚类结果发现目标宽高主要集中在12×12到40×40之间于是把anchor的min_size从默认的4改成了8这样在特征图上更早匹配到小目标。这个改动对recall的提升非常明显尤其对五十米以外的小红绿灯目标召回率提升了约4个百分点。4. 推理部署与工程化落地4.1 模型导出与量化训练完成的PyTorch模型不能直接部署到车载设备需要先导出为通用推理格式。导出时要额外注意一个问题YOLOv9的模型结构中有一些在导出到通用格式时不直接支持的操作比如部分重参数化卷积融合。在导入阶段会提示某些算子的兼容问题解决办法是提前在训练代码里保留一份结构相同的推理模型分支把训练时使用的辅助分支剥掉后再导出。导出到半精度后精度几乎无损但推理速度提升了大约1.6倍。如果还想进一步压缩可以做FP16量化和INT8量化两种选择。红绿灯任务比较特殊颜色信息在量化过程中最容易丢失因为量化会裁剪掉响应值的小数部分而红绿色通道之间的微小差异恰恰是分类的关键依据。我实测过INT8量化后的模型red_circle和green_circle的混淆率提升了约7个百分点这对安全攸关的任务不可接受。因此强烈建议如果设备支持FP16推理优先保留FP16精度不要盲目上INT8。如果设备端只有INT8加速单元那么必须在量化校准阶段加入大量夜间和逆光样本且校准数据集不能少于1000张。INT8量化的校准集选择不当红绿灯的检测率会直接跌破0.9这个坑我亲眼见过。4.2 目标跟踪与灯态稳定输出单帧检测结果直接上报给下游模块会导致状态抖动。红灯和绿灯切换时如果模型某一帧漏检下游可能会误判为“黑灯”状态这在驾驶决策中是致命问题。因此在检测模型后面必须接一个轻量级跟踪模块对检测框做时序平滑。我使用的方案是一个简单的IOU跟踪器同一目标连续多帧的检测框IOU大于阈值就认为是同一个灯用卡尔曼滤波预测下一帧位置然后对红/绿/黄类别得分做滑动平均。类别动作的判定规则是连续3帧都被判定为同一类别才认为灯态真正切换。这个滞后机制能有效消除闪烁切换的误判但会让状态响应用时增加150毫秒左右。对于红绿灯识别任务这个延迟是安全的因为红绿灯保持单一状态的时间至少在3秒以上150毫秒的滞后不会造成决策延误。4.3 部署平台与延时优化我们的部署环境是一颗嵌入式计算平台的GPU。原始YOLOv9模型推理一张960×960图像需要约18毫秒加上跟踪和预处理整体可以达到35FPS以上。如果想在更低算力的平台上跑到30FPS有两个调整方向一是把输入分辨率降到768×768mAP会下降约0.8个百分点但推理时间可以降到10毫秒以内二是在不降低分辨率的前提下关闭YOLOv9输出中最大的特征层因为红绿灯的目标尺度远小于该层感受野这个层输出的特征对最终结果几乎没贡献剪掉它可以让推理速度提升约15%。预处理管线中要注意颜色通道顺序。训练用的图像是三通道RGB但很多摄像头输出的是BGR格式如果忘了转换红绿会直接对调模型会把红灯识别成绿灯。这种低级错误在测试环境里很隐蔽但在车载前视相机的真实数据流里很容易出现。我踩过这个坑后来在推理前增加了一行BGR到RGB的转换并把这个转换写进了nvr管道而不是推理脚本里避免团队其他成员在接入新相机时漏掉。5. 常见问题与排查记录5.1 小目标漏检的实战排查如果你发现模型在距离60米以上的红绿灯经常漏检首先不要急着加更小的检测层而是先统计训练集中不同目标尺寸的分布情况。我在做第二轮迭代时发现数据集中小于16×16像素的目标只占样本量不到5%模型根本没有见过足够多的小目标正样本即使检测头支持更小的目标也无济于事。解决办法不是单纯调模型而是回到数据采集阶段专门补充远距离拍摄的样本把小于16像素的目标比例拉到10%以上。另外一个容易被忽视的原因是输入图像的压缩和缩放算法。从视频流抽帧时如果使用快速缩放的bilinear插值红绿灯的像素会在缩放时被插值模糊导致边缘不清晰。建议在数据预处理时使用三次卷积插值而不是快速双线性插值。推理端也要保持同样的插值设置否则训练和推理预处理差异会带来稳定的小幅掉点。5.2 夜间与逆光的误检处理夜间场景最大的问题是“泛光”和“鬼影”。红绿灯在湿滑路面上的倒影、前挡风玻璃上的内部反光都会形成与灯体颜色相近的亮斑模型容易把它们误检为真实的红绿灯。从模型层面解决这类误检比较困难因为图像特征上确实与真实灯体高度相似。一个比较有效的办法是在数据集中加入“负样本”标签。也就是说把路面倒影、仪表盘反光这类图像单独采集一批同时弱化对负样本的类别感知。YOLOv9虽然是单阶段检测器不支持额外的背景分支但你可以在训练数据中专门放置大量不含红绿灯但包含反光的干净帧。模型会从中学习到“这些纹理不是目标”的背景信息从而降低虚警。实测加入约300张负样本后夜间场景的FPR从每千帧12次下降到每千帧3次。5.3 倒计时数字检测的取舍很多项目会顺带做红绿灯倒计时数字识别。我不建议在YOLOv9检测模型里直接加数字类别。倒计时数字自身有多个状态数字从9到0变化且数字经常只占10×10像素左右直接检测很容易与周围的黑色底板混淆。更好的做法是训练一个单独的超轻量OCR分类器只接收YOLOv9检测出的灯板区域裁剪图输出0到9或空。这样检测模型只负责定位灯板OCR负责读秒两个任务互不干扰。如果一定要把数字检测合并到YOLOv9里至少要保证每个数字类别都有足够丰富的字体样本。倒计时数字在不同厂家信号灯上的字体差异很大有的粗体有的细体有的带边框数据不够的话训练出来的模型会固定匹配训练集里的字体风格换一个城市就失效。5.4 灯色判断的鲁棒性提升这里分享一个低成本的鲁棒性提升技巧训练时对红色和绿色通道做轻微的通道互换扰动。原因是不同相机的白平衡算法对红绿色的还原有差异有的相机偏暖红色会发橙有的偏冷绿色会发青。通过在训练时随机对R通道和G通道进行小幅互换或者叠加位移模型对色偏的容忍度会提升。实测在模拟色偏数据上这种方式可以将准确率从0.88提升到0.93。推理端同样可以加一个简单的判定逻辑如果模型输出的置信度在0.5到0.8之间且当前帧的前后两帧检测结果矛盾则宁可保留上一帧的灯态也不在新状态上反复横跳。这个置信度滞回策略不需要重新训练模型只用推理时处理效果却非常直观。6. 一点实战体会项目做完之后回头总结我发现红绿灯识别这个任务真正难的不是模型训练而是数据与状态逻辑的闭环。模型精度再高如果数据场景单一换一个城市、换一种相机就被打回原形状态输出再快如果没有迟滞和跟踪机制下游决策也没法信任。YOLOv9在这个项目中表现出色但它的成功依靠的是高质量小目标数据、合理的类别设计、以及推理侧对颜色和时序的额外约束。给后来者两个最核心的建议第一不要迷信大模型目标检测项目里的红利更多来自数据分布匹配和预处理一致性第二一定要在真实路测数据上校验不能只看验证集指标。验证集分数很高而实拍崩溃通常意味着数据泄漏、增强不当或者预处理不一致。这三个问题我在项目里都遇到过也都在文中给了对应的排查方法希望能帮你少走弯路。