基于YOLOv8的交通信号灯识别与通行规则判断源码深度解析
发布时间:2026/9/28 16:25:49来源:尧图网络
简介Python基于YOLOv8的路口交通信号灯通行规则识别模型及算法源码源自个人毕业设计答辩评审成绩达98分代码已完成调试并确保可运行。资源面向计算机、通信、人工智能、自动化等相关专业的学生、教师与从业者既可用于期末课程设计、课程大作业或毕业设计也能作为深度学习和目标识别方向的入门练习。压缩包共17个文件以11个Python脚本为核心覆盖数据读取、图像处理、检测与分类预测等完整流程另有YAML配置、Markdown说明与示例图片便于快速理解项目结构和运行方式。整包仅705KB轻量易用已有117人学习具备一定参考热度。通过文档注释与模块化源码可掌握YOLOv8在交通信号灯场景下的识别逻辑并在此基础上修改参数或扩充数据实现个性化功能。1. 这个项目解决的不是“检测”而是“能不能走”路口交通信号灯识别核心难点从来不是训练一个 YOLOv8 把灯框出来。框出来之后的三连问才是关键这是什么灯色、这个灯管哪个方向、我当前到底能不能走。这套 Python 源码把 YOLOv8 检测、灯色与箭头分类、通行规则编排串成了一条完整链路图片、视频、摄像头输入都能跑。项目自述答辩评审 98 分代码拆成了 detect、classify、recognize、porcess、paint 几个独立模块改起来不用动主干。适合拿来复现毕设、课程设计也适合自动驾驶感知方向想快速看完整落地链路而非只跑一个 demo 的从业者。2. 先拆目录再选型YOLOv8 做信号灯识别为什么比两阶段模型顺手2.1 为什么是 YOLOv8小目标、实时性和改造成本的三个约束交通信号灯识别有几个很实际的约束灯体在画面里通常只占几十个像素属于典型小目标路口场景要求近实时推理不能把整块 GPU 算力全吃掉还有就是源码要可改、可复现不能是黑匣子。YOLOv8 在这三个约束下是比两阶段模型更顺手的基线。它用 anchor-free 的方式省掉了 anchor 后处理那一堆参数调优的玄学问题Decoupled Head 把分类和回归分支分开对遮挡、重叠目标比 YOLOv5 的耦合头更稳。C2f 结构把梯度流拆得更细小目标特征在浅层和深层之间的传递比之前几代模型更充分——这一点对信号灯这种小目标很关键我用 YOLOv5 跑同样的灯体数据mAP 比 v8 低两三个点推断时间却差不了太多。两阶段模型比如 Faster R-CNN 不是不能做但部署和改造成本高半实时场景下没必要。2.2 源码目录拆解每个文件到底在干什么拿到压缩包后先别急着跑把目录结构过一遍弄清楚模块之间的调用链后面改参数才不慌。这套源码的核心文件大概可以按三层来理解detect 负责找目标classify 负责判断灯色和箭头recognize 和 porcess 负责把前两步的结果翻译成通行指令。文件/模块职责说明main.py程序入口读取图片/视频/摄像头调度识别流程recognize.py主识别流程把检测、分类、规则串起来detect/predict.py目标检测推理封装 YOLOv8 模型输出 bbox、置信度、类别 IDclassify/predict.py灯色与箭头分类对检测框做状态分类porcess.py后处理与规则编排灯组匹配、通行指令生成filter.py结果过滤置信度过滤、区域过滤、NMSpaint.py可视化画检测框、颜色标签、通行指令文字displays展示模块终端/窗口输出结构化结果configs/config.yaml训练与推理配置数据路径、类别数、超参数data.py数据读取与预处理加载图片、缩放、归一化utils工具集合公共函数、可视化辅助调用链大致是 main.py 拿到一帧图像后交给 recognize.pyrecognize 分别调 detect/predict.py 拿候选框、调 classify/predict.py 拿灯色状态然后 filter.py 把低置信度框和不在关注区域的框滤掉最终由 porcess.py 按规则表输出通行指令paint.py 负责画框标注。源码包里只要能跑通 main.py 一行命令这条链就是通的。2.3 环境搭建依赖版本和 Python 环境的三个注意点解压后先建虚拟环境再用 requirements.txt 装依赖。常见做法是 conda 建一个独立 Python 3.8 或 3.10 环境避免把系统环境搞乱。建议按下面的顺序操作conda create -n traffic_light python3.8 conda activate traffic_light cd 项目解压目录 pip install -r requirements.txt python -c import torch; print(torch.__version__, torch.cuda.is_available())requirements.txt 里一般是 ultralytics、opencv-python、numpy、Pillow 这些基础依赖。最后一行检查 torch 是否能正常调用 CUDA如果输出 cuda 不可用说明装的是 CPU 版训练会慢很多但推理小规模图片还是能跑。这里我习惯先确认 torch 版本和显卡驱动匹配不然后面训练到一半报 CUDA out of memory 是常态。README.md 里如果写了推荐版本以它为准不要盲目升级到最新版这套代码依赖的是 YOLOv8 某一代 APIultralytics 升级后部分接口有变化运行时报错再回退会浪费时间。3. 数据准备与模型训练labelme 标注转 YOLO 格式、config.yaml 超参与收敛判断3.1 信号灯数据集怎么准备类别体系和 labelme 转 YOLO 坐标格式信号灯识别和通用目标检测不一样类别不能只分“红灯、绿灯、黄灯”还要考虑箭头方向。常见做法是把类别体系定义成两套一套按灯色分一套按箭头分或者干脆混合成单一类别列表。我一般会这样定义red红灯green绿灯yellow黄灯arrow_left左转箭头arrow_straight直行箭头arrow_right右转箭头如果场景里有倒计时数字建议单独做 OCR 或者扩一个 digit 类别不要混进灯色类别里。标注工具用 labelme 比较多但 labelme 默认输出 JSON 格式的 polygonYOLO 训练需要 txt 格式的归一化 bbox所以要做一次坐标转换。转换逻辑是把 polygon 外接矩形算出来再除以图片宽高import json import os def labelme_to_yolo(json_path, out_dir, class_map): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] 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) # YOLO 格式: class_id, x_center, y_center, w, h全部归一化到 0~1 x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h class_id class_map[label] lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) txt_path os.path.join(out_dir, os.path.splitext(os.path.basename(json_path))[0] .txt) with open(txt_path, w) as f: f.write(\n.join(lines))这段脚本的关键在两点一是 class_map 要把中文或英文标签固定映射成整数 ID必须和后续 config.yaml 里的 names 顺序一致二是坐标必须归一化YOLO 训练时不读原始像素坐标。容易踩坑的是 labelme 标注的 polygon 如果是不规则形状直接取外接矩形会引入背景噪声信号灯本身是圆形或矩形外接矩形误差不大但如果标注的是倒计时数字这种细长目标外接矩形会把大量背景包进来建议标注时贴近边缘。3.2 config.yaml 超参说明这些参数改错了训练直接不收敛训练前先看 configs/config.yaml这份文件控制数据路径、类别数量和训练超参。YOLOv8 的数据配置格式大致如下path: ./datasets/traffic_light train: images/train val: images/val nc: 6 names: [red, green, yellow, arrow_left, arrow_straight, arrow_right] # 训练超参 epochs: 200 imgsz: 960 batch: 16 patience: 50 device: 0 workers: 4 optimizer: SGD lr0: 0.01path 是数据集根目录train 和 val 是相对于根目录的图片文件夹路径。nc 必须和 names 列表长度一致这条不对会在训练一开始就报维度错误。imgsz 默认是 640但信号灯是小目标我一般会提到 960代价是显存占用翻倍、训练时间变长换来的是小目标召回率明显提升。batch 要根据显存调16G 显存跑 960 分辨率建议 batch 设 8 到 16显存不够就降 imgsz 或 batch不要硬顶。patience 是早停轮数连续 50 轮验证集 mAP 不涨就自动停这个参数在数据量小的时候尤其有用防止过拟合浪费算力。3.3 训练与收敛判断损失曲线该看哪几个指标目录结构和配置没问题就可以开始训练。常见做法是在项目根目录直接敲训练命令yolo detect train dataconfigs/config.yaml modelyolov8s.pt epochs200 imgsz960 batch16 device0yolov8s.pt 是官方预训练权重用它初始化会比随机初始化收敛快很多、精度也更高。训练日志里每一轮都会输出 box_loss、cls_loss、dfl_loss 以及验证集 mAP50、mAP50-95。判断模型是否收敛不能只看 mAP三条 loss 曲线要同时看box_loss 到后期如果还在明显下降但 mAP 不动说明过拟合了cls_loss 抖动很大多半是类别样本不均衡red 样本远多于 arrow 类就会出现这种情况。训练产生的 results.csv 里存了全部指标想画损失曲线图直接读这个文件绘图比截图训练日志靠谱。训练结束后 best.pt 和 last.pt 会存在 runs/detect/train 目录下后续推理用 best.pt。4. 从检测框到通行指令颜色分类、灯组匹配与规则编排4.1 检测层拿到 bbox 之后先过滤还是先分类推理阶段用 YOLOv8 的预测接口拿检测结果拿到的是每个目标的 bbox、置信度、类别 ID。这里有个顺序问题先过滤还是先分类。我的做法是先做一次粗过滤把低置信度框丢掉再做分类不然背景误检框会干扰灯色判断。置信度阈值一般设在 0.25 到 0.4 之间信号灯场景可以放宽到 0.2因为灯体小、特征弱阈值太高容易漏检。filter.py 里除了置信度过滤通常还会做区域过滤——信号灯只出现在路口上方把图片下方 1/3 区域的检测框直接丢弃这个先验知识能减少大量误检。def filter_detections(boxes, scores, class_ids, conf_thres0.25, region_ratio0.35): h 1.0 # 归一化坐标下图片高度为 1 keep [] for box, score, cls_id in zip(boxes, scores, class_ids): if score conf_thres: continue y_center (box[1] box[3]) / 2.0 if y_center h * (1 - region_ratio): continue # 丢弃画面下方区域的误检 keep.append((box, score, cls_id)) return keepregion_ratio 是保留区域比例0.35 表示只保留画面上方 65% 区域的检测结果。这个值要按实际相机安装位置调如果相机俯视角度大信号灯可能出现在画面中部这个参数设太狠会把真目标也滤掉。我一般先跑一帧打印出所有候选框坐标再决定区域裁剪线在哪。4.2 分类层HSV 颜色空间比 RGB 更抗光照变化detect 输出的是 bboxbbox 里到底是红灯还是绿灯需要 classify/predict.py 处理。信号灯颜色判断用 HSV 空间比 RGB 稳得多。RGB 对亮度变化太敏感同一个红灯在逆光和顺光下 RGB 分量能差出一倍但 HSV 的 H 通道只表达色相亮度变化主要影响 V 通道。基础逻辑是把灯体中心区域的像素转成 HSV统计 H 值落在哪个颜色区间。import cv2 import numpy as np def classify_light_color(crop_bgr): hsv cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2HSV) h, s, v cv2.split(hsv) # 只统计饱和度、亮度较高的像素排除暗部干扰 mask (s 80) (v 100) h_vals h[mask] if len(h_vals) 0: return unknown red_cnt ((h_vals 10) | (h_vals 170)).sum() yellow_cnt ((h_vals 15) (h_vals 35)).sum() green_cnt ((h_vals 35) (h_vals 85)).sum() counts {red: red_cnt, yellow: yellow_cnt, green: green_cnt} return max(counts, keycounts.get)颜色区间的阈值是 H 通道的经验值不同相机色调会有偏移黑色灯壳反射会让饱和度下降所以要先做饱和度过滤。这段逻辑在 classify 模块里通常不是纯规则也会用分类模型兜底但规则部分负责快速通道。还有一类情况是黄灯闪烁和绿灯闪烁闪烁灯在单帧里看颜色没问题要在时序上做帧间判断连续多帧同一灯体颜色变化且间隔规律才判定为闪烁。4.3 灯组匹配与规则编排红灯停、绿灯行只是最粗的规则单灯识别做完真正的难点来了一个路口有多个灯组直行灯、左转灯、右转灯各管各的车道不能把左转红灯当成直行红灯。porcess.py 做的就是这件事。常见做法是拿灯体 bbox 的中心点坐标做聚类按空间位置把同一根灯杆上的灯归到一组再根据当前车道方向选对应灯组。车道方向和灯组位置的对应关系一般用检测框中心点的相对位置判断左转灯组通常在路口左侧直行灯组在中间偏下。def decide_pass_action(light_group, lane_direction): # light_group: 该车道上匹配到的灯组状态 if light_group[color] red: return stop elif light_group[color] yellow: # 黄灯已越过停止线可继续通行未越过停车等待 return wait if light_group[crossed_stopline] else stop elif light_group[color] green: if light_group[arrow] is not None: if light_group[arrow] lane_direction: return go else: return stop # 直行绿灯亮、左转箭头没亮时左转车道仍禁止 return go return unknown这段规则表看起来很直白但实际路口有很多边缘情况左转待转区、黄闪警告、红灯倒计时每个路口还可能有自己的信号相位。我一般建议在规则层维护一张可配置的表而不是把判断逻辑写死在代码里。把 lane_direction 和 arrow 的匹配关系做成外部配置这样换一个路口不用改代码只改配置。倒计时场景里如果源码的 classify 分类头没覆盖数字类别就让倒计时区域走背景过滤别让它干扰灯组选择。4.4 可视化与结果输出paint.py 负责把识别结果画出来检测框、颜色标签、通行指令文字都是它干的。displays 模块则是把结构化结果输出到终端或窗口比如当前时间戳、帧号、灯组状态、通行指令。调试阶段我习惯先跑图片而不是视频把每一帧的中间结果保存下来看灯组匹配对不对。如果识别错了先看 paint 画出来的框是不是紧贴灯体再看标签颜色对不对最后看指令和标签是否矛盾。三步定位法基本能覆盖绝大多数问题。5. 避坑环境依赖、小目标漏检与规则误判的四个重灾区5.1 现象装好依赖后 import 直接报错跑 main.py 报 ImportError提示某个模块找不到或者 NumPy 版本不兼容。原因是 requirements.txt 里锁定的版本和当前 Python 环境冲突最典型的就是 NumPy 1.24 之后移除了部分旧 API老代码里 np.float 这类写法直接崩溃。解决先看报错里涉及的库是哪个单独降级或升级不要整体重装。我遇到过 ultralytics 自动升级到最新版后 predict 接口签名变化项目里 recognize.py 还按老接口传参导致报错。用 pip list 查看当前版本和 README 里要求的版本比对有出入就按 README 锁版本。环境装一次不顺利很正常多试两次就知道这套代码吃哪几个版本。5.2 现象模型训练完 mAP 很高但推理时小目标大量漏检训练集 mAP50 到 0.8 以上但跑真实路口视频时远处的灯体经常检测不到。原因是信号灯在整幅画面里像素占比太小且训练时 imgsz 用了默认 640小目标特征在多次下采样后已经丢失。另一个常见原因是标注框不紧把灯体外壳的黑色区域也包进去正样本特征被稀释。解决训练前把 imgsz 提到 960 或 1280mAP 可能短期下降但小目标召回会提升。同时检查标注框用 labelme 重新精修灯体边缘。如果数据集里小目标占比低可以按图像裁剪的方式把大图切成小块再训练相当于变相放大目标。还有一个技巧把训练数据里的灯体图像做 mosaic 增强YOLOv8 默认开启 mosaic小目标较多的数据集建议把 mosaic 概率调低一点不然画面里目标过多导致小目标被拼没。5.3 现象灯色判断忽红忽绿同一张图换角度就误判HSV 阈值区间是固定的但不同品牌相机的色彩偏好不一样有的偏暖有的偏冷同一个红灯在不同相机里 H 值能偏移 10 到 20。另外一个隐藏坑是 OpenCV 读图默认 BGR 顺序如果 classify 模块里 cvtColor 前忘转通道红色和蓝色的位置会互换——灯色判断瞬间全反。解决先用 cv2.cvtColor 明确转一次确认输入是 BGR 还是 RGB这个调试时很容易被忽略。HSV 阈值不要拍脑袋定从训练集里随机抽 50 张灯体裁剪图统计 H、S、V 的分布范围再定阈值。还有一点代码包里的 porcess.py 文件名是源码里带的拼写不是 error导入时保持原样不要“好心”改成 process否则 from porcess import xxx 会直接把整个项目的引用链打断。5.4 现象规则层对绿灯判断正确但黄灯和红灯临界帧老翻车黄灯变红灯的过渡帧里灯体颜色在 HSV 空间中会先变暗再变红如果单帧判断很容易在临界瞬间误判成红灯或黄灯。另一个场景是绿灯闪烁结束直接切红灯闪烁帧被当成正常绿灯放行规则层就出错了。解决规则层要引入时序状态机不要单帧独立决策。维护一个灯组最近 3 到 5 帧的颜色序列连续两帧出现新颜色才切换状态。闪烁绿灯和正常绿灯的区别在于颜色周期性变化连续性判断天然能缓解这个问题。临界帧宁可直接输出 unknown 也不要硬判对自动驾驶系统来说unknown 比错误指令安全得多。调这种问题时把日志打出来逐帧对比颜色序列用不了几分钟就能定位是分类错还是规则时序错。6. 进阶离线视频回放评估通行规则正确率的脚本与阈值技巧调完模型和规则后最怕的是拿真车到路口试结果在某个临界帧翻车。我习惯的做法是任何一次修改不管改的是置信度阈值、HSV 区间还是规则表都先做一轮离线视频回放评估。做法是把一段路口视频逐帧喂给 recognize.py把输出的通行指令和人工标注的真值比对统计误判帧和规则冲突次数。import cv2 from recognize import TrafficLightRecognizer def evaluate_video(video_path, label_file): cap cv2.VideoCapture(video_path) labels load_ground_truth(label_file) # 每帧人工标注的期望指令 recognizer TrafficLightRecognizer() stats {total: 0, wrong: 0, unknown: 0, conflict: 0} frame_idx 0 while True: ret, frame cap.read() if not ret: break result recognizer.process_frame(frame) expected labels.get(frame_idx) if expected is not None: stats[total] 1 if result.action ! expected: stats[wrong] 1 if result.action unknown: stats[unknown] 1 frame_idx 1 cap.release() print(f总帧数: {stats[total]}, 误判: {stats[wrong]}, unknown: {stats[unknown]})评估脚本的核心是 ground truth 标注常见做法是每 10 帧标注一次中间帧做插值不用逐帧标能省一半时间。第一次跑评估会把问题全暴露出来可能是检测框不稳定同一灯体时有时无可能是灯色分类在光线变化时抖动也可能是规则层对黄灯临界帧处理不当。哪里错得多就回对应模块调阈值不要全局乱调。几个实用的调参习惯检测置信度阈值从 0.25 往下调漏检率下降但误检率上升时找一个平衡点HSV 区间根据抽样的色相分布微调规则层的灯组匹配阈值要和相机安装高度绑定一车一相机一标定。从那以后我每次改规则或调阈值都强制自己先跑一遍离线回放把误判次数压到零再上真机这个习惯救了我好多次。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网