首页 / 资讯中心 / 文章详情

基于YOLOv8的信号灯识别与通行规则算法实战

基于YOLOv8的信号灯识别与通行规则算法实战 ★ FEATURED ARTICLE
简介基于YOLOv8的路口交通信号灯通行规则识别模型是一套完整可运行的毕设级Python项目主要面向计算机、通信、人工智能、自动化等专业的学生、老师及从业者适用于毕业设计、课程设计及进阶学习场景。压缩包共17个文件以Python源码为主11个py涵盖检测、分类、识别等核心算法模块另含配置YAML、Markdown说明文档、示例图片等整体仅705KB轻量精炼便于快速部署与二次开发。项目已经过调试测试答辩评审分达98分具备较高的完整性与工程规范性目录结构清晰模块划分合理能够帮助使用者快速定位关键流程。目前已有117人学习下载适合希望基于YOLOv8理解交通信号灯识别算法、熟悉项目工程结构、或在此基础上进行改进创新的读者。基础较强的使用者还可以调整现有代码拓展更多实际应用功能实现不同场景下的信号灯通行规则识别。1. 路口信号灯通行规则识别为什么必须自己训 YOLOv8在路口视频里做交通信号灯通行规则识别最常踩的一个坑是检测模型在测试集上表现挺好一上实际路口就开始红绿混淆、远灯漏检。用 Python 基于 YOLOv8 训练一套自己的信号灯识别模型再做一层通行规则算法是自动驾驶数据闭环和智慧路口项目里被反复验证的做法。它的意义不在于把灯框出来而在于把“哪个灯、什么颜色、对应哪条车道、当前能不能走”完整输出给下游。适合要做路侧感知、辅助驾驶测试或路口视频结构化分析的工程师。这篇就按从数据集到部署的路径把模型和算法源码的完整思路拆开讲。2. 信号灯识别模型的第一步把数据集类别体系定对2.1 先理清检测任务为什么我的做法是把灯体和灯色分开很多人第一次做信号灯识别第一反应是类别直接设成 red、green、yellow然后标注、训练、收工。这个做法在实验里能跑通但放到真实路口会持续返工。原因有三个黄灯在路口亮的时间极短样本量天然少直接学颜色会把黄灯学成红灯夜间和过曝场景下红色和黄色的像素分布非常接近如果再叠加箭头方向类别会膨胀成“左转红灯、左转绿灯、直行红灯……”这种组合类任何一个方向样本不够整个类别的 AP 就崩。我一般会把任务拆成两段第一段是目标检测只负责找出灯体并判断灯型——圆灯、左转箭头、直行箭头、右转箭头必要时加一个数字灯牌类别第二段是灯色分类对检测框内的灯珠区域判断红、黄、绿、灭四种状态。灯型类别少样本均衡检测器好训灯色分类用一个小分类头或者像素统计就能做不需要把颜色塞进检测类别里。这样拆还有一个额外好处黄灯样本少的问题可以在分类阶段单独处理不用重新训练检测器。如果项目周期很紧非要把灯型和灯色做进同一个检测器也不是不行但你要接受两个代价一是夜间过曝时红黄混检很难收敛二是新增一个路口灯型就要重新标注全部数据。两阶段方案里检测器换场景通常还能用真正要重新适配的只有分类层。我的建议是除非你的场景只是固定点位、固定相机角度否则优先拆开。类别体系可以参考下面这个表格阶段类别名称含义标注要求检测circle_light圆形满盘灯体框住灯罩不包含周边背景检测arrow_left左转箭头灯体框住箭头区域不含文字检测arrow_straight直行箭头灯体同上检测arrow_right右转箭头灯体同上检测countdown_digit倒计时数字区域框住整个数字显示区分类red / yellow / green / off灯珠颜色状态只对灯珠核心区域取值这里有个容易忽略的标注细节灯体框不要含整个灯头外壳更不要把灯杆标进去。信号灯目标在画面里通常只有 20 到 40 像素框稍微大一点NMS 之后预测框和真实框的 IoU 就掉得厉害小目标 AP 直接受影响。我的习惯是只框到灯罩边缘宁可紧一点不要松。2.2 数据划分、标注规范与 YOLOv8 的数据配置信号灯数据集的划分方式和一般目标检测不一样。普通场景随机切分训练验证集问题不大但路口视频相邻帧高度相似如果随机抽帧切分验证集里全是训练集同一段视频的邻居帧损失曲线漂亮mAP 虚高换一个路口立刻现原形。正确做法是按视频片段或者按相机 ID 分组切分保证同一个路口的同一段连续画面只出现在训练集或验证集其中之一。数据规模方面我的经验是单路口固定机位3000 到 5000 张标注帧就能得到一个可用的检测器如果要覆盖多个路口、不同朝向、早晚高峰和夜间场景建议按场景类别各补样本而不是无脑堆总张数。夜间样本至少要占总量的三成否则白天训练出来的模型在夜间过曝画面上会漏掉一半灯体。标注完成之后YOLOv8 需要一份数据集配置文件和相应的目录结构。常见的目录组织方式如下traffic_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── traffic_light.yamlYAML 配置文件内容如下path: /path/to/traffic_dataset train: images/train val: images/val names: 0: circle_light 1: arrow_left 2: arrow_straight 3: arrow_right 4: countdown_digit这里路径用绝对路径最省事YOLOv8 对相对路径的解析在跨平台时容易出问题。标签文件是 YOLO 格式的 txt每一行是“类别 id 归一化中心 x 归一化中心 y 归一化宽 归一化高”用 LabelImg 或者 X-AnyLabeling 导出都可以。需要注意归一化坐标是基于原图宽高的如果训练时 imgsz 设置得和原图分辨率差距过大小灯体在缩放过一次之后可能只剩十几个像素这是后面训练效果差的常见原因之一。还有一个增强上的坑YOLOv8 默认开启上下翻转增强但对信号灯场景必须关掉。灯体上下翻转之后红灯会出现在原本绿灯的位置灯型语义完全错乱模型学到的位置先验会被破坏。训练配置里设置flipud0.0即可。左右翻转可以保留但要配合类别映射否则左转箭头翻成右转箭头标注类别就错了。2.3 用 Python 按相机维度切分数据集的脚本为了避免同源视频污染这里给一个按视频文件维度做划分的 Python 脚本。假设每一段视频抽帧后放在单独的子目录里目录名作为相机或片段的唯一标识import os import random import shutil random.seed(2024) dataset_root /path/to/traffic_dataset source_images /path/to/frames_by_video val_ratio 0.2 video_dirs [d for d in os.listdir(source_images) if os.path.isdir(os.path.join(source_images, d))] random.shuffle(video_dirs) val_count max(1, int(len(video_dirs) * val_ratio)) val_videos set(video_dirs[:val_count]) for video_id in video_dirs: frames os.listdir(os.path.join(source_images, video_id)) dest_img_root os.path.join(dataset_root, images, val if video_id in val_videos else train) dest_lbl_root os.path.join(dataset_root, labels, val if video_id in val_videos else train) os.makedirs(dest_img_root, exist_okTrue) os.makedirs(dest_lbl_root, exist_okTrue) for frame in frames: if not frame.endswith(.jpg): continue base os.path.splitext(frame)[0] src_img os.path.join(source_images, video_id, frame) src_lbl os.path.join(source_images, video_id, base .txt) if not os.path.exists(src_lbl): continue shutil.copy(src_img, dest_img_root) shutil.copy(src_lbl, dest_lbl_root)这段脚本的核心逻辑是按“视频目录”而不是“单帧”做随机分配。val_ratio控制验证集占比一般 0.15 到 0.2 就够。脚本会跳过没有标签文件的帧避免把空标签混进训练集。实际项目中我还会在复制之前打印每个类别的样本数分布确认黄灯和左转箭头这类长尾类别没有在切分时被极端稀释。很多开源数据集喜欢用 VOC 或 COCO 格式发布如果你拿到的标注是 VOC 的 XML需要先转成 YOLO 的 txt 格式再喂给 YOLOv8。常见的做法是用ultralytics自带的转换工具或者自己写一个 XML 解析脚本提取bndbox的坐标并做归一化。这一步没有技术难度但要注意 VOC 的坐标是绝对值必须除以图片实际宽高很多人在这丢掉小数点导致框偏移好几个像素。3. 用 YOLOv8 训练信号灯模型损失曲线、超参与输出3.1 环境配置与第一条训练命令在写训练命令之前先解决运行环境。YOLOv8 用的是ultralytics这个 Python 包依赖 PyTorch、OpenCV、NumPy 等常见库。如果你是第一次搭环境建议直接用 conda 建一个干净的虚拟环境避免和系统 Python 的其他包冲突conda create -n traffic python3.10 -y conda activate traffic pip install ultralyticsultralytics会自动带上适配当前机器的 PyTorch 版本。如果你需要 GPU 训练建议提前确认 PyTorch 的 CUDA 版本和驱动匹配常见做法是先装好 PyTorch 再装 ultralytics否则容易出现 torch 的 CUDA 不可用。CPU 机器也能训练但信号灯数据集图像分辨率普遍偏高CPU 训练一个 150 轮的模型可能要跑几天不太现实。数据配置就绪后训练命令如下yolo detect train \ datatraffic_light.yaml \ modelyolov8s.yaml \ pretrainedTrue \ imgsz640 \ batch16 \ epochs150 \ close_mosaic10 \ projectrun_traffic \ nameexp1 \ seed42这段命令里有几个参数值得细说。modelyolov8s.yaml指定模型结构这里我一般用 s 而不是 n。信号灯是典型小目标n 模型参数量太少对 30 像素左右的灯体召回率会明显偏低。如果你用的显卡显存只有 6G 左右比如 GTX 1660 Ti可以继续用 s 但把batch降到 8imgsz保持 640强行上 m 模型只会导致训练中频繁显存溢出。close_mosaic10表示最后 10 个 epoch 关闭马赛克增强。YOLOv8 默认开启 mosaic 和 mixup对常规目标很有效但信号灯本身很小mosaic 拼接后灯体更容易被裁剪掉一半。最后几个 epoch 关掉 mosaic相当于让模型在接近真实分布的干净数据上做微调能明显减少对增强噪声的过拟合。pretrainedTrue用的是 COCO 预训练权重。有些人觉得信号灯和 COCO 里的 traffic light 类别有重叠想从头训练我的看法是不要这么做。COCO 预训练权重虽然对灯体细节不敏感但它已经学会了丰富的纹理和边缘特征迁移到路口场景能省下大量的训练时间。对于 3000 张的小数据集从头训练很容易过拟合预训练权重相当于一种正则化先验。训练过程中的关键参数可以整理成下面这张表参数推荐值说明modelyolov8s兼顾显存与小目标召回追求极致速度可选 nimgsz640 或 960灯体小于 20 像素时优先用 960batch8~16按显存调整6G 显卡用 8epochs150~200小数据集 150 足够过拟合早停即可close_mosaic10最后 10 轮关闭 mosaicflipud0.0禁止上下翻转避免灯色位置语义错乱seed42固定随机种子方便复现训练启动后如果 loss 前几轮降不下去不要急着调学习率先确认标签是否出错。我遇到过最多次的情况是 YAML 中类别顺序和标注文件不对应模型一直在学一个错误的映射关系。3.2 损失曲线图里到底要看什么训练完成以后run_traffic/exp1/目录下会生成results.png里面包含 box_loss、cls_loss、dfl_loss 和验证集指标曲线。很多人一上来就看 mAP50但信号灯这类小目标场景mAP50-95 偏低是正常的因为检测框稍微偏移几个像素IoU 掉到 0.5 以下就会拖低曲线。更有意义的判断方式是看三件事第一box_loss 和 cls_loss 在最后 20 个 epoch 是否进入平稳段。如果还在持续下降就提前停掉说明欠拟合可以加大 epoch 或去掉早停如果验证集 loss 开始掉头回升训练集 loss 还在降这是过拟合信号此时最有效的操作不是调参数而是回数据层面补样本或加增强。第二看每个类别的 AP 而不是总的 mAP。YOLOv8 的验证日志里会输出 per-class 的 AP 表格重点观察circle_light和arrow_left。如果某个箭头类别 AP 特别低多半是标注时箭头区域画得太小或者左转箭头样本只有几十张。类别的 AP 差异比整体 mAP 更能暴露数据问题。第三打开混淆矩阵图。信号灯场景里最常见的问题是红和黄互相混、直行箭头和左右箭头互相混。混淆矩阵能在视觉上直接告诉你哪些类别边界分不开。我通常会单独看红灯和绿灯的混淆比例一旦绿灯被误判成红灯的比例超过 1%这个模型就不敢直接接下游规则因为方向反了会导致通行策略出错。如果想自己画损失函数曲线ultralytics的训练结果里已经保存了 CSV 格式的指标直接用 Pandas 读取再画图即可。实际项目里我习惯同时开启 wandb 日志这样可以在浏览器里实时看每个 epoch 的变化和团队协作时也不需要传一堆训练日志文件。3.3 用训练好的权重先跑一遍视频建立“坏样本回收”习惯模型训练完第一件事不是调参而是把验证集视频完整跑一遍推理输出亲眼看看模型在真实连续帧里的表现。单独看几张贴图没有意义连续视频才能暴露灯色闪烁、漏检跳变和时间维度的不连贯。推理脚本很简单from ultralytics import YOLO model YOLO(run_traffic/exp1/weights/best.pt) results model.predict( sourcenight_clip.mp4, imgsz640, conf0.3, iou0.5, saveTrue, save_txtTrue )这里conf0.3是一个偏低的阈值目的是让小尺寸、远距离的灯体尽量召回代价是会增加一些误检。iou0.5控制 NMS 的合并力度信号灯这种小目标密集场景如果 iou 阈值设太高相邻两个灯体的预测框会被合并成一个后面会专门讲这个坑。跑完视频后重点抽帧看两类情况夜间过曝帧和黄灯闪烁帧。如果模型在连续 5 帧里漏掉了同一个灯或者红灯和黄灯来回跳把这几帧提取出来补充标注并加入训练集。这个习惯叫“困难样本回灌”比单纯加训练轮数有效得多。我当时做第一个路口项目时白天场景 mAP 已经很好一到晚上灯体大面积过曝误检率飙到白天的三倍后来就是靠翻夜间视频、挑错误帧、回灌训练集花了三周才把夜间召回率拉回来。另外提醒一点训练命令、数据集版本、坏样本回收的记录都要写进项目文档说明里不要只留在命令行历史里。三个月后回看你大概率记不清当时用了哪些增强、哪个版本的数据集有一份结构化的训记录返工时能省下很多时间。4. 从检测框到通行规则区域映射、灯色状态机与剩余时间4.1 为什么仅靠检测框撑不起“通行规则识别”检测模型的输出是“类别 坐标框”它可以告诉你画面里有一个圆灯、颜色是红色但它回答不了三个关键问题这个红灯管的是直行车道还是左转车道这个绿灯还能持续几秒黄灯闪烁应该输出“小心通行”还是“禁止通行”这些是通行规则识别模型和普通检测模型的分界线。常见的工程方案是给检测输出加一层“车道区域映射”。具体做法是在图像平面上预先标定若干感兴趣区域每个区域对应一个车道组。比如画面下半部分是停止线前的直行等待区对应lane_group0左侧是左转待转区对应lane_group1。标定只需要在图像上点几个多边形不需要精确的相机外参和世界坐标转换。灯体的检测框中心点落在哪个多边形内就认为它控制哪个车道组。这一步我强烈建议做成配置文件而不是硬编码进代码。路口相机角度一变ROI 坐标就全部失效把多边形坐标放在 JSON 或 YAML 里换场景时只改配置不重启服务。ROI 划分越贴近实际路口后续规则就越简单反之如果只画了一个覆盖全图的 ROI那车道级规则就无从谈起。4.2 一个可跑的灯色状态机代码与参数说明检测是逐帧独立的但通行规则天然是时间序列问题。单帧输出红灯下一帧输出绿灯中间没有过渡逻辑直接交给下游会导致刹车和启动的连续性决策混乱。我在项目里的做法是一个轻量状态机对每个灯体维护最近几帧的投票结果并限制状态跳变必须经过中间态。下面是一个可运行的 Python 状态机骨架from collections import deque class SignalLightRuleEngine: def __init__(self, vote_window5, min_confidence0.45, state_ttl2.0): self.vote_window vote_window self.min_confidence min_confidence self.history {} # key: lane_group_id, value: deque of states self.current_state {} # key: lane_group_id, value: state self.state_ttl state_ttl # 单位帧数 def match_lane_group(self, bbox_center, lane_groups): for group_id, polygon in lane_groups.items(): if self._point_in_polygon(bbox_center, polygon): return group_id return None def update(self, lane_group_id, det_state, conf): if conf self.min_confidence: det_state unknown if lane_group_id not in self.history: self.history[lane_group_id] deque(maxlenself.vote_window) self.history[lane_group_id].append(det_state) states list(self.history[lane_group_id]) majority max(set(states), keystates.count) self.current_state[lane_group_id] self._apply_transition( lane_group_id, majority ) return self.current_state[lane_group_id] def _apply_transition(self, lane_group_id, new_state): old_state self.current_state.get(lane_group_id, None) if old_state red and new_state green: return transition # 红灯到绿灯必须经过过渡状态 if new_state yellow and old_state green: return caution return new_state def _point_in_polygon(self, point, polygon): # 用射线法判断点是否在多边形内 x, y point n len(polygon) inside False px, py polygon[0] for i in range(1, n 1): nx, ny polygon[i % n] if (y py) ! (y ny) and x (nx - px) * (y - py) / (ny - py) px: inside not inside px, py nx, ny return inside这段代码的核心是deque窗口投票和状态跳变约束。vote_window5表示用最近 5 帧的多数结果作为当前灯色避免单帧误检导致输出抖动。min_confidence过滤低置信度检测检测器对夜间远距离灯体的置信度普遍偏低设太低会引入噪声设太高又会丢掉真实灯体。_apply_transition里做了红到绿的中间态保护防止相邻两帧状态跳变过于突兀。调用时需要注意检测器的输出要先经过 NMS 再进入这个引擎不要直接把模型的原始预测框全部塞进来否则同一个灯体会有多个重复框参与投票窗口会被污染。状态机本身不做跟踪它依赖的输入是“每个车道组内已经选出的最优灯体框”所以前面要有一个简单的跟踪器或用检测框中心点距离做帧间关联。状态机相关的参数表参数推荐值说明vote_window5投票窗口越长越平滑但反应变慢min_confidence0.4~0.5夜间可降到 0.3但要接受误检上升state_ttl2.0同一状态最多保持多少帧超时置为 unknowntransition 状态不对外输出只作为内部中间态不能直接给下游4.3 倒计时剩余时间的降级方案很多路口的信号灯带数字倒计时如果能识别剩余秒数通行规则输出会更有价值——可以告诉下游“绿灯剩 3 秒不建议加速通过”。但训练一个倒计时数字识别模型是另一套工作不是所有项目都愿意为此投入标注成本。我的降级方案是如果检测类别里包含countdown_digit就对数字区域做一个轻量 OCR 或模板匹配输出剩余秒数如果没有训数字类别就用状态机记录当前相位已经持续了多少帧。在SignalLightRuleEngine里加一个time_in_state字段即可做到。每次update被调用时如果状态没有跳变计数器加一状态跳变则清零。输出结构可以设计成rule_output { lane_group: 0, light_type: circle_light, state: green, time_in_state: 42, # 单位帧 remaining_time: None, # 有倒计时识别时填入秒数 confidence: 0.87, timestamp: 1699999999.0 }remaining_time在有 OCR 输出时直接用没有则用time_in_state作为降级字段。下游控制逻辑可以约定remaining_time为空时不要做“剩余 3 秒”这类激进判断只输出可通行或不可通行。这样既保证了系统可用性也不会因为降级方案强行预测而给出错误秒数。4.4 多方向灯组与右转常亮规则真实路口往往同时存在直行箭头、左转箭头、右转箭头和圆形满盘灯一个信号机同时输出多个灯组。通行规则识别的输出必须是一个按车道组组织的结构而不是单一路口状态{ lane_group_0: {state: red, direction: straight}, lane_group_1: {state: green, direction: left}, lane_group_2: {state: off, direction: right} }注意右转车道在很多路口是常亮或不受控的不能因为检测到圆形红灯就认为右转也必须停止。这里需要在 ROI 配置里给右转车道单独标一个always_caution属性状态机对这类车道组只输出caution或proceed不参与红绿投票。否则右转车辆会收到错误的禁行指令影响通行效率。灯组之间的关联也值得一提。同一个信号灯杆上左转箭头和圆形灯可能是两个独立灯体它们的检测框距离非常近。如果 ROI 划分不够细两个框会落到同一个车道组里状态机不知道该采纳哪一个。我的处理方式是ROI 配置里给每个车道组绑定一个preferred_light_type字段比如直行车道的 ROI 优先选择circle_light左转车道优先选择arrow_left避免多灯体竞争同一个槽位。5. 信号灯识别避坑清单数据、训练到部署的踩坑记录5.1 夜间过曝让红灯变“白灯”黄灯变“红灯”现象夜间路口灯体周围有一圈光晕检测器能把灯框出来但灯色分类把红灯输出为其他颜色或直接判为 unknown。原因夜间自动曝光下灯珠的高光区域溢出红色通道饱和后色相信息丢失黄灯在低色温路灯的干扰下像素分布和红灯非常接近。解决夜间样本单独建一个子集训练时按比例混入训练集不要靠白天样本凑数。对过曝样本做局部直方图均衡或引入合成过曝增强让模型见过“灯珠发白”的形态。分类阶段优先用 HSV 空间的色相和饱和度判断不要只看 RGB 三通道的均值。5.2 黄灯样本太少模型把黄灯直接学成红灯现象混淆矩阵里red 那一行里混入了大量 yello本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站