资讯动态

基于YOLO的交通信号灯识别系统:从数据标注到边缘部署实战

发布时间:2026/10/18 4:31:19 来源:尧图企业网站定制
深夜开车经过一个陌生路口导航突然提示“前方红灯请停车等候”那一瞬间我意识到这不只是导航地图的功劳背后很可能有一套实时视觉识别系统在悄悄工作。后来我花了两周时间用YOLO做了一个基于深度学习的智能交通信号灯识别系统从数据标注、模型训练、误检排查到轻量化部署完整走了一遍。这篇博文就是这次项目的全过程记录包含我在实际踩坑中总结的原理拆解、参数选择和部署经验适合正在做目标检测项目、或者想把YOLO模型真正部署到边缘设备上的开发者参考。信号灯识别听起来只是目标检测的一个普通应用但真正上手之后你会发现它和普通的目标检测任务差别很大。如果你直接拿一个预训练模型去识别路口信号灯大概率会得到一堆误检框和混乱的灯色预测。问题不在YOLO本身而是我们对任务的理解不够细致。1. 信号灯识别和通用目标检测的三个本质差异先说结论信号灯识别不是把通用目标检测模型拿过来就能用的任务它有三个非常特殊的地方直接决定了后续的数据标注、模型训练和部署策略。1.1 目标小到离谱远超COCO数据集的常规尺度通用目标检测数据集里的物体比如行人、汽车、猫狗通常占据画面面积的百分之几到几十。但路口信号灯不一样。一个标准的信号灯灯体在1080p画面里可能只有30×30像素左右单颗灯珠甚至只有8×8到12×12像素。这个尺寸在YOLO的特征金字塔里属于最深层的小目标分支信息经过多次下采样后已经非常微弱。我们在某城市路口试点项目中统计过信号灯目标平均占整张图像的0.35%左右而COCO数据集中小物体的平均占比通常在2%到3%。这是数量级的差距。所以直接用COCO预训练权重做迁移学习对信号灯的检测效果会很差因为预训练模型几乎没有见过这么小的目标。1.2 颜色是关键特征但YOLO的骨干网络不完全买账普通目标检测靠形状和纹理就能区分目标类别信号灯却主要靠颜色信息。红灯、黄灯、绿灯的区别几乎就是色相的区别形状完全一致。YOLO的骨干网络在Feature Extraction时倾向于提取边缘、纹理、形状特征颜色特征相对处于次要位置在深层特征图中甚至会互相干扰。这就解释了为什么很多初学者训练信号灯模型时绿灯识别率不错红灯却老是漏检因为夜间红色灯珠在图像中呈现高饱和高亮度与车尾灯的视觉特征高度相似网络很容易学糊涂。输入RGB空间里红绿蓝三个通道对模型提取颜色特征的贡献是不均衡的我们需要通过数据增强、色彩空间变换甚至特殊后处理来放大颜色差异。1.3 时序信息是天然优势但很多人浪费了它单帧识别信号灯很容易因为曝光、遮挡、车辆抖动导致瞬时误判但如果利用连续多帧的时序信息检测稳定性会大幅提升。交通信号灯本身在一个周期内灯色不会频繁跳变这是一个极强的先验。好多人忽略了这个信息把视频解码成一帧帧图片来做检测白白浪费了最可靠的时间维度。我在工程化阶段用了一个简单的帧间投票机制误检率直接下降了60%以上这个后面细说。2. 数据准备信号灯识别的成败80%在这里训练数据是信号灯识别项目里最枯燥但最关键的环节。我在这块花的时间占整个项目的一半以上效果也最明显。如果你只打算跑通Demo随便抓几百张图就能训练但要做成可用的系统数据准备的颗粒度决定了模型性能的边界。2.1 数据来源与场景覆盖我们的基础数据来自三个渠道一是路口高位监控视频抽帧这个视角最接近真实部署场景二是行车记录仪视频抽帧覆盖驾驶员视角三是有条件时用手持相机在路口补拍夜间和逆光场景。最关键的一点是场景覆盖必须多维不同时段白天、傍晚、夜间、凌晨、不同天气晴天、阴天、雨天、雾天、不同角度平视、俯视、侧视、不同距离近距离、中距离、远距离、不同路口形态十字路口、T型路口、环形路口。我见过太多项目因为夜间数据太少模型白天效果很好一到晚上就崩这是典型的过拟合到光照条件的问题。2.2 标注规范的颗粒度设计信号灯标注不要只标一个灯框要区分灯色和方向。我们的类别体系这样设计类别名含义备注red_circle红色全屏灯圆盘红灯green_circle绿色全屏灯圆盘绿灯yellow_circle黄色全屏灯圆盘黄灯red_left红色左转箭头灯箭头红灯green_left绿色左转箭头灯箭头绿灯red_right红色右转箭头灯箭头红灯green_right绿色右转箭头灯箭头绿灯为什么不直接标成red、green、yellow因为箭头灯和全屏灯在交通规则中的通行逻辑不同如果模型后期要对接控制逻辑必须能区分它们。即便你的系统只做红灯提醒箭头方向信息也能帮助你过滤掉很多误检。标注时还有一个细节框要尽量贴合灯体不要包含背后的黑色背板太多因为背板颜色在不同路口中差异很大框太大等于把噪声也学进去了。2.3 样本均衡与难例挖掘我们统计过初始数据集的类别分布red_circle和green_circle占了近70%左转和右转箭头灯样本明显偏少。如果直接训练小样本类别的召回率通常只有不到50%。解决办法是针对性扩充对箭头灯样本做水平翻转和旋转增强夜间过曝样本单独增加曝光模拟增强还在标注时特别标记了遮挡样本。最难处理的其实是部分遮挡情况比如树枝挡住半颗灯珠、大型车辆短暂遮挡灯体。这类样本对检测器来说是硬样本建议通过难例挖掘不断补充。我们的做法是把模型在验证集上的漏检样本全部收集起来人工复核后补充到训练集迭代了三轮之后漏检率显著收敛。2.4 YOLO格式转换与数据集划分标注工具用的LabelImg导出PascalVOC格式后需要转换成YOLO的txt格式。转换时最容易踩的坑是坐标归一化的基准搞错。YOLO格式的坐标是相对宽高的比例值类别的下标必须和配置文件里的类名顺序一致否则训练出来的模型张冠李戴。我写了一个小脚本做格式转换核心逻辑如下import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, class_list, output_dir): tree ET.parse(xml_file) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) txt_path os.path.join(output_dir, os.path.basename(xml_file).replace(.xml, .txt)) with open(txt_path, w) as f: for obj in root.findall(object): class_name obj.find(name).text if class_name not in class_list: continue class_id class_list.index(class_name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h f.write(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n)数据集划分我建议训练集、验证集、测试集按7:2:1并且要保证同一路口的图像尽量划分到同一个集合中不然模型会通过记忆背景信息获得虚高的验证精度换成新路口就泛化失败。3. 模型选型与训练调参从YOLOv5s到YOLOv8n的真实对比模型选型阶段我重点对比了YOLOv5s和YOLOv8n后面又测试了YOLOv8s最终综合考虑精度、推理速度、部署难度选了YOLOv8n作为基础版本做轻量化剪枝。3.1 为什么用YOLO系列信号灯识别需要实时性单帧推理延迟必须控制在几十毫秒以内。YOLO系列是典型的单阶段检测器速度和精度平衡得很好。两阶段检测器如Faster R-CNN系列精度可能高一点但推理延迟在边缘设备上很难满足实时要求。Transformer类的检测器在小目标上有优势但计算量偏大对边缘端部署不友好。YOLO在工程社区生态最好ONNX导出、TensorRT加速、量化工具链都成熟迭代速度快。具体到版本选择YOLOv8n相比YOLOv5s在COCO上的mAP更高模型体积更小head部分的解耦设计对小目标检测也有帮助。我实际用同一份信号灯数据集做了对比测试YOLOv8n在验证集上的mAP50比YOLOv5s高了2.3个百分点而模型参数量只有YOLOv5s的约三分之一这直接决定了部署阶段的帧率上限。3.2 输入分辨率与anchor的取舍输入分辨率我最终定为640×640。有人为了小目标检测把分辨率提到1280精度确实会提升但推理耗时几乎翻倍在边缘设备上完全不实用。对于信号灯这种小目标单纯提高输入分辨率不如优化标注质量和增加小目标样本的数量。YOLOv8的anchor-free设计省去了手动聚类anchor的麻烦但YOLOv5需要关注anchor配置建议用训练集自己聚类不要用默认的COCO anchor。3.3 训练超参数与损失曲线判读训练配置如下# train.yaml path: dataset/traffic_lights train: images/train val: images/val names: 0: red_circle 1: green_circle 2: yellow_circle 3: red_left 4: green_left 5: red_right 6: green_right训练命令用的YOLOv8官方命令行关键超参数如下yolo detect train datatraffic_lights.yaml modelyolov8n.pt \ epochs200 batch32 imgsz640 optimizerAdamW \ lr00.002 lrf0.01 warmup_epochs3 \ mosaic1.0 mixup0.2初始学习率我特意调得比默认值低一些因为信号灯数据量远小于COCO学习率太大很容易在初期就震荡发散。损失曲线方面我主要看box_loss和cls_loss的收敛形态。正常情况是前50个epoch快速下降到150个epoch后趋于平缓。如果cls_loss在后期还在抖动说明学习率偏大或训练数据里有噪声标注需要回头检查标注质量而不是强行拉满epoch数。3.4 训练后的验证与坏案例分析验证时不要只盯着一堆mAP数字要结合混淆矩阵分析。我导出过一张混淆矩阵发现red_circle和red_left之间的混淆占了所有错误预测的一半以上。原因是这两种灯在远处目视几乎长得一样加上夜间红色过曝后细节丢失网络只能靠位置上下文去猜。另一个典型坏案例是夜间黄灯被识别成红灯。原因是黄灯在白平衡偏冷的摄像头下色温偏白偏红分类分支被误导。针对这个问题我在训练数据里专门模拟了不同白平衡偏移的增强版本同时部署阶段做了色彩校正预处理后面会讲到。4. 真正的坑不在训练而在误检和频闪训练完一轮模型跑通测试视频你会发现一个尴尬的事实验证集精度很高但放到真实路口视频里误检还是层出不穷。车尾灯、刹车灯、路灯倒影、LED广告牌都会成为模型眼中的信号灯。我有一段时间几乎每天都在检查误检视频帧总结下来最大的两类问题就是灯色误判和灯体闪烁。4.1 车尾灯和路灯引发的系统性误检夜间场景下红色车尾灯和红色信号灯的视觉特征高度相似模型很容易把前方车辆的刹车灯识别为红灯。这种误检在夜间跟车场景中尤其严重。单纯增加训练数据可以缓解但很难根除因为特征的相似性是客观存在的。我的解决方案是增加位置先验。路口信号灯通常位于画面中固定的上中区域在部署推理时设置一个ROI区域只对高于地平线的区域执行检测低于地平线的区域直接丢弃。这个方法非常简单但极其有效误检率下降了70%以上。再配合目标尺寸过滤信号灯在画面中的面积占比应该在一个合理的区间内过大或过小的检测框都直接过滤掉。4.2 帧间频闪与灯色抖动模型对同一盏灯在不同帧的预测灯色可能不一样比如连续三帧预测是绿灯第四帧突然变成红灯第五帧又变回来。这在视频里看起来就是灯色闪烁非常影响实际使用体验。产生这种情况的原因很多视频压缩带来的块效应、摄像头自动曝光导致亮度突变、模型对模糊帧的置信度下降。处理方案就是利用时序信息做帧间投票import collections class TemporalVoter: def __init__(self, window_size5, min_votes3): self.window_size window_size self.min_votes min_votes self.history collections.deque(maxlenwindow_size) def predict(self, detections): # detections: {class_name: confidence} self.history.append(detections) vote_counter collections.Counter() for frame_dets in self.history: for cls_name, conf in frame_dets.items(): if conf 0.5: vote_counter[cls_name] 1 best_class, vote_count vote_counter.most_common(1)[0] if vote_count self.min_votes: return best_class return None这段代码的思路是维护一个长度为5的滑动窗口每个窗口内至少3帧预测同一种灯色才输出最终结果。延迟带来的影响是输出比真实状态慢约2到3帧200毫秒级别对驾驶预警场景完全可以接受换来的是稳定性的大幅提升。4.3 夜间过曝的处理经验在夜里LED灯珠发出的光在摄像头传感器上很容易过曝红色和黄色变成一片白色光团颜色信息几乎完全丢失。我排查后发现很多漏检并不是模型笨而是图像本身就不包含足够的信息。硬件层面能做的改进是开启摄像头的短曝光模式避免高光溢出软件层面则可以在推理前做一次高光恢复预处理用局部自适应阈值把过曝区域的分层信息尽量还原出来。更实际的做法是数据层面模拟过曝光效果把正常的信号灯图片做亮度拉伸和高光模糊让模型见过这种形态。具体的增强手段我用的是OpenCV的gamma校正加高斯模糊import cv2 import numpy as np def simulate_glow(image, gamma1.8, blur_ksize5): gamma_corrected np.power(image / 255.0, 1.0 / gamma) * 255.0 gamma_corrected gamma_corrected.astype(np.uint8) blurred cv2.GaussianBlur(gamma_corrected, (blur_ksize, blur_ksize), 0) return cv2.addWeighted(gamma_corrected, 0.6, blurred, 0.4, 0)训练集中加入这类增强样本后夜间过曝场景的召回率从68%提升到了84%效果很明显。5. 从电脑搬到路口轻量化部署的完整链路模型在GPU服务器上跑通了还只是第一关真正要做成路口可用的系统还要走完模型导出、量化、剪枝、后端推理优化这一整套流程。我部署的目标设备是一块嵌入式开发板算力远不如服务器显卡帧率要求是至少30FPS。5.1 ONNX导出与TensorRT加速第一步是把训练好的PyTorch模型导出成ONNX格式然后再转成TensorRT的engine文件。导出时最容易忽略的参数是opset版本和动态batch支持。如果后续推理需要考虑多路视频流或多batch并发一定要在导出时声明动态维度否则后面转换会很被动。yolo export modelbest.pt formatonnx dynamicTrue opset12转TensorRT的命令比较直接关键是根据部署硬件选对精度模式trtexec --onnxtraffic_light.onnx \ --saveEnginetraffic_light_fp16.engine \ --fp16 \ --workspace4096我对比了FP32和FP16两种精度的实际效果FP16的推理速度提升了约1.8倍mAP下降不到1%这个精度损失完全可以接受。FP16对信号灯这种小目标检测的影响主要在极暗光条件下的置信度略微下降但不影响整体的可用性。5.2 INT8量化能省就省但要盯紧细节INT8量化能进一步把推理帧率推高近一倍但代价是精度损失更明显。我在实测中发现INT8量化后红灯类别的精度下降最严重因为红色信号灯的高饱和红色在INT8表示空间里容易发生色调偏移淹没在量化噪声中。解决的办法有两个一是选取合适的校准数据集校准集要充分包含夜间和过曝样本让量化器学会保留高光区域的动态范围二是如果精度仍然不达标可以采取混合精度方案只对最后一层分类头保留FP16其余层量化成INT8。这个做法在TensorRT里可以通过层级别的精度设置实现虽然麻烦一些但能在速度和精度之间找到最好的平衡点。5.3 剪枝与帧率调优的成效INT8量化之后我在那块嵌入式板子上把模型推理平均帧率做到了52FPS已经超出了30FPS的目标。但考虑到实际路口可能需要同时处理两路甚至四路视频流我又做了通道剪枝。剪枝的思想很简单把权重接近零的通道裁剪掉模型变瘦计算量变小。我用的是稀疏化训练配合结构化剪枝YOLOv8n本身的冗余度已经不大最终剪掉15%的通道mAP几乎不变单路推理帧率从52FPS提到了58FPS左右。后处理阶段还有一个常被忽略的优化点NMS非极大值抑制。YOLO默认的NMS在后处理时对每个类别都做一遍抑制在类别多的情况下耗时明显。信号灯类别数量不多可以把NMS的IOU阈值适当调大从默认的0.45调到0.65减少重复框的同时也能省掉一部分计算量。不过这个值不是越大越好调太大会让同一灯体上多个重叠框共存影响下游状态判断逻辑。5.4 缓存与整体延迟优化推理帧率提上去之后我进一步优化了端到端的延迟链路。视频帧解码使用硬件解码接口避免CPU软解预处理阶段把颜色空间转换、resize和归一化合并到一次内存拷贝里完成减少数据搬运次数。检测结果用零拷贝方式直接传给下游的时序投票模块省掉序列化和反序列化开销。实测下来从摄像头取帧到输出最终灯色状态端到端延迟稳定在48毫秒左右完全满足实时提醒的需求。6. 踩坑之后想明白的几个设计思路项目收尾阶段我复盘了整个系统有几点体会特别深刻这些内容在官方文档里很难直接看到但在工程上非常关键。关于数据我的遗憾是前期扩充场景时花了过多精力在增加样本数量上后来才发现更难的是样本的多样性。同一路段不同季节、不同雾浓度、不同逆光方向带来的影响比单纯增加一万张类似图片大得多。如果你要复现这个项目我建议优先保证样本的多样性而不是数量。关于模型迭代我建议走小步快跑路线。不要一上来就追求高精度模型先用一个粗糙的模型跑视频把误检样本收集起来再针对性地优化数据。这个闭环比一次性堆数据更高效因为你的注意力始终集中在模型真正的短板上。关于部署我最后悔的是没有早一点把TensorRT的精度模式测试纳入训练阶段。很多调参动作在PyTorch里有效但经过ONNX到TensorRT的转换后效果可能被削弱或放大。以后做类似项目我会把部署验证提前到训练中期而不是训练结束后才想起这一步。关于时序投票它解决的是模型输出的稳定性问题但它也引入了额外的延迟。如果下游应用对延迟极其敏感可以再用状态机优化一下只在检测置信度低时启用投票置信度高时直接输出。这个动态开关能兼顾稳定性和实时性。最后分享一个我在实车测试里学到的经验千万不要只依赖模型的输出做决策。信号灯识别系统应该和导航地图、车辆速度、刹车踏板状态等外部信号做交叉验证。模型永远有可能犯错用多路信息源互相校验才是工程系统真正稳妥的做法。这套系统跑了几百公里的实路测试累计检测正确率稳定在97%以上各种极端天气和光照条件下也保持了高可用性。希望这篇记录能让你少走一些我走过的弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑