简介:基于YOLOv5与OpenCV打造的道路红绿灯识别检测系统,以完整工程压缩包交付。面向深度学习目标检测学习者、自动驾驶与智慧交通开发者,解决交通信号灯实时检测场景中的模型训练、效果评估与快速部署问题。压缩包共79个文件,包含17个Python脚本、17个YAML配置、3个PyTorch权重、23个pyc编译文件以及jpg/png图片、txt文本、sh脚本等,整体约41.68MB,其中py/yaml负责训练与推理逻辑,pt为训练好的模型权重,图片则直观展示评估曲线与预测结果,目录结构清晰、开箱即用。检测覆盖红灯、绿灯、黄灯、交通灯4种类别,模型迭代200次拟合良好,随包附有loss下降曲线、精确率、召回率及mAP等完整评估指标曲线,使用说明txt帮助快速上手。已有1487人学习下载,适合需要复现目标检测流程、分析训练曲线并在此基础上扩展或微调的读者。
1. 道路红绿灯识别:一套能跑通的 YOLOv5 + OpenCV 工程,而不是论文
做红绿灯识别有个很现实的问题:网上能找到一堆算法讲解,但真正能下载下来、跑通、看到检测框和评估曲线的完整工程极少。大多数时候你拿到的是一个孤零零的.weights文件,或者一段只能跑单张图片的推理脚本,想训练自己的数据集、嵌入到自己的检测流程里,又得从头捋。这套基于 YOLOv5 + OpenCV 的红绿灯识别检测系统,把源码、模型文件、评估指标曲线和使用说明打包在一起,覆盖了从数据集组织、模型训练、指标评估到 OpenCV 部署推理的完整链路,适合两类人:一是做课程设计、毕业设计需要交一个可演示系统的学生,二是实际做辅助驾驶、城市交通感知项目、需要快速验证红绿灯检测效果的一线工程师。
先说结论:这套东西能落地,核心在于它不是把 YOLOv5 的官方仓库原样扔给你,而是针对红绿灯这个垂直场景做了数据组织、训练配置和推理脚本的裁剪。你拿到手之后,重点不是去重训一个模型(那需要时间和算力),而是先理解它的目录结构、数据标注格式和评估指标怎么读,再替换成你自己的数据做增量训练。下文按「数据集准备 → 训练与指标 → OpenCV 部署 → 避坑 → 进阶优化」的顺序拆开讲。
2. 数据与标注:YOLOv5 训练红绿灯前,先过目录组织这一关
2.1 红绿灯数据集的目录约定:images 和 labels 必须一一对应
YOLOv5 的训练脚本对数据目录的约定非常死板,它不关心你的图片放在哪个盘,只关心train.txt/val.txt里写的路径能不能找到文件,以及data.yaml里的nc(类别数)和names(类别名)是否匹配模型配置。这套红绿灯系统在数据组织上沿用了 YOLOv5 的标准布局,你拿到压缩包后,先把数据集目录结构整理成下面这样:
dataset/ ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ └── ... │ └── val/ │ ├── 000501.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 000001.txt │ │ └── ... │ └── val/ │ ├── 000501.txt │ └── ... └── data.yaml这里images/train下的每张图片,在labels/train下必须有同名同前缀的.txt文件,后缀只看前面那段数字。label 文件里每一行是class_id x_center y_center width height,注意这五个值都是相对图片宽高的归一化坐标,不是像素坐标。很多人在这个环节翻车:用 LabelImg 标注的时候导出的是 Pascal VOC 的 XML 格式,直接拿来训练 YOLOv5 必然报错,需要先转成 YOLO 的 txt 格式。
这套资源提供的模型文件对应的是它自己打包的数据集分布,所以在你跑通推理之前,别急着换自己的数据。先用它自带的图片和标注跑一遍训练或验证脚本,确认环境没问题了,再动数据替换。
2.2 标注类别:红绿灯不止「红灯/绿灯/黄灯」三种
红绿灯检测的类别设计是个细节,但直接影响模型效果。很多初学者把它做成二分类「有没有灯」,实际工程里更常见的是区分灯的颜色和类型,因为下游逻辑(比如判断是否允许通行)需要知道到底是亮了一个红圆灯还是红箭头。这套系统在data.yaml里可以看到类别定义,常见做法是包含类似red, green, yellow三个颜色类别,或者进一步细分red_circle, green_circle, red_arrow_left, green_arrow_left等组合类别。
类别颗粒度取决于你的下游任务。如果只是做「检测到红灯就提醒」,三个颜色类就够;如果要做红绿灯状态推断,建议至少把圆灯和箭头灯分开。这套模型文件选择的类别方案,你在data.yaml里能直接看到,训练新数据时保持nc和names的一致性是第一原则,改类别数不重新定义模型输出层,训练会直接报维度不匹配的错误。
3. 训练与评估:看懂评估指标曲线,才知道模型行不行
3.1 训练入口:修改 paths 和超参数,然后跑 train.py
模型训练这部分,这套资源的使用说明里会给出具体的启动命令。基于 YOLOv5 的常见训练方式是:
python train.py --data data.yaml --cfg models/yolov5s.yaml --weights '' --batch-size 16 --epochs 100 --img 640各参数含义:--data指向数据配置文件;--cfg指定网络结构,yolov5s是轻量级版本(参数量约 7M),适合在单卡 GPU 或 CPU 上训练和部署;--weights设为空字符串表示从零训练,如果想做迁移学习,改成yolov5s.pt预训练权重路径即可;--img 640是训练时输入图像的尺寸,推理时也要保持一致。红绿灯这类小目标检测场景,不建议用太小的输入尺寸,不然在远距离场景下灯体可能只有十几个像素,模型学不到有效特征。
如果不清楚自己机器能不能跑得动,先看显存:--batch-size 16在 1080Ti / 2080Ti 级别显存(11GB)上基本能跑;如果是 6GB 显存,降到 8;GPU 都没有的话,--device cpu能跑但速度慢,100 个 epoch 可能需要几小时到几十小时不等。
3.2 评估曲线怎么读:Precision / Recall / mAP 不只是三个数字
这套资源里带了评估指标曲线的图表文件,通常来自训练过程中自动产出的results.png,里面包含Precision、Recall、mAP@0.5、mAP@0.5:0.95、train/box_loss、val/box_loss等曲线。很多人只盯着 mAP 一个数,其实对红绿灯检测来说,更值得看的是 Precision 和 Recall 的平衡点。
红绿灯检测的误检和漏检代价不同:红灯漏检可能造成车辆闯红灯,是安全事故;绿灯误检成红灯会导致不必要的刹车,影响通行效率。所以你要根据落地场景找 Precision-Recall 曲线的拐点。YOLOv5 训练结束会输出一张混淆矩阵(confusion matrix)和一张 F1 曲线,调试模型时比看 mAP 更直观——你可以在验证集上找到所有误检样本,逐一判断是标注错误、同类混淆还是小目标漏检。
评估模型时还有一个常见做法是画 PR 曲线:
from sklearn.metrics import precision_recall_curve import numpy as np # 假设 pred_scores 是模型输出的置信度,true_labels 是真实标签 precision, recall, _ = precision_recall_curve(true_labels, pred_scores) # 计算 F1 分数最高的阈值 f1_scores = 2 * (precision * recall) / (precision + recall + 1e-12) best_threshold = f1_scores[np.argmax(f1_scores)] print(f"F1 最高的置信度阈值: {best_threshold:.3f}")这段代码的意义在于:部署时不能盲目用 0.25 作为置信度阈值,应该从验证集 PR 曲线上取 F1 最大的点。红绿灯检测的类别相对简单,如果某类别的 AP 明显低于其他类别,大概率是该类别的训练样本太少,或者灯体在图像中过小。此时优先补足该类别的样本量,而不是调模型结构。
3.3 评估指标曲线文件的另一种用途:判断训练是否收敛
模型训练到 100 个 epoch,results.png里 loss 曲线应该呈下降趋势并在后期趋于平缓。如果 precision 和 recall 两条曲线波动很大、不收敛,常见原因是学习率步长太大或 batch size 太小。YOLOv5 默认配置里学习率是--lr 0.01,如果训练集只有几千张图,建议把学习率降到 0.001 再试。这套资源里如果带了训练日志,你可以直接看到实际的 loss 下降过程,作为调参参考。
4. 使用 OpenCV 部署推理:从 PyTorch 模型到实际检测流程
4.1 部署思路:不是直接在 OpenCV 里跑 YOLOv5
很多人误会「基于 YOLOv5 + OpenCV」的意思是 YOLOv5 用 OpenCV 的 DNN 模块加载并推理,其实在工程上更常见的是两段式:先用 PyTorch 训练并导出模型(ONNX 或 TorchScript),再在推理脚本里用 OpenCV 做图像预处理和后处理。这套资源的使用说明大概率也是这个思路,因为 YOLOv5 官方的export.py提供了 ONNX 导出功能,OpenCV 的cv2.dnn_DetectionModel可以直接加载 ONNX 模型做推理。
推理脚本的核心逻辑分三步:第一步用cv2.dnn.readNetFromONNX加载模型;第二步把输入图像letterbox缩放到 640×640 并归一化;第三步把网络输出的三组张量(目标分数、类别分数、边框坐标)解析成检测框。YOLOv5 在 ONNX 导出时的输出节点有两个,分别是(1, 25200, 80)的最终预测和一组中间特征,但导出时通常会精简成最终输出,具体要看导出命令里的--grid和--end2end参数。
4.2 一段可用的 OpenCV 推理核心代码
这里给出一段基于 OpenCV DNN 的 YOLOv5 推理代码框架,实际运行时需要把模型路径和类别列表替换成你的:
import cv2 import numpy as np # 1. 加载 ONNX 模型 net = cv2.dnn.readNetFromONNX("best.onnx") # 如果 CUDA 可用,优先设置 CUDA 加速 net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA) # 2. 图像预处理:letterbox 保持长宽比 def letterbox(img, new_size=640): h, w = img.shape[:2] scale = min(new_size / h, new_size / w) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((new_size, new_size, 3), 114, dtype=np.uint8) canvas[:new_h, :new_w] = resized return canvas, scale image = cv2.imread("test.jpg") input_blob, scale = letterbox(image) blob = cv2.dnn.blobFromImage(input_blob, 1/255.0, (640, 640), swapRB=True) # 3. 推理 net.setInput(blob) outputs = net.forward() # 4. 解析输出:YOLOv5 的 ONNX 输出形状通常是 (1, 25200, 85) # 85 = 4 个边框坐标 + 1 个目标置信度 + 80 个类别分数 # 红绿灯场景的类别数较少,按实际类别数剪裁 boxes, confs, class_ids = [], [], [] for det in outputs[0]: obj_conf = det[4] if obj_conf < 0.25: continue class_scores = det[5:] class_id = np.argmax(class_scores) class_conf = class_scores[class_id] * obj_conf if class_conf < 0.25: continue # 把边框坐标从 640x640 还原回原图尺寸 x1, y1, x2, y2 = det[:4] / scale boxes.append([x1, y1, x2, y2]) confs.append(class_conf) class_ids.append(class_id) # 5. NMS 去除重叠框 indices = cv2.dnn.NMSBoxes(boxes, confs, score_threshold=0.25, nms_threshold=0.45)代码拆开看:letterbox函数是 YOLOv5 预处理的标准做法,它把原图等比例缩放后填充到 640×640,而不是直接拉伸变形,这样小目标的形状不会被扭曲,但坐标还原时要用缩放比例scale换回原图坐标。blobFromImage里的swapRB=True是把 BGR 转成 RGB,因为训练时用的是 RGB 输入。NMS 的nms_threshold决定两个重叠框是否合并,对红绿灯这种密集小目标场景建议保持 0.45 左右,不要太低,否则近处两个并排的灯体可能被合并成一个。
4.3 部署时的性能边界:CPU 实时性与 GPU 加速
红绿灯识别要落地到实际场景,性能是个绕不开的问题。在纯 CPU 上跑yolov5s的 ONNX 模型,640×640 输入大约需要 200~500ms 一帧,达不到实时要求。如果只是做离线图片检测或课程设计演示,这个速度够用;如果是做视频流实时检测,就需要 GPU 推理或换成更轻量级的模型。这套资源里的模型文件是哪个尺寸的版本,你在模型文件名里能看到,yolov5s是速度和精度相对均衡的选择,追求更高精度可以换成yolov5m,但推理时间会翻倍。
另一个容易忽略的点是 OpenCV DNN 版本兼容性。如果你用的是 OpenCV 4.5 以下版本,读取某些 YOLOv5 导出的 ONNX 可能会报Unsupported activation之类的错误,解决方案是升级 OpenCV 或改用 TorchScript 模型,后者需要 PyTorch 环境。
5. 避坑与常见问题:红绿灯识别最常见的五个翻车现场
坑一:ModuleNotFoundError: No module named 'cv2'
现象:按说明文档执行python detect.py,立即报错缺少 cv2。
原因:当前 Python 环境没装 OpenCV,或者装的是 opencv-python-headless 版本在某些交互环境里不可用。
解决:用pip install opencv-python安装完整版。如果还在国内网络环境,用清华源或阿里源加速:pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple。确认安装成功的方法是python -c "import cv2; print(cv2.__version__)",能打印出版本号才算真的装好。
坑二:cv2.error: OpenCV(4.4.0) ... 无法读取 ONNX 模型
现象:推理脚本在readNetFromONNX处报类似Can't read ONNX file或Unsupported ops的错误。
原因:ONNX 导出时算子版本和你本机 OpenCV 支持的算子不匹配,常见于 YOLOv5 新版本导出的模型中包含 GridSample 等 OpenCV 不支持的算子。
解决:在导出 ONNX 时加上--simplify参数,用 onnx-simplifier 精简模型,或者把 OpenCV 升级到 4.6+。如果还不行,就不要碰运气了,直接用 PyTorch 的torch.hub.load方式加载模型权重,代价是需要 torch 环境和 GPU 或稍慢的 CPU 推理。
坑三:训练时 loss 变成 NaN,或者 mAP 一直为 0
现象:训练跑了十几个 epoch,loss 数值变成nan,或者验证集的 mAP 始终是 0。
原因:可能是标注文件里出现了width=0或height=0的无效框、类别 ID 超出nc范围、或者学习率过大导致梯度爆炸。我用check_labels.py排查出一批标注文件里存在坐标值等于 1 的边界样本。
解决:训练前先写一个小脚本扫描所有 label 文件,剔除坐标值小于等于 0 或大于等于 1 的行。学习率从默认 0.01 降到 0.001 再试,尤其当你的数据集规模在万张以下时,0.01 的初始学习率偏高。
坑四:模型把路牌/车尾灯误检成红绿灯
现象:验证集 PR 曲线很好看,但一拿到真实道路视频上,频繁把路边广告牌、自行车尾灯、交通标志杆上的红色圆形物体识别成红灯。
原因:数据集中训练样本和真实场景分布有差异,模型学到了颜色加形状的强相关特征,但没有学会上下文语义——它不知道红绿灯必须在路口、在特定位置出现。另一个常见原因是标注时把背景里的红色圆形都标注进去了,标签噪声直接教坏了模型。
解决:清洗训练数据,去掉模棱两可的标注;推理后处理里加位置先验约束,比如限制检测框在图像上半部分、高度不超过某阈值;或者使用基于跟踪的帧间投票,把单帧偶发的误检过滤掉。
坑五:评估指标曲线和实际表现对不上
现象:训练集 mAP 高达 0.9,但你打开摄像头实测,红绿灯稍微远一点就大概率漏检。
原因:训练和验证用的是同一批数据分布,验证集里的「远距离红灯」样本可能本身就很少,评估指标自然偏向于检测效果好的近距离样本。
解决:单独做一个分布更均匀的测试集,别只用训练时自动划分的 val 集。把测试集按距离分组,分别统计每个距离区间内的 mAP。这个习惯帮我避免了不少「看起来模型很好,实际上路就废」的尴尬。
6. 进阶:把单帧检测升级成红绿灯状态推断,附一个帧间投票技巧
红绿灯识别检测的最终目的是判断当前信号灯的状态,而不是简单地画方框。单帧检测只能回答「这个位置有一个红色灯」,但实际行驶中你看到的是持续几秒的红灯,中间可能伴随黄灯切换和箭头变化。所以进阶做法是把单帧检测结果串联成状态序列:每帧只做检测,连续 N 帧的检测结果送入一个状态机,状态机一票否决地把偶发抖动过滤掉。
我一般会做一件事:在检测环节之后加一个时间窗口投票器。核心逻辑是维护一个长度为 10 帧的循环队列,每一帧的检测结果写入队列,窗口内超过 60% 的帧都识别到同一个位置的红灯时,才判定当前是红灯亮起。这比直接输出单帧结果稳定得多,尤其当摄像头帧率不稳或车辆颠簸导致画面抖动时。
一段简单的帧间投票示意:
from collections import deque class TrafficLightVoter: def __init__(self, window_size=10, vote_ratio=0.6): self.window = deque(maxlen=window_size) self.vote_ratio = vote_ratio def update(self, detections): # detections: list of (class_id, confidence, bbox) # 这里按类别投票,简化成只看红灯类别 self.window.append(1 if any(d[0] == 0 for d in detections) else 0) if len(self.window) < self.window.maxlen: return None red_ratio = sum(self.window) / len(self.window) return "red" if red_ratio >= self.vote_ratio else "unknown"这个技巧的代价是输出延迟约等于窗口长度乘以帧间隔,10 帧在 30fps 下约 0.33 秒延迟。如果你做的是辅助驾驶,这个延迟可以接受;如果做的是路口信号灯抓拍,也可以把窗口缩短到 5 帧。从那以后我每次做红绿灯检测,都会强制在检测后走一遍帧间投票的流程,先投确认再输出状态。单张图跑得再准,丢到视频流里抖两下就露馅了。希望这套 YOLOv5 + OpenCV 方案的拆解,能帮你少走几趟弯路。
本文还有配套的精品资源,点击获取