简介:整合PyQt5与YOLOv5的多目标检测GUI项目,面向刚接触PyQt5开发和YOLO算法的初学者,提供一个现成的完整工程用于练手,帮助读者快速上手图形界面开发与目标检测的联动实现。压缩包为zip格式,共112个文件,包含26个Python源码、33个pyc编译文件、25个YAML配置、3个模型权重pt,以及界面ui文件、Shell脚本、示例图片和演示视频等,整体大小83.46MB,目录结构合理。其中py文件是项目核心逻辑,yaml用于配置模型参数,pt为预训练权重,ui为Qt界面文件,mp4展示运行效果,便于按需查阅。该资源在CSDN已有8869人学习,适合作为入门练手项目。项目重点展示了PyQt5常用控件的用法、界面设计与后端逻辑分离的思路,并基于PyTorch框架集成了YOLOv5算法源码,读者可从中掌握信号槽机制、布局管理以及网络结构与推理流程,同时附带动画演示和mp4视频,便于对照学习多目标检测的完整流程,实现从算法到GUI应用的有效落地。 把yolov5跑通其实不难,真正难的是怎么让检测能力变成一个别人愿意用的工具。我做了好几个目标检测相关的项目,最后界面层都落在了pyqt5上:pyqt5负责窗口、按钮、图表和交互,yolov5负责算出目标框和置信度,python把这两部分粘在一起。这套组合很适合做毕设、做内部工具、做小范围验收演示,不依赖服务器,本地双击就能跑。
我见过太多人卡在“模型能出结果,但不知道怎么在窗口里显示”这一步,也见过不少人把视频检测直接写在界面线程里,一启动程序就白屏转圈。这篇文章想把整套链路讲完整:环境怎么搭、线程怎么设计、界面怎么开发、模型怎么训练优化、最后怎么打包成exe给别人用。适合正在用pyqt5+yolov5做毕设的人,也适合想把检测算法快速落地成桌面上工具的开发者和爱好者。
1. 环境搭建:版本匹配是省时间的第一步
1.1 python版本为什么锁定3.10
刚开始折腾这套组合时,我直接在官网下了最新的python版本,结果装yolov5依赖时一堆编译报错,后来才发现很多C扩展包在最新的python上还没有对应的预编译wheel。折腾一圈下来,我的建议非常明确:只要你是做pyqt5+yolov5,python版本直接选3.10,这是目前兼容性最稳的版本。
为什么是3.10而不是3.11或3.12?因为torch、opencv-python、pyqt5这些核心依赖,在3.10上都有成熟的预编译包,pip安装基本不用碰编译器。装python时记得勾选“Add Python to PATH”,不然后面命令行敲python会提示找不到命令。装完在终端里输入python --version确认一下,能正常输出版本号再继续。
顺带说一句,如果电脑上已经装了多个python版本,建议给这个项目单独建一个虚拟环境。我习惯用python -m venv venv,然后用venv\Scripts\activate激活。这不是矫情,而是yolov5的依赖版本和别的项目经常打架,尤其是opencv和numpy,隔离好了一劳永逸。
1.2 pyqt5安装和国内镜像加速
pyqt5的安装本身没有技术难度,唯一的痛点是默认源下载慢。我用的是清华源:
pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simple安装完建议顺手验证一下能不能正常导入:
from PyQt5.QtWidgets import QApplication, QLabel import sys app = QApplication(sys.argv) label = QLabel("pyqt5 ok") label.show() sys.exit(app.exec_())能弹出一个窗口就说明环境没问题。这里要提前打个预防针:运行时会遇到两个长得像的包,一个是PyQt5,一个是PyQt5-tools。PyQt5是核心库,工具包只有在你想用Qt Designer拖界面时才需要。如果你习惯手写界面代码,不装PyQt5-tools完全没问题。
1.3 yolov5依赖:torch版本别乱装
yolov5的依赖集中在requirements.txt里,官方推荐做法是克隆仓库后直接安装:
git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt但这里有个新手最容易踩的坑:这个命令会尝试安装官方默认的torch版本,如果你的显卡驱动和CUDA版本不匹配,大概率会遇到torch导入失败。我的建议是先装torch,再装其他依赖。
如果你没有独立显卡,或者只想先跑通流程,直接装CPU版本最快,推理速度够用来调试界面:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu如果你有NVIDIA显卡,先去命令行跑nvidia-smi看CUDA版本,然后到pytorch官网选择对应的安装命令。装完torch后,再回来执行pip install -r requirements.txt,这时候就不会重复装torch了。
有个小细节:yolov5的requirements里对numpy和opencv-python有版本范围要求,如果安装时提示冲突,优先保留yolov5要求的版本,因为opencv版本太新有时会导致标注工具和推理脚本行为异常。
2. 架构设计:检测线程和界面线程必须分家
2.1 卡死的根源是GUI事件循环被阻塞
很多人的第一个版本是这样的:点击按钮→读图片→调yolov5推理→把结果画到界面上。单张图片问题不大,一但换成摄像头或者视频流,窗口立刻卡死。
原因是qt的界面程序运行在一个事件循环里,鼠标点击、窗口重绘、按键响应都是事件,必须排队被处理。如果你在事件循环里做了一次耗时推理(一张图几十毫秒,视频流每秒无数次),界面就一直在“等”,表现出来就是白屏、转圈、无响应。
这不只是用户体验问题,程序甚至会直接被系统判定为“未响应”而强制关闭。所以架构上的第一条铁律是:耗时操作一律不进主线程。
2.2 用QThread+信号槽完成解耦
我常用的做法是继承QThread写一个检测工作线程,内部跑循环,通过信号把检测结果发回主线程。核心代码如下:
from PyQt5.QtCore import QThread, pyqtSignal class DetectWorker(QThread): result_ready = pyqtSignal(object, object) def __init__(self, model): super().__init__() self.model = model self.running = True def run(self): while self.running: frame = self.get_frame() # 从摄像头或视频读取一帧 dets = self.model(frame) # yolov5推理 self.result_ready.emit(frame, dets)主线程里只需要连接信号,然后刷新界面:
self.worker.result_ready.connect(self.update_ui)注意一个容易忽略的点:在槽函数里不能做耗时处理,比如把检测结果保存到磁盘这种操作,应该再丢给另一个线程或者用队列异步处理。槽函数只负责把图像转成QImage、画框、刷新QLabel,这些操作都在毫秒级,没问题。
2.3 摄像头取流与跳帧策略
摄像头实时检测还有一个隐藏问题:视频流的帧率可能高于模型的推理速度。比如摄像头输出30帧每秒,yolov5s在你的机器上只能跑10帧每秒,如果每帧都推理,累积的帧会越来越多,延迟越来越大。
我的做法是在循环里做跳帧控制。用一个计时器记录上次推理的时间,间隔不到设定阈值就直接丢弃当前帧,只保留最新帧用于下一次推理。这样能保证实时画面不卡,检测频率稳定。如果想让检测结果更流畅,可以加一个中间帧队列,设置最大缓存为2,避免内存无限增长。
另外,摄像头对象的读取也要在子线程里做,不要在界面线程直接cap.read()。USB摄像头在某些驱动下读取会阻塞,一旦阻塞,界面同样会卡住。我在项目里的习惯是,让DetectWorker内部创建并持有cv2.VideoCapture,这样线程生命周期由自己管理,界面只管收结果。
3. 核心功能开发:图片、视频、摄像头三合一
3.1 图片检测与结果绘制
所有检测功能的基础是图片检测。yolov5的模型接口很简单:
results = model(img)但这里有两个坑必须解决,否则你会在显示环节反复调试。
第一个是letterbox预处理。yolov5会把输入图片等比例缩放到640×640,不足的部分用灰色填充,检测结果的坐标也是在这个缩放后的坐标系里算的。如果直接把结果坐标画到原图上,框就会偏移。项目里要自己实现一个坐标映射:先记录原图缩放比例和填充尺寸,推理完再把xyxy坐标映射回原图坐标系。
第二个是颜色通道顺序。OpenCV读取的图像是BGR顺序,而QImage默认使用RGB顺序。直接转换会导致画面偏蓝偏红,看起来很别扭。标准做法是:
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888).copy()记住最后那个.copy(),少了它QImage和numpy共享同一块内存,当原数组被回收或修改时,界面上的图像可能出现花屏、撕裂甚至崩溃。
3.2 视频和摄像头检测的帧推送
视频和摄像头的检测逻辑和图片几乎一样,差别只在于数据来源是循环读取。我把这个循环放在DetectWorker的run方法里,检测完直接emit。主线程收到结果后,做三件事:显示当前帧、绘制检测框、更新检测计数信息。
这里要特别处理的是程序关闭时线程的退出。如果主窗口关掉了,子线程还在跑,程序会报“QThread: Destroyed while thread is still running”的错。规范流程是:在窗口关闭事件里设置worker.running = False,然后worker.wait()等线程安全退出,再真正销毁窗口。不处理这个,程序偶尔会闪退,而且只在退出时出现,特别难排查。
3.3 用下拉框做多模型切换
项目里通常不会只跑一个模型,比如既要用yolov5n做快速检测,也要用yolov5s做精度更高的检测。用QComboBox做模型选择很自然,切换时加载对应的.pt权文件即可。
我见过不少人在这个功能上出问题:切换模型时程序直接崩溃,或者界面卡死。核心原因是模型加载是耗时操作,不能写在currentIndexChanged信号对应的槽函数里。正确做法是:下拉框切换只记录一个“待加载模型路径”,然后在后台线程里完成卸载旧模型、加载新模型、更新状态栏提示这一整套操作。
模型加载完成后,子线程里的推理器要及时替换,这里需要加一个线程锁或者简单的原子标志位,防止旧推理还没结束就被替换掉,导致内存访问错误。
4. 热搜里的两个坑:下拉框闪退与超链接自定义操作
4.1 下拉框闪退的完整排查链路
“pyqt5 下拉框闪退”这个关键词热度一直很高,我也在这个坑里栽过跟头。现象是:程序启动正常,但一旦点击下拉框选择某个选项,界面瞬间消失。
我先说结论,最常见的根因是槽函数访问了已经被释放的C++对象。具体到yolov5项目里,典型场景是你把模型实例作为属性绑定到了下拉框某个item上:
self.combo.addItem("模型A", userData=model_a)然后在槽函数里取出来用:
def on_change(self): model = self.combo.currentData() result = model(img) # 如果模型已经被销毁,这里就会崩问题出在如果你在某个地方重新创建了模型,旧的模型对象被Python垃圾回收,但Qt对象(如果模型内部封装了Qt资源)仍然被界面层引用,槽函数调用时访问的就是已经被释放的C++资源,程序直接崩溃。
排查这个问题的思路,我个人建议按下面的顺序来:
- 看报错信息。如果是
RuntimeError: wrapped C/C++ object of type X has been deleted,基本就是访问了已释放对象。 - 用信号阻塞法验证。在更新下拉框数据时,先调用
blockSignals(True),更新完再blockSignals(False),排除信号被重复触发的问题。 - 检查模型对象所有权。明确模型的唯一创建者和唯一销毁者,不要在槽函数里重新赋值给局部变量,更不要在一个新线程里直接修改主线程持有的检测器引用。
修复方案我通常这样写:
self.combo.blockSignals(True) self.combo.clear() for name, path in model_list.items(): self.combo.addItem(name, userData=path) self.combo.blockSignals(False)同时模型加载和替换必须放在线程里,通过信号通知主线程刷新界面状态。
4.2 让文本框里的链接执行自定义函数
另一个高频需求是“pyqt5 文本框超链接点击后执行自定义操作”。默认情况下,QLabel显示的文字里如果带了链接,点击之后只会调用系统浏览器打开。但很多项目需要点链接执行自己的函数,比如打开文件、切换页面、弹出自定义对话框。
实现方式不复杂。先设置:
label.setOpenExternalLinks(False) label.linkActivated.connect(self.handle_link)关键点在于setOpenExternalLinks(False)。很多人不知道这个开关的作用:它设成True时,链接点击行为由Qt自己接管,直接丢给默认浏览器;设成False时,程序才会发出linkActivated信号,你才能在handle_link里按需处理。
handle_link收到的参数就是点击链接的href值。我不建议在链接文本里直接拼路径,因为中文和特殊字符到HTML里要做转义,容易出错。更稳的做法是:在链接里放一个简单的标识符,比如#open_model_folder,在槽函数里通过字典映射到真实操作。
4.3 用QTextBrowser渲染检测结果
要显示复杂的检测信息,比如每个目标的类别、置信度、坐标列表,用QLabel纯文本会显得很乱。我习惯用QTextBrowser配合HTML来渲染,这样可以直接把yolov5的pandas结果转成表格样式,看起来正规很多。
你要注意setHtml和append之间的区别。setHtml会重置整个文档,append是在末尾追加内容。所以在做连续检测结果展示时,如果每帧都使用setHtml,会造成闪烁,因为界面要整体重排。我的做法是:结果更新频率不高时用setHtml,如果是视频流实时刷新,用一个临时div加setHtml整体替换,但把表格设计得尽量简单,避免重排开销。
另外,QTextBrowser默认开启富文本,如果你的检测类别名里有特殊字符,记得用html.escape()做一下转义,否则显示会错乱。
5. 模型训练、超参数与版本选型
5.1 用自己的数据集训练yolov5
如果你的检测目标是特定场景(比如车辆、口罩、工地安全帽),直接用官方预训练权重效果大概率不理想,需要用自己的数据集微调。数据组织格式yolov5已经固化得很标准了:
dataset/ images/ train/ val/ labels/ train/ val/标签是txt格式,每行是class x_center y_center width height,坐标是归一化后的。我第一次标注数据时用的是LabelImg,标注完导出YOLO格式。要注意的是,标注框坐标归一化要除以原图宽高,很多人忘记这一点,训练出来的框位置全偏。
数据准备好后,写一个yaml配置文件:
train: dataset/images/train val: dataset/images/val nc: 2 names: ['person', 'car']然后开始训练:
python train.py --img 640 --batch 16 --epochs 150 --data mydata.yaml --weights yolov5s.pt --name myexp这里我建议至少100个epoch起步。很多人看到几十轮loss下降不明显就早早停了,其实yolov5的mAP往往在100轮之后才稳定爬升。
5.2 超参数解析与网络结构选择
yolov5的超参数在data/hyps/hyp.scratch-low.yaml里,核心几个值得关注:
- lr0:初始学习率,0.01是比较常用的起点。如果训练loss震荡明显,降到0.005试试。
- momentum:动量,默认0.937,一般不用动。
- weight_decay:权重衰减,默认0.0005,可以防止过拟合。
- batch_size:显存允许范围内尽量大一点。我看很多人用8或16,如果显存够用,提到32反而更稳定。
网络结构方面,yolov5提供了n、s、m、l、x五个档位。结构上差异主要在depth_multiple和width_multiple这两个缩放系数上。n版本最小最快,s是均衡型,m和l精度更高但速度慢。做桌面应用或者边缘设备部署,我强烈建议先用n或s版本跑通全流程,后面再根据性能测试结果决定是否换更大的模型。
| 模型 | 参数量 | 推理速度 | 适用场景 |
|---|---|---|---|
| yolov5n | 最小 | 最快 | 树莓派、低配CPU |
| yolov5s | 均衡 | 快 | 大多数桌面应用 |
| yolov5m / l | 较大 | 较慢 | 精度要求高的离线任务 |
5.3 yolov5和yolov8的推理速度对比
这个话题在社区里一直在讨论。我实测过同一台机器上yolov5s和yolov8s的差别,结论是:yolov5s在推理速度上仍然有优势,yolov8s在精度上略好,但差距没有想象中大。
| 对比项 | yolov5s | yolov8s |
|---|---|---|
| 参数体量 | 约7.2M | 约11.2M |
| CPU推理延迟 | 更低 | 稍高 |
| 检测精度 | 中规中矩 | 更好一些 |
| 后续部署生态 | 成熟稳定 | 官方更新更积极 |
如果你的项目重点在“交付”、“稳定”、“快速跑通”,yolov5s依然是稳妥的选择。但如果你打算做更长期的项目,考虑未来换检测头、做多任务或者蒸馏,yolov8的代码结构更现代。还有一点,yolov5训练后的.pt权重可以直接导出torchscript或onnx格式,格式兼容性很好,这对接下来的打包和边缘部署很有帮助。
6. 打包分发:把GUI应用变成可执行文件
6.1 PyInstaller打包流程与常见报错
开发完pyqt5+yolov5程序后,目标通常是打包成一个exe,方便别人双击使用。我用的打包工具是PyInstaller:
pip install pyinstaller pyinstaller -w --onedir --name DetApp main.py-w表示不显示命令行控制台窗口,--onedir是把程序打在目录里,--onefile是打成单个文件。我更推荐--onedir,因为启动速度快,排查问题也方便,毕竟torch和yolov5的库体积摆在那,单文件打包启动时要解压到临时目录,速度慢很多。
打包最常见的报错是ModuleNotFoundError: No module named 'models'以及yolov5相关模块找不到。这是因为PyInstaller的静态分析找不全动态导入的模块,需要手动补hidden imports:
pyinstaller -w --onedir --name DetApp main.py \ --hidden-import models.common \ --hidden-import models.experimental \ --collect-submodules utils这套参数用过很多次,基本能解决yolov5的导入缺失问题。如果打包后运行发现自己训练的数据集yaml文件找不到,记得用--add-data把配置文件和权重文件一并加入:
--add-data "best.pt;." --add-data "mydata.yaml;."6.2 体积优化和分发注意点
打包出来的目录动辄几个GB,这是正常的,torch和cuDNN都很大。如果你不需要GPU推理,只装了CPU版torch,体积能少不少。想进一步减小体积,可以试试UPX压缩,PyInstaller自带支持,但注意有些dll文件压缩后可能被杀毒软件误报。
分发时有两件事必须处理:一是确保目标机器上已经安装了对应的显卡驱动,如果是CPU版就无所谓;二是启动时给程序配置正确的环境变量路径,特别是用--ondir模式时,模型文件和yaml文件的相对路径要以exe所在目录为基准,不要用开发时的绝对路径。
我习惯在代码里这么处理:
import sys, os base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__) model_path = os.path.join(base_dir, 'best.pt')这样打包后,只要把exe和权重文件放在同一级目录,怎么移动都不会丢路径。
最后再分享一个个人经验:做这类工具,先把最小的demo跑通再逐步加功能,比一开始就追求大而全要省力。另外,程序里一定要给子线程的异常留一个日志出口,很多露出来的白屏、闪退、无响应问题,本质都是线程里的异常没有被主线程看到。把这个口子留好,排错效率至少提升一倍。
本文还有配套的精品资源,点击获取