ARTICLE DETAIL

资讯详情

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

YOLOv8路口信号灯通行规则识别:从训练到RK3588部署实战

YOLOv8路口信号灯通行规则识别:从训练到RK3588部署实战 简介这是一份基于YOLOv8的路口交通信号灯通行规则识别项目面向计算机、人工智能、自动化等专业的学生、教师及从业者可支撑毕业设计、课程大作业或工程实践。项目包含完整算法源码与文档说明代码结构按功能拆分入口主程序、识别与预测模块负责核心逻辑配置管理、数据预处理、可视化等模块辅助流程衔接层次清晰易于定位和修改。资源包共17个文件以11个Python脚本为主另有YAML参数配置、Markdown文档、示例图片等项目必需内容整体仅705KB轻量而完整。代码均经过调试测试确保可直接运行作者以此完成毕业设计并获98分答辩成绩具备较高的学习与借鉴价值。目前已有117人学习下载适合希望深入理解YOLOv8在真实交通场景中应用、并快速搭建同类模型的读者参考。1. 为什么路口信号灯识别比普通目标检测更难YOLOv8方案的价值一个直观的错觉是红绿灯识别嘛框出来、认颜色就行。可真正把模型送进路口测试时你会遇到红灯被路灯照成橙黄色、绿灯和旁边广告牌撞色、车在左转车道却看着全屏绿灯直接放行——这种“检测对了、判断错了”的情况才是路口交通信号灯通行规则识别的核心难点。Python基于YOLOv8的路口交通信号灯通行规则识别模型及算法源码价值不在“用YOLOv8框灯”这一层而是把目标检测、灯色判定、车道方向约束和帧间状态机串成一条完整链路最后能回答“当前车道能不能走”这个业务问题。它适合正在做辅助驾驶、车路协同、路口监控的工程师也适合想拿YOLOv8做一个真实落地项目而不是只跑官方示例的同学。下面按我自己的调试顺序从数据准备一直讲到RK3588部署验证。2. 训练准备信号灯数据集、标注策略与YOLOv8环境配置拿到一个带源码和文档说明的信号灯识别项目我习惯先不急着跑模型而是把文档说明里的数据集格式、类别顺序、Python版本要求看一遍。很多源码包训练翻车不是模型写错而是标签文件、类别索引和环境版本三者对不上。先把地基打稳后面所有操作才可复现。2.1 数据集来源与选型原则公开数据、自采数据与负样本路口信号灯识别能用的公开数据集不算少常见的有LISA交通灯数据集、Bosch Small Traffic Lights这类专为小目标检测设计的红绿灯数据网上也能找到一些路口监控脱敏数据。公开数据的问题是场景固定、摄像头角度单一直接拿来做你的路口场景泛化往往不够。我一般会以公开数据做预训练或联合训练再自采当前路口的视频抽帧补一批数据数量不用贪多关键是覆盖不同时段。标注策略上类别定义直接决定后面规则层的复杂度。最简单也最稳的一套类别设计是red红色全屏灯亮起green绿色全屏灯亮起yellow黄色全屏灯亮起有箭头灯的路口再拆成red_left、green_left、red_forward、green_forward等不用把“灯灭”也标成一个类灭灯只会在判定阶段造成干扰模型把它框出来反而增加误检。我见过有人把整个灯壳标成一个框结果红灯、绿灯两种状态下框内的视觉特征几乎一样模型怎么训都分不清颜色这就是标注策略的问题。负样本一定要加。路口场景里红色刹车灯、红色广告牌、LED倒计时数字、对向车灯都可能被误检成信号灯。我通常会在训练集里掺20%左右的负样本图片也就是完全不包含信号灯的路口背景图标签文件写成空文件。这个习惯能明显压低红灯误报率因为YOLOv8会从负样本里学到“这不是目标”的背景特征。2.2 最小环境配置用conda装好torch和ultralyticsYOLOv8本身的依赖不复杂核心就是PyTorch和ultralytics包。最容易踩的坑是装了CPU版PyTorch训练时才发现GPU用不上所以我第一步总是先用conda建一个干净环境再按需装对应版本的torchconda create -n yolo python3.10 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics python -c import torch; print(torch.cuda.is_available())这段命令里cu118对应CUDA 11.8运行时如果你的显卡驱动支持更高CUDA版本可以把索引地址里的cu118换成cu121或cu124。最后一行torch.cuda.is_available()输出True才算环境正常输出False就回头检查GPU驱动和torch版本是否匹配。很多入门机器的显卡是GTX 1660Ti6G显存跑YOLOv8是有压力的。我的经验是yolov8n模型、640分辨率、batch设16能勉强跑起来但训练后期显存容易吃紧稳妥一点用yolov8s.pt预训练权重配batch 8。如果训练中没有GPUCPU也能跑小数据集但速度慢几十倍建议先把样本量压到几百张验证流程再上GPU训全量。2.3 训练参数和自定义yamlimgsz、batch、epochs的取舍YOLOv8训练前要准备一个数据集描述文件文件名随意内容是指向图片和标签的路径映射。路口信号灯项目里我的traffic_light.yaml长这样path: ./traffic_light_data train: images/train val: images/val nc: 3 names: 0: red 1: green 2: yellowpath是数据集根目录train和val是相对于根目录的图片文件夹路径YOLOv8会自动去对应目录下找同名的.txt标签文件。nc是类别数量names里的索引顺序必须和标签文件里第一列的数字一一对应否则模型会把红灯当绿灯训。环境没问题后我一般这样启动训练yolo detect train \ datatraffic_light.yaml \ modelyolov8s.pt \ epochs120 \ imgsz640 \ batch16 \ patience20 \ device0参数含义和调整策略可以看这张表参数我的常用值调整思路modelyolov8n.pt / yolov8s.pt显存小选n精度要求高选s或mimgsz640小目标密集时可试1280信号灯占图比例小分辨率高对召回有帮助但训练和推理都变慢batch16或8OOM时减半不要硬扛epochs120300配合patience自动早停patience20连续20轮val loss不下降就停省时间device0指定第一张GPUCPU训练就填cpuimgsz1280不是越高越好。训练时用1280部署到RK3588这类NPU上往往也要求同样的分辨率推理耗时直接翻倍。我建议先用640跑一轮统计一下小目标漏检情况如果远端信号灯完全框不出来再考虑升级分辨率。3. 训练自己的数据集从VOC转换到损失曲线判读与模型评估环境就绪、数据也整理好之后进入训练正题。这一章最容易翻车的是数据格式转换和训练过程判读。YOLOv8不是黑匣子它把每个epoch的损失和指标都写进了CSV关键在于你会不会看。3.1 把VOC/COCO标注转成YOLO格式转换脚本与四个边界坑很多公开红绿灯数据集的标注是VOC XML格式而YOLOv8要的是txt格式每行一个目标内容为类别编号 中心点x 中心点y 框宽 框高数值都归一化到01。我写过一个最简转换脚本可以照着改成自己的路径import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_dir, txt_dir, classes): os.makedirs(txt_dir, exist_okTrue) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in classes: continue cls_id classes.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) x_center (x1 x2) / 2 / img_w y_center (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) txt_name os.path.splitext(xml_name)[0] .txt with open(os.path.join(txt_dir, txt_name), w, encodingutf-8) as f: f.write(\n.join(lines)) classes [red, green, yellow] voc_to_yolo(path/to/xml, path/to/txt, classes)这段脚本的逻辑是解析XML里的bndbox四角坐标除以图片真实宽高得到归一化坐标再按类别索引 中心点 宽高的格式写入txt。代码里的classes列表顺序就是模型里的类别索引我在2.3节强调过这个顺序一旦确定就不要改否则重新训练等于从零开始。转换时有四个边界坑值得标记图片宽高必须读原图信息不能写死640之类否则归一化坐标全错。XML里如果存在不在classes列表里的类别直接跳过不要报错中断。txt文件必须和图片同名YOLOv8按文件名匹配后缀差异会导致标签丢失还不报错。如果一张图里完全没有目标txt文件要保留为空文件不能删除否则该图会被跳过或报警告。3.2 启动训练并用results.csv画损失函数曲线图训练命令在2.3已经给出。训练开始后终端会滚动输出每个epoch的box_loss、cls_loss、mAP50等指标同时训练结果会写到runs/detect/train目录下。我每次都会等训练结束后用下面这段代码把损失曲线画成一张图判断模型是否收敛import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) df.columns [c.strip() for c in df.columns] fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(df[epoch], df[train/box_loss], labeltrain_box_loss) axes[0].plot(df[epoch], df[val/box_loss], labelval_box_loss) axes[0].set_title(Box Loss) axes[0].legend() axes[1].plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) axes[1].plot(df[epoch], df[metrics/mAP50-95(B)], labelmAP50-95) axes[1].set_title(mAP) axes[1].legend() plt.savefig(training_curve.png)注意results.csv的列名在不同版本的ultralytics里略有差异比如旧版本可能是train/box_loss新版本可能是train/box_loss加后缀。运行前先print(df.columns)看清列名再画图。判读曲线时我的习惯是看三件事第一train/box_loss是否持续下降且最终平滑第二val/box_loss有没有在某个epoch后掉头上升如果上升说明过拟合应该提高patience或者增加数据第三mAP50有没有冲到0.9附近。如果val/box_loss一直高位震荡那大概率不是参数问题而是标签错乱或者数据增强把红绿灯状态搞反了后面避坑章节会专门讲。3.3 评估指标用混淆矩阵和PR曲线定位红灯漏检训练完不能只看mAP50。对路口信号灯这种安全敏感场景红灯漏检比绿灯误判严重得多而mAP是一个综合量它无法告诉你“红灯这一类到底漏掉多少”。我每次训练完都会补一条验证命令yolo detect val \ modelruns/detect/train/weights/best.pt \ datatraffic_light.yaml \ splitval验证结束后runs/detect/val目录下会生成confusion_matrix.png和PR_curve.png。混淆矩阵里最需要关注的是red这一类被预测成background的比例以及red和green之间有没有系统性互认。如果红灯和绿灯大量混淆先回头查标注类别顺序和数据增强配置而不是急着换模型。信号灯小目标居多mAP50-95通常比常见的大目标数据集低一些这属于正常现象。你可以把更多权重放在mAP50和召回率上部署阶段再用下游规则层兜底。我见过不少团队把时间和算力耗在把mAP50-95从0.45刷到0.5但实际路口测试并没有变好不如把同样的精力拿去补夜间的负样本和灯色判定逻辑。4. 通行规则识别灯色判定、车道方向与帧间状态机模型把信号灯框出来这只是第一步。真正的通行规则识别要做的是把“检测框”变成“当前车道可通行”这个布尔结果。这一章我会讲三个环节检测框内的灯色判定、车道方向的规则映射、以及连续帧的防抖状态机。4.1 用OpenCV做灯色判定HSV阈值法的参数细节模型输出的类别如果已经区分了红灯、绿灯和黄灯理论上可以不依赖颜色判定。但实际路口场景中模型分类远没有人类对颜色的直觉稳定同一盏红灯在清晨、傍晚、夜间色温变化很大分类置信度也会波动。所以我会在检测框内再做一次HSV颜色判定作为分类结果的二次确认import cv2 import numpy as np COLOR_HSV { red: [(0, 100, 100), (10, 255, 255), (170, 100, 100), (180, 255, 255)], green: [(35, 80, 80), (85, 255, 255)], yellow: [(15, 80, 80), (35, 255, 255)], } def judge_color(crop_bgr): hsv cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2HSV) scores {} for name, ranges in COLOR_HSV.items(): mask np.zeros(hsv.shape[:2], dtypenp.uint8) for i in range(0, len(ranges), 2): lower np.array(ranges[i], dtypenp.uint8) upper np.array(ranges[i1], dtypenp.uint8) mask | cv2.inRange(hsv, lower, upper) bright float((mask 0).sum()) scores[name] bright / max(crop_bgr.size, 1) return max(scores, keyscores.get), scores这段代码把检测框从BGR转到HSV色彩空间然后分别统计红、绿、黄三色的像素占比。红色需要两个H区间因为HSV里红色的Hue在0附近是一个环形区间010和170180都是红色只取一个区间会漏掉一半。crop_bgr.size是裁剪图的总像素数用亮色像素占比做归一化可以避免灯壳本身偏灰导致的误判。参数上V即亮度通道我设了最低100低于这个值的像素不算亮色可以有效排除灰暗的灯壳。如果你用的是偏色严重的监控摄像头可能需要把色相区间的上下界放宽例如绿色从3585改成3090但放宽后误判也会增加建议用一组实际抓拍图做离线测试后再定。4.2 全屏灯和箭头灯的规则映射从检测框到能否通行灯色判定的输出是“这个框是什么颜色”但路口通行规则要回答“当前车道可不可以走”。两者之间差了一个车道映射。最简单的做法是把画面按车道区域预划分用检测框中心点落在哪个区域来判断车辆当前所在车道。规则层我一般用一个配置加一个判定函数来写。配置里定义每种信号灯状态对应的通行结果# 简化版规则direction表示当前车辆目标方向 # 全屏灯状态和箭头灯状态分开判断 def can_pass(signal_status, direction): # 箭头灯优先如果对应方向有绿色箭头直接放行 if signal_status.get(f{direction}_green): return True if signal_status.get(f{direction}_red): return False # 没有箭头灯时看全屏灯 if signal_status.get(green): return True if signal_status.get(yellow): return wait # 黄灯进入等待观察 return False这里direction只取left、forward、right三种signal_status是字典对应全屏灯和箭头灯的检测结果。箭头灯优先于全屏灯的规则必须写进代码里因为很多路口同时存在全屏灯和左转箭头灯如果全屏是红灯但左转箭头是绿灯左转车道应该放行。车道和信号灯的映射关系依赖摄像头安装位置每个路口都不一样。常见做法是在配置文件里为每个检测框ROI指定它管辖的车道方向例如画面左侧1/4区域内的灯属于左转车道中间属于直行车道。这个先验知识用肉眼标一次就行不需要训练。4.3 帧间状态机连续判决与超时兜底单帧的灯色判定结果抖动很严重尤其是LED交通灯在摄像头下会出现频闪黄色箭头在临界帧可能被识别成红色。如果直接把单帧结果送进下游逻辑通行指令会来回跳变这是最危险的。我在项目中用了一个简单的状态机来平滑输出class LightStateMachine: def __init__(self, window5, change_frames3, timeout75): self.window window self.change_frames change_frames self.timeout timeout self.history [] self.state None self.since_change 0 def update(self, raw_state): self.history.append(raw_state) self.history self.history[-self.window:] candidate max(set(self.history), keyself.history.count) if candidate self.state: self.since_change 0 return self.state self.since_change 1 if self.since_change self.change_frames and self.history.count(candidate) self.window - 1: self.state candidate self.since_change 0 if len(self.history) self.window and candidate unknown: self.state red # 连续无法判定时保守进入红灯状态 return self.state这个状态机的逻辑是维护最近5帧的灯色判定结果只有连续3帧以上出现同一个新状态才切换输出状态。timeout用来处理长时间无法判定的情况——比如摄像头被卡车挡住连续75帧拿不到状态干脆输出red让车辆进入保守停车模式。我给这种“连续3帧一致才切换”的做法取了个外号叫通行规则后悔药因为如果没有这个缓冲视频里一个瞬间的绿色反光就会让系统在红灯时误放行。宁可晚一拍切换状态也不能频繁跳变。5. 路口信号灯识别部署避坑指南5个真实翻车现场这一章是血泪经验。YOLOv8本身不难跑通但红绿灯识别项目的坑都藏在数据、增强、色彩和部署转换这些环节里。下面5条都是我在实际调试中遇到过、也定位过根因的问题按“现象、原因、解决”三条写清楚。5.1 红灯和绿灯在训练时被翻转数据增强把标注搞反了现象训练loss降得很低验证集mAP也不差但视频实测时红灯和绿灯经常互换而且互换规律像是左右镜像。原因YOLOv8默认开启左右翻转增强fliplr。对一般目标来说翻转前后类别不变但红绿灯是左右对称的实物翻转后红灯还在红灯位置语义上应该不变。问题出在标注框和类别没有同步翻转或者你用了带方向属性的箭头灯类别red_left翻转后变成了red_right而你的类别表里只有red_left没有red_right模型只能硬把它认成别的类。解决训练命令里加一行关闭水平翻转fliplr0.0。如果是箭头灯方向问题要么补全左右类别要么也关闭翻转。红绿灯场景对翻转增强的收益本来就小关掉后模型更稳。5.2 标注框范围玄学框太大mAP不稳框太小训练不收敛现象同一份数据标注人员有的框住整个灯壳有的只框发光圆点模型训练后mAP在0.7和0.85之间大幅波动怎么调参数都压不下来。原因灯壳框里包含了外壳、黑色背景和反光不同状态下视觉特征差异变小只框发光点又会让目标尺寸过小YOLOv8的anchor匹配难度剧增。信号灯在整幅图中往往只有20×20像素标注口径稍微不统一损失就一直在震荡。解决统一标注规范以发光灯芯的椭圆外切矩形为准略微外扩23个像素让框内主要是发光区域而不是灯壳。最好由一个人标注所有数据并在转换脚本里做一次框尺寸统计异常大的框单独排查。5.3 摄像头偏色导致HSV误判白灯被认成黄灯现象现场测试时明明是白色LED倒计时数字HSV判定结果却是黄色车辆在路口跟着倒计时乱走。原因监控摄像头自动白平衡在夜间会偏向暖色白色灯珠色温漂移后落入黄色HSV区间。HSV阈值是固定的而真实世界的色温不是固定值用单一阈值覆盖所有时段必然翻车。解决几种方案叠加使用。先把模型分类置信度和HSV判定结果做加权融合两者一致才输出再把HSV阈值改成动态校准取检测框周围天空或路面的灰度值做白平衡参考最省事的办法是固定摄像头曝光和白平衡参数禁止自动调节。如果在低成本项目里我会优先用模型分类结果把HSV降级为辅助角色。5.4 导出ONNX后输出维度算错84个通道不是80类现象用yolo export formatonnx导出模型后自己写Python推理发现输出形状是[1, 84, 8400]按COCO的80类去解析得到的坐标全乱。原因YOLOv8的输出通道数是4 类别数。COCO预训练模型是4 80 84但你的红绿灯模型只有3类正确输出应该是4 3 7个通道。如果你直接套网上的COCO后处理代码类别数对不上坐标和置信度解析自然全错。解决导出后先打印模型输出形状确认类别维度。解析时按4个坐标 你的类别数切分不要写死84。还有个更省事的做法是直接用ultralytics的model.predict跑ONNX内部后处理已经替你处理了维度变化。5.5 RK3588部署时精度骤降量化校准集不能只放白天现象同一个模型在PC上FP32推理mAP正常转成RKNN放到RK3588上之后夜间红灯漏检率明显上升白天的结果倒是还行。原因RKNN默认做INT8量化需要一组校准图片来统计激活值分布。如果校准集全是白天的路口图模型对夜间暗光、车灯光晕、路灯偏色的激活范围没有覆盖量化后这些场景的特征被截断精度就掉了。解决准备200500张覆盖白天、夜晚、逆光、雨天的路口图作为量化校准集。校准集不需要标注但必须贴近真实部署场景。转模型时把校准集路径分好转换后先在PC上对比量化前后每张测试图的输出差异超过预期就扩大校准集或者改用混合量化。6. 进阶与最终验证在RK3588上跑通并统计FPS和漏检率最后一公里是部署验证。如果你只是做PC端离线分析模型在GPU上跑就够了但要做路侧或车载实时检测RK3588的NPU是很常见的目标。标题里提到的源码和文档落到部署这一步我建议重点验证两点帧率和红灯漏检率。6.1 RK3588部署路径YOLOv8转ONNX再转RKNN先把训练好的best.pt导出成ONNXyolo export modelruns/detect/train/weights/best.pt formatonnx opset12 imgsz640然后在PC上使用RKNN-Toolkit2把ONNX转成RK3588用的rknn模型。转换脚本的核心部分大概是这样from rknn.api import RKNN rknn RKNN() rknn.config(mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588) rknn.load_onnx(modelbest.onnx) rknn.build(do_quantizationTrue, datasetcalibration.txt) rknn.export_rknn(traffic_light_rk3588.rknn)calibration.txt里放的是量化校准图片路径每行一张内容建议按5.5的要求准备。mean_values和std_values要和你训练时的预处理保持一致YOLOv8默认是除以255所以均值0、标准差255没问题。板端推理代码可以用RKNN的Python接口推理前把图像resize到640×640输出后按检测框格式解析。验证时拿一段真实路口视频循环跑统计每秒帧数FPS并记录红灯漏检和绿灯误放行两个指标验证维度测试方法通过标准FPS连续推理1000帧取平均RK3588上建议大于25红灯漏检率统计标注红灯帧中被漏检的比例小于1%绿灯误放行统计红灯状态下输出可通行的帧数必须为0判定抖动统计3秒内状态翻转次数小于2次6.2 验证之后我养成的调优习惯每次部署完我都会把低置信度检测框和HSV判定不一致的帧单独存成图片积累一周后拿去做二次标注补进训练集。这个习惯看起来笨但效果比调任何超参数都明显。我也学会了先怀疑规则层再怀疑模型本身——很多“模型识别错了”的结论最后发现是车道方向映射写反了。希望这篇笔记能帮你少走几段弯路也希望你的路口信号灯项目一次跑通。本文还有配套的精品资源点击获取
返回列表