☰
YOLOv11+PyQt红绿灯检测系统:从模型到界面全链路实战
2026/10/7 11:05:58 网站建设 项目流程

1. 从红绿灯检测说起:这套系统到底解决了什么问题

红绿灯目标检测这个方向,看起来只是目标检测里一个很窄的细分场景,但真正落地过的人都知道,它比通用目标检测要麻烦得多。通用检测你只要框出物体、给个类别就行,红绿灯检测不行——它要求你在复杂的城市道路场景里,准确区分红、黄、绿三种状态,还要应对强光、逆光、雨雾、遮挡、远距离小目标等一系列现实问题。更关键的是,检测结果往往要直接对接决策逻辑,错一帧可能就意味着一次误判。

我这次做的这套系统,核心目标很明确:用YOLOv11作为检测引擎,配一套PyQt的可视化界面,把图片推理、视频推理、摄像头实时推理三条链路全部打通,形成一个可以直接交付、可以演示、可以继续迭代的完整工程。它不是那种跑个demo就结束的脚本,而是一个带界面、带状态管理、带结果保存的完整系统。

为什么选YOLOv11而不是YOLOv8或者更早的版本?原因很实际。YOLOv11在Ultralytics的维护下,API统一、文档清晰、导出格式丰富,而且在小目标检测上的表现比前代有可见提升。红绿灯在远距离画面里往往只占几十个像素,这种小目标场景下,YOLOv11的特征融合结构确实更占优势。至于PyQt,选它的理由更简单:Python生态里做桌面端界面,PyQt的成熟度、控件丰富度和跨平台能力都是第一梯队,配合OpenCV做视频流渲染几乎没有学习成本。

这套系统适合谁?如果你是想入门目标检测但不知道怎么做成完整项目的学生,这套代码可以让你直接看到从模型加载到界面交互的全流程;如果你是需要快速搭建演示原型的工程师,这套架构可以省掉你至少一周的界面和线程管理调试时间;如果你是做智慧交通方向的产品验证,这套系统可以直接作为算法验证平台来用。

2. 整体架构设计与技术选型拆解

2.1 为什么采用分层架构而不是单文件脚本

很多人做目标检测项目,习惯把所有逻辑塞进一个main.py里,加载模型、读图片、画框、显示,几百行搞定。这种写法跑demo没问题,但一旦要加视频、加摄像头、加界面,代码会迅速失控。我踩过这个坑,所以这套系统从一开始就采用分层设计。

整个系统分成四层:模型推理层负责YOLOv11的加载和前后处理,业务逻辑层负责图片、视频、摄像头三种输入的统一调度,界面表现层负责PyQt的控件布局和信号响应,线程管理层负责把耗时推理放到子线程里避免界面卡死。这四层之间通过信号槽和回调函数通信,耦合度低,任何一层要替换都不会牵动全局。

这样设计的好处在实际开发中非常明显。比如后来我想把YOLOv11换成YOLOv11-seg做分割,只需要改推理层的模型加载和输出解析,界面层完全不用动。再比如我想加一个检测结果导出CSV的功能,只需要在业务逻辑层加一个回调,界面加一个按钮就行。

2.2 YOLOv11模型选型:n/s/m/l/x怎么挑

Ultralytics提供了YOLOv11的五个规格:n、s、m、l、x。参数量从2.6M到56.9M不等,mAP从39.5到54.7。选哪个不是拍脑袋决定的,要看你的部署环境和精度要求。

模型规格参数量mAP(val)推理速度(CPU)适用场景
YOLOv11n2.6M39.5约80ms边缘设备、实时性优先
YOLOv11s9.4M47.0约120ms平衡型、推荐起点
YOLOv11m20.1M51.5约200ms精度优先、有GPU
YOLOv11l25.3M53.4约280ms高精度离线分析
YOLOv11x56.9M54.7约450ms极致精度、不计速度

我的建议是:先用s跑通全流程,再根据实际badcase决定是否升级。红绿灯检测如果只是近距离、清晰画面,n就够用;如果是远距离小目标,s或m更稳妥。不要一上来就上x,推理速度会让你在摄像头实时场景里痛不欲生。

2.3 PyQt界面框架的线程模型设计

这是整套系统里最容易出问题的地方。PyQt的主线程负责界面刷新,如果你在按钮点击的回调里直接跑推理,界面会直接卡死,用户点什么都点不动。我见过太多人在这里翻车。

正确的做法是:所有推理任务放到QThread子线程里执行,通过pyqtSignal把结果传回主线程更新界面。具体来说,我定义了一个InferenceWorker类继承QThread,在run方法里循环读取帧、推理、发信号。主线程收到信号后只做一件事:把带框的图像转成QImage显示到QLabel上。

这里有个细节要注意:OpenCV读到的帧是BGR格式,PyQt显示需要RGB,转换时要用cv2.cvtColor,而且QImage构造时要注意数据内存的生命周期,否则会出现花屏或者崩溃。我的做法是在子线程里完成BGR到RGB的转换,再把numpy数组拷贝一份传给主线程,避免内存被回收。

3. 核心模块实现细节与实操要点

3.1 模型加载与推理封装

模型加载看起来简单,就一行YOLO("yolo11s.pt"),但实际项目里要考虑的事情不少。首先是权重文件的管理,我习惯在项目根目录建一个weights文件夹,把pt文件统一放进去,代码里用相对路径引用。这样换模型只需要改一个配置项,不用满代码找路径。

推理封装我写了一个Detector类,对外暴露detect(frame)方法,内部处理resize、归一化、推理、NMS、坐标还原。这里有个关键点:输入尺寸的选择。YOLOv11默认640,但红绿灯检测如果画面里目标很小,可以适当提高到960或1280。代价是推理时间增加,实测下来640到960大约增加1.8倍耗时,960到1280再增加1.6倍。你需要根据实际场景权衡。

from ultralytics import YOLO import cv2 import numpy as np class Detector: def __init__(self, weight_path, conf=0.25, iou=0.45, imgsz=640): self.model = YOLO(weight_path) self.conf = conf self.iou = iou self.imgsz = imgsz def detect(self, frame): results = self.model.predict( source=frame, conf=self.conf, iou=self.iou, imgsz=self.imgsz, verbose=False ) return results[0]

置信度阈值conf的设置也有讲究。红绿灯检测里,漏检比误检更危险,所以conf可以适当放低到0.2左右,宁可多框几个再后处理过滤。但也不能太低,否则背景里的红色物体、绿色广告牌都会被误检。我的经验值是0.25到0.35之间,具体看你的数据集质量。

3.2 图片推理链路的实现

图片推理是最简单的链路,但要做好用户体验,细节不少。用户点击"选择图片"按钮后,弹出文件对话框,选中后要立即显示原图,然后自动触发推理,推理完成后在同一区域显示带框结果。这里我用了一个小技巧:原图和结果图用同一个QLabel显示,通过状态变量切换,这样界面简洁,用户也能直观看到对比。

结果保存功能我做了两种:一种是保存带框图像,用cv2.imwrite直接写;另一种是保存检测信息到txt,每行格式是类别 置信度 x1 y1 x2 y2。后者对于后续做数据分析很有用,比如统计某个路口红灯出现的频率。

注意:保存路径不要用中文,OpenCV在某些平台上对中文路径支持不好,会静默失败。我一般用时间戳命名,比如result_20250101_143022.jpg,避免重名覆盖。

3.3 视频推理与进度控制

视频推理比图片复杂的地方在于:你要逐帧读取、逐帧推理、逐帧显示,还要处理视频结束、暂停、继续这些状态。我用OpenCV的VideoCapture读视频,用一个while循环驱动,每读一帧就调用detector推理,然后emit信号给界面显示。

这里最大的坑是视频帧率和推理速度不匹配。比如视频是30fps,但你的推理只能跑到10fps,如果直接按视频帧率播放,画面会严重滞后。我的处理方式是:不做帧率同步,读一帧推一帧,推完就显示,这样视频会变慢但不会丢帧。如果你要求实时播放,那就需要跳帧策略,比如每3帧取1帧推理,中间帧复用上一帧结果。

cap = cv2.VideoCapture(video_path) while cap.isOpened(): ret, frame = cap.read() if not ret: break result = detector.detect(frame) annotated = result.plot() self.frame_signal.emit(annotated) if self.is_paused: while self.is_paused: QThread.msleep(100) cap.release()

暂停功能的实现用了一个布尔标志位加循环等待,简单但有效。不要用time.sleep,会阻塞整个线程导致无法响应恢复操作。

3.4 摄像头实时推理的特殊处理

摄像头推理和视频推理代码结构几乎一样,但有几个关键差异。第一,摄像头没有总帧数,进度条没法用,我改成了显示实时FPS。第二,摄像头要处理设备索引问题,一般笔记本自带摄像头是0,外接USB摄像头是1或2,我加了一个下拉框让用户选择。第三,摄像头推理对延迟极其敏感,我做了两件事来降低延迟:一是把imgsz降到480,二是开启半精度推理(如果有GPU)。

FPS的计算我用了一个滑动窗口,取最近30帧的平均耗时倒数,这样显示的数字不会剧烈跳动。实测在CPU上YOLOv11s跑480尺寸,FPS大约在8到12之间;如果有中端GPU,可以到30以上,基本满足实时要求。

实操心得:摄像头刚打开的前几帧往往是黑的或者过曝的,这是自动曝光在调整。我一般会丢弃前5帧再开始推理,避免第一帧结果异常吓到用户。

4. 完整实操流程:从零跑通这套系统

4.1 环境配置的坑与正确姿势

环境配置是新手最容易卡住的地方。我的建议是:用conda建独立环境,Python版本选3.9或3.10,这两个版本和PyQt5、OpenCV、PyTorch的兼容性最稳。3.11以上有些包还没跟上,3.8以下又太老。

conda create -n yolo11_gui python=3.10 conda activate yolo11_gui pip install ultralytics pip install PyQt5 pip install opencv-python

PyTorch的安装要注意,如果你有NVIDIA显卡,去官网查对应CUDA版本的安装命令,不要直接pip install torch,那样装的是CPU版。CPU版跑推理慢不说,还占内存。装完之后用torch.cuda.is_available()验证一下,返回True才算成功。

Ultralytics安装完会自动装依赖,但有时候会和已有的numpy版本冲突。如果报numpy.core.multiarray failed to import,就是版本问题,pip install numpy==1.24.3基本能解决。

4.2 权重文件获取与自定义训练

官方权重可以直接从Ultralytics的release页面下载,yolo11s.pt大约18MB。下载后放到weights文件夹,代码里指定路径即可。

但红绿灯检测如果你要真正好用,必须用自己的数据微调。官方权重是在COCO上训练的,COCO里根本没有红绿灯这个类别(只有traffic light一个粗类,不分颜色)。你需要准备自己的数据集,标注格式用YOLO的txt格式,每行类别id x_center y_center width height,坐标都是归一化到0到1的。

训练命令很简单:

yolo detect train data=traffic_light.yaml model=yolo11s.pt epochs=100 imgsz=640 batch=16

data.yaml里指定训练集、验证集路径和类别名称。类别就三个:red、yellow、green。训练100轮在单张RTX3060上大约需要2到3小时。训练完成后best.pt就是你的自定义权重。

注意事项:红绿灯数据集最容易出现类别不平衡,红灯样本远多于黄灯。我一般会对黄灯样本做过采样,或者在loss里给黄灯更高权重,否则模型会倾向于把黄灯预测成红灯。

4.3 界面布局与信号槽连接

PyQt界面我用Qt Designer画了初版,然后手写代码微调。主窗口分三个区域:左侧是控制面板(按钮、下拉框、滑块),中间是显示区域(QLabel),底部是状态栏(显示FPS、检测数量、耗时)。

信号槽的连接是界面逻辑的核心。比如"开始检测"按钮的clicked信号连接到start_detection槽函数,槽函数里创建worker线程并启动。worker的frame_signal连接到主线程的update_frame槽,槽函数里把图像显示到QLabel。停止按钮则设置worker的stop标志位,等线程自然结束。

self.start_btn.clicked.connect(self.start_detection) self.stop_btn.clicked.connect(self.stop_detection) self.worker.frame_signal.connect(self.update_frame) self.worker.stats_signal.connect(self.update_stats)

这里有个容易忽略的点:线程结束后要调用wait()等待真正退出,否则下次启动会报"QThread: Destroyed while thread is still running"。我在stop_detection里加了self.worker.wait(3000),等3秒,超时再强制terminate。

4.4 打包成exe分发给别人用

如果你要把这套系统给别人用,打包成exe是最省事的。用PyInstaller,命令大致是:

pyinstaller --windowed --onefile --add-data "weights;weights" main.py

但PyInstaller打包PyQt和OpenCV的坑非常多。常见问题包括:缺少Qt插件导致界面起不来、OpenCV的dll没打进去、权重文件路径在打包后变成临时目录。我的经验是:不要用--onefile,用--onedir,把weights文件夹手动拷到dist目录里,代码里用sys._MEIPASS判断运行环境来定位资源路径。这样虽然文件夹大一点,但稳定性高很多。

5. 常见问题与排查技巧实录

5.1 推理结果异常问题速查

现象可能原因排查方法解决方案
画面全黑或全白图像格式转换错误检查cvtColor的转换码BGR转RGB用COLOR_BGR2RGB
框位置偏移坐标还原时缩放比例算错打印原图尺寸和输入尺寸按比例还原,注意padding
检测不到目标conf阈值过高逐步降低conf观察降到0.1测试,再回调
类别全是同一类权重文件不匹配检查names字典用对应数据集的权重
界面卡死推理在主线程执行看是否用了QThread所有推理放子线程

5.2 摄像头打不开的几种情况

摄像头打不开是最常见的报错。首先确认设备索引对不对,Windows下可以用cv2.VideoCapture(0, cv2.CAP_DSHOW)试试,DSHOW后端有时候比默认后端更稳。如果还是不行,检查是不是被其他程序占用了,比如微信、钉钉的视频通话会占用摄像头。另外,某些USB摄像头需要先插上再启动程序,热插拔识别不到。

还有一种情况是能打开但读不到帧,cap.read()一直返回False。这通常是驱动问题,换个USB口或者换台电脑试试。我在一台老笔记本上遇到过,最后发现是USB供电不足,换到USB3.0口就好了。

5.3 长时间运行的稳定性处理

这套系统如果要做长期现场测试,稳定性是重中之重。我做过连续72小时的摄像头推理测试,总结了几条经验。第一,内存泄漏要防,每处理1000帧手动调用一次gc.collect(),虽然Python有自动回收,但OpenCV的Mat对象有时候回收不及时。第二,摄像头断连要能自动重连,我在循环里加了异常捕获,读帧失败超过10次就释放重新打开。第三,日志要落盘,用logging模块把关键事件写到文件,出问题了能回溯。

实操心得:长时间运行建议把显示帧率降下来,比如每3帧才更新一次界面,减少GUI刷新开销。推理照常每帧都做,只是显示抽帧,这样既保证检测不丢帧,又降低界面负担。

5.4 小目标检测效果差的优化思路

红绿灯在远距离下就是典型小目标,几十个像素,YOLOv11默认的640输入很难检到。我试过几种优化手段,效果从低到高排列:提高输入尺寸到960或1280、在数据集中增加小目标样本、用SAHI切片推理、修改模型结构加P2层。前两种成本最低,建议先试。SAHI切片推理是把大图切成小块分别检测再合并,对小目标提升明显,但推理时间成倍增加。改P2层需要重新训练,适合有充足数据的情况。

实测下来,输入尺寸从640提到960,小目标召回率能提升15到20个百分点,代价是推理时间增加约80%。如果你的场景对实时性要求不高,这个交换是值得的。

6. 后续可扩展的方向与个人体会

这套系统目前是单模型单任务的,但架构上留了扩展空间。比如你可以把检测结果接一个简单的状态机,连续多帧检测到红灯才判定为红灯,避免单帧误检。也可以把检测框坐标传给一个跟踪器,做红绿灯状态跟踪,这样即使某帧漏检也能靠跟踪补上。

再往大了说,这套PyQt加YOLO的框架不只能做红绿灯,换成其他权重就是其他检测系统。我后来用同样的架构做了工地安全帽检测、停车场车位检测,界面和线程管理代码几乎没改,只换了模型和类别名称。这就是分层架构的价值。

最后分享一个我在调试时常用的小技巧:在推理结果上叠加显示当前conf阈值和imgsz,截图发给别人的时候一目了然,省得反复问"你用的什么参数"。这个信息我放在状态栏右侧,不占地方但很实用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询