YOLOv11红绿灯检测系统实战:PyQt5界面与多线程推理完整实现
很多做视觉检测的朋友一开始都有一个想法把算法跑通不算完还得有个能给人演示、能落地试用的界面。而这个“红绿灯目标检测 YOLOv11 完整系统代码PyQt界面图片/视频/摄像头推理”的项目恰好就是把我最近一直在打磨的一套东西浓缩成了实际可跑的工程。简单说它是用 YOLOv11ultralytics 框架做交通场景里的红绿灯检测外面套了一层 PyQt5 桌面界面支持图片、视频文件、摄像头实时流三种输入方式识别结果实时显示在界面上还能手动选择保存检测后的图片或视频。适合谁看如果你会一点 Python想从“只会跑 yolov8 官方 demo”跳到一个“带界面、能切换三种输入源、能保存结果的完整系统”这篇文章基本可以照着抄作业。这套系统不是那种只在 Jupyter Notebook 里给你画张图就完事的玩具。它要有界面、有实时推理、有结果反馈还要考虑到摄像头推流连续跑几个小时的稳定性。所以我花了不少时间在处理 QThread 线程、信号槽通信、视频读取的断帧重连这些问题上。下面我把这套系统的设计思路、环境配置步骤、核心代码实现、以及调试时踩过的那些坑从头到尾梳理一篇希望能让你少走点弯路。1. 拆解项目核心红绿灯检测为什么要单独做个系统1.1 红绿灯检测本身的难点在哪红绿灯检测不是普通的目标检测任务。它有几个非常实际的问题红绿灯目标小、环境光线变化大、同一种灯在不同角度下颜色差别很大、而且红黄绿三个灯的目标之间极其相似只差一个颜色维度。如果直接用 COCO 预训练的 YOLO 模型去推理哪怕模型已经能认出“交通灯”这个大类别你也很难拿到“红灯、绿灯、黄灯”这种细粒度结果。这也是我在这套系统里没有直接套用官方默认权重的原因——红绿灯检测必须用一个针对交通灯数据集微调过的权重或者至少在你的推理代码里加上对类别 ID 到“红/黄/绿”的映射逻辑。很多初学者以为装个 ultralytics 就万事大吉跑起来一看结果全是“traffic light”完全没法用就是这个细节没处理明白。1.2 系统要解决的三类应用场景这套系统代码里最核心的三个输入模式实际上对应着三类不同的使用场合。图片模式适合快速验证、批量测试数据集效果视频模式适合对一段录好的路口监控做离线分析你可以调整置信度阈值反复看输出摄像头模式适合在现场做实时验证比如把笔记本摄像头对准路口或者接入一个 RTSP 网络摄像头做长时间稳定性观察。这三种模式看起来只是换了数据源但工程实现上完全不一样。图片是一次性推理消耗资源低可以做高分辨率输入视频要考虑帧率稳定、输出视频编码、以及中间跳帧策略摄像头要考虑设备占用、读取超时、画面尺寸适配。一个系统如果能把这三种模式统一到同一个推理后端再通过界面切换那么这个架构在多数检测项目里都能复用。1.3 适合谁参考、能学到什么这个项目最适合两类人。一类是刚学完 YOLO 原理想把检测算法变成完整小软件的初学者你能学到 PyQt 界面怎么和深度学习模型互相配合而不是把模型丢在命令行里输出一串数字。另一类是想在交通类项目里快速出原型做验证的人比如做车路协同、路口拥堵分析、或者辅助驾驶验证的这套系统能让你直接换权重文件就能针对自己的场景做测试。可能有人会问我之前只跑过 YOLOv8YOLOv11 会不会上手成本很高答案是几乎零成本。ultralytics 框架把 YOLOv5 之后的所有版本统一成了同一个 API你只需要把 model 文件名换成 yolo11n.pt然后代码里的 Model 名字改一改其他流程基本通用。所以即便你是从 YOLOv8 转过来的这套代码的架构也能直接吃透。2. 整体架构设计为什么这样组合才算“完整系统”2.1 技术栈选型YOLOv11 PyQt5 OpenCV这套系统的技术组合很典型YOLOv11ultralytics负责检测OpenCV 负责所有图像和视频流的读取、预处理、结果绘制PyQt5 负责桌面界面和交互逻辑。选 PyQt5 而不是 PySide、Tkinter、或者 Web 界面是我在实际项目里反复权衡过的结果。PyQt5 在桌面应用上的成熟度非常高QThread 的信号槽机制做多线程推理非常顺手而且 PyQt5 的图像显示可以直接和 OpenCV 的 numpy 数组互相转换不用经过额外的编码转换。用 Web 界面还得起 Flask 或 FastAPI 服务摄像头推流还要处理 WebRTC对一个小型检测工具来说太重了。Tkinter 虽然自带但做不出复杂的交互布局而且它的线程处理能力比 Qt 弱很多。所以我在这套项目里坚定地选了 PyQt5。2.2 界面与后端解耦信号槽架构怎么设计这套系统界面和推理逻辑是完全解耦的。界面只管接收用户操作——选择文件、点击开始、拖动阈值滑块推理后端跑在 QThread 子线程里每次检测完通过 pyqtSignal 把结果丢回主界面线程。为什么必须这么做因为 YOLO 推理是一个耗时的阻塞操作。如果在主线程里直接跑模型界面会瞬间卡死摄像头画面会一顿一顿的连“停止推理”的按钮都点不了。用 QThread 跑推理以后界面主线程的 Qt 事件循环永远能及时响应这样用户体验就从“卡成幻灯片”变成了“画面稍有延迟但是流畅可操作”。这是所有带界面检测系统里最核心的一条架构原则。2.3 数据流从输入到显示的完整链路整条链路是这样的用户点击“打开摄像头”或者“选择视频文件”后台的 MediaReader 线程开始读取帧读到的帧先送到一个预处理模块做尺寸调整和颜色空间转换然后送入 model.predict() 做推理推理结果里面框坐标、置信度、类别信息通过信号发回界面界面线程把结果画在帧上转换成 QImage 之后 setPixmap 到 QLabel 上。另外每一次推理之后的统计信息——用了多少毫秒、当前帧率、检测到多少目标——也会一起显示在状态栏。这个链路看起来简单但如果不把“读帧线程”和“推理线程”分开视频源读取速度就会受推理速度拖累。尤其是摄像头源一旦处理不过来缓冲区就会堆满延迟会越堆越大。我的做法是读帧线程只管不断读新帧并放到一个带锁的缓存队列里推理线程每次只拿最新一帧这样延迟始终是固定的而不是越跑越卡。3. 环境配置0基础也能一步步复现3.1 Python 虚拟环境与基础依赖先说结论建议用 Python 3.10 或 3.11配合 ultralytics 8.3.x 版本。太老的 Python 版本装新版 PyTorch 会缺依赖太新的 Python 3.13 又容易出现某些包还没有预编译轮子的问题。我这个项目的环境搭建流程是这样conda create -n yolo_ui python3.10 -y conda activate yolo_ui pip install ultralytics pip install PyQt5 pip install opencv-python这里有个新手特别容易踩的坑opencv-python 和 opencv-contrib-python 不要同时装否则会有动态库冲突。只需要装 opencv-python 就足够做视频读取和图像绘制了。PyQt5 和 PyQt5-sip 的版本会自动匹配一般装 PyQt5 时 pip 会一起装好不需要手动处理。3.2 显卡环境用 GPU 还是只跑 CPU如果你只是验证界面逻辑CPU 完全足够跑 yolo11n 模型速度大概在 510 FPS 之间看分辨率而定。但如果要做实时摄像头检测我强烈建议用 Nvidia GPU。CUDA 版本的 PyTorch 安装方式略有不同你要先去 PyTorch 官网看最新的安装命令不要把之前在 CPU 机器上装的 torch 直接拿到 GPU 机器上用。安装 GPU 版 PyTorch 之后可以快速验证一下 CUDA 是否可用import torch print(torch.cuda.is_available())如果输出 True 就说明 YOLO 推理会自动走 GPU。在 GPU 上yolo11n 模型处理 640×640 输入一般能跑到 3060 FPS具体取决于显卡型号。实测下来即使是 GTX 1660 或者 RTX 3050 这种入门级显卡也完全能满足实时界面演示的需求。3.3 权重文件准备训练好的红绿灯权重从哪来这套系统默认加载的是官方预训练的 yolo11n.pt它只能检测“traffic light”这个粗类别。要做红绿灯状态识别你有两条路。一条是去 Roboflow、Kaggle 或者 GitHub 上找开源的交通灯检测权重很多项目都发布了针对红绿灯状态检测的 YOLO 权重文件另一条是自己标注训练用大概几百到一千张路口图片就能得到一个效果不错的模型。换权重的方式很简单只要在代码里修改模型路径就行self.model YOLO(best_rgb.pt) # 换成你训练好的权重路径如果你用的权重是自定义训练的一定要在代码里维护一个类别映射字典比如 0 对应 red1 对应 green2 对应 yellow。否则画框的时候显示的标签名会牛头不对马嘴。4. 核心代码实现从模型推理到界面显示全流程4.1 推理核心类统一的检测后端我在系统里写了一个 Detector 类它把所有和模型相关的逻辑都封装起来。这样做的好处是界面层完全不需要关心模型内部怎么推理切换权重、调整输入尺寸、改置信度阈值都只动这一个类。from ultralytics import YOLO class Detector: def __init__(self, weights_pathyolo11n.pt, conf0.35, deviceauto): self.model YOLO(weights_path) self.conf conf self.device device def predict(self, frame): results self.model.predict( sourceframe, confself.conf, deviceself.device, verboseFalse ) return results[0]这里有个容易忽略的细节model.predict() 每次调用都会有模型初始化的额外开销所以 Detector 实例必须在整个应用生命周期里只创建一次不要每检测一帧就 new 一个 YOLO 对象。我见过有人把 model 创建放在循环里面结果推理速度慢得离谱还以为是模型太复杂。4.2 图片、视频、摄像头三种输入的统一封装三种输入模式在代码里统一通过一个 get_frame 方法来获取帧这样推理线程就不用管数据源是什么。图片模式最简单一次性返回整张图片然后结束。视频模式用 cv2.VideoCapture 打开文件每次读取一帧到最后一帧重新回到开头方便循环审查。摄像头模式稍微麻烦点因为摄像头设备有可能被占用或者断开所以每读一帧都要检查返回状态。import cv2 class FrameReader: def __init__(self, source_type, source_path): self.source_type source_type self.cap None self.image None self._init_source(source_path) def _init_source(self, source_path): if self.source_type image: self.image cv2.imread(source_path) else: self.cap cv2.VideoCapture(source_path) def read(self): if self.source_type image: return self.image ret, frame self.cap.read() if not ret: if self.source_type video: # 视频播放完毕回到开头继续循环 self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) ret, frame self.cap.read() return frame return frame这里要注意一个问题OpenCV 读取的摄像头帧是 BGR 格式而 ultralytics 内部会自动处理 BGR 到 RGB 的转换所以你在给模型喂数据之前不要再手动转成 RGB。但显示到 PyQt 界面上时必须用 cvtColor 转成 RGB 才能让颜色恢复正常否则画面会偏蓝。这个坑几乎是每个新手都会踩一次的。4.3 多线程推理QThread 与 pyqtSignal 的配合这是整套系统的重点。我写了一个 InferenceThread 类它继承 QThread当界面点击“开始检测”按钮时启动这个线程去读取帧并推理推理完成后通过信号把结果发回来。from PyQt5.QtCore import QThread, pyqtSignal class InferenceThread(QThread): result_ready pyqtSignal(object, object, float) update_fps pyqtSignal(float) def __init__(self, detector, reader, parentNone): super().__init__(parent) self.detector detector self.reader reader self.running True def run(self): import time prev_time time.time() while self.running: frame self.reader.read() if frame is None: continue result self.detector.predict(frame) curr_time time.time() fps 1.0 / (curr_time - prev_time) prev_time curr_time self.result_ready.emit(frame, result, fps) self.update_fps.emit(fps)界面层只需要连接 result_ready 信号到 update_display 槽函数就能在 Qt 主线程里安全地更新界面。这里必须注意绝不要在子线程里直接操作 QLabel 或者 QPixmapQt 界面元素只能在主线程访问。如果你非要在子线程里直接改界面轻则出现闪烁、显示异常重则直接程序崩溃。4.4 推理结果的绘制与保存拿到 result 对象之后需要解析里面的框和类别信息然后在图像上画出来。这一步在界面线程里做因为要在画布上实时预览也在检测完成后保存图片。import numpy as np import cv2 def draw_results(frame, result): annotated frame.copy() boxes result.boxes if boxes is not None: for box in boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) conf float(box.conf[0]) cls_id int(box.cls[0]) label f{result.names[cls_id]} {conf:.2f} cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(annotated, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) return annotated如果你用的是自定义红绿灯权重我强烈建议把框的颜色按照类别动态设置红灯用红色、绿灯用绿色、黄灯用黄色。这样在界面上看起来一目了然现场演示的时候效果特别好。另外保存图片用的是 cv2.imwrite保存视频用的是 cv2.VideoWriter视频编码可以选 mp4v大部分播放器都能兼容。4.5 界面布局设计要点界面我用的是 QMainWindow 作为主窗体左侧放控制面板右侧放显示区域。控制面板上放了一排按钮“打开图片”“打开视频”“打开摄像头”“停止”“保存结果”下面放了一个 QSlider 用来调置信度阈值再下面是一个 QLabel 用来显示当前状态和 FPS。显示区域用一个 QLabel 作为画布QLabel 的 setScaledContents 属性设为 True让图片自动缩放适应控件大小。这里有个小技巧如果图片缩放后显示模糊可以在缩放时用 SmoothTransformation 来提升图像质量。具体实现是在显示之前把原图像缩放到控件尺寸def update_display(self, frame, result, fps): annotated draw_results(frame, result) rgb_image cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w qt_image QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) pixmap QPixmap.fromImage(qt_image) pixmap pixmap.scaled(self.label_display.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.label_display.setPixmap(pixmap)5. 红绿灯场景专项优化从“能检测”到“测得准”5.1 提高输入分辨率解决小目标问题红绿灯在画面中通常只占很小一块区域尤其当摄像头架在路口远端时整个红绿灯可能只有十几个像素高。这种情况我用默认的 640×640 输入去推理漏检率会明显偏高。一个直接的优化方式是把模型的输入尺寸调大比如 imgsz1280这样小目标在输入特征图里占据的像素更多检测率会显著提升。代价是推理时间增加在 GPU 上大概会慢一倍左右但换来的精度提升在红绿灯这个任务里完全值得。results self.model.predict(sourceframe, confself.conf, imgsz1280)如果你做的是摄像头实时检测可以只在画面底部区域做重点检测减少不必要的计算量。因为红绿灯一般出现在道路上方或者路口上方相对固定的区域。用一个 ROI 区域做裁剪然后只对该区域做 1280 分辨率推理速度和精度都能兼顾。5.2 置信度阈值与 NMS 的调参红绿灯目标小很容易出现误检和漏检同时存在的情况。置信度阈值设得低比如 0.15会有一堆路牌、车灯被误检成红绿灯设得高比如 0.7又会丢掉很多模糊的小目标。我的建议是从 0.35 起步配合界面上的阈值滑块实时调。界面上我增加了实时调节功能拖动滑块不需要重新启动程序下一条消息的推理就会生效这样在现场测试时效率特别高。5.3 注意力机制与轻量化改进的参考方向如果你的训练数据有限或者现场环境特别复杂还可以在模型层面做一些扩展。最近我看到一些交通检测论文里提到 HCANet 这类混合注意力结构核心思路是在骨干网络后面加上通道注意力和空间注意力并联分支让模型更关注红绿灯所在的颜色区域和小尺度特征。这个方向对于小目标检测确实有帮助但需要改模型结构并重新训练适合有余力再深入研究。如果只是想快速部署跑通建议先不要动模型结构直接通过输入分辨率和后处理逻辑来优化会更省事。5.4 推理结果保存与长期稳定运行这套系统在长时间运行场景下最需要关注的是内存泄漏。Qt 的信号槽机制如果连接后没有正确断开反复开关摄像头会导致对象无法被垃圾回收内存会缓慢增长。我在代码里增加了每次切换输入源时清理并重新创建线程对象的逻辑确保旧线程被安全终止。def stop_thread(self): if self.thread is not None: self.thread.running False self.thread.wait() self.thread None如果你要做 24 小时不间断的路口检测建议每 12 小时重启一次应用或者做一个定时重置逻辑。Python 的 numpy 在处理连续视频流时的内存碎片问题比较难以完全规避定期重启是最省心的方案。6. 踩坑实录与问题排查速查表6.1 运行过程中的常见问题我整理了一份红绿灯检测 PyQt 系统最常见的运行问题速查表都是我在实际测试中碰到过的直接对照着排查就好。现象可能原因解决办法摄像头画面黑屏摄像头被其他程序占用或设备号错误在系统相机管理里确认设备号换成 cv2.VideoCapture(1) 再试界面卡死无响应推理逻辑写在了主线程或没有用 QThread把推理搬进 QThread主线程只处理信号视频播放结束自动退出没有处理 cap.read() 返回 False 的情况设置 CAP_PROP_POS_FRAMES 回到 0循环播放检测框和画面错位显示时做了缩放但没有同步缩放框坐标画框之前先对帧进行统一缩放或者直接把原始帧送模型推理模型无输出结果置信度阈值太高或输入图像太暗调低 conf 阈值检查图像亮度必要时做自适应直方图均衡CPU 推理极慢没有安装 GPU 版 PyTorch或 torch 误装了 CPU 版重装 CUDA 版 PyTorch确认 torch.cuda.is_available() 为 True保存的视频打不开编码器选择不受支持改用 mp4v 或 avc1 编码确认输出文件后缀为 .mp46.2 现场部署的几点经验在现场演示或者实际部署的时候有几个坑我反复踩过值得单独说一说。首先是摄像头第一次打开比较慢有些 USB 摄像头从设备唤醒到出画面需要 23 秒代码里要在打开后面加一个等待几帧的逻辑别急着读第一帧就当读取失败。其次是界面显示分辨率和实际摄像头分辨率不一致的问题。如果摄像头输出 1920×1080而界面显示区域只有 800×600就会有缩放。缩放本身没问题但检测框是根据原始分辨率画的显示时也要按照同样的比例缩放。我的做法是统一把帧缩放到 1280×720 再送推理和显示这样速度和画面大小都兼顾。最后是用 RTSP 网络摄像头的时候网络不稳定会导致 read() 阻塞或者报错。要在 FrameReader 里加一个超时机制读不到帧就重试连续失败超过一定次数就提示用户检查网络。我发现如果在现场只显示黑屏而不提示具体原因用户基本都会认为是程序写崩了所以要尽量把异常信息显示到界面上。6.3 这个系统的扩展方向建议这套代码的架构决定了它非常容易扩展。比如你已经有了红绿灯检测下一步很自然就可以加一个“闯红灯判断”模块根据检测到的红灯状态和车辆位置做一个交集判断就能实现简单的违法检测逻辑。或者把 YOLOv11 的注意力改进结构加进来重新训练一个更适合小目标检测的版本然后把新权重换进去就行。如果你对多模态感兴趣还可以在这套系统的基础上接一个 AI 分析接口把检测到的红绿灯状态做轨迹统计、流量分析生成结构化报告。后端逻辑完全不用改只需把推理结果转发出去即可。这套系统从代码量上看不算大但它把 YOLO 算法工程化的关键环节都覆盖了线程管理、界面交互、视频处理、模型封装、结果保存。我花了很多时间在这些“看起来不难但做起来很碎”的细节上这次把它们整理出来分享给你。如果你跟着跑通了这套代码后面做任何基于 YOLO 的桌面检测工具基本都能复用这套架构。我个人的建议是先不要急着加功能把图片、视频、摄像头三种模式分别跑通理解了信号槽和线程的关系之后再开始加你的业务逻辑这样最稳。