首页 / 资讯中心 / 文章详情

基于YOLOv9的红绿灯检测与分类实战:小目标、训练调优与部署避坑指南

基于YOLOv9的红绿灯检测与分类实战:小目标、训练调优与部署避坑指南 ★ FEATURED ARTICLE
做了几年视觉检测落地我一直有个感触越是最常见的场景往往越容易在阴沟里翻船。红绿灯识别就是典型例子。乍一看不就是把YOLO跑起来、训一个“交通灯”类别吗等你真的把模型接到实车视频里面对烈日逆光、夜间灯珠过曝、远处信号灯只有十几个像素、前车尾灯疯狂闪烁干扰这些场景就明白这绝不是换个数据集重训一下这么简单。这篇文章想分享的是我做的一个基于YOLOv9的交通信号灯检测与分类项目完整走了一遍数据准备、标注策略、训练调优、部署推理和误报排查。YOLOv9在红绿灯这个任务上的表现确实让我比较满意但更值钱的是过程中踩过的坑和总结出来的一套处理思路希望能给同样在做辅助驾驶、智慧路口、V2X感知的朋友一些直接能用的参考。1. 红绿灯识别为什么比普通目标检测更别扭先聊一个很多人没意识到的问题红绿灯检测的目标定义其实比COCO那套80类任务要别扭得多。1.1 核心难点既要检测信号灯位置又要判断灯色状态普通目标检测比如检测行人、检测小轿车本质上回答的是画面里有什么、在哪。但红绿灯识别必须回答三个问题画面里有没有信号灯信号灯在哪个位置当前灯色是什么状态是红灯、绿灯、黄灯还是左转箭头灯、倒计时数字第三点才是工程落地的灵魂。很多初版方案直接把traffic light当成一个类别去训结果模型倒是能框住灯业务方却拿着结果质问框住了有什么用到底是红灯还是绿灯你倒是告诉我啊。像我这次做系统设计就把类别彻底拆开了。一个信号灯杆上可能同时存在多个灯组每个灯组独立类别红灯圆盘/方向指示绿灯圆盘/方向指示黄灯右转箭头红灯右转箭头绿灯倒计时数字如果画面里亮度够可以单独检测这里要注意一个细节同样是红灯圆盘红灯和箭头红灯在业务语义上是完全不同的。圆盘红灯意味着直行、左转都得停但右转在确保安全的前提下我国很多路口是可以过的箭头红灯则必须停。如果只统一标一个Red类下游的驾驶决策根本没法做。1.2 小目标问题有多严重十几像素的信号灯真的很难搞红绿灯在1080p画面里的尺寸远比想象中要小。我实测过一批路侧摄像头的素材在距离路口50米开外一个标准信号灯灯珠在图像里通常只有12~20像素宽。这种尺寸在目标检测里属于典型的极小小目标tiny object特征少、容易被backbone下采样丢掉。普通行人目标通常有几十甚至上百像素YOLO这类anchor-based或者anchor-free架构设计之初是优先服务中大目标。模型下采样32倍后一个18像素的信号灯在最后特征层上连一个格子都占不满梯度信息很难回传。这也是为什么用默认配置训COCO预训练权重会出现训练集mAP很好、一到实景就漏检的经典事故。1.3 光照和曝光带来的类内差异爆炸红绿灯是自发光物体这对检测来说是个麻烦。白天日光下灯珠亮度低和背景对比弱夜间灯珠过曝颜色信息直接溢出成白色逆光时信号灯杆整个处于剪影状态下雨天玻璃反光灯珠边缘泛光。同一个绿灯在不同时间、不同天气、不同相机参数下成像的差异甚至比小红人到大卡车的差异还大。这意味着模型不能只依赖颜色通道做判断还得学会结合灯珠形状、灯杆位置、周围背景来做上下文推理。我给训练集做增强时加了很大幅度的亮度扰动和Gamma变换模拟从正午强光到凌晨暗光的各种情况这件事在后面数据准备章节再展开。2. YOLOv9选型逻辑为什么是它以及哪些架构特性真正起了作用选型这事我不想说得太玄乎直接摆结论我从一开始就把焦点锁定在YOLOv9上面理由有三条。2.1 信息瓶颈问题与可编程梯度信息YOLOv9发布时最核心的宣传点是Programmable Gradient InformationPGI可编程梯度信息。这个概念很多文章讲得很绕我用大白话翻译一下。深度学习训练时浅层网络需要从loss回传的梯度里学到该关注什么。但传统深层网络在梯度回传过程中会不断丢失信息特别是对于小目标、细节纹理少的物体浅层根本拿不到有效的梯度信号——这就是所谓的信息瓶颈information bottleneck。YOLOv9引入了可逆分支结构相当于给梯度信息加了一条快车道让浅层网络即使是面对很小的目标也能拿到足够清晰的回传信号。红绿灯恰恰是极度依赖浅层细节特征的检测任务。灯珠边缘、颜色分界、过曝后的光晕形态这些特征如果被深层网络稀释掉框就会飘、漏检就会频繁。用YOLOv9之后我对小灯的召回率提升体感非常明显这跟PGI机制有直接关系。2.2 和YOLOv5、YOLOv8、RT-DETR的横向对比我当时做了一个快速比较从几个维度拉了一张表模型小目标潜力训练成本部署生态我的评价YOLOv5中规中矩需要额外加P2层很低极成熟适合快速验证但上限一般YOLOv8有Anchor-Free改进但小目标仍一般低成熟通用场景首选红绿灯差点意思YOLOv9强PGI对浅层梯度友好中低有官方TensorRT方案我最终选的方案RT-DETR 等端到端DETR系强但小目标收敛慢高需要大算力依赖定制效果上限高落地成本也高我的结论是红绿灯这种目标数量不会太多、但尺寸特别小、且极其依赖颜色细节的任务YOLOv9的梯度保留设计是实打实有用的性价比比无脑上DETR要高得多。2.3 模型规模选择不是越大越好YOLOv9提供了t/s/m/c/e几个规格。我最终采用的是v9-s中杯尺寸作为训练基线另外单独训了一个v9-c大模型用来做知识蒸馏的Teacher还训了一个v9-t小模型专门给低算力设备用。分享一下我的判断逻辑目标数量少一个路口最多同时出现几个信号灯组不需要超大head容量小目标需要的是特征保真度不是特征广度模型plenty太大反而容易在小数据集上过拟合灯杆背景边缘设备比如车机的Jetson系列、路侧盒子的NPU推理速度和功耗都有硬指标s档在性能和算力消耗之间最平衡。所谓YOLOv9很适合红绿灯并不是某个魔法般的效果但PGI机制、成熟的仓库和灵活的规模选项确实让我在这个场景上少折腾了很多。3. 数据准备信号灯数据集的标注策略与坑红绿灯识别项目的成败七成在数据。模型结构再先进喂进去的标注是一张图一个框包住整根灯杆输出永远没法用。3.1 数据来源与自采方案首先我用了几个公开信号灯相关数据集做底盘。这些数据集覆盖面比较广但是有个问题它们的标注风格和类别定义各家不一致有的把整个信号灯框成一个box有的把灯杆和灯珠分开标注。我在使用时统一做了二次清洗和重新标注。其次我自采了一大批真实路口数据。具体做法是行车记录仪和路侧固定摄像机同时采集覆盖不同方位正对路口、斜侧45度、近距离停车线、远距离逼近不同天气晴天、阴天、雨天、雾天不同时段早晚高峰背景车多、深夜路面空旷灯珠过曝、黄昏环境光和灯光混合不同路口类型十字路口、丁字路口、多车道大路口、非标准临时路口。这里给一个关键提醒划分训练集和验证集时要按路口划分不能按视频帧随机划分。否则同一个路口的连续帧会同时出现在训练集和验证集里验证集指标会虚高得离谱部署到新路口时马上露馅。我第一版就是因为按帧切分验证集mAP刷到了0.91后来按路口切分只剩0.84这才是真实水平。3.2 标注类别体系具体到业务语义的粒度我的标注分类表如下类别名业务含义说明R圆盘红灯直行/左转禁止G圆盘绿灯允许通行Y黄灯警示通常出现在红绿切换之间ALR左箭头红灯强制左转禁止ALG左箭头绿灯左转专用相位AR_R右箭头红灯部分场景用右转禁止AR_G右箭头绿灯部分场景用右转允许CNT倒计时数字区域可选用于时间预估后处理很多人会质疑这样标注类会不会太多导致样本稀疏我的经验是在有5000张以上有效图的情况下这个粒度完全撑得住而且带来的业务收益远大于标注成本。因为下游如果只拿到一个Red还得靠几何位置猜是圆盘还是箭头猜错一次就是一次违规。3.3 一盏灯会被遮挡、熄灭、起雾标注时怎么办数据里必然存在灯珠被树枝挡住灯珠故障不亮镜头沾水导致灯光糊成一片这些情况。我的处理原则是灯珠被部分遮挡但肉眼仍能判断颜色和形状正常标注且不能裁剪掉遮挡区域灯珠完全熄灭不要标注为熄灭灯而是放弃这个样本除非你打算单独做一个故障灯检测分支灯珠因镜头水渍完全模糊放弃标注避免让模型学习糊某种颜色的错误关联。不少团队在处理脏数据时舍不得丢样本结果模型学会了用背景颜色猜灯色比如蓝天反光就被判成绿灯这是典型的标注污染。3.4 针对小目标的增强策略不要无脑MosaicUltralytics的训练默认打开Mosaic但对红绿灯这种极小目标Mosaic把四个图缩放到一张图后灯珠往往只有五六像素反而进一步恶化了小目标的学习难度。我的做法是关闭或大幅调低Mosaic的启用比例或者改用Copy-Paste策略把信号灯小目标从其他图里切割出来随机粘贴到当前图的角落、远处、路灯附近等合理位置人为增加正样本数量用亮度抖动、Gamma变换、局部过曝模拟覆盖夜间过曝场景不做随机裁剪会切掉小目标改用保持原图比例的整体缩放。这一套增强下来小目标验证集的漏检率大约降了三分之一。4. 训练过程与调参实录4.1 从COCO预训练还是从零训练我的选择是用COCO预训练权重做初始化然后在自建红绿灯数据集上做全量微调。直接从零训练一个YOLOv9-s在几千张红绿灯图上也能收敛但泛化到不同城市的道路环境会差不少。COCO预训练权重虽然类别跟红绿灯完全不搭但它已经学会了边缘、纹理、光照变化、物体结构这些通用视觉特征这对小目标特征提取是白捡的好处。一个具体的风险COCO里没有红色灯珠这种自发光小物体所以预训练权重的浅层滤波器未必对过曝光斑敏感。因此我在微调的前几个epoch把backbone学习率调低、head学习率调高让head快速学会信号灯的形状特征而backbone保留通用的纹理能力后面再统一微调。4.2 输入分辨率、patch大小和多尺度训练红绿灯太小我直接把训练输入从默认的640x640拉到了1024甚至1280。这里要算一笔账1080p原图里20像素的灯珠缩放到640p输入后只剩约12像素缩放到1280p后能保留到24像素。特征层的分辨率翻倍对极小小目标的召回提升是显著的。代价是显存占用提升、训练时间延长。我的batch size从16降到了8靠梯度累积来弥补稳定性和BN统计的问题。多尺度训练时我在640~1280之间随机拉伸。注意不要用正方形拉伸要配合原始宽高比做随机缩放后再padding避免把灯珠拉成椭圆形让模型学到错误的形状先验。4.3 关键超参数和NMS策略总结超参数我的配置备注输入尺寸1024x1024训练1280x1280精调加大输入是红绿灯mAP提升最直接的手段优化器SGD带动量0.937比AdamW在检测头收敛上更稳基础LR0.01warmup 3 epochsbatch8时配合梯度累积等效batch32的LREpochs150早停在约120轮数据集大概1.2万张标注图类别数9按上节方案类别粒度细化有利于梯度区分度NMS IoU阈值0.5信号灯候选框较少不需太高Conf阈值训练用0.1统计部署用0.35部署阈值高了会漏检低了误报多要按误报代价权衡4.4 指标分析红灯容易学绿灯难搞训练完成后验证集上的mAP50大约在0.88mAP50-95大约在0.63。按类别拆开看很有意思红灯类mAP最高接近0.92。原因很简单红色灯珠在暗背景下是全局最强的红色信号与环境的区分度大绿灯类和倒计时数字类最容易混主要原因是白天LED显示屏上的绿色数字、绿色路牌反光都会干扰黄灯类别样本少尽管mAP不低但方差大换一个路口就掉。这提醒了一点红绿灯识别难的点不在找出红色圆点而在于区分哪些是信号灯、哪些只是红灯样子的背景物体。5. 部署与工程化落地从模型到能用5.1 推理优化TensorRT FP16与端到端延迟训练完的PyTorch模型只是半成品。我的部署链路固定三步导出ONNX - 转为TensorRT FP16 - 用TensorRT C/Python API推理。在Jetson Orin设备上实测YOLOv9-s处理1280x1280输入FP16推理单帧耗时大约14毫秒。再加前后处理图像缩放、NMS、类别映射端到端延迟稳在25ms以内完全能满足车端实时性要求。这里有一个容易忽略的点输出特征层的后处理时要把小目标的置信度阈值设得比普通大目标低。因为红绿灯在远距离时的置信度天然偏低我用了一个尺寸越小、置信度阈值越低的动态策略能额外捞回不少漏检。5.2 时序平滑单帧判断不能信模型再强单帧预测也难免抖动。实车落地时我做了连续多帧投票逻辑只有连续3帧以上检测到同一位置且状态相同才输出状态变更事件状态从红灯切到绿灯时必须连续2帧确认防止灯色跳变导致的误刹使用卡尔曼滤波对灯框中心位置做轻量跟踪把同一盏灯的跨帧检测关联起来。这套逻辑在减少瞬时误报上作用显著。尤其是在夜间对向车辆的LED大灯扫过画面时经常会有1~2帧把白色光斑误检成信号灯的情况多帧投票直接过滤掉了这种瞬时干扰。5.3 与业务系统的联动周期预测与警示策略检测出现在是什么灯色只是第一步真正有用的系统还会结合信号灯历史状态做相位预测。我在部署时将每个路口的信号灯ID、历史状态切换时间戳存下来建立简单的状态机模型。当某盏灯连续30秒都是红灯模型突然输出一帧黄灯即使单帧置信度稍低我也会提高接受概率因为黄灯本应出现在红绿切换瞬间。反过来如果某盏灯刚刚从红灯变绿灯仅过了2秒又检测出红灯那基本可以断定是误检因为现实中绿灯最短相位也需要10秒以上。这种检测时序先验的结合比纯单帧检测可靠太多。也是我认为红绿灯识别项目里最值钱的工程经验之一。5.4 不同摄像头的泛化问题实际部署时车端摄像头和路侧摄像头的安装角度、焦距完全不同。同一个模型直接互相套用会明显掉点。我的应对方案是做传感器域自适应在推理管线里加入自动白平衡和ROI裁切预处理让不同相机的成像尽量对齐然后每个相机类型单独微调最后一层检测头。微调量很小每个场景几十张图就够了效果却能把掉点的几个点都补回来。6. 踩坑记录排查链路分享这部分写几个我真实遇到、花了很多时间才定位的坑希望能让大家少走弯路。6.1 坑一红色大货车车厢被识别成红灯现象项目第一版上线后频繁出现正在直行却突然报警的情况。查看日志发现模型把一辆红色集装箱货车的车厢侧面识别成了红灯置信度还高达0.6。排查链路先看原始检测框的位置发现框落在车身中部而不是车尾灯位置查看训练数据发现红灯标注中有相当一部分是路口远处的小红点模型学到了大范围红色区域红灯的错误关联用热力图可视化如grad-CAM确认模型确实在关注红色纹理而不是灯珠结构定位根因数据集中包含大量红色车尾灯未标注样本被当作背景学进了负样本分布而红色车厢这类大面积纯色块和灯珠有共同的红色特征。解决我专门挖了一批包含红色车辆、红色广告牌、红色雨伞的易混淆负样本加入训练集并标注为背景。同时把红灯类别的loss权重稍微拉高提升模型对红色形状先验的依赖。当时这套组合拳下来红色车厢误报基本清零。6.2 坑二夜间过曝导致白灯误判现象夜间测试时模型把过曝成白色的LED交通信号灯框住但类别完全随机有时候是Yellow有时候是Green有时候干脆输出两个类别。排查链路先把过曝帧单独导出来发现灯珠中心完全白化颜色信息已经被相机裁切掉了看标注夜间素材中确实有大量灯珠看起来白色但因为环境我知道它是红灯的样本模型被迫做了一个白灯不确定状态的映射尝试用图像处理方式在推理前做局部去曝光效果一般反而引入额外延迟。解决在训练增强环节专门加了一个过曝模拟变换——把灯珠区域亮度拉升到255截断并给样本同时标注原始正确类别。这样模型学到的是即使灯珠中心变白周围仍有红色泛光和灯壳结构依然可以判断原始灯色。最终夜间白灯的误判率下降了约一半。6.3 坑三验证集指标很好但实车一开就露馅现象这是最伤的一次。训练完mAP50高达0.91团队都准备上线了结果实车在傍晚跑了一圈漏检率明显尤其是逆光路口的信号灯几乎全漏。排查链路先怀疑是部署精度问题检查TensorRT FP16和PyTorch之间的精度差发现确实有差异但不大不至于这么明显把实车视频帧导出按角度、时段、天气做分层分析发现漏检集中在逆光、侧逆光场景回头看训练集分布发现大部分素材是顺光和均匀光照下拍的逆光样本占比不足5%根因模型没见过足够多背对太阳、灯杆剪影化的场景一旦出现强逆光整体特征分布偏移mAP再高也枉然。解决专门补采了一轮傍晚和清晨逆光数据同时用光度失真ColorJitter 大幅度降低曝光做增强。调整后再测逆光场景的召回率拉回到了正常水平。6.4 坑四倒计时数字训练了很久精度还是很差现象CNT类别倒计时数字区域的AP始终在0.5左右怀疑是标注问题或者类别定义问题。排查链路抽查标注样本发现数字区域框不是标准矩形有些把边框也包进去了模型学到的是模糊的红色矩形发亮区域而数字本身特征被噪声掩盖看数据分布很多路口根本没有倒计时器CNT样本只占总量的7%类别极不均衡试过用OCR方案单独识别数字又与检测任务耦合很深。解决我最后把这个类别从主检测任务里降级了。如果业务方只需要知道还剩大概几秒可以用检测出的灯色切换频率来估算倒计时而不是直接识别数字。这个方向留给专门的OCR模块处理效果更好也避免拖累主任务。7. 最后再分享两个实用细节第一个是评估指标要分层看。整体mAP会掩盖很多问题一定要按距离、按天气、按时段拆开观察。我在项目后期专门建了一套场景穿透表算每个场景子集下的召回率和误报率。因为红绿灯识别的末级判断不是由整体mAP决定的而是由最差场景的稳定性决定的。第二个是建议项目一开始就保留原始大图标注元数据的存档结构。后面想补充训练、复现问题、对比实验随时能从原始素材出发重新跑。省掉了一次次翻原始视频的功夫也不容易把中间产物弄丢。红绿灯识别这个项目技术栈说破天也就那些东西YOLOv9、TensorRT、数据增强、时序滤波。真正拉开差距的是能不能把整个链路里的细节做扎实尤其是在小目标特征保留、易混淆负样本、场景覆盖这几个环节上多花功夫。希望我的这次实践能帮后来者少踩几个坑。
阅读完成 · 觉得有帮助?
咨询建站