简介这是一份基于PyQt5与YOLOv5的多目标检测GUI项目源码包主要面向刚接触PyQt5开发与YOLO算法的新手也适合需要现成项目练手的开发者。资源将YOLOv5检测能力封装成图形界面完整展示了界面设计与后端逻辑分离的架构思路涵盖PyQt5常用控件、YOLOv5算法源码以及PyTorch环境下的模型加载与推理。压缩包共112个文件核心内容包括py源码、pyc编译产物、yaml模型配置、pt权重文件并附带sh运行脚本、ui界面文件、Dockerfile环境配置、测试图片与视频等整体约83.46MB目录结构清晰便于按模块查阅和二次开发。目前已有8873人浏览学习热度与认可度较高。通过该项目可快速搭建一套可运行的检测GUI从源码中理解YOLOv5的完整检测流程同时借助界面录屏直观查看交互效果也可参考PyQt5项目组织方式学会用控件、信号槽和线程分离UI与业务逻辑。适合在动手实践中巩固深度学习与桌面开发技能。1. 用 PyQt5 给 YOLOv5 套个壳目标检测从命令行到桌面客户端很多人跑通 YOLOv5 之后下一个真实诉求是把它交给不懂命令行的同事、甲方或者领导点开就用。命令行推理能出结果但「双击打开、拖进图片、看框和置信度」这件事还是得有个界面。pyqt5yolov5python 这条技术路线解决的就是这个从模型到产品的一公里。PyQt5 负责桌面端YOLOv5 负责检测Python 负责把两者粘在一起最后交付的是一个 exe 或者一个能直接运行的脚本。这篇文章面向的是已经能跑通 YOLOv5 检测脚本、想把它做成工具的人。我会把界面骨架、推理线程、参数调节和打包发布按步骤拆开讲碰到玄学问题也会把血泪经验写出来。新手照着做能跑出第一个窗口熟手可以跳过环境部分直接看线程和打包那两章。2. 先把四件事装齐Python 环境、模型权重、PyQt5 和必要的依赖2.1 环境选择Python 版本和虚拟环境是后续所有坑的源头YOLOv5 官方在 requirements.txt 里写的是 Python 3.8我实际用过 3.8 到 3.11 都能跑。但 PyQt5 对 Python 3.12 的支持节奏慢半拍保险起见选 Python 3.9 或 3.10这两个版本同时兼容 torch 和 PyQt5不用跟版本依赖较劲。建立虚拟环境是一个值得养成的习惯。YOLOv5 的依赖里有 torch、numpy、opencv-python 这些重库和 PyQt5 的依赖有交集直接装进系统环境过两个月再看多半已经乱了。用 venv 把项目隔离是给未来的自己留后悔药。# 创建虚拟环境python3.9 需要已安装 python -m venv yolo_env # 激活环境Windows yolo_env\Scripts\activate # 激活环境Linux / macOS source yolo_env/bin/activate激活后命令行提示符前面会多出(yolo_env)这表示当前 shell 已经切到虚拟环境后续所有 pip 安装都会落在项目目录下的 yolo_env 里不会污染全局。Windows 上如果激活命令报「禁止运行脚本」去 PowerShell 执行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned再重新激活就行。Linux 用户注意别用 sudo 激活sudo 会把环境切到 root等于白建。2.2 安装 PyQt5 与 labelme 的隐藏冲突PyQt5 本身安装很简单一个 pip 命令就够。但有个高频翻车场景很多做标注的人同时装了 labelme而 labelme 默认依赖 PyQt5如果你先装的 labelme 再装 PyQt5或者反过来版本对不上会出现「ModuleNotFoundError: No module named PyQt5.sip」——这个错误 90% 是 PyQt5 和 PyQt5-sip 版本不匹配导致的。# 安装 PyQt5同时固定版本避免和已有依赖打架 pip install PyQt55.15.10 PyQt5-sip12.13.0 # 验证安装是否干净 python -c from PyQt5.QtWidgets import QApplication; print(PyQt5 OK)sip是 PyQt5 的底层绑定库它和主库版本必须配套。5.15.10 对应 12.13.0 是比较稳的组合太新的 sip 反而会和老版本 PyQt5 冲突。验证命令能打印出PyQt5 OK说明 Qt 绑定层能正常导入。如果 labelme 已经占用了 PyQt5建议在虚拟环境里重新装一遍 labelme 或干脆用其他标注工具省得来回折腾。2.3 下载 YOLOv5 源码和权重其实不需要全量 clone很多人一上来 git clone 整个 YOLOv5 仓库其实不必要。你只需要 models、utils、detect.py 这几个目录和文件以及一份预训练权重。最省事的方式还是 clone 后固定版本号因为 YOLOv5 的 API 一直在小幅变动固定版本能保证代码复制过去能跑。# 克隆并切换到我们测试过的稳定点 git clone https://github.com/ultralytics/yolov5.git cd yolov5 git checkout v6.0 # 安装推理所需依赖注意这里不能全量装会有冲突 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python pillow pyyaml requests tqdmv6.0 这个 tag 的 detect.py 和 models 模块结构干净适合嵌入到 PyQt5 应用。PyTorch 我建议装 CPU 版起步先把流程跑通有 NVIDIA 显卡再换成 cu118 或 cu121 的版本后者在--index-url上选对应 CUDA 版本的 whl 即可。全部依赖装好后下载一个 yolov5s.pt 权重放到 weights 目录命令行先跑一次python detect.py --weights yolov5s.pt --source data/images/bus.jpg确认检测链路本身没问题再开始动界面。3. 让 PyQt5 窗口和 YOLOv5 模型握手架构设计与代码骨架3.1 界面布局的三个区域怎么摆比怎么画重要PyQt5 的界面设计我通常不走 Qt Designer 拖拽而是直接写布局代码。原因之一是想做到「界面逻辑一目了然」另一个原因是 Designer 生成的 .ui 文件还要转 .py迭代起来多一步。对于目标检测工具界面只需三个区域顶部是操作栏打开图片、打开视频、选择摄像头、开始检测中间是图像画布底部是结果列表和置信度滑条。import sys from PyQt5.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QLabel, QSlider, QFileDialog) from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap class DetectorWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle(PyQt5 YOLOv5 检测客户端) self.resize(960, 640) # 操作按钮 self.btn_image QPushButton(打开图片) self.btn_video QPushButton(打开视频) self.btn_camera QPushButton(启动摄像头) self.btn_detect QPushButton(开始检测) # 顶部按钮栏 top_bar QHBoxLayout() top_bar.addWidget(self.btn_image) top_bar.addWidget(self.btn_video) top_bar.addWidget(self.btn_camera) top_bar.addWidget(self.btn_detect) top_bar.addStretch() # 图像画布 self.canvas QLabel(检测结果显示区) self.canvas.setAlignment(Qt.AlignCenter) self.canvas.setStyleSheet(border: 1px solid #aaa; background: #f5f5f5;) # 底部置信度滑条 检测结果文本 self.slider_conf QSlider(Qt.Horizontal) self.slider_conf.setRange(1, 99) self.slider_conf.setValue(25) self.label_conf QLabel(置信度阈值: 0.25) self.result_text QLabel(检测结果: 等待中) bottom_bar QHBoxLayout() bottom_bar.addWidget(self.label_conf) bottom_bar.addWidget(self.slider_conf) bottom_bar.addWidget(self.result_text) # 总布局 layout QVBoxLayout(self) layout.addLayout(top_bar) layout.addWidget(self.canvas, stretch8) layout.addLayout(bottom_bar)这段代码把窗口拆成横竖嵌套的三个布局顶部按钮栏、中间可拉伸画布、底部滑条信息条。stretch8让画布占据绝大部分垂直空间窗口拉大时图像区跟着变不会出现按钮挤在一起的失衡感。置信度滑条的值范围对应 0.01 到 0.99默认 0.25 与 YOLOv5 官方 detect 脚本一致后面把滑条数值传给模型时记得除以 100。3.2 不把模型当黑匣子在 Qt 里调用 detect 的核心代码PyQt5 和 YOLOv5 的握手方式常见有三种直接调用官方 detect.py不推荐、调用 torch.hub.load灵活但耦合重、复写 Detect 类的 postprocess 拿原始张量最干净。第三种最容易被低估——它不只是省掉命令行进程开销还能让你在 Qt 里直接拿到框的坐标、类别、置信度三元组做任何界面展示都方便。import torch import cv2 import numpy as np from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords from utils.torch_utils import select_device class YOLOEngine: 封装 YOLOv5 推理供 PyQt5 界面调用 def __init__(self, weights_path, devicecpu): self.device select_device(device) self.model attempt_load(weights_path, map_locationself.device) self.names self.model.module.names if hasattr(self.model, module) else self.model.names def infer(self, img_bgr, conf_thres0.25, iou_thres0.45): # 从 BGR 转 RGB归一化加 batch 维度走模型 forward img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img_tensor torch.from_numpy(img_rgb).to(self.device) img_tensor img_tensor.float() / 255.0 img_tensor img_tensor.permute(2, 0, 1).unsqueeze(0) with torch.no_grad(): pred self.model(img_tensor)[0] # NMS 后处理去掉重复框和低置信度框 pred non_max_suppression(pred, conf_thresconf_thres, iou_thresiou_thres) results [] for det in pred: if det is not None and len(det): # 坐标映射回原图尺寸 det[:, :4] scale_coords(img_tensor.shape[2:], det[:, :4], img_bgr.shape).round() for *xyxy, conf, cls in reversed(det): label self.names[int(cls)] results.append({ box: [int(x) for x in xyxy], conf: float(conf), label: label }) return results上面这段是核心推理引擎。attempt_load是 YOLOv5 官方加载权重的入口兼容 .pt 和 torchscript 两种格式。non_max_suppression里有两个关键参数conf_thres控制置信度过滤阈值滑条调低会出现很多误检框调高会漏检iou_thres控制两个框重叠到什么程度算重复默认 0.45实测在密集小目标场景要往低调到 0.3 才不打架。scale_coords这步不能漏模型输入尺寸和原始图尺寸不一致时不映射回来的坐标是缩过的直接画框会整体偏移。result里的box是[x1, y1, x2, y2]形式后面在 Qt 画框直接用这四个整数。3.3 把检测结果画到画布上QPixmap 和 QPainter 的配合拿到 results 列表后界面上要做两件事把原图画进 QLabel然后把检测框叠加到图像上。有一种新手常踩的坑是直接修改原图再转 QPixmap导致窗口显示和结果数值对不上。正确做法是保持原图数组不动另外复制一份画框图用来显示。from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor from PyQt5.QtCore import QRect def draw_results(self, img_bgr, results, filepath): # 画框前复制一份避免污染原始数组 vis_img img_bgr.copy() for r in results: x1, y1, x2, y2 r[box] label f{r[label]} {r[conf]:.2f} # OpenCV 画框 cv2.rectangle(vis_img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(vis_img, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 转成 Qt 能显示的 QPixmap h, w, ch vis_img.shape bytes_per_line ch * w qimg QImage(vis_img.data, w, h, bytes_per_line, QImage.Format_RGB888) self.canvas.setPixmap(QPixmap.fromImage(qimg).scaled( self.canvas.width(), self.canvas.height(), Qt.KeepAspectRatio)) # 统计结果文本 from collections import Counter counter Counter(r[label] for r in results) self.result_text.setText( | .join(f{k}: {v} for k, v in counter.items()))QImage的构造里有个关键细节bytes_per_line ch * w因为 OpenCV 的图像数据是紧密排列的没有额外 padding如果图像宽度不是 4 的倍数直接默认构造会花屏。更稳妥的是用QImage(vis_img.data, w, h, vis_img.strides[0], QImage.Format_BGR888)直接把步长传进去任何尺寸都不会翻车。scaled时保持宽高比否则图像会被拉变形。OpenCV 画框用的是 BGRQImage 在 PyQt5 5.15 以上支持Format_BGR888颜色不会串。4. 视频流和摄像头场景多线程是唯一正确解没有捷径4.1 界面卡死的根源不要在主线程里跑模型推理很多人在把检测接到按钮回调里之后发现窗口立刻「未响应」原因只有一个YOLOv5 的推理耗时 100 毫秒到 1 秒期间 Qt 的事件循环被阻塞界面无法重绘、无法响应点击。PyQt5 的QThread就是为这个设计的——耗时的推理放到工作线程界面线程只管接收信号。这也是所有 PyQt5 界面工程的通用骨架不是 YOLOv5 特有。from PyQt5.QtCore import QThread, pyqtSignal class DetectWorker(QThread): frame_ready pyqtSignal(object, object) # (原图, 结果列表) status pyqtSignal(str) def __init__(self, engine, source_type, source_pathNone): super().__init__() self.engine engine self.source_type source_type # image / video / camera self.source_path source_path self.running True def run(self): if self.source_type image: img cv2.imread(self.source_path) results self.engine.infer(img) self.frame_ready.emit(img, results) self.status.emit(单张检测完成) elif self.source_type video or self.source_type camera: cap_idx 0 if self.source_type camera else self.source_path cap cv2.VideoCapture(cap_idx) while self.running and cap.isOpened(): ret, frame cap.read() if not ret: break results self.engine.infer(frame) self.frame_ready.emit(frame, results)frame_ready信号携带两个对象原始帧和检测结果。注意这里是object类型而不是cv2.Mat因为 PyQt 的信号机制对 Python 原生对象都支持但显式声明object能避免导入歧义。线程里的running标志位用来安全退出——当用户关闭窗口或停止摄像头时主线程把running置 Falsewhile循环自然退出视频句柄释放干净。摄像头场景下循环里必须有cap.read()失败分支不然设备拔出或视频结尾会导致线程空转。4.2 信号槽连接把线程结果安全送回界面QThread 的子类里发信号主线程里用connect接收这是 PyQt5 跨线程通信的标准姿势。特别注意绝对不要在线程里直接调用窗口控件的方法比如self.canvas.setPixmap()这会触发跨线程 UI 操作轻则警告重则崩溃。def start_video(self): # 停掉上一次的线程避免两个线程同时读摄像头 if hasattr(self, worker) and self.worker.isRunning(): self.worker.running False self.worker.wait(2000) self.worker DetectWorker(self.engine, video, self.video_path) self.worker.frame_ready.connect(self.update_canvas) self.worker.status.connect(self.show_status) self.worker.start() def update_canvas(self, img, results): # 这个函数运行在主线程可以安全操作控件 self.draw_results(img, results) def show_status(self, text): self.result_text.setText(text) def closeEvent(self, event): # 关窗口前停线程防止后台进程残留 if hasattr(self, worker) and self.worker.isRunning(): self.worker.running False self.worker.wait(3000) event.accept()closeEvent的 padding 是很多初阶项目忽略的线程不退出Python 程序就会「关不掉窗口但进程还在任务管理器里」。wait(3000)给线程 3 秒时间跑完当前帧如果还没结束就强制释放。摄像头场景下同时打开多个线程会直接把设备占死因此开启新线程前先停旧线程是铁律。4.3 摄像头场景的帧率和分辨率取舍摄像头推理和图片推理完全不同图片只要做一次推理摄像头是每帧都推理。YOLOv5s 在 CPU 上跑一帧约 200~400 毫秒这时候如果用 1080p 摄像头流画面会明显掉帧到两三帧每秒。常见的取舍是缩放入帧尺寸比如把宽限定到 640。def preprocess_frame(self, frame, target_w640): h, w frame.shape[:2] if w target_w: ratio target_w / w new_w target_w new_h int(h * ratio) frame cv2.resize(frame, (new_w, new_h)) return frame这段放在DetectWorker.run()循环里、engine.infer()之前把输入宽度压到 640推理耗时能降到 100 毫秒左右。视觉上仍然流畅检测精度下降可接受。如果显存或内存允许也可以换 yolov5m 配合 480p 入帧。总之摄像头场景的显式指标是「帧耗时 150ms」达不到就往下调分辨率或换小模型优先保证界面响应。4.4 模型切换运行时换权重而不重启程序界面里加一个「模型文件选择」按钮点开后用QFileDialog选择 .pt 权重重载YOLOEngine即可。这里最致命的问题是 torch 的全局状态换模型后旧的模型占用的显存不会自动释放连续切换会让显存越涨越高直至 OOM。def reload_model(self, weights_path): import gc # 释放旧的模型引用 if hasattr(self.engine, model): del self.engine.model gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() # 创建新引擎 self.engine YOLOEngine(weights_path, devicecpu)torch.cuda.empty_cache()释放缓存块而不是显存本身但如果旧的模型引用被del干净配合gc.collect()显存曲线就能明显回落。如果切完还是爆显存多半是模型对象被界面的其他变量暗中引用检查有没有把model赋给别的地方。另一个隐蔽问题是旧线程还在用旧 engine 做推理新 engine 已经换掉所以切换按钮应该先停线程再 reload再允许启动。5. 避坑指南YOLOv5 后处理和 PyQt5 集成时最常翻车的 6 个问题5.1 labelme 安装时把 PyQt5 搞坏现象import PyQt5报错No module named PyQt5.QtWidgets装了几次 PyQt5 都没用。原因labelme 自动装了 PyQt5 的某个版本且 pip 解析依赖时覆盖了已有版本导致部分模块路径错乱或 sip 版本不匹配。解决先卸载全部相关包再重装顺序是先pip uninstall PyQt5 PyQt5-sip labelme再按 2.2 节的版本组合重装。通配符卸载写全名字不要用pip uninstall PyQt5*有时候通配符会漏掉PyQt5-Qt5和PyQt5-Qt5-bin这两个配套包。5.2 OpenCV 图像通道顺序在 QImage 里显示偏蓝现象检测框的位置正确但图像整体变成蓝橙色。原因PyQt5 的QImage.Format_RGB888默认按 RGB 顺序解释数据而 OpenCV 是 BGR。解决用Format_BGR888PyQt5 5.15或者在转 QImage 前cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。前者零拷贝速度更快后者内存开销略大但兼容老版本 PyQt5。两者都行但不要在同一个项目里混用显示函数里统一用一种。5.3 检测框坐标在图片缩放显示后对不上现象小图检测正常窗口拉大后框的位置和物体明显错位。原因显示时用了scaled缩放但绘制框时用的是原始图片坐标两个坐标系不一致。解决计算缩放比例把原始坐标映射到显示坐标再画框或者在原始图上画好框之后再整体缩放。推荐后者因为文本框的字体大小和线宽会跟着等比缩放视觉一致。5.4 模型推理偶尔崩溃报错CUDA out of memory现象连续检测几分钟后显存耗尽程序直接退出。原因批次积累和未释放的 tensor。解决推理循环里务必用with torch.no_grad():并且每次迭代后清理不需要的变量批次大小设置为 1如果仍然增长在循环末尾加torch.cuda.empty_cache()。注意这个函数有性能开销不要每帧都调建议每 30 帧调一次。5.5 摄像头线程退出后再打开摄像头失败现象第一次启动摄像头正常关闭后再开黑屏或报错。原因VideoCapture对象没有真正释放设备句柄被占用。解决在run()循环退出后显式调用cap.release()并且closeEvent里wait()确保线程真正结束再允许下次启动。摄像头设备本身在 Windows 上还会被系统的相机权限弹窗锁住第一次启动时务必点击允许。5.6 打包成 exe 后找不到模型权重文件现象在 Python 环境里运行正常用 PyInstaller 打成 exe 后双击闪退或报找不到 .pt 文件。原因exe 运行时的工作目录可能是临时目录或桌面相对路径失效。解决打包前把权重路径改为运行时动态获取os.path.join(os.path.dirname(sys.executable), weights, yolov5s.pt)并把权重目录随程序一起分发。开发环境下这个判断可以写成if getattr(sys, frozen, False): base_path os.path.dirname(sys.executable) else: base_path os.path.dirname(os.path.abspath(__file__)) weights_path os.path.join(base_path, weights, yolov5s.pt)这段在 PyInstaller 冻结环境下取 exe 所在目录在开发环境取脚本所在目录。中间有个 zip 探针容易踩坑打包时如果用--onefile临时解压目录和 exe 目录不一致权重文件应该通过--add-data绑定进包或者在程序里读取环境变量_MEIPASS。实测更好的方式是--onedir模式目录结构清晰模型文件放外面好替换。6. 进阶与验证把工具从「能跑」打磨到「能交付」做到这一步你的 PyQt5 YOLOv5 客户端已经可以完成基础检测任务了。再往下走建议把精力放在产品化三件事上批量推理能力、结果导出格式、以及一个能说服自己的性能基线测试。批量推理的价值在于让工具真正替代人工排查。界面里加一个「选择文件夹」按钮遍历目录下所有图片逐一推理并汇总结果导出成 CSV。这个功能 50 行代码但能让你的检测工具从「单张查看」升级到「批量质检」。def batch_infer(self, folder_path, output_csv): import csv, os rows [] for fname in os.listdir(folder_path): if not fname.lower().endswith((.jpg, .png, .jpeg)): continue fpath os.path.join(folder_path, fname) img cv2.imread(fpath) results self.engine.infer(img) for r in results: rows.append([fname, r[label], r[conf], *r[box]]) with open(output_csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([filename, label, conf, x1, y1, x2, y2]) writer.writerows(rows)utf-8-sig编码是为了让 Excel 打开 CSV 不乱码这是个细节但很能体现产品意识。导出文件里的坐标是原始像素坐标配合图像查看器可以随时反查。性能基线测试是验证模型选型和硬件匹配度的关键一步也是被很多人直接跳过的。建议写一个循环跑 100 张图的耗时统计算平均推理时间对比 CPU 和 GPU 的差异。这个数据直接决定你交付给用户时的硬件最低要求。实测 yolov5s 在 Intel i5 上 CPU 推理约 250ms/帧GPUGTX 1660约 30ms/帧差距接近 8 倍。如果目标场景只有 CPU那秒级以下的响应基本做不到需要在模型选择上做出妥协。最后一个建议把置信度滑条的数值真正传到推理函数里而不是只在界面上显示。很多人做了滑条却忘了联动engine.infer的conf_thres参数结果滑条怎么拖都没变化用户会直接判定工具是坏的。在update_canvas函数里读滑条值并触发重新推理这是个高频遗漏点。做这类工具我现在的习惯是先量化性能指标再写界面最后补导出和参数联动。第一次做 PyQt5 和 YOLOv5 集成时我直接跳过线程设计用主线程跑推理结果窗口每次检测都卡死几秒后来花了一晚上重构才理顺。老实说踩一遍这些坑是好事但对着这篇文章能少走一半弯路。希望帮到你。本文还有配套的精品资源点击获取