
简介一套基于Python和OpenCV的红绿灯实时检测方案核心采用YOLOv3预训练模型yolov3.weights适用于智能交通、自动驾驶等目标检测场景适合希望快速部署深度学习识别任务的开发者。包体为ZIP压缩包共444个文件大小10.77MB包含309个Python脚本、57个动态链接库、27个pyd扩展、11个exe可执行程序以及jpg/png测试图像、xml/cfg配置文件、pth权重和虚拟环境组件解压后即可调用相关依赖完成检测。已有938人学习下载。通过阅读代码可掌握加载YOLOv3权重、调用OpenCV处理视频流、执行目标检测并绘制结果的关键步骤还能了解预训练模型在实际交通信号识别中的落地方式适合作为红绿灯识别项目入门或算法调优的参考资料。1. 红绿灯识别项目速览加载 yolov3.weights 就能检测视频里的信号灯这个项目压缩包表面看是「红绿灯识别 yolov3 yolov3.weights」实际上是一套可以直接跑起来的 Python OpenCV 目标检测落地代码。核心思路很简单不自己训练模型而是直接加载 YOLOv3 在 COCO 数据集上预训练好的 yolov3.weights 权重文件对视频或图片里的红绿灯做实时检测框选。我最初拿到这个资源是在做交通信号灯相关项目的前期验证阶段当时最大的诉求就是「别让我先花三天训练模型再花两天调参」这套代码恰好跳过了这两个环节适合刚接触目标检测、想快速看到检测框的新手也适合熟手拿来做基线效果对比。压缩包里还打包了 Python 3.7 的虚拟环境文件理论上解压就能用但实际运行时有几个环境上的坑后面会展开说。2. 为什么是 YOLOv3预训练权重、COCO 类别与红绿灯小目标检测2.1 从 YOLO 家族里选 v3 的理由YOLO 系列发展到现在已经有 v5、v8 甚至更轻量的变体但这个项目用的是 YOLOv3不少刚接触的人第一反应是「版本太老了吧」。放在 2018 年之后、2022 年之前这个时间段YOLOv3 是工业界部署目标检测性价比最高的选择之一。它比 v1、v2 多了多尺度预测和残差结构对小目标的召回率明显提升相比 Faster R-CNN 这类两阶段检测器它又保住了实时性。OpenCV 的 dnn 模块对 Darknet 格式的支持非常成熟readNetFromDarknet 可以直接加载 cfg 和 weights 两个文件不需要额外安装 PyTorch 或 TensorFlow 环境这对一个交给公司内部使用的脚本来说部署代价足够低。更重要的是网络上现成的 yolov3.weights 是 Darknet 官方在 COCO 数据集上训练好的通用权重覆盖了 person、car、traffic light 等 80 个类别。这就意味着拿到权重就能检测红绿灯不需要自己标注数据、训练模型。这类「预训练权重直接落地」的做法在工业项目里是常态尤其是红绿灯这类 COCO 里已有的目标类别先跑基线、再考虑微调流程上完全说得通。2.2 yolov3.weights 到底是什么Darknet 官方权重与 traffic light 的类别 IDyolov3.weights 是 Darknet 官方发布的预训练权重文件体量在两三百 MB 这个级别里面存的是 YOLOv3 卷积层和全连接层的全部参数。COCO 数据集把目标分成 80 类红绿灯traffic light在 COCO 类别表里排在具体某一位实际代码里通常用类别索引来判断检测结果是不是红绿灯。COCO 80 类中 traffic light 对应的类别 ID 是 9这个数字在项目代码里会频繁出现。加载权重时网络结构由 yolov3.cfg 定义权重只负责填充各层的参数值两者必须配套使用。这个细节很关键后面环境搭建章节会再强调一次如果压缩包里只给了 weights 而没有 cfg需要去 Darknet 官方仓库找对应版本的 yolov3.cfg版本不匹配时加载会直接报错或者输出乱框。另外要明确一点这个权重识别的是「画面里有没有红绿灯、灯在哪个位置」它本身不判断灯是红灯、绿灯还是黄灯。因为 COCO 的 traffic light 类别只标注了信号灯整体没有细分红绿黄状态。如果你需要状态识别要么换专门训练的信号灯状态模型要么在检测到灯之后用灯罩区域内的色相信息做二次判断这个我在最后一章展开讲。2.3 三个尺度的检测头红绿灯为什么吃小目标分支YOLOv3 相比前代最大的改进是引入了类似 FPN 的多尺度检测头。网络在三个不同尺寸的特征图上做预测13×13、26×26、52×52。每个网格单元预测 3 个锚框总共 9 组 anchor。13×13 特征图感受野最大负责大目标52×52 特征图感受野最小负责小目标。红绿灯在视频画面里通常只占几十个像素属于典型的小目标主要靠 52×52 这个分支来召回。这也是为什么你在实际检测时如果发现红绿灯没框出来优先检查的两个方向是输入分辨率是不是被压得太低比如 320×320以及置信度阈值是不是设得太高。YOLOv3 官方默认支持 320、416、608 三档输入尺寸红绿灯这种小目标场景416 是起步608 效果更好但耗时翻倍。后面避坑章节我会给具体的参数建议这里先记住结论小目标检测能力是 YOLOv3 三个尺度分支共同作用的结果单独调哪个都没用。3. 先把环境拉起来打包的 Python 虚拟环境与依赖安装3.1 解压后你会看到什么压缩包解压后目录里有一批看起来很奇怪的文件activate.bat、deactivate.bat、pyvenv.cfg、python37.dll、libcrypto-1_1-x64.dll、tcl86t.dll、tk86t.dll、sqlite3.dll。懂 Python 的人一眼就能看出来这是把整个 Python 3.7 的虚拟环境venv连解释器带依赖库一起打包了。python37.dll 说明使用的是 CPython 3.7 运行库tcl86t.dll 和 tk86t.dll 是 Tkinter 图形界面的底层依赖sqlite3.dll 是 Python 内置 sqlite3 模块的支撑库。正常情况下venv 虚拟环境只需要拷贝激活脚本、Lib 目录和 pyvenv.cfg但很多 Windows 上的打包工具会把整个环境目录原样塞进去包括 Python 的 DLL 和标准库。这么做的好处是理论上解压即用坏处是跨机器迁移时大概率会翻车原因在下一节说。3.2 先别急着激活虚拟环境很多人拿到压缩包后的第一反应是双击 activate.bat然后直接跑代码。在实际操作里这个动作十次有八次会失败。原因是 venv 的 pyvenv.cfg 文件里记录了创建环境的 Python 解释器的绝对路径比如 C:\Users\某个用户名\AppData\Local\Programs\Python\Python37\python.exe。打包时的路径和你的解压路径不一致activate.bat 生成的临时环境变量就会出现路径错位结果就是 python 命令还是能用但 import 的库全是系统环境的甚至直接报错找不到解释器。提示打包环境中如果带了 python37.dll这个文件的路径问题在 Windows 上尤为常见优先走重建环境的路线不要花时间在修复 activate.bat 上。我的习惯是拿到这类带虚拟环境的压缩包后只保留项目源码环境全部重建。解压后先确认代码目录下的文件比如 yolov3.cfg、yolov3.weights、检测脚本、coco.names 是否存在然后自己创建一个干净的环境。这一步看似绕远实际上是最稳的路径能避免后面所有由环境路径引起的玄学问题。3.3 干净环境重建与依赖版本选择重建环境的操作流程通常是这样的python -m venv traffic_env traffic_env\Scripts\activate.bat pip install --upgrade pip pip install opencv-python numpy如果你本机还没装 Python先去 Python 官网下载 3.8 或 3.9 版本安装安装时记得勾选 Add Python to PATH。这个项目原环境是 Python 3.7但代码本身用的是标准 OpenCV dnn 接口Python 3.8、3.9 甚至 3.10 都能跑不需要刻意复刻 3.7。依赖方面opencv-python 和 numpy 是必需的安装命令里不需要额外指定版本pip 会自动拉取当前 Python 版本兼容的最新版。验证安装是否成功用一段很短的命令python -c import cv2, numpy; print(cv2.__version__, numpy.__version__)正常情况下会输出类似4.9.0 1.26.4这样的版本号。如果这里报了 ModuleNotFoundError: No module named cv2说明 opencv-python 没装进当前虚拟环境回到第 3.2 节检查激活状态。如果装的是 opencv-python-headless图像窗口显示功能会被去掉检测脚本里凡是调用 cv2.imshow 的地方都会报错注意区分两个包的区别一般项目用 opencv-python 就对了。另外提醒一句如果代码里用了 cv2.dnn.readNetFromDarknet 配合 yolov3.weights 这种组合OpenCV 版本建议大于 4.0。4.4、4.5、4.8 都验证过可以正常跑但 4.7 之后的 NMSBoxes 接口有行为变化这个坑在避坑章节单独说。4. 红绿灯检测主流程拆解从加载权重到画框输出4.1 加载 Darknet 模型cfg 与 weights 缺一不可OpenCV dnn 模块加载 YOLOv3 的代码非常简洁核心就一行import cv2 import numpy as np cfg_path yolov3.cfg weights_path yolov3.weights net cv2.dnn.readNetFromDarknet(cfg_path, weights_path) with open(coco.names, r) as f: classes [line.strip() for line in f.readlines()] if cv2.cuda.getCudaEnabledDeviceCount() 0: net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA) else: net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)readNetFromDarknet 解析 cfg 文件中的网络结构再通过 weights 文件给每一层填充参数。coco.names 是一个文本文件每行一个类别名用来把网络输出的类别索引翻译成人能看懂的字符串。这里我把 GPU 支持做了条件判断因为 OpenCV 的 CUDA 后端只在编译了对应模块的版本里可用普通 pip 安装的 opencv-python 没有 CUDA 支持强制设置会直接报错。注意如果压缩包没有提供 yolov3.cfg这个脚本会直接抛 FileNotFoundError这时候需要从 Darknet 官方源码仓库获取对应版本的 cfg 文件。4.2 前处理blobFromImage 的每个参数都影响召回加载完模型接下来要把图像转成网络输入格式这一步的坑最多。image cv2.imread(traffic_scene.jpg) H, W image.shape[:2] input_size 416 blob cv2.dnn.blobFromImage( image, scalefactor1.0 / 255.0, size(input_size, input_size), mean(0, 0, 0), swapRBTrue, cropFalse ) net.setInput(blob) outs net.forward(get_output_layers(net))blobFromImage 做的事情是把图像缩放到 416×416、把像素值从 0~255 归一化到 0~1、按 swapRB 参数交换通道顺序。这里最容易犯的错是 swapRB 写成 False导致检测框大量丢失。原因是 Darknet 训练模型时用的是 RGB 图片而 OpenCV 的 imread 读出来是 BGR 顺序不交换的话模型看到的就是偏色调的输入。mean 参数在 YOLOv3 里保持 (0,0,0) 即可不需要像 ResNet 那样减均值。scalefactor1.0/255.0 是归一化丢失这个参数的话输入像素值在 0~255 范围模型输出置信度会整体偏低甚至接近零。这四个参数scaler、size、mean、swapRB组合在一起决定了网络的输入分布任何一个写错红绿灯的检测结果都会明显下滑且这种下滑往往不报错隐蔽性很强。4.3 推理与输出层getUnconnectedOutLayersNames从 YOLOv3 网络里取输出结果不能直接拿最后一层因为 YOLOv3 有三个输出层分别对应三个尺度。OpenCV 提供了一种写法def get_output_layers(net): layer_names net.getLayerNames() unconnected net.getUnconnectedOutLayers() return [layer_names[i - 1] for i in unconnected.flatten()] outs net.forward(get_output_layers(net))getUnconnectedOutLayers 返回的是输出层在层列表中的索引flatten() 是必要的因为在某些 OpenCV 版本里它返回的是二维数组直接遍历会取到错误的元素。每个输出层是一个三维数组形状是 (批量大小, 目标候选数量, 85)85 这个数字由 80 个类别概率加 4 个坐标参数加 1 个置信度组成。三个输出层的数据需要统一处理所以代码里用一个循环遍历 outs而不是只取第 0 个元素。在实际运行时先打印一下每个 out.shape能帮你确认网络结构加载是否正确如果 shape 里的第二个维度异常大概率是 cfg 和 weights 版本不匹配。4.4 后处理置信度过滤 NMS再把 traffic light 单独筛出来原始输出里包含大量置信度很低的预测框需要过滤和去重。YOLOv3 后处理的完整逻辑如下boxes, confidences, class_ids [], [], [] conf_threshold 0.4 for out in outs: for detection in out: scores detection[5:] class_id np.argmax(scores) confidence scores[class_id] if confidence conf_threshold and class_id 9: center_x int(detection[0] * W) center_y int(detection[1] * H) w int(detection[2] * W) h int(detection[3] * H) x int(center_x - w / 2) y int(center_y - h / 2) boxes.append([x, y, w, h]) confidences.append(float(confidence)) class_ids.append(class_id) indices cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, 0.4) if len(indices) 0: for i in indices.flatten(): x, y, w, h boxes[i] cv2.rectangle(image, (x, y), (x w, y h), (0, 0, 255), 2) label f{classes[class_ids[i]]}: {confidences[i]:.2f} cv2.putText(image, label, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)这里有一个关键设计class_id 9意味着代码直接把结果限制为 traffic light 一个类别。COCO 80 类中 index 9 对应 traffic light这个索引来自 coco.names 的逐行顺序如果你的 coco.names 是从其他来源下载的行顺序可能有差异务必确认第 10 行是 traffic light。用了这个限制之后画面里即使有 car、person 也不会出框输出面板干净全是红绿灯。detection[0] 到 detection[3] 是归一化坐标范围在 0~1必须乘以原图的 W 和 H 才能还原成像素坐标。模型输出的是中心点坐标加宽高画矩形时需要换算成左上角坐标。如果检测框位置明显偏移、框的大小不对优先怀疑这里的坐标换算尤其是宽高是否乘以了正确的 W、H。NMSBoxes 是 NMS 非极大值抑制的封装它把同一个目标上的多个重叠框合并避免一个红绿灯被重复框三次。第二个参数 0.4 是 NMS 的 IoU 阈值取值越低相近的框被合并得越凶一般设 0.4 或 0.5 合适。4.5 坐标换算与可视化可视化部分用的是 OpenCV 的画框和文字 API。cv2.rectangle 接受左上角坐标和右下角坐标这里 x、y 是左上角x w、y h 是右下角。cv2.putText 在框上方标注类别名和置信度展示效果直观。这一段有一个容易踩的坑在视频处理场景里如果每帧都执行完整检测输出出的坐标没有问题但如果你的输入图像尺寸在检测前经过裁剪或者视频分辨率中途改变W 和 H 必须在每一帧实时更新不能拿第一帧的尺寸锁死。常见做法是读取视频帧后立即取 shape而不是在循环外缓存尺寸。另外一个更隐蔽的问题是 letterbox。Darknet 官方对输入图像的处理有一些细节OpenCV 的 blobFromImage 默认直接拉伸缩放不保持宽高比。如果你的测试视频是 16:9 的原生比例拉伸到 416×416 不会明显影响效果但如果画面比例接近 4:3拉伸比例不同会导致目标形状变形检测框会比实际位置略偏移。解决方案是保持宽高比缩放并在图像四周补黑边letterbox推理前记录缩放比例和补边偏移推理后反向换算坐标。这段逻辑稍复杂但效果稳定适合对检测精度有要求的人。5. 运行踩坑与排查从 ModuleNotFoundError 到检测不到红绿灯5.1 cv2 导入失败打包环境跨机器失效现象按第 3 章激活了压缩包里的 activate.bat跑检测脚本时提示 ModuleNotFoundError: No module named cv2。原因压缩包里的 Python 3.7 环境是在打包者的机器上创建的venv 结构记录了原机器上的绝对路径跨机器激活后 site-packages 路径无法正确解析解释器加载不到 OpenCV。同时打包环境里如果包含 python37.dll 这类解释器核心文件路径错位时系统可能会加载到错误的依赖。解决放弃修复原有虚拟环境用 3.3 节的方式重建新环境重新安装依赖。这是最省时间的路径。5.2 一直检测不到红绿灯阈值、输入尺寸与类别 ID现象脚本正常跑视频画面打开正常但红绿灯一个框都没有。原因三类问题最常见。第一置信度阈值设得过高默认 0.5 在红绿灯这种小目标上会滤掉大量本可以命中的预测框第二输入尺寸太小320×320 输入在小目标场景的召回率很低第三类别 ID 判断错误如果你的 coco.names 文件顺序不对class_id 9 指向的可能是别的类别。解决把 conf_threshold 调到 0.3 到 0.4 区间再试红绿灯检测通常不需要 0.5 以上的阈值。输入尺寸从 416 起步有条件就换 608。检查 coco.names 文件的第 10 行是否为 traffic light不是的话修正文件或调整类别索引。5.3 检测框偏了swapRB 和坐标缩放现象红绿灯能被检测到但框的位置明显偏移比如框在灯的上方或一侧或者框的尺寸与实际目标不一致。原因swapRB 参数设置错误会导致模型看到的颜色分布异常弱化目标特征输出位置偏差。更常见的是坐标换算错误检测框的 x、y、w、h 在乘以 W、H 前没有保持统一的坐标系或者检测框在 fromBLOB 输入前经过了 letterbox 处理而坐标用的是原始图尺寸。解决先确认 swapRBTrue再检查坐标换算逻辑。如果用了 letterbox需要把原始坐标做反向映射公式是 original_x (x - pad_x) / scale其中 pad_x 是水平方向的补边宽度scale 是缩放比例。简单做法是直接用 OpenCV 的 blobFromImage 默认拉伸模式避免引入 letterbox 的坐标还原问题。5.4 NMSBoxes 返回值形状变化4.5 与 4.7 的坑现象同一套代码在一台机器的 OpenCV 4.5 上正常换到 OpenCV 4.7 以上的机器上报错或者输出的框数量异常。原因OpenCV 4.5.x 版本的 cv2.dnn.NMSBoxes 返回 N×1 的二维数组而 4.7 及以后版本返回扁平的一维数组。如果代码里直接遍历 indices遍历出来的元素一个是数组、一个是整数索引 boxes 时行为完全不同。解决统一在一次遍历前对 indices 做 flatten 处理。代码写法是indices cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, 0.4)之后立即indices indices.flatten()再做遍历这样可以兼容两个版本。安装依赖时也可以直接钉版本比如pip install opencv-python4.5.5.64不过更推荐代码兼容的方式。5.5 视频跑得很慢CPU 推理的真实瓶颈现象单帧图片检测很快但跑视频流时可以明显感觉到卡顿帧率经常只有个位数画面像幻灯片。原因OpenCV 默认用 CPU 推理416×416 的输入尺寸在 CPU 上跑 YOLOv3一帧需要几秒钟是正常表现。视频流每帧都做完整推理自然严重掉帧。解决优先思路是降低推理频率比如每 3 帧只跑一次检测中间帧沿用上一次结果再就是把输入尺寸降到 320×320速度能提升约 50%。如果仍然需要实时级别的检测必须换 GPU 推理后端普通 pip 安装的 opencv-python 是不带 CUDA 的需要从源码编译带 DNN_CUDA 的版本或者把模型转成 ONNX 用 ONNX Runtime GPU 来跑。这条路的工程量明显更大适合对性能要求高的场景。6. 进阶把单帧检测改成视频流巡检并压缩推理开销6.1 一段可以抄走的视频流检测模板把前面所有模块拼装成适合视频流的形式代码结构类似下面这样import cv2 import time cap cv2.VideoCapture(traffic_video.mp4) fps_count 0 start_time time.time() frame_skip 2 while True: ret, frame cap.read() if not ret: break fps_count 1 if fps_count % frame_skip ! 0: continue detections detect_traffic_light(frame) for box in detections: x, y, w, h, conf box cv2.rectangle(frame, (x, y), (x w, y h), (0, 0, 255), 2) fps fps_count / (time.time() - start_time) cv2.putText(frame, fFPS: {fps:.1f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Traffic Light Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码把检测封装进了循环frame_skip 控制推理间隔fps 用计数和时间差动态计算方便直观评估性能。注意 cap.read() 返回的 frame 每帧都在变检测函数里必须实时获取 frame.shape不要在外面缓存。cv2.waitKey(1) 的参数 1 表示等待 1 毫秒保证视频按正常速度播放按 q 键退出。这套模板在实际项目里可以直接套用替换掉 detect_traffic_light 的内部实现就能复用。6.2 小目标专属调参与降频策略红绿灯小目标检测的核心里除了上一章的阈值调整还有一个实用技巧——联合跟踪器降低推理频率。检测到红绿灯之后用 OpenCV 的跟踪器如 KCF 或 CSRT锁定目标框在后续若干帧内只做跟踪、不跑卷积推理等跟踪置信度下降再重新检测。这样做的收益很直接YOLOv3 推理从每帧一次降为每 10 帧一次CPU 场景下帧率可以提升数倍。关于红绿灯状态识别红灯、绿灯、黄灯如果在实际业务里需要一个可行的方案是先通过 YOLOv3 拿到灯罩的检测框后在框内区域用 HSV 色彩空间做色相统计红色像素占比高判为红灯绿色像素占比高判为绿灯。这个方案依赖灯罩区域的像素质量夜景场景下效果略差但它能在不改模型的前提下补上状态识别能力作为 yolov3.weights 通用权重的补充是够用的。这套项目我拆过很多次最深的一个教训是有两次为了省时间直接激活压缩包里的虚拟环境结果 python37.dll 路径错位导致整个环境不可用白白折腾了一个下午。从那以后我每次拿到这种自带环境的压缩包都强制走一遍「只留源码、重建环境」的流程十分钟换回一整天的安稳希望帮到你。本文还有配套的精品资源点击获取