ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv3与OpenCV实战:红绿灯识别完整指南

YOLOv3与OpenCV实战:红绿灯识别完整指南 简介基于Python和OpenCV、利用YOLOv3预训练权重实现红绿灯实时检测的完整项目代码包主要面向智能交通、自动驾驶等领域的开发者和研究者既适合快速搭建检测原型也可作为YOLO系列目标检测的入门示例。压缩包共444个文件整体约10.77MB其中包含309个py源码以及配套的dll、pyd、exe运行组件还有模型配置文件、测试图片、XML标注与简要文档等结构清晰解压后可直接上手调试。已有937人参与学习下载说明内容具有一定参考价值。项目源自实际实习任务代码覆盖了加载yolov3.weights权重、读取视频/图片、执行推理、绘制检测框并输出结果的核心流程同时展示了OpenCV与深度学习模型的结合方式通过阅读和运行其中脚本可以理解预处理、多尺度预测等关键环节并快速迁移到其他交通信号识别场景中。1. 红绿灯识别为什么绕不开 YOLOv3一个 yolov3.weights 就能跑通的现实当你在搜索栏里敲下“trafficlightdetection using python opencv”的时候多半已经在传统图像处理上栽过跟头HSV 颜色阈值换个场景就失效轮廓检测在逆光和雨天里全是噪点程序跑得像玄学。把方案换成 YOLOv3 之后整个项目的起点就变成读取一个 yolov3.weights 权重文件配合 OpenCV 的 dnn 模块一次把“红绿灯在哪、置信度多高、哪些是灯”都拿回来。这个标题方案最大的优势是不用自己标注数据、也不需要重新训练因为 yolov3.weights 是在 COCO 数据集上训练好的traffic light 本身就是 COCO 的 80 个类别之一。下面按实际动手的顺序把一条能落地的路径拆开讲环境怎么装、代码每行在干什么、视频里怎么用以及真正跑真实道路视频时最容易翻车的那几个坑。2. 从 python 安装到 opencv 加载 yolov3.weights环境搭建的先后顺序先纠正一个常见误解标题里的 YOLOv3 并不要求你安装 Darknet也不需要自己编译 CUDA 版本的任何东西。常见做法是直接用 OpenCV 的 dnn 模块去读 Darknet 训练出来的权重文件所以本质上是“python opencv-python 三个模型文件”的问题。很多新手死磕 CUDA 编译结果连一个 demo 都没跑起来属于方向性踩坑。2.1 python 和 opencv 的版本选择与安装命令python 版本我一般固定 3.8 到 3.10不要一上来就装最新的 3.12。OpenCV 的 python 包更新很快但新版本接口变化很容易让教程代码报错最典型的是cv2.dnn.NMSBoxes的返回值类型在不同版本里不一样。为了少踩坑建议用虚拟环境把版本锁死conda create -n tl_detect python3.8 conda activate tl_detect pip install opencv-python4.5.4.60 numpy说明一下版本选择的逻辑。opencv-python是 OpenCV 官方发布的 pip 包安装后直接import cv2即可。4.5.4 这个版本在 Windows 和 Linux 上都稳定cv2.dnn.readNetFromDarknet和NMSBoxes的行为都符合 YOLOv3 教程里最常见的写法。如果你用的是 Anaconda经常出现“anaconda prompt 里面没有 opencv”的情况那是没激活对应虚拟环境或装到了别的环境重回检查conda activate后再pip list | findstr opencv就能定位。确认安装是否成功一句命令就够了python -c import cv2; print(cv2.__version__)打印出 4.5.4 就说明环境通了。这里注意两点一是不要用pip install opencv-contrib-python替代这个包虽然功能更全但体积大与本项目的功能无关二是如果你在 Linux 上确实需要 GPU 推理常见做法是源代码编译带 CUDA 的 OpenCV但那个步骤至少要多花半天时间建议先把 CPU 版本的完整流程跑通再考虑。2.2 模型配套的三个文件各管什么跑 YOLOv3 缺一不可的三个文件yolov3.cfg、yolov3.weights、coco.names。它们的关系是这样的yolov3.cfg是文本文件描述 YOLOv3 的网络结构包括层数、卷积核大小、anchor 尺寸、三个 yolo 输出层的配置。OpenCV 靠它来构建网络。yolov3.weights是二进制权重文件保存了所有卷积层和全连接层的参数。常见 YOLOv3 官方权重大约 236 MB下载后注意看大小如果只有几十 MB 说明文件没下完整。coco.names是类别名清单每一行一个类名。必须和权重训练时用的类别顺序完全一致。COCO 数据集 80 类里 traffic light 排在后面如果你用的 names 文件类别顺序和权重不匹配检测结果会乱套。我习惯把三个文件放到同一个目录yolov3/ ├── yolov3.cfg ├── yolov3.weights └── coco.names有个容易踩的坑是coco.names从网上随便下一个。不同来源的 COCO names 顺序可能不一致。建议从 Darknet 官方项目配套的data/coco.names里复制保证与 yolov3.weights 训练时一致。这个文件一共 80 行第 10 行是 traffic light也就是索引 9。2.3 一行代码验证 opencv 能加载 darknet 权重正式写检测逻辑之前先用一段极简代码确认网络能加载出来。这一步能帮你把“环境问题”和“代码问题”隔离开import cv2 net cv2.dnn.readNetFromDarknet(yolov3.cfg, yolov3.weights) print(网络加载成功) layer_names net.getLayerNames() output_layers [layer_names[i - 1] for i in net.getUnconnectedOutLayers()] print(输出层, output_layers)readNetFromDarknet第一个参数是 cfg 配置文件第二个是权重文件顺序不要写反。getUnconnectedOutLayers()返回的是 YOLOv3 三个输出层的索引减 1 是因为 OpenCV 的层索引从 0 开始而 Darknet 从 1 开始。如果打印结果是类似[yolo_82, yolo_94, yolo_106]的三个层说明网络结构解析成功。这里加载失败最常见的表现是直接抛异常或进程崩溃等会在避坑章节细说。3. 用 cv2.dnn 跑通 yolov3 红绿灯检测模型推理全链路解析网络加载成功只是第一步。要在图片上真正检测出红绿灯需要完成预处理、前向推理、后处理三个环节。很多教程只贴一个大杂烩代码参数含义完全不说导致你调一次结果变一次。这一章把链路拆开每个环节的参数都讲清楚。3.1 blobFromImage 预处理与 416×416 输入的含义YOLOv3 训练时输入是固定尺寸方图常见是 416×416 或 608×608。OpenCV 里用blobFromImage完成从原始图像到网络输入的转换blob cv2.dnn.blobFromImage( img, # 原始 BGR 图像 1.0 / 255.0, # scale像素值归一化到 0~1 (416, 416), # 统一缩放到 416×416 (0, 0, 0), # 减均值这里不减 swapRBTrue, # OpenCV 的 BGR 转成模型期望的 RGB cropFalse # 不裁剪不足部分用 0 填充 ) net.setInput(blob) outs net.forward(output_layers)这几个参数里最容易被忽略的是swapRB和crop。Darknet 训练时图像是 RGB 顺序而 OpenCV 读进来是 BGRswapRBTrue负责交换通道。cropFalse表示缩放后不居中裁剪而是保持宽高比后用 0 填充这对检测任务更友好因为裁剪会丢失边缘目标。416 这个数字不是随便定的。YOLOv3 对输入做 32 倍下采样416 可以整除 32得到 13×13 的网格加上另外两个尺度得到 52×52 和 26×26 的特征图分别负责小、中、大目标。如果你把输入改成 608×608小目标检测会改善但推理时间增加约一倍。对红绿灯这种本身尺寸不大的目标我第一次跑项目时建议先用 416后面再根据效果权衡。3.2 输出层解析与 NMS 非极大值抑制net.forward(output_layers)返回三个输出张量分别对应 13×13、26×26、52×52 三个尺度。每个尺度每个网格预测 3 个 anchor 框所以总共候选框数量是(13×13 26×26 52×52) × 3 10647 个每个候选框对应一个长度为 85 的向量前 4 个是中心坐标和宽高第 5 个是 objectness 置信度后 80 个是 COCO 各类别的分类得分。后处理要做的就是从这 1 万多个框里挑出属于 traffic light 的哪些框再用 NMS 把重叠框合并boxes, confs, class_ids [], [], [] for out in outs: for detection in out: scores detection[5:] class_id int(np.argmax(scores)) if class_id ! traffic_light_id: continue confidence float(scores[class_id]) if confidence CONF_THRESH: continue # 还原到原图坐标系 cx, cy, bw, bh detection[:4] * np.array([w, h, w, h]) x int(cx - bw / 2.0) y int(cy - bh / 2.0) boxes.append([x, y, int(bw), int(bh)]) confs.append(confidence)CONF_THRESH是置信度阈值我一般设 0.5。设低会出现大量误检尤其在夜晚场景各种亮着的灯都会被当成目标设高会漏掉远处模糊的红绿灯。traffic_light_id从coco.names里现取避免硬编码数字。NMS 的作用是解决同一个红绿灯被多个相邻框重复检测的问题indices cv2.dnn.NMSBoxes(boxes, confs, CONF_THRESH, NMS_THRESH)NMS_THRESH是 IoU 阈值设 0.4 或 0.45 都行。IoU 指的是两个候选框的交并比超过这个值就认为两个框指同一个目标保留分数高的那个。如果设成 0.1同一个灯会出现大量重复框合并不干净如果设成 0.9两个高度重叠但确实不同的目标可能被误合并。3.3 可直接复制到本地运行的完整检测脚本把上面内容串起来给出一个能直接跑的完整脚本。这个脚本的作用对象是一张静态图片跑通后再扩展成视频循环。注意这段代码里我加了np.array(indices).flatten()是为了兼容不同 OpenCV 版本下NMSBoxes返回值格式的差异import cv2 import numpy as np CFG yolov3.cfg WEIGHTS yolov3.weights NAMES coco.names CONF_THRESH 0.5 NMS_THRESH 0.4 INPUT_W INPUT_H 416 with open(NAMES, r) as f: classes [line.strip() for line in f.readlines()] traffic_light_id classes.index(traffic light) net cv2.dnn.readNetFromDarknet(CFG, WEIGHTS) layer_names net.getLayerNames() output_layers [layer_names[i - 1] for i in net.getUnconnectedOutLayers()] img cv2.imread(test.jpg) if img is None: raise FileNotFoundError(test.jpg 不存在检查路径或文件名) h, w img.shape[:2] blob cv2.dnn.blobFromImage(img, 1.0 / 255.0, (INPUT_W, INPUT_H), (0, 0, 0), swapRBTrue, cropFalse) net.setInput(blob) outs net.forward(output_layers) boxes, confs [], [] for out in outs: for detection in out: scores detection[5:] class_id int(np.argmax(scores)) if class_id ! traffic_light_id: continue confidence float(scores[class_id]) if confidence CONF_THRESH: continue cx, cy, bw, bh detection[:4] * np.array([w, h, w, h]) x int(cx - bw / 2.0) y int(cy - bh / 2.0) boxes.append([x, y, int(bw), int(bh)]) confs.append(confidence) indices cv2.dnn.NMSBoxes(boxes, confs, CONF_THRESH, NMS_THRESH) if len(indices) 0: indices np.array(indices).flatten() for i in indices: x, y, bw, bh boxes[i] cv2.rectangle(img, (x, y), (x bw, y bh), (0, 0, 255), 2) label ftraffic light {confs[i]:.2f} cv2.putText(img, label, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow(result, img) cv2.waitKey(0) cv2.destroyAllWindows()这段代码里值得解释的细节有两处。第一detection[:4] * np.array([w, h, w, h])是把相对坐标还原成绝对像素坐标因为 YOLOv3 输出的 cx、cy、bw、bh 都是相对原图尺寸的 0~1 小数。第二绘制框的厚度我固定为 2如果图片分辨率很高框线看起来会很细可以按原图宽度动态设置为max(2, int(w / 600))。运行脚本前确保test.jpg是一张道路场景图片最好正对着红绿灯距离不要太远。第一次跑通后你会看到图片上出现红色矩形框这代表 yolov3 把目标识别为 traffic light 类。如果什么都没检测出来先把CONF_THRESH降到 0.3 试试确认模型本身工作正常。4. 视频流里的红绿灯检测帧循环、颜色分类与提速静态图跑通之后接下来就是把检测逻辑应用在视频流上。红绿灯识别项目最终要面对的往往是行车记录仪或定点点位的视频帧与帧之间目标位置连续变化处理逻辑和单帧不完全一样。这一章解决三个问题怎么读视频、怎么判断灯的颜色、怎么让速度不至于卡到没法看。4.1 VideoCapture 读取视频并按帧检测OpenCV 用VideoCapture读视频文件本质是一个 while 循环逐帧送入检测函数。但我不会每一帧都做一次完整推理常见做法是抽帧处理例如每 3 帧推理一次中间跳过的帧沿用上一次的检测结果。这样既保证视觉上连续又大幅降低计算量cap cv2.VideoCapture(traffic_video.mp4) if not cap.isOpened(): raise IOError(视频打开失败检查格式和路径) frame_idx 0 skip 2 # 每 3 帧推理一次 while True: ret, frame cap.read() if not ret: break frame_idx 1 if frame_idx % (skip 1) ! 0: # 不推理的帧直接显示检测框沿用上一帧的 cv2.imshow(traffic light, frame) if cv2.waitKey(1) 0xFF ord(q): break continue # 推理当前帧并绘制 detections detect_traffic_lights(frame) frame draw_detections(frame, detections) cv2.imshow(traffic light, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()detect_traffic_lights就是把上一章的检测逻辑封装成函数输入一帧图像输出该帧中和 traffic light 相关的检测框列表。这样封装之后处理视频和处理图片用的是同一套逻辑只是数据来源不同。waitKey(1)的作用是让 OpenCV 响应键盘事件并刷新窗口参数 1 表示等待 1 毫秒。如果这里不写窗口会无响应这是新手常见的新手问题。4.2 检测框出来之后怎么判断灯是红绿黄YOLOv3 只能告诉你“这是一个 traffic light”不能告诉你它到底亮的是红灯还是绿灯。判断颜色属于分类问题最简单有效的方式是切下检测框区域转 HSV 颜色空间做阈值判断。HSV 比 BGR 对光照变化更鲁棒。下面这个函数是实际项目里我一直在用的实现def classify_light_color(roi): 输入检测框裁剪出的 roiBGR 格式 返回 red / green / yellow / unknown hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 红色在 HSV 空间中横跨 0 度附近的两个区间 red_a cv2.inRange(hsv, (0, 70, 70), (10, 255, 255)) red_b cv2.inRange(hsv, (156, 70, 70), (180, 255, 255)) red cv2.countNonZero(red_a) cv2.countNonZero(red_b) # 绿色约在 35~85 度区间 green cv2.countNonZero(cv2.inRange(hsv, (35, 70, 70), (85, 255, 255))) # 黄色约在 20~34 度区间 yellow cv2.countNonZero(cv2.inRange(hsv, (20, 70, 70), (34, 255, 255))) # 用像素数量比较最少的 30 像素阈值过滤噪声 if red green and red yellow and red 30: return red if green red and green yellow and green 30: return green if yellow red and yellow green and yellow 30: return yellow return unknown这段代码需要解释的关键是为什么红色要判断两个区间。HSV 中色相是 0 到 180 度红色恰好位于 0 度附近往左一点是 0 到 10往右一点是 156 到 180所以红色需要两个 mask 合并。饱和度 S 和明度 V 的下限设为 70是为了滤掉暗光和低饱和度的灰色目标。如果你是在夜间场景下限可能要降到 40 左右这个参数和摄像头白平衡强相关要实际调。countNonZero统计的是 mask 中像素值为 255 的像素个数也就是满足颜色条件的像素数量。最后的 30 像素阈值是用来排除小噪声的但具体数值取决于你的检测框有多大。如果框很大但阈值太小可能误判反之如果框里有大面积反光也可能误判后续要在避坑章节处理。4.3 让视频检测从幻灯片变成可用的三个方向CPU 上跑 416×416 的 YOLOv3单帧推理时间通常在 0.2 到 0.5 秒之间直接逐帧跑就是幻灯片效果。提速不一定要上 GPU按顺序尝试下面三个办法第一个是抽帧加跟踪。在推理的帧之间加入简单的目标跟踪比如用 OpenCV 的TrackerCSRT或直接沿用上一帧框只在关键帧做检测。路灯位置在固定点位场景下基本不变抽帧间隔可以大胆拉到 5 帧以上。第二个是缩小输入尺寸或改用训练好的大输入模型。把输入从 416 降到 320推理时间约减少一半但远距离小目标漏检率会快速上升。如果你射的固定点位、离红绿灯很近320 可以接受如果是行车场景不建议低于 416。第三个是换用支持 CUDA 的 OpenCV。在 Linux 上手动编译带 CUDA 的 OpenCV或者在 Windows 上找预编译的 opencv-python GPU 版本推理时间能缩短一个数量级。但这一步的坑很多我建议先确认 CPU 版本流程完全正确再碰 GPU否则错误来源会叠加。这三招可以叠加使用。我做过一个固定点位试验416 输入加间隔 3 帧在 i5 笔记本上能跑出接近 15 帧的处理速度对红绿灯识别应用来说足够用。5. YOLOv3 红绿灯识别常见问题与排查避坑这一章收集的是我在实际复现红绿灯识别项目时踩过的坑每一条都按“现象→原因→解决”的顺序写。你如果照着前两章的代码跑仍然出问题优先来这一章对照。5.1 报错 FileNotFoundError 而文件明明存在现象readNetFromDarknet或cv2.imread抛出文件不存在异常但资源管理器里文件明明就在那个路径下。原因有两个。一是 Windows 下路径里带中文或空格OpenCV 旧版本的 imread 对非 ASCII 路径支持不好二是你用了反斜杠转义但没加 raw 字符串例如yolov3\weights里的\w被转义了。解决把项目路径统一改为纯英文目录同时代码里用绝对路径或相对路径时都写成/分隔符例如yolov3/yolov3.weights。再不行就打印os.path.dirname(__file__)检查当前工作目录是否在项目根下。5.2 模型加载时进程直接被系统终止现象readNetFromDarknet执行后没有报错而是整个进程直接退出控制台没有任何 Python 异常或者提示unable to allocate ...内存分配错误。原因最常见是 yolov3.weights 文件不完整下载中断导致二进制文件尾部缺失其次是权重和 cfg 不匹配例如用 YOLOv3 的 cfg 去加载 YOLOv3-tiny 的权重。解决先检查文件大小。常见 YOLOv3 官方权重是 236 MB 左右如果只有几十 MB重新下载并确认下载工具没有断点续传的损坏。其次确认 cfg 里的classes和filters是否和权重训练时一致最简单的方法是直接重新下载配套的官方 yolov3.cfg不要自己手工改。5.3 远处的小红绿灯完全检测不到现象近处的红绿灯能检测到距离超过 30 米后目标始终没有框无论怎么调阈值都没用。原因YOLOv3 在 COCO 训练时对交通灯这种小目标本身就很弱。460×416 输入下一个 30 米外的红绿灯宽度可能只有十几个像素在 13×13 的网格上连一个格子都占不满特征已经被下采样洗掉了。解决把输入尺寸从 416 提到 608 会好一些但代价明显。另一个实用方案是固定点位时在画面中手动画一个 ROI 区域只对包含红绿灯的区域做大尺寸推理其他区域跳过。如果你有摄像头内参还可以根据检测框的位置去估计灯的物理尺寸再决定是否放大局部图像。5.4 把汽车尾灯、红色招牌当成红绿灯现象检测框在夜间密集出现盯着一片红色尾灯或者路边红底招牌画框。原因COCO 预训练模型里的 traffic light 类别并没有接受过“这是路口场景”的上文培训它只是学会了形状和颜色之间的统计关系。汽车尾灯形态和红灯很像误检是必然的。解决三层过滤。第一层把置信度从 0.5 提到 0.7能过滤掉部分低分误检。第二层配合颜色分类函数如果框内红色和绿色像素数太少判定为 unknown 并忽略。第三层加场景先验如果项目是固定点位直接限定检测区域如果是车载摄像头可以根据车道位置过滤掉画面底部和两侧的框。这些都是工程上“够了就行”的办法想要根治只能走最后一章的迁移学习重新训练。5.5 OpenCV 版本不同导致同样的代码结果不一致现象同一段检测代码在自己的电脑上正常拷贝到另一台电脑后NMSBoxes报错或者检测框消失。原因cv2.dnn.NMSBoxes在 OpenCV 4.5.4 之前返回的是嵌套 list之后返回 numpy 数组getUnconnectedOutLayers在不同版本里返回的可能是 int 或 ndarray。不同环境装到的 opencv 版本不同行为就不一样。解决用pip install opencv-python4.5.4.60固定版本同时在代码里加np.array(indices).flatten()这行兜底。我前文脚本里已经把常见的兼容问题处理掉如果你自己参照别人的旧教程改代码要特别注意这两处差异。6. 跑通 demo 之后想让它工作得更准权重微调与数据验证如果你只是为了交课程设计或跑通 demo前面五章已经足够了。但真实路口的红绿灯识别尤其是要接入到判断红灯停、绿灯行的逻辑里只靠预训练的 yolov3.weights 是不够的因为那个权重在 COCO 上学的 traffic light 概念和实际路口的红绿灯之间存在位置偏差。想让模型认清“有灯杆、有灯头、有规律变灯周期”的目标正确方向是迁移学习。先准备自己的数据集。常见做法是录制固定点位视频每隔几帧抽一张挑出白天、夜间、雨天、逆光等不同场景至少准备 300 到 500 张。标注时有两种选择保持 traffic light 一个类或者拆成 red、green、yellow 三个类。后者能直接让模型输出颜色状态就不需要再用 HSV 判断后面发现准确率更高但标注成本大我一般建议先用一个类颜色逻辑仍旧交给 HSV。训练前要修改 cfg 里的两个关键参数这一点是新手最容易踩的[convolutional] size1 stride1 pad1 filters24 # 修改点filters 3 × (classes 5) activationlinear [yolo] mask 0,1,2 classes3 # 修改点按你的类别数改 num9filters 的公式是 3 × (classes 5)。如果还是单类 traffic lightclasses1 时 filters 应为 18如果拆成红绿黄三类classes3 时 filters 应为 24。这个数字不匹配训练过程中会直接报错。训练完成后用训练时未参与标注的测试集跑一遍统计 mAP 和每类别的 AP不要只靠肉眼判断效果。我在第一次微调红绿灯时只用了 400 张图训练了不到 2 万轮误检率就从原来的一半下降到了不到一成这条路比我预想的可靠。这套方案做完后给你一个经验数值验证视频里检测帧率能接受、三类灯色判断准确率在 95% 以上、远距离小目标在能看清灯颜色前能被检测出框就算达标。我自己的习惯是留一份带时间戳的验证视频每次改参数后都让它在同一段视频上跑对比前后结果。一来防止调参翻车退步二来也方便向团队展示改善幅度。希望这些经验能让你少走我走过的弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表