ARTICLE DETAIL

资讯详情

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

YOLOv8信号灯识别到通行规则判断:训练、规则层与状态机落地指南

YOLOv8信号灯识别到通行规则判断:训练、规则层与状态机落地指南 简介基于YOLOv8的路口交通信号灯通行规则识别项目提供完整Python源码与文档说明是个人毕业设计成果答辩评审得分98分代码已调试测试可运行。压缩包内共17个文件以11个Python脚本为核心数据处理、目标检测、信号灯分类与结果可视化等模块均有对应实现另含YAML配置、Markdown说明、PNG示例及TXT文件整体大小约705KB。已有117人浏览学习适合计算机、通信、人工智能、自动化等专业学生、教师或从业者用于毕业设计、课程大作业或进阶学习。项目目录结构清晰从数据加载、模型推理到通行规则判断与结果绘制都有完整代码支撑便于快速理解YOLOv8在真实交通场景下的应用流程。基础扎实者可直接在此基础上调整修改实现自定义功能配套文档也能为答辩或课程设计提供有力参考。1. 信号灯识别难点不在检测层难的是看懂通行规则做车路协同或者辅助驾驶感知的朋友应该都碰到过这个场景用 Python 和 YOLOv8 已经能检测出红灯和绿灯可单帧的检测输出根本回答不了“现在这个车道能不能走”。信号灯通行规则识别不是从这个路口画面里抠出一个类别就算完成还得回答灯组归属、方向优先级和时序状态变化。这条路径要讲清楚的是从数据集标注、YOLOv8 环境搭建与训练到规则层和状态机的落地再补上最容易踩的坑。适合正在做交通感知算法、拿信号灯识别做毕业设计或想快速搭一个通行规则识别 demo 的工程师和研究者。2. 拆解信号灯识别任务检测层与规则层的职责边界2.1 为什么“只训练一个红灯绿灯检测器”在路口会翻车普通十字路口的画面里检测目标其实不是“一个灯”而是“多个灯组”。目标检测模型在一张单帧图上的输出只有三样东西类别、位置、置信度。类别能告诉你“这个框里是红灯还是绿灯”但它回答不了“这个绿灯属于哪个车道”“是不是箭头灯优先”“前面还有一组红灯在单独控制左转”。通行规则识别真正需要的是三个层次的信息。第一层是语义层同一块灯箱里当前点亮的究竟是圆盘红灯、圆盘绿灯、左转箭头绿灯还是右转箭头绿灯这一层是 YOLOv8 的主场。第二层是拓扑层画面里多个灯组分别对应直行车道、左转车道还是右转车道需要在坐标空间里把灯组归属关系梳理出来。第三层是时序层连续几十帧内信号状态怎么切换、黄灯闪烁时能不能误判成放行这一层靠状态机做时间过滤。如果你只用一个 red/green/yellow 三类别模型直接输出通行结论遇到左转专用灯立刻翻车圆盘灯变绿左转箭头还是红灯直行车道可以起步左转车辆必须停在停止线内模型输出的 green 却让下游把两个车道全部放行。这个例子说明检测层和规则层必须分开设计检测层只负责把“灯的颜色和指向”从视觉上找出来规则层负责把这些字段翻译成车道级的通行结论。YOLOv8 的网络结构里backbone 和 head 的设计决定了它非常适合做这个“找出来”的工作但别指望它替你完成拓扑和时序判断。2.2 数据准备信号灯类别体系设计与标注格式转换动手标数据前先把类别体系定好。我建议按“颜色×指向”拆开而不是只按颜色分。一组典型类别是下面这 9 类类别名含义规则层的用途circle_red / circle_yellow / circle_green圆盘灯三种状态优先级箭头灯高于圆盘灯arrow_left_red / arrow_left_green左转箭头灯左转车道是否放行arrow_straight_green / arrow_straight_red直行箭头灯直行车道是否放行arrow_right_green / arrow_right_red右转箭头灯部分路口右转常绿需单独考虑如果你用视频帧做数据集最顺手的标注工具是 labelme。有一个细节特别容易被忽略框的是“发光灯珠”不是整个黑底灯箱。如果框整个灯箱同一灯组里红绿两盏灯位置很近标注框之间大面积重叠训练时 YOLOv8 的损失函数会在矛盾标签上僵住出现 loss 一直不降、mAP 上不去的情况。我把框收在发光区边缘之后同类框之间的 IoU 小了很多训练曲线肉眼可见地顺了。标注完成后 labelme 默认存 JSON要转成 YOLOv8 的 txt 格式。转换脚本的主逻辑如下import json def labelme_to_yolo(json_path, out_txt, cls_map, img_w, img_h): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label] if label not in cls_map: continue xs [p[0] for p in shape[points]] ys [p[1] for p in shape[points]] x1, y1, x2, y2 min(xs), min(ys), max(xs), max(ys) w, h x2 - x1, y2 - y1 if w 10 or h 10: continue # 过滤过小的标注框 cx, cy (x1 x2) / 2 / img_w, (y1 y2) / 2 / img_h nw, nh w / img_w, h / img_h lines.append(f{cls_map[label]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines))这个脚本核心做三件事取 JSON 里多边形点的外接矩形换算成归一化中心坐标和宽高过滤掉小于 10 像素的框。过滤逻辑值得保留——远距离小灯珠放大后标签漂移严重小于 10 像素的框对训练没有正贡献留下它反而让模型去拟合像素级噪声。要注意 cls_map 的 key 必须和后面 dataset.yaml 里的 names 完全对应不然训练时会报“label not in dataset”之类的问题。2.3 数据增强夜间、逆光和过曝要单独加信号灯数据增强不能照搬通用目标检测套餐。通用的随机 HSV、椒盐噪声在信号灯上会把颜色语义带偏红绿黄三色是强语义特征色相抖动稍微大一点训练出来的模型就开始在绿箭头和红箭头之间摇摆。我常用的增强配置是亮度抖动 ±20饱和度 ±15色相保持为 0对夜间样本单独做一次暗化增强整图乘 0.6 左右的亮度系数随机模糊和运动模糊各 5% 概率模拟雨天抖动和手持拍摄随机旋转只在 ±3° 以内超过之后灯箱外观变化太大模型会把旋转角度误当成目标特征。另外要提一下 mosaic 增强。YOLOv8 默认训练是带 mosaic 的几张图拼成一张大图能增加背景多样性但对信号灯这类小目标拼接过程会把小灯珠进一步缩小甚至截掉。我用 close_mosaic10 让训练最后 10 轮回到不打 mosaic 的真实分布。实际操作上白天和夜间数据建议分开采样特别是有强逆光的路口单独用一组训练集而不是全部混在一起。常见做法是先按 9:1 切 train/val并保证同一个路口、同一天的帧不要同时出现在 train 和 val 里否则验证集指标会虚高换一个新路口就直接打回原形。3. 从搭建 YOLOv8 环境到训练自己的信号灯权重最小可跑通流程3.1 Ubuntu 20.04 CPU 环境怎么搭最小环境很多做验证的同学手里是 Ubuntu 20.04 加 Python 3.10 的 CPU 机器或者 GTX 1660 Ti 这种老卡。调试和短训练完全够用。我建议直接建虚拟环境不要往系统 Python 里塞包后面包一多就会互相踩依赖python3 -m venv yolov8env source yolov8env/bin/activate pip install ultralytics如果你用 NVIDIA GPU再补 torch 的 CUDA 版本CPU 版直接默认安装就好。vscode 里把 Python 解释器切到 yolov8env后续调试都在这个环境里。第一次跑 vscode 里的脚本前先确认解释器路径确实是 venv 里的 python而不是系统的 /usr/bin/python否则会出现包已经装了但 import 失败的鬼打墙问题。用下面两条命令把环境的底摸清楚python -c import ultralytics; print(ultralytics.__version__) python -c import torch; print(torch.__version__, torch.cuda.is_available())torch.cuda.is_available() 输出 False 不代表环境坏了只说明当前装的是 CPU 版或者 CUDA 没配上CPU 调试阶段完全正常。接下来要处理权重离线加载的问题YOLO 在指定 modelyolov8s.pt 时默认会联网下载预训练权重网络受限的环境会一直卡在下载。常见做法是先在能联网的机器上下好 .pt放到自己用户目录的 ~/.cache/ultralytics/weights/ 下面再设置环境变量 YOLO_CONFIG_DIR 指向已有目录就能完全离线加载。环境有没有通一条命令见分晓yolo predict modelyolov8s.pt sourcetest_intersection.jpg imgsz640 conf0.25能在图里看到带框结果说明环境已经通了。这里有个经验先把默认权重跑通再上自己的数据和权重能省掉大半“环境问题还是代码问题”的排查时间。3.2 准备 dataset.yaml 和训练命令训练开始前把数据组织成 YOLO 目录约定traffic_light_dataset/ images/ train/ val/ labels/ train/ val/ traffic_light.yamltraffic_light.yaml 的内容如下train: ./images/train val: ./images/val nc: 9 names: 0: circle_red 1: circle_yellow 2: circle_green 3: arrow_left_red 4: arrow_left_green 5: arrow_straight_green 6: arrow_straight_red 7: arrow_right_green 8: arrow_right_red训练这一段我用 Python API 而不是命令行因为后面要接规则层和状态机训练、验证、推理全在同一个 Python 工程里方便保持数据版本和代码版本同步from ultralytics import YOLO model YOLO(yolov8s.pt) model.train( datatraffic_light.yaml, epochs150, batch16, imgsz640, patience20, device0, cos_lrTrue, close_mosaic10, lr00.001, )这里几个训练参数的含义值得单独说。epochs150 是起步值信号灯场景类别少、目标单一150 轮通常足够真正起作用的是 patience20表示连续 20 轮验证指标不涨就早停防止夜间难例多时在局部最优附近震荡浪费时间。batch16 在 8GB 显存上放 640 输入没问题再大对 BN 没有明显帮助反而把显存占光。cos_lrTrue 让学习率按余弦退火下降这类边界清晰的类别收敛更快。close_mosaic10 配合上一章讲的小目标问题最后回到真实分布再精调。lr00.001 比 YOLOv8 默认的 0.01 低一些信号灯标注框普遍偏小过大的初始学习率会让定位头在前十轮抖动box_loss 曲线很难看。训练结束后runs/detect/train 下面会生成 best.pt、last.pt 和 results.csv。很多人不知道 loss 曲线从哪看最常见做法是直接把 results.csv 拉出来用 matplotlib 画import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) plt.plot(df[epoch], df[train/box_loss], labeltrain box) plt.plot(df[epoch], df[val/box_loss], labelval box) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.savefig(loss_curve.png, dpi150)如果 val 曲线在第 30 轮后抬头而 train 还在下降多半是过拟合优先检查标注一致性和增强强度而不是盲目加 epoch。如果 train/box_loss 从头到尾都在高位先回查标签归一化范围有没有出错常见是 PIL 读图的宽高顺序和 JSON 里存的顺序对不上导致框位整体错乱。3.3 用训练好的权重做单帧推理测一张夜间路口的图from ultralytics import YOLO import cv2 model YOLO(runs/detect/train/weights/best.pt) frame cv2.imread(night_intersection.jpg) results model.predict(frame, conf0.35, iou0.5, device0) for res in results: for box in res.boxes: cls int(box.cls[0]) conf float(box.conf[0]) x1, y1, x2, y2 [int(v) for v in box.xyxy[0]] print(cls, conf, x1, y1, x2, y2)conf0.35 是我在信号灯场景的默认值。通用目标检测常用 0.25但信号灯误检的代价值高框错一个灯会让规则层给错车道放行所以阈值放到 0.35 以上。iou0.5 保持默认到规则层再处理多框重叠的问题。输出坐标是像素坐标直接喂给第 4 章的灯组归属函数。4. 从“检测到灯”到“知道能不能走”规则判断与状态输出4.1 把检测框归属到灯组参考点聚类与可视化推理输出是一串无序框首先要把这些框归到不同的灯组。固定机位路口最常见直接在配置里写死每个灯组的参考坐标点用最近邻归属def assign_to_group(boxes, ref_points, max_dist80): groups {i: [] for i in range(len(ref_points))} for box in boxes: cx (box[0] box[2]) / 2 cy (box[1] box[3]) / 2 best, best_d -1, 1e9 for gi, (rx, ry) in enumerate(ref_points): d (cx - rx) ** 2 (cy - ry) ** 2 if d best_d: best_d d best gi if best_d ** 0.5 max_dist: groups[best].append(box) return groupsmax_dist 是归属半径单位像素取值思路是“大于同一灯组内灯珠的最大间距小于相邻灯组的最小间距”。固定机位且灯箱间距大时80 像素是个稳妥起点我后来换到一个灯箱密集的路口同一灯组内两灯珠距离是 45 像素、相邻灯箱最近距离是 90 像素max_dist 取 60 就既能收全同一组又不会串组。调这个参数时务必可视化我常把每个灯组的中心点画到原图上人工核对直接打印坐标判断容易漏掉极端机位。机位不固定的时候参考点方案会失效。模拟车载摄像头在路口移动时检测框中心点跟随视角变化常见做法是换成聚类归属把所有检测框中心点做一次基于距离的聚类簇数约为灯组数量每个簇就是一个灯组。注意聚类半径不能超过同一灯组内最远两个灯珠的距离这个值可以从已有的参考点标注反推出来不需要每帧都重算。4.2 灯组级规则评估箭头灯、圆盘灯与冲突优先级一个灯组内的多个检测框合并完成后规则层看到的是一串带类别名的候选。确定“能不能走”之前先处理同一灯组内的冲突。最典型的冲突是检测器同时给出 circle_red 和 circle_green 两个高置信框原因通常是红绿灯珠距离极近、候选框重叠NMS 也压不干净。我用下面的优先级箭头灯高于圆盘灯红灯高于绿灯置信度作为同类别内部的裁决依据def evaluate_group(group): # group: [{cls: str, conf: float, box: [x1,y1,x2,y2]}] arrow_green [b for b in group if b[cls].startswith(arrow_) and b[cls].endswith(green)] arrow_red [b for b in group if b[cls].startswith(arrow_) and b[cls].endswith(red)] circle_green [b for b in group if b[cls] circle_green] circle_red [b for b in group if b[cls] circle_red] if arrow_green: direction arrow_green[0][cls].split(_)[1] return fgo_{direction} if arrow_red: return stop_arrow if circle_green and not (circle_red or arrow_red): return go_straight if circle_red: return stop return unknown这个函数把规则层核心的优先级写清楚了左转、直行、右转箭头绿灯各自独立表达只要箭头绿灯在圆盘灯是否同时在画面里都不影响放行结论只有没有任何箭头冲突时圆盘绿灯才代表本方向放行。实际项目里还要补一条右转箭头绿灯和圆盘红灯同时亮是合法状态表示“右转不受红灯限制”这一步不写清楚会在下游把合法状态判成冲突。4.3 连续帧状态机别让单帧抖动毁掉输出把单帧规则层的输出直接往外面发是不行的。夜间或强光下YOLOv8 偶尔会把红灯闪成黄灯单帧漂移如果立刻切换状态下游的预警或控制逻辑就会跟着抖。真实物理信号在几百毫秒内不会变所以时序上要加一道保险。我给每个灯组单独配一个状态机只有连续 N 帧检出同一个状态才切换输出N 取 3 到 5也就是大约 100 到 200 毫秒的连续确认时间class LightStateMachine: def __init__(self, n4): self.n n self.buf [] self.state None def update(self, obs): if obs self.state: return self.state if obs is None: if self.buf: self.buf.pop(0) else: self.buf.append(obs) if len(self.buf) self.n: if all(x obs for x in self.buf[-self.n:]): old self.state self.state obs self.buf.clear() return self.state这个状态机比“直接比较上一帧”多了一个滑动窗口核心逻辑是状态切换必须由连续 n 帧的同一观察推动单帧抖动只是噪声不会触发切换。obs 为 None 时表示检测丢失此时不立即清空窗口保留一点容错避免频繁抖动。真实路口有黄灯闪烁的场景黄灯单独闪烁表示“不可通行”状态机应在 stop 状态上多锁一两帧避免闪烁结束瞬间立刻切到绿灯放行。5. 避坑/常见问题信号灯识别里最容易被翻车的五个点5.1 夜间过曝灯珠变成一个大亮斑现象夜间路口的路灯和暗背景反差太大摄像头自动曝光把灯珠区域过曝红绿高光区域熔成一片检测器要么漏检要么把圆盘灯误判成箭头灯。原因单帧原始信号里灯珠的颜色相位信息被高光掩盖YOLOv8 学到的“红色圆形轮廓”特征不再成立。解决数据侧加入过曝样本把过曝后的亮斑形状当作新的标注边界不要按正常灯珠形状硬标。推理侧做一个轻量预处理把画面里 R 通道和 G 通道差值很小的过饱和像素先置成中性灰再送检测。做过一组对比实验加了过曝样本和“高光中性化”预处理之后夜间红绿灯 mAP 大概抬升两到三个点误检也明显变少。5.2 倒计时数字被当成灯现象红绿灯旁的倒计时数字区域在夜间也是高亮细长条经常被检出成绿色箭头灯导致绿灯状态输错方向。原因检测器学到的特征里高亮、细长、绿色这些线索同时在数字区和箭头灯上出现类别区分度不够。解决最实用的一招是单开一个 countdown 类别把倒计时数字区域也标出来放进训练集给模型一个“这是数字不是灯”的语义出口。加了这一类别后箭头灯误检通常能压掉一大半。如果不想扩类别可以走规则过滤检测框宽高比大于 3 的一律判为数字区不参与放行判断。还要注意红灯变绿灯瞬间倒计时数字会同时出现规则层里必须写死“数字永远不参与放行判断”。5.3 远距离小目标漏检现象摄像头往路口纵深看远侧信号灯只有 12×12 像素imgsz640 的推理几乎检测不到辅助驾驶系统在远距离上丢状态。原因小目标主要由 YOLOv8 浅层特征负责输入分辨率不够的时候小灯珠无法形成有效特征候选框数量也不足。解决把 imgsz 从 640 提到 960是成本最低且见效最直接的一步代价是推理时间增加但车路协同通常能接受。算力不够就做 tile 推理把原图按路口区域裁成多个块分别送检再合并回原坐标系。用 1920×1080 的原始视频时不要直接拉伸成 960×960 方图先等比缩放再居中填充否则灯珠长宽比被压扁训练和推理的数据分布就不一致。5.4 相邻灯组框被 NMS 合并成一个框现象直行灯组和左转灯组并排两个绿灯同时亮推理时只出一个大框把两个灯全部包住下游拿到这个大框无法判断直行和左转谁在放行。原因YOLOv8 后处理 NMS 对 IoU 高的同类框做抑制合并相邻同色灯珠距离近候选框交叠面积大后处理阈值把两个候选框合并了。解决两招配合。推理阶段调低 nms_iou从 0.5 调到 0.3让重叠度稍大的同类框不被抑制代价是产生一些重复框调试阶段同时看 NMS 前后的框集合确认哪些候选框被吞掉了。对车载场景按类别分组做 NMS 更实用直行绿灯和左转绿灯属于不同类别互不抑制只有同类别同位置的高重叠框才该合并。5.5 绿灯样本过多红灯黄灯不够现象训练集里超过六成是绿灯黄灯不到 5%训练出来的模型对黄灯召回率很低真实路口黄灯亮起时迟迟不输出“黄灯”状态。原因信号周期里绿灯时长远大于红灯和黄灯按时间抽帧天然就是绿色偏多黄灯窗口最短样本最少。解决样本层面做类别重采样把红灯和黄灯的样本复制若干份并加一点颜色抖动和亮度变换混进训练集把红绿黄比例大致调到 1:2:1。损失层面可以在配置里给黄灯类别加重惩罚YOLOv8 的 cls_loss 是二值交叉熵按类别权重放大稀缺类别的信号即可。更重要的一点是验证时按类别单独看 AP不要只看整体 mAP——整体数值可能很漂亮但黄灯仍然是短板这对通行规则识别是致命的。6. 让检测结果在路口真正可用帧缓存、动态灯组归属与置信度反馈6.1 加一个帧缓存投票层避免单帧丢失拖垮输出第 4 章的状态机只回答“切不切状态”如果系统需要输出带置信度的状态可以在状态机之前加一个长度 10 的循环队列每次取最近 10 帧规则层结果做简单投票得票过半的状态才作为当帧推荐。这个思路在传感器丢一两帧时也能扛住缓存缺帧权重自动向历史多数倾斜不需要额外补帧。6.2 动态灯组归属让参考点跟随机位漂移固定参考点在摄像头轻微抖动时会在灯组边界上抖。常见做法是维护一张以检测框中心为坐标的聚类模型每隔一段时间用最近 50 帧的检测中心重新计算簇中心让参考点跟随机位缓慢漂移。实现时只要在 4.1 的 assign_to_group 外部加一个周期性更新的逻辑不要每帧都聚类那样反而在灯组切换时引入高频抖动。6.3 置信度反馈把 YOLOv8 的 conf 接到状态机参数上YOLOv8 输出的置信度本身包含对当前目标可靠性的估计把它的历史均值接进状态机参数可以在“快速响应”和“抗误判”之间取平衡。我用的策略是YOLOv8 平均置信度状态机连续帧要求效果大于 0.82 帧高可靠场景响应快0.45 到 0.84 帧常规场景保持抗抖小于 0.456 帧低可靠场景宁可慢这个表的意义是让状态机阈值跟随感知置信度自适应而不是给整条链路定死一个恒定延迟。低置信度的夜间样本本来就容易被误检多等两帧换一次正确切换血泪经验是值得的。我现在接入一个新路口时一定会先跑一遍无状态机的纯检测流把灯组归属、类别名称、优先级全部可视化成日志确认规则层输出和真实信号周期对得上再往上挂状态机和置信度反馈。宁可让检测层多花十几毫秒做后处理也不能让规则层吃进一个错框。这套逻辑我用了大半年的项目踩过上面五个坑之后才逐步补齐希望帮到你。本文还有配套的精品资源点击获取
返回列表