十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于YOLOv8的路口信号灯通行规则识别实战

基于YOLOv8的路口信号灯通行规则识别实战 简介针对路口交通信号灯识别场景这份基于YOLOv8的通行规则识别项目提供从模型训练到预测部署的完整算法源码与配套文档。资源面向计算机、通信、人工智能、自动化等专业的学生、教师或从业者尤其适合作为毕业设计、课程设计或进阶学习参考。压缩包共17个文件以Python源码为主另含配置文件、示例图片、文本说明及Markdown文档整体约705KB目录划分清晰便于按模块检索与调试。当前已有117人学习浏览。代码经过实际调试测试覆盖数据读取、目标检测、分类预测、结果可视化等关键环节可直接运行或在此基础上调整参数实现不同功能配套文档对项目整体结构进行了说明可帮助新手快速理解YOLOv8在路口交通信号灯识别任务中的典型应用也为后续算法改进和功能扩展提供了良好基础。1. 路口信号灯识别从“看见红绿灯”到“读懂通行规则”的最后一公里做无人小车或辅助驾驶项目时很多团队一开始就走偏了把“信号灯识别”当成一个普通目标检测来做模型跑通了红绿灯框出来了但拿不到“到底能不能左转”这个答案。Python基于YOLOv8的路口交通信号灯通行规则识别模型核心不是“识别灯”而是“判断规则”。本文要解决的是怎么用YOLOv8把灯检出来再把检测结果翻译成车道级别的通行指令——左转、直行、右转、停车、减速等待。适合正在做毕业设计、无人车竞赛或L2级辅助驾驶 Demo 的开发者。我会按数据集设计、训练调参、规则算法、避坑、部署这条完整链路讲代码可直接复现。2. 把信号灯识别拆成建模问题类别设计决定规则判断的复杂度2.1 三种建模思路灯色分类、方向分类、直接输出通行状态接手这个题目第一件要决定的事不是选YOLOv8还是YOLOv5而是类别怎么定。我见过最省事的做法是只分三类red、green、yellow。训练很快mAP也漂亮但到了规则判断环节就傻眼了——左转箭头灯是红的圆盘灯是绿的到底能不能走检测器答不上来因为信息在建模时就被丢掉了。常见做法有三种我逐个对比过。第一种是“灯色方向”组合类别。这也是我在实际项目中默认的方案。把类别设计成 green_circle、green_left、green_right、red_circle、red_left、red_right、yellow_circle再加上一个 unlighted熄灯状态用来兜底。这样检测器输出的是完整语义规则判断只需要查表几乎不需要额外逻辑。代价是类别数变多某些类别比如黄灯左转箭头样本会非常少需要刻意采集。第二种是“灯色本体”和“箭头方向”分开检测。检测器只负责输出圆盘灯或箭头灯的颜色方向用额外的图像处理从灯板内部的箭头形状里提取。好处是类别少、数据好凑坏处是多一个处理步骤箭头灯在低分辨率下本来就糊阈值稍微没调好就把箭头形状切没了。第三种是端到端输出通行状态直接让模型学会输出“GO / STOP / WAIT”。听起来很诱人但对训练数据的场景多样性要求极高换个路口、换套灯架样式就失效。“黑匣子”味道很重我不建议在规则敏感的场景里用。图像识别可以做概率预测但通行规则必须是确定性的、可审查的逻辑出事故了能查原因——这是工程底线。所以我的选择一直很明确YOLOv8做检测类别上做细粒度设计规则判断用确定性代码实现。2.2 自建信号灯数据集标注规范与类别不平衡处理YOLOv8训练自己的数据集第一步是把数据准备好。信号灯场景和通用目标检测有个很大不同样本来源单一基本就两个渠道一是行车记录仪视频抽帧二是路口监控视频截取。我一般用 OpenCV 按每5帧抽一帧保留连续帧做时序验证但训练时把连续帧打散。标注工具我用 Labelme 用得最多导出为 JSON 后再写脚本转成 YOLO 格式。每个灯框标注一个闭合多边形还是矩形信号灯灯板通常是圆角矩形但灯芯是圆的。我的标注规范是框住发光灯芯本身不框整个灯壳。原因很简单训练时如果框的是灯壳模型会把“暗灯”也当成正样本学进去推理时遇到熄灯状态的信号灯会莫名其妙输出一个高置信度的 green。转 YOLO 格式的脚本我每次都要用核心逻辑是读 JSON、归一化、写入 txtimport json import os from pathlib import Path def labelme_to_yolo(json_path: str, target_dir: str, class_map: dict) - None: with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] yolo_lines [] for shape in data[shapes]: label shape[label] if label not in class_map: continue # 跳过未定义的标签防止脏数据进训练 points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) box_w x_max - x_min box_h y_max - y_min # YOLO 格式类别 中心x 中心y 宽 高全部归一化到 0~1 cx (x_min box_w / 2) / img_w cy (y_min box_h / 2) / img_h nw box_w / img_w nh box_h / img_h yolo_lines.append(f{class_map[label]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) out_name Path(json_path).stem .txt with open(os.path.join(target_dir, out_name), w, encodingutf-8) as f: f.write(\n.join(yolo_lines)) class_map { green_circle: 0, green_left: 1, green_right: 2, red_circle: 3, red_left: 4, red_right: 5, yellow_circle: 6, unlighted: 7, }这段脚本在制作数据集时一定会反复跑每次标注完一批图执行一次就能合并进训练集。注意 class_map 里我留了 unlighted 类不要觉得它多余它会用来区分“红灯真的灭了”和“这个灯当天没亮”规则判断里这个状态代表不可用不能直接放行。标注完成后必须做一次类别分布检查。我吃过亏第一版数据集里 red_circle 有 4000 个框green_left 只有 260 个训练完 green_left 的召回率只有 0.4。信号灯数据天然不平衡一个路口红灯周期比绿灯长左转箭头灯只在特定相位亮样本量能差一个数量级。我会统计每个类别的框数量低于平均数量 1/3 的类别要么补数据要么用类别权重。2.3 数据增强雨夜、过曝、反光怎么模拟信号灯数据增强要往“真实世界不走样”的方向做不要猛堆通用增强。亮度和对比度扰动必须加但幅度要克制。我用的是 albumentations 里的 RandomBrightnessContrastlimit 设在 0.2 到 0.3 之间模拟白天强光和阴天的差距。HSV 的 H 通道只能微调±5因为信号灯颜色本身就是强语义调多了红灯变橙灯类别边界就糊了。雨雾场景我是直接在采集阶段解决的——雨天专门开车出去录半小时素材比任何生成式增强都管用。如果实在没有雨天素材用 RandomFog 顶一下也行但要有心理预期合成的雾和真实雨滴对灯光的折射完全不同。夜间增强是必须做的。红灯在夜间会过曝发白如果训练集里没有这类样本模型到了晚上就把红灯误判成 unlighted。我的做法是把夜间抽帧样本单独提出来和其他样本按 1:1 混合进训练集而不是靠随机增强碰运气。3. 基于YOLOv8训练信号灯检测器环境搭建与训练参数调优3.1 Ubuntu 20.04 下搭建YOLOv8训练环境CPU 版也能跑通先说环境。很多人一上来就卡在 CUDA 和 PyTorch 版本匹配上折腾两天还没跑起第一行训练命令。我用的是 Ubuntu 20.04 Python 3.10 PyTorch 2.x这套组合对 YOLOv8 支持最省心。如果你的机器只有 CPU不用灰心我帮人调过 GTX 1660 Ti 和纯 CPU 两种环境。CPU 版本的意义是你可以先把数据处理流程、训练脚本、规则判断代码全部调通再上 GPU 做正式训练。代码层面 GPU 和 CPU 无缝切换唯一区别是训练速度。# 在 Ubuntu 20.04 上创建虚拟环境并安装依赖 python3 -m venv venv_yolo source venv_yolo/bin/activate pip install --upgrade pip pip install ultralytics opencv-python albumentationsultralytics 是 YOLOv8 的官方库装好之后自带命令行入口不用手动 clone 源码。这里我用虚拟环境是为了隔离 OpenCV 和其他项目的依赖冲突。如果你更习惯用 VSCode 连远程服务器调试直接在 VSCode 里选中这个 venv 解释器就行。接着验证环境是否正常。用一张包含红绿灯的测试图跑一下官方预训练权重yolo predict modelyolov8n.pt sourcetest_signal.jpg能输出检测结果就说明环境 OK。这一步很重要别急着用自己的数据训练先把链路跑通后面出问题才知道是环境问题还是数据问题。3.2 训练命令与关键参数imgsz、epochs、freeze、batch 怎么设数据准备好后先组织好目录结构。YOLO 系列对数据集格式要求很死板images 和 labels 必须按 train/val 分开dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── signal.yamlsignal.yaml 内容如下path: /home/user/dataset # 数据集绝对路径别用相对路径容易踩坑 train: images/train val: images/val names: 0: green_circle 1: green_left 2: green_right 3: red_circle 4: red_left 5: red_right 6: yellow_circle 7: unlighted训练命令长这样yolo detect train \ modelyolov8s.pt \ datasignal.yaml \ epochs200 \ imgsz960 \ batch16 \ freeze10 \ patience30 \ projectruns/signal \ nameexp01参数逐个说。model 我选了 yolov8s没用 nano因为信号灯目标小nano 的浅层特征表达能力不够漏检率会明显偏高。imgsz 设成 960 而不是默认的 640这是信号灯场景最重要的一次调参——路口画面里一个灯板通常只有 20×20 像素640 尺度下特征太少960 能把召回率拉上来好几个点。代价是训练时间变长但换来的是检测效果值。batch 根据显存来16GB 显存跑 960 分辨率设 16 没问题。freeze10 表示冻结模型前 10 层的权重这个参数在迁移学习里很管用。信号灯特征和 COCO 通用物体特征差异较大我一般只冻结前 10 层做骨架特征适配后面层全部微调比不冻结收敛快比全冻结效果好。patience30 是早停机制验证集损失连续 30 轮不下降就自动停。信号灯数据集通常不大200 轮可能跑不满早停能帮你省时间。3.3 用损失函数曲线判断训练是否健康训练开始后不要傻等YOLOv8 会在 runs/signal/exp01 下持续输出 results.csv用脚本画损失函数曲线图一眼就能看出训练状态。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/signal/exp01/results.csv) # 列名里带空格读进来后去掉首尾空格 df.columns [c.strip() for c in df.columns] fig, axes plt.subplots(2, 2, figsize(12, 8)) axes[0, 0].plot(df[epoch], df[train/box_loss], labelbox_loss) axes[0, 0].set_title(Box Loss (越小越好, 持续下降)) axes[0, 1].plot(df[epoch], df[train/cls_loss], labelcls_loss) axes[0, 1].set_title(Cls Loss) axes[1, 0].plot(df[epoch], df[val/box_loss], labelval_box_loss) axes[1, 0].set_title(Val Box Loss) axes[1, 1].plot(df[epoch], df[metrics/precision(B)], labelprecision) axes[1, 1].plot(df[epoch], df[metrics/recall(B)], labelrecall) axes[1, 1].legend() axes[1, 1].set_title(Precision / Recall) plt.tight_layout() plt.savefig(loss_curve.png, dpi150)看曲线我不会只看损失值是否降到最低而是看三件事。第一验证集 box_loss 是否和训练集同步下降如果训练损失一路走低但验证损失在第 30 轮开始回升那就是过拟合的早期信号。第二precision 和 recall 是否都在涨如果 recall 上去了但 precision 崩了说明模型开始乱框需要回去检查标注质量。第三如果损失在震荡而不是平滑下降大概率是 batch 太大或学习率太高把 batch 减半再试。训练结束后在验证集上跑yolo val modelruns/signal/exp01/weights/best.pt datasignal.yaml重点看每个类别的 recall特别是 green_left、red_left 这类少样本类别。如果某个类别 recall 低于 0.5不要慌着加轮次先回去看是不是标注框太紧把箭头切掉了一半。4. 通行规则算法从检测框到“能不能走”的完整链路4.1 灯态与车道的绑定ROI 区域与信号灯指向关系检测器输出的是灯的状态通行规则必须知道这个灯管的是哪条车道。这是整个项目里最少被人讲清楚、也最容易翻车的地方。常见做法是用 ROI 区域做绑定。在图像坐标系里预先画几个多边形区域每个区域对应一条车道或一个转向方向然后把检测框和 ROI 的包含关系算出来。import cv2 import numpy as np def build_lane_rois(frame_shape): h, w frame_shape[:2] # ROI 用多边形定义坐标按实际路口标定这里是示例值 lane_rois { left_turn: np.array([[w*0.20, h*0.85], [w*0.38, h*0.85], [w*0.38, h*0.60], [w*0.20, h*0.60]]), straight: np.array([[w*0.40, h*0.85], [w*0.60, h*0.85], [w*0.60, h*0.60], [w*0.40, h*0.60]]), right_turn: np.array([[w*0.62, h*0.85], [w*0.85, h*0.85], [w*0.85, h*0.60], [w*0.62, h*0.60]]), } return lane_rois def assign_signal_to_lane(boxes, rois): result {} for lane_name, poly in rois.items(): result[lane_name] [] for box in boxes: cx (box[0] box[2]) / 2 cy (box[1] box[3]) / 2 if cv2.pointPolygonTest(poly, (cx, cy), False) 0: result[lane_name].append(box) return result这段代码做了什么事把每条车道对应一个梯形区域近似车头视角下的车道投影然后用信号灯框的中心点去检测它落在哪个 ROI 里。需要说明的是ROI 坐标必须在部署前做一次现场标定不同路口的灯杆位置、车道数量都不一样代码里的比例值只是模板不能直接用于所有路口。为什么不用端到端让模型自己去学映射因为车道和信号灯的对应关系是几何问题用确定性代码写出来可解释、可调试。模型输出的类别概率可以有误差但“左转车道对应的是左转箭头灯”这条规则不能是概率性的。有一次在测试时模型把左转箭头灯误检成了直行灯但 ROI 绑定逻辑正确最终规则判断里的左转指令没被错误触发这救了我一次。4.2 方向箭头灯的规则映射表灯态绑定到车道后规则判断就是一张查表逻辑。表设计得好后面各种奇葩路口都能扩。信号灯的逻辑分成两个层级第一层是判定这个车道“有没有权限走”第二层是判定“往哪个方向走”。我把映射规则写成配置字典# 通行规则表键是灯态类别值是对应车道的通行决策 # 决策含义go放行, stop禁止通行, caution警示观察, unknown不可用 TRAFFIC_RULES { green_circle: {left_turn: go, straight: go, right_turn: go}, green_left: {left_turn: go, straight: stop, right_turn: stop}, green_right: {left_turn: stop, straight: stop, right_turn: go}, red_circle: {left_turn: stop, straight: stop, right_turn: caution}, red_left: {left_turn: stop, straight: stop, right_turn: stop}, yellow_circle: {left_turn: caution, straight: caution, right_turn: caution}, unlighted: {left_turn: unknown, straight: unknown, right_turn: unknown}, }这里有个细节值得展开右转。国内大部分路口圆盘红灯亮时右转是可以通行的前提是礼让行人。所以我把 red_circle 对 right_turn 的决策设成 caution 而不是 stop这样下游控制模块会根据人行道是否有行人再决定是否停车。这个细节处理不好车在红灯右转道上直接刹停后面车会滴你。箭头灯的红灯比如 red_left对 left_turn 就是绝对的 stop。如果左边同时有圆盘绿灯和左转箭头红灯说明当前是“圆盘绿灯放行直行、左转箭头红灯禁行左转”的相位左转车需要进入待转区等待这个规则在中国大多数路口成立。4.3 黄灯与倒计时闪烁状态的判定逻辑黄灯是规则判断里最容易出 bug 的环节。因为黄灯持续时间短通常只有 3 秒检测器如果对单帧图片判断很容易把黄色识别成红色或绿色。一种处理思路是针对黄灯做专门逻辑。我的方案是一旦检测到 yellow_circle 或 yellow_arrow当前车道的决策不是直接给“走”或“停”而是给“caution”并附带一个计数器。当黄灯连续出现 2 帧以上大约 0.1 秒才真正切换到 caution 状态如果只是单帧闪黄就忽略继续沿用上一帧的决策。倒计时数字也会干扰判断。很多路口是“倒计时 信号灯”同时显示倒计时数字就在灯板旁边模型在训练时如果把倒计时数字的一部分框进了灯芯区域就会把 red 学成 yellow 或反过来。处理办法是标注时严格只框灯芯圆形区域数字不要画进框。推理时如果遇到倒计时和灯色冲突——比如灯色检测是红色但倒计时显示还剩 2 秒要以灯色检测结果为准。倒计时是辅助信息信号灯色才是规则法定的决策依据。5. 信号灯识别避坑指南5 个高频翻车点与排查思路5.1 小目标漏检红灯在画面里只有 12 像素现象模型在测试视频里对远处的信号灯完全无感数值上表现为 recall 偏低近距离拉近后才检出。原因信号灯在路口画面中是典型的小目标。YOLOv8 的主干网络下采样到 1/32640 分辨率下只有 20 像素的灯板在深层特征图上不到 1 个像素信息几乎被压没了。解决把 imgsz 从 640 提到 960 或 1280如果显存撑不住可以先把图像按 2 倍切片再送进模型。经我测试960 分辨率比 640 在这个场景下召回率能高 5 到 8 个百分点。还有一招是在推理时使用模型的 P2 层特征Ultralytics 默认不输出 P2需要改模型配置新手不建议动。5.2 红色过曝与白色灯壳导致的误检现象夜间红灯在画面里是一团发白的光晕模型把它识别成 unlighted 或漏检白天阳光直射在白色灯壳上模型反而输出一个高置信度的 green_circle。原因红灯过曝后像素值接近白色光谱信息被破坏模型学到的“红色 低亮度高饱和度”特征失效白色灯壳反射阳光后亮度高、形状圆润和绿色灯芯在特征层面混淆。解决增强夜间过曝样本。我专门录了一段夜间经过路口的视频挑出红灯过曝的帧做训练集补充。如果你没有雨夜素材可以用图像处理在白天样本上做高光模拟把红色区域提升亮度、降低饱和度再叠加高斯光晕。但说实话效果不如真实夜间样本。对白色灯壳误检加一个后处理规则检测框内的平均红色通道值低于阈值就判定为“伪灯”过滤掉。代价是极暗环境下真灯也可能被误杀需要调阈值。5.3 倒计时数字闪烁引发的红绿跳变现象部署到路口测试时同一盏灯在连续几帧内从 red 跳成 green 又跳回 red车辆控制模块收到跳变信号后急刹。原因倒计时数字会周期性地变化某些数字形状和另一个灯色的特征有局部相似模型在边缘帧上出现误判。加上我的规则判断里没有做时序平滑单帧结果直接被拿来下发抖动自然被放大了。解决加时序投票机制。我实现的方案是用一个长度为 5 的滑动窗口每帧推入当前检测结果只有窗口中同一状态出现 3 次以上才更新最终决策。这样单帧误判会被压制代价是反应延迟增加约 3 帧对车辆控制来说可以接受。5.4 红绿样本比例严重失衡现象训练完看 per-class recallgreen_left 只有 0.3red_circle 却 0.92整体 mAP 还不错但一到有左转箭头的路口就失灵。原因左转箭头灯在路口一个周期内亮的时间很短采集的视频里这一类的框数量远少于普通圆盘灯。模型学到的是类别先验少样本类别天然被压制。解决标注完成后强制统计类别分布低于平均量 1/3 的类别做针对性补充采集。如果没法再采集就用困难样本挖掘先用当前模型预测所有未标注帧把模型置信度低于 0.5 的帧挑出来人工筛查能快速补充难例。5.5 圆盘灯和箭头灯混标的“标注灾难”现象模型在验证集上表现不错但拿到真实路口后把直行箭头灯识别成圆盘绿灯导致直行车道误放行。原因标注员在框选时没有区分灯板形状把箭头灯框得和圆盘灯一样。YOLO 分类靠的正是框内的纹理差异标注不规范等于主动抹掉了类别差异。解决重新制定标注标准圆盘灯必须框住发光圆形箭头灯必须完整框住箭头的三个方向。一条血泪经验给标注员看一份“对与错”对比图再开工不要只发一份文字规范。最后再用脚本检查每类样本的宽高比分布灯板形状应该集中在几个典型值附近出现异常说明该类标注一致性有问题。6. 进阶落地时序投票、ONNX 导出与边缘部署6.1 时序投票消除单帧抖动上面避坑提了时序投票这里给出完整实现。状态机维护每种灯态的候选计数器只有计数达到阈值才切换最终状态from collections import defaultdict class TrafficStateVoter: def __init__(self, window5, min_votes3): self.window window self.min_votes min_votes self.buffer [] self.current_state None def update(self, new_state): self.buffer.append(new_state) if len(self.buffer) self.window: self.buffer.pop(0) if len(self.buffer) self.min_votes: return self.current_state counter defaultdict(int) for s in self.buffer: counter[s] 1 best_state, best_cnt max(counter.items(), keylambda x: x[1]) if best_cnt self.min_votes: self.current_state best_state return self.current_state这里两个参数注意理解window 是统计窗口长度min_votes 是状态切换所需的票数门槛。窗口拉长能滤掉更多的噪声但会引入额外延迟至少要保证min_votes window。在这个项目里我用的 window5、min_votes3实测可以消除大部分闪烁导致的误判。6.2 ONNX 导出与国产芯片部署模型从 PT 权重转到部署有两种主流路径。一种是导出 ONNX 后用 ONNX Runtime 在 X86 工控机也就是我们常说的边缘盒子设备跑另一种是导出 ONNX 后再转成 RKNN部署到 RK3588 这类边缘计算板子上。我在 RK3588 上部署过流程不算复杂但有几个坎绕不过去。# 导出 ONNX注意保持和训练一致的 imgsz yolo export modelruns/signal/exp01/weights/best.pt formatonnx imgsz960 opset12导出后先用onnxruntime验证精度是否和 PyTorch 版本一致再用onnx2rknn转成 RKNN 格式。最容易出问题的是 opset 版本和某些算子的不支持我的建议是直接用opset12不要追新。板子上的推理代码需要额外注意图片预处理要保证归一化方式、通道顺序RGB/BGR跟训练时完全一致否则部署后精度会掉得很离谱。6.3 验收方法与最后一公里最后说下怎么验收这个系统而不是只看训练集上的 mAP。我一般会在没参与训练的路口录一段 20 分钟的视频把每一帧的灯态检测结果导出来和人工标注做逐帧对比至少人工确认的 500 次灯态变化不能被错过。再专门测一次夜间和雨天场景——这两个场景如果过不了说明数据集覆盖有缺口需要回到第二步补数据。这类项目的“最后一公里”往往不是模型精度而是交通规则的完整性。左转待转区、全屏红灯与箭头灯组合、黄闪灯这种特殊状态都需要在规则表里显式写出来。做这个项目半年多我最深的体会是检测模型可以调数据可以补但交通规则表必须每个字都推敲过因为模型给出的不确定性可以接受规则判断的不确定性是真的会出事故的。希望这些经验帮到你。本文还有配套的精品资源点击获取
返回列表