☰
基于YOLOv8的交通信号灯识别实战:数据增强与部署优化
2026/10/18 14:41:42 网站建设 项目流程

1. 项目整体设计与思路拆解

做这个项目的起因其实很朴素——城市道路上的交通信号灯识别,一直都是智能驾驶和智慧城市绕不开的基础环节。很多朋友一开始觉得这不过是个“目标检测的入门练手”,但实际上手之后才发现,信号灯识别比想象中要刁钻得多:目标尺寸小、曝光变化剧烈、夜间灯光发白、雨雾天对比度低,再加上红黄绿三类之间的颜色区分不能靠简单阈值搞定。单纯的图像处理方案在特定场景下能跑,但泛化能力太差,换一个路口、换一种灯珠排列就可能失效。所以这个项目选择用 YOLO 系列模型来做端到端的检测识别,把“定位”和“分类”一步到位完成。

1.1 核心需求解析

这个系统的核心需求可以从两个层面来拆。

第一层是“检测”,也就是在图像或视频帧里找到信号灯的位置。第二层是“识别”,也就是判断这个灯当前显示的是红色、黄色还是绿色。传统做法通常是把两步拆开:先做颜色分割,找出灯珠区域,再用形态学或模板匹配去分类。但实际路况下,这种方法的脆弱性会被无限放大。逆光环境下红色灯珠的颜色会被白光淹没,雨夜中灯珠周围全是霓虹灯的反射,单纯靠颜色特征根本分不开。

YOLO 的解题思路完全不同。它直接把整张图作为输入,通过网络一次性预测出所有目标的位置和类别,而不是先产生候选区域再做二次分类。这种“单阶段”设计让它在速度和精度之间取得了非常实用的平衡。对于交通信号灯这种实时性要求极高的场景来说,一帧图像的处理时间必须控制在毫秒级,YOLO 系列显然是最适合的起点。

1.2 技术选型背后的取舍逻辑

在模型选型上,我做了一番对比。从最初的 YOLOv5 到后面出的 YOLOv8、YOLO11,模型规模从 nano 到 xlarge 跨度很大。考虑到信号灯识别往往要部署在边缘设备上,比如路侧计算单元或者车载盒子,算力不可能无上限,所以最终选了 YOLOv8n 作为基础模型。这个选择有几个理由:

  • 模型体积只有几十兆,INT8 量化后可以压到十几兆,内存占用友好。
  • 推理速度在 CPU 和 GPU 上都有不错的表现,满足实时处理需求。
  • 训练生态成熟,数据标注格式、预训练权重、部署工具链都很完善。

当然,如果追求更高的识别精度,可以换成 YOLOv8s 或 YOLOv8m,代价是推理速度会下降。这个项目定位于“能跑、能落地”,所以网络结构上不做魔改,重点放在数据侧和部署侧挖细节。

提示:绝大多数信号灯识别项目效果不佳,问题不是出在模型不够强,而是数据没有处理好。后面会专门展开讲数据这部分。

2. 数据集准备与预处理方案

很多初学者拿到开源数据集就直接开跑,跑出来的 mAP 看起来不错,一上真实路况就原形毕露。核心原因在于数据分布和真实场景之间的差异。信号灯识别的难点不在“识别信号灯”本身,而在于克服各种极端条件。

2.1 数据来源与标注规范

这个项目一共用了三个数据来源:

  • 公开的交通信号灯数据集,包含多个城市路口的视频片段。
  • 自己拍摄的路口素材,覆盖不同天气、不同时段。
  • 从行车记录仪视频中抽取的关键帧,补充夜间和逆光场景。

标注规范上,我重点关注类别定义的一致性。信号灯类别我定义了四种:red、yellow、green、off。其中off类别对应的是熄灭状态下的信号灯灯盘,这是一个很容易被忽略的细节。如果模型没见过熄灯状态的信号灯,它可能会把整个灯盘背景误检成某个颜色类别,导致连续帧之间出现不合理的状态跳变。

标注工具用的是开源的标注平台,导出为 YOLO 格式的 txt 文件,每一行是class_id center_x center_y width height格式,坐标都是归一化之后的数值。

2.2 数据增强与难点聚焦

数据增强是信号灯项目的胜负手。我用了以下几种组合方式:

  • 亮度抖动:模拟逆光、阴影、过曝。
  • 高斯噪声:模拟低照度环境的传感器噪声。
  • 随机裁剪与缩放:模拟不同距离下的目标尺度变化。
  • 上下翻转:这个对信号灯项目来说可以谨慎使用,因为红灯和绿灯的位置通常有固定规律,过度翻转可能破坏空间先验。
  • 运动模糊:模拟车辆行驶过程中由于曝光时间过长产生的拖影。

硬要说哪个增强最有用,我会选亮度抖动和运动模糊。实测下来,加了这两项之后,夜间和雨天场景的漏检率明显下降。

2.3 一个重要的数据处理技巧

我额外做了一步离线数据筛选,把视频帧按 5 帧间隔抽样,然后通过一个简单的差异算法,只保留画面有明显变化的帧。这样做的目的是减少重复帧,避免模型在训练时过度拟合静止背景。事实证明,这一步对提升模型的泛化能力很有帮助,尤其是在跨路口测试时,效果差异肉眼可见。

注意:不要盲目堆数据量。如果数据集里大量帧都是同一个路口的相同画面,模型学到的其实是“记住了这个路口”,而不是“理解了信号灯”。

3. YOLOv8 模型训练全流程

数据准备完毕之后,就进入模型训练阶段。这里我把流程拆解成几个关键环节,每一步都有值得记录的坑和经验。

3.1 环境配置与预训练权重选择

训练环境我用的是单卡 GPU,显存 16GB 的配置。训练框架选择 YOLOv8 的官方工具链,依赖项比较简洁,安装也流畅。

预训练权重选的是 COCO 数据集上训练出来的版本。虽然 COCO 里没有信号灯类别,但底层特征提取器已经学会了丰富的纹理和边缘特征,迁移到信号灯场景时收敛速度远快于从头训练。我试过从随机初始化开始训练,效果很差,几百轮之后 mAP 还是上不去,所以强烈建议用预训练权重做起点。

3.2 关键训练参数解析

训练参数我是这样设置的:

# 训练配置文件关键项 model: yolov8n.pt data: signal_light.yaml epochs: 200 batch: 32 imgsz: 640 optimizer: AdamW lr0: 0.0001 weight_decay: 0.0005 patience: 30

关于输入分辨率imgsz,我做过一个对比实验。640 分辨率下,小目标信号灯的检测效果一般,漏检率偏高。提升到 960 之后,检测精度明显提升,但训练时间和推理时间也随之增大。如果你的部署设备性能不错,建议优先用 960;如果在边缘设备上跑,640 会更稳妥。

学习率和权重衰减对上图有一个实际经验值参考,需要根据损失震荡情况微调。

3.3 训练过程中的监控指标

训练时我主要盯着三个指标:

  • mAP@0.5:判断模型整体检测能力是否在稳定上升。
  • mAP@0.5:0.95:反映定位精度的好坏,这个指标对信号灯这种小目标尤其重要。
  • Precision/Recall曲线:用于判断阈值设置是否合理。

我遇到的一个典型情况是:mAP 不低,但实际视频测试时频繁漏检。后来发现是因为测试阈值设太高,模型明明预测出了信号灯位置,但置信度只有 0.55,被阈值过滤掉了。所以推理时我把置信度阈值调到了 0.25,虽然会引入少量误检,但整体可用性提升了很多。

提示:训练曲线只是参考,最终效果要用完全没见过的视频去验证。不要只盯着训练集的损失值自嗨。

4. 系统实现与部署细节

模型训练完成之后,真正的考验才开始——把它变成一个能实时工作的完整系统。

4.1 整体架构设计

这个系统的运行链路可以概括为“视频输入 → 帧提取 → 模型推理 → 状态判定 → 输出结果”。在实际实现过程中,我把它拆成了三个模块:

  • 视频流处理模块:负责读取视频文件或摄像头实时流,做帧率控制和画面预处理。
  • 检测识别模块:加载训练好的 YOLOv8 模型,对每一帧执行推理,输出目标框和类别。
  • 状态逻辑模块:结合连续帧的检测结果,做时间维度的滤波,输出稳定的信号灯状态。

为什么要做时间维度的滤波?因为单帧检测结果可能存在抖动,比如某帧误检成红色,下一帧又恢复正常。如果不加滤波,信号灯状态会频繁闪烁,对下游逻辑来说是不可接受的。

4.2 推理加速与性能优化

推理阶段我用了几层优化:

  • 将模型导出为 ONNX 格式,再用 TensorRT 进行 FP16 量化,推理速度比原生 PyTorch 快了两倍多。
  • 开启 CUDA 流并行处理,让视频解码和模型推理叠加执行,而不是串行等待。
  • 限制输入帧的分辨率,在保证检测效果的前提下,用 960 分辨率作为推理输入。
  • 使用批处理(batch)方式处理多路视频流,充分利用 GPU 算力。

实测下来,在单张消费级 GPU 上,这个系统可以同时处理 4 路 1080p 视频流,单路的端到端延迟控制在 50 毫秒以内,完全满足实时性要求。

4.3 信号灯状态稳定判定逻辑

这里分享一个状态判定的核心逻辑:

# 信号灯状态滤波示意逻辑 def get_stable_state(detections, history_frames=5): color_votes = {} for det in detections: if det.conf < 0.25: continue color_votes[det.cls] = color_votes.get(det.cls, 0) + 1 if len(color_votes) == 0: return "unknown" # 取最近5帧中出现次数最多的类别作为当前状态 best_color = max(color_votes, key=color_votes.get) return best_color

这个逻辑的核心思想是“少数服从多数”,用连续几帧的检测结果来投票,抑制单帧随机噪声。

注意:这里要处理的边界情况是视频画面中同时出现多个信号灯(比如左转、直行、右转三个灯盘)。此时不能简单取全局最高票,而应该根据灯盘位置划分区域,再在每个区域内独立投票。

4.4 部署框架选择

部署层我推荐用 FastAPI 搭建一个轻量级的 HTTP 推理服务,接收图像 POST 请求,返回检测结果。这样做的优势是解耦了前端和后端,不同的下游应用只要按照约定的接口格式请求即可,不需要关心内部模型实现。接口返回结构可以定义成这样:

{ "status": "green", "confidence": 0.92, "timestamp": 1699862400 }

5. 常见问题与排查技巧实录

在项目调试过程中,我踩过不少坑,这里挑几个最有代表性的记录下来,希望能帮后来者省点时间。

5.1 训练损失不下降

现象:训练集损失函数前 10 轮就降到 0.05 附近,但验证集 mAP 一直上不去。

排查过程:

  • 第一步检查数据标注,发现标注框中心点坐标有部分偏移,导致正样本匹配失败。
  • 第二步检查类别不平衡,off类别样本占比过高,模型严重偏向预测“熄灭”状态。
  • 第三步调整正负样本采样策略,把off类别的样本数限制到总量的 20%,损失曲线随后正常。

5.2 夜间误检严重

现象:白天测试效果不错,晚上视频里频繁把路灯、车尾灯误检成信号灯。

排查过程:

  • 夜间照明确实低,模型容易把发光的圆形物体误判为信号灯。
  • 解决思路是在数据增强中加入“夜光特效”,把正常图片的亮度降低,同时加入高饱和色块模拟发光效果。
  • 另一个很有效的做法是加入上下文约束,利用信号灯通常出现在画面中上部、靠近路口的位置这一空间先验,过滤掉其他区域的检测框。

5.3 视频流推理掉帧

现象:摄像头推流延迟越来越高,画面明显卡顿。

排查过程:

  • 最先怀疑是模型推理太慢,但实际排查后发现瓶颈在视频解码环节,默认的解码器无法实时处理高码率视频流。
  • 解决方法是改用硬件解码,同时把视频解码和模型推理放在两个线程中,中间用队列缓冲。
  • 这里分享一个经验:做实时系统,不要只盯着模型推理时间,全链路的耗时分析才是关键。

5.4 多路视频流并发冲突

现象:增加路数之后,GPU 显存越来越吃紧,容易出现显存不足的报错。

排查过程:

  • 显存溢出的主要原因是每路视频流都创建了一套独立的上下文环境,没有共享模型。
  • 解决办法是把模型做成全局单例,所有视频流推理都复用同一份模型权重和 CUDA 上下文,显存占用直线下降。

6. 一些实操心得与后续扩展思路

说实话,做完这个项目之后我的一个深刻感受是:信号灯识别本身没有那么难,难的是把它做成一个在不同天气、不同路口、不同光照条件下都能稳定运行的系统。数据的分量远比模型的结构重要,这一点在信号灯项目上体现得尤为明显。

如果后续继续扩展,我会从这几个方向入手:

  • 加入多模态信息融合,比如结合地图先验数据,知道当前路口的信号灯位置和配时方案,进一步缩小检测范围。
  • 引入时序建模能力,让系统能够预测信号灯即将变化,而不是单纯检测当前状态,这对车辆速度规划会有很大帮助。
  • 做更细粒度的分类,比如区分箭头灯和圆盘灯,区分左转红灯和直行红灯,这对自动驾驶的决策逻辑是必要的。
  • 尝试模型蒸馏,把大模型的检测能力迁移到更小的端侧模型中,让嵌入式设备也能跑得动。

最后再分享一个小技巧:训练完成后,一定要收集一批“困难样本”单独做测试,比如严重逆光、瓢泼大雨、夜晚路灯过曝这几种情况。如果模型在这批样本上的表现达标,那基本就能放心的上线了。如果没达标,不要急着调模型,先回数据侧看看,往往会有更大收获。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询