
简介这份资源是一套面向PyQt5与YOLOv5初学者的完整GUI目标检测项目适合刚接触界面开发或YOLO算法、希望有现成工程练手的人群。项目以PyTorch为深度学习框架将YOLOv5多目标检测能力与PyQt5桌面界面结合重点演示界面设计与后端逻辑分离的实现思路读者可借此熟悉PyQt5常见控件的用法并对照YOLOv5源码理解检测流程。压缩包共112个文件约83.46MB包含26个py源码、33个pyc编译文件、25个yaml配置、3个pt权重、1个ui界面文件以及sh脚本、jpg与png图片、mp4演示视频、xml标注和Dockerfile等覆盖代码、配置、模型与素材多个层面。目前已有8874人学习下载可作为理论书籍之外的实操补充帮助读者把PyQt5开发与YOLOv5算法知识落到一个可运行、可拆解的真实项目上。1. 从零搭一套 PyQt5 YOLOv5 桌面检测工具这条路到底值不值得走你手头有一批图片或者接了一路摄像头想做一个带界面的目标检测工具给产线同事或者实验室同学直接用而不是每次开命令行敲detect.py。这个场景下PyQt5 YOLOv5 Python 是最常见的一套组合Python 做胶水YOLOv5 负责推理PyQt5 负责把结果画到窗口里。它解决的核心问题就一个——把模型能力封装成一个双击就能跑的桌面程序让不懂代码的人也能用。这套方案适合谁适合已经跑通过 YOLOv5 推理、想加一层界面的开发者适合做课程设计、毕业设计、内部工具原型的同学也适合需要把检测结果快速演示给非技术同事看的一线工程师。它不适合追求极致推理速度的嵌入式量产场景那种情况一般会把模型量化后丢到 RK3568、树莓派这类板子上单独跑。桌面端这套组合的价值在于开发快、调试直观、界面可定制代价是打包体积大、跨平台部署要额外处理。2. 环境搭建PyQt5 安装、YOLOv5 依赖与 Python 版本怎么配2.1 Python 版本与虚拟环境的选择Python 安装这一步看着简单翻车的人不少。YOLOv5 官方对 Python 版本有要求太老的 3.6 会在部分依赖上出问题太新的 3.12 又可能碰到 PyTorch 轮子还没跟上。我一般会选 Python 3.8 到 3.10 之间3.8 是兼容性最稳的一档很多教程和预编译包都围绕它。装的时候记得勾选 “Add Python to PATH”不然后面在 cmd 里敲python会提示找不到命令。虚拟环境强烈建议建一个不要往系统 Python 里直接灌。YOLOv5 依赖的 torch、torchvision、opencv-python 版本之间耦合比较紧跟其他项目混在一起迟早出事。用 venv 或者 conda 都行我习惯 conda因为切环境方便。# 创建并激活一个 Python 3.8 的虚拟环境 conda create -n yolo_gui python3.8 conda activate yolo_gui # 验证 Python 版本 python --version逻辑说明conda create的-n指定环境名python3.8锁定解释器版本。激活后所有 pip 安装都落在这个环境里删环境时一条命令就能清干净不留后悔药都来不及吃的残留。参数上如果你机器上只有 venv用python -m venv yolo_gui再激活也一样。2.2 PyQt5 与 YOLOv5 依赖的安装顺序安装顺序有讲究。先装 PyTorch再装 YOLOv5 的 requirements最后装 PyQt5。原因是 YOLOv5 的 requirements.txt 里会带 opencv-python而 PyQt5 本身不冲突但如果你先装了 opencv 的某个版本后面 torch 装的时候可能触发重装。PyTorch 的安装命令要去官网按你的 CUDA 版本选没有 GPU 就选 CPU 版。# 以 CUDA 11.8 为例安装 PyTorchCPU 版把 index-url 换成 cpu 即可 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 YOLOv5 依赖在 YOLOv5 项目根目录下执行 pip install -r requirements.txt # 安装 PyQt5 pip install PyQt5逻辑说明--index-url指定 PyTorch 官方轮子源避免从默认源拉到不匹配的版本。requirements.txt里包含 numpy、opencv-python、matplotlib、pyyaml 等这些是 YOLOv5 推理和绘图的基础。PyQt5 单独装是因为它不在 YOLOv5 依赖里属于界面层。参数上如果你要用到界面里显示 HTML 报告PyQt5 的 QtWebEngine 需要额外装pip install PyQtWebEngine这个包体积不小按需装。装完做个自检确认三个核心库都能导入# check_env.py import torch import cv2 import PyQt5.QtCore as QtCore print(torch:, torch.__version__) print(cuda available:, torch.cuda.is_available()) print(opencv:, cv2.__version__) print(qt:, QtCore.QT_VERSION_STR)逻辑说明这段脚本一次性验证推理后端、图像处理和界面框架是否就位。torch.cuda.is_available()返回 False 不代表装错了只说明当前是 CPU 推理。QT_VERSION_STR能打印出来就说明 PyQt5 的底层 Qt 库链接正常。如果这里报ImportError: DLL load failed九成是 PyQt5 和 Python 位数不匹配或者 conda 环境里混了系统级的 Qt。2.3 目录结构把界面代码和模型代码分开项目一上来就把所有文件堆在根目录后面改起来会很痛苦。我一般会分成三层ui/放界面文件core/放推理封装weights/放模型权重。YOLOv5 的models/和utils/直接以子模块或者复制进来的方式保留不要改动它的内部结构升级时好替换。project/ ├── main.py # 程序入口 ├── ui/ │ └── main_window.py # 界面逻辑 ├── core/ │ └── detector.py # YOLOv5 推理封装 ├── weights/ │ └── best.pt # 训练好的权重 └── yolov5/ # 官方仓库代码 ├── models/ └── utils/逻辑说明main.py只负责启动应用和加载主窗口不写业务逻辑。detector.py把模型加载、预处理、推理、后处理包成一个类界面层只调用它的方法。这样做的价值是哪天你想把 YOLOv5 换成 YOLOv8只改core/一层界面不用动。权重放weights/是为了打包时好定位用相对路径引用别写死绝对路径。3. 把 YOLOv5 推理封装成界面能调的类3.1 加载模型与推理接口设计界面线程里直接调torch.load会卡住 UI所以模型加载要放在初始化阶段推理要能接受单张图或者一帧 numpy 数组。我一般封装一个Detector类构造时加载模型detect方法返回检测框、置信度、类别和画好框的图像。YOLOv5 官方提供了DetectMultiBackend这个类能同时兼容.pt、ONNX、TensorRT 等格式直接用它比自己写加载逻辑稳。# core/detector.py import sys import numpy as np import torch # 把 yolov5 目录加入路径才能导入它的模块 sys.path.append(./yolov5) from models.common import DetectMultiBackend from utils.general import non_max_suppression, scale_boxes from utils.augmentations import letterbox class Detector: def __init__(self, weights./weights/best.pt, devicecpu, imgsz640): self.device torch.device(device) # 加载模型dnnFalse 表示用 PyTorch 后端 self.model DetectMultiBackend(weights, deviceself.device, dnnFalse) self.stride self.model.stride self.names self.model.names self.imgsz imgsz def detect(self, img_bgr, conf_thres0.25, iou_thres0.45): # letterbox 保持长宽比缩放并填充到 640x640 im letterbox(img_bgr, self.imgsz, strideself.stride, autoTrue)[0] im im.transpose((2, 0, 1))[::-1] # BGR 转 RGBHWC 转 CHW im np.ascontiguousarray(im) im torch.from_numpy(im).to(self.device).float() / 255.0 if im.ndimension() 3: im im.unsqueeze(0) pred self.model(im, augmentFalse, visualizeFalse) # NMS 去重agnostic 表示不区分类别做 NMS pred non_max_suppression(pred, conf_thres, iou_thres, agnosticFalse) results [] for det in pred: if len(det): # 把框坐标还原回原图尺寸 det[:, :4] scale_boxes(im.shape[2:], det[:, :4], img_bgr.shape).round() for *xyxy, conf, cls in reversed(det): results.append({ box: [int(x) for x in xyxy], conf: float(conf), cls: int(cls), name: self.names[int(cls)] }) return results逻辑说明letterbox是 YOLOv5 的标准预处理保证输入尺寸是 stride 的整数倍且不变形。transpose加[::-1]完成 BGR 到 RGB 和通道顺序调整这一步漏了会导致检测结果全乱。non_max_suppression的conf_thres控制最低置信度iou_thres控制重叠框合并阈值。scale_boxes把 640 尺度下的框映射回原图坐标不做这一步画出来的框位置会偏。参数上imgsz默认 640如果你的模型训练时用的是 416这里要改成 416否则精度会掉。3.2 用 QThread 把推理放到后台线程PyQt5 的界面刷新在主线程推理如果也放主线程点一下按钮界面就假死用户以为程序崩了。标准做法是把推理逻辑放进QThread子类通过信号把结果传回主线程更新界面。这里有个血泪经验不要在子线程里直接操作界面控件Qt 不允许跨线程操作 UI会随机崩溃。# ui/worker.py from PyQt5.QtCore import QThread, pyqtSignal import cv2 from core.detector import Detector class DetectWorker(QThread): # 定义信号传回画好框的图像和检测结果列表 finished pyqtSignal(object, list) def __init__(self, detector: Detector, image_path: str): super().__init__() self.detector detector self.image_path image_path def run(self): img cv2.imread(self.image_path) if img is None: self.finished.emit(None, []) return results self.detector.detect(img) # 在图像上画框 for r in results: x1, y1, x2, y2 r[box] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f{r[name]} {r[conf]:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) self.finished.emit(img, results)逻辑说明pyqtSignal(object, list)定义了两个参数object用来传 numpy 图像list传检测结果。run方法是线程启动后自动执行的入口里面做读图和推理。画框用 OpenCV 完成因为比 Qt 的绘图 API 快且简单。finished.emit把结果发出去主线程连接这个信号来更新QLabel。参数上QThread的start()会触发run()不要直接调run()那样还是在主线程执行。3.3 界面里显示图像和检测结果主窗口里放一个QLabel显示图像一个QTextEdit或者QTableWidget显示检测明细。图像显示要注意 numpy 数组到QImage的转换通道顺序和内存连续性都得处理不然会花屏或者颜色错乱。# ui/main_window.py import cv2 import numpy as np from PyQt5.QtWidgets import (QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QTextEdit, QFileDialog) from PyQt5.QtGui import QImage, QPixmap from core.detector import Detector from ui.worker import DetectWorker class MainWindow(QMainWindow): def __init__(self): super().__init__() self.detector Detector(weights./weights/best.pt, devicecpu) self.worker None self.init_ui() def init_ui(self): self.setWindowTitle(YOLOv5 检测工具) self.resize(900, 700) self.image_label QLabel(请选择图片) self.result_text QTextEdit() self.result_text.setReadOnly(True) btn QPushButton(选择图片并检测) btn.clicked.connect(self.on_select) layout QVBoxLayout() layout.addWidget(self.image_label) layout.addWidget(btn) layout.addWidget(self.result_text) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def on_select(self): path, _ QFileDialog.getOpenFileName( self, 选择图片, , Images (*.png *.jpg *.jpeg *.bmp)) if not path: return self.worker DetectWorker(self.detector, path) self.worker.finished.connect(self.on_finished) self.worker.start() def on_finished(self, img, results): if img is None: self.result_text.setText(图像读取失败) return # numpy BGR 转 QImage RGB rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(qimg)) lines [f{r[name]} 置信度 {r[conf]:.2f} 位置 {r[box]} for r in results] self.result_text.setText(\n.join(lines) if lines else 未检测到目标)逻辑说明QFileDialog.getOpenFileName返回路径和过滤器空路径表示用户取消。DetectWorker实例化后连接finished信号到on_finished再start()。QImage构造时传入rgb.data、宽高、每行字节数ch * w和格式ch * w这个参数写错会花屏。QPixmap.fromImage把 QImage 转成可显示的像素图。参数上device如果改成cuda:0且环境有 GPU推理会快很多但模型加载时就要指定对。4. 避坑与排查PyQt5 YOLOv5 集成时最容易翻车的 5 个点4.1 界面卡死无响应现象点击检测按钮后窗口变灰标题栏显示“未响应”几秒到几十秒后恢复。原因推理在主线程执行阻塞了 Qt 的事件循环。解决把推理放进QThread如 3.2 节所示。如果推理本身很慢还要考虑在界面上加一个进度提示或者禁用按钮避免用户重复点击。4.2 图像显示颜色错乱或花屏现象QLabel里显示的图片颜色发蓝或者出现斜条纹。原因numpy 数组是 BGR 顺序QImage默认按 RGB 解析或者QImage构造时每行字节数参数传错。解决用cv2.cvtColor转成 RGBQImage的bytesPerLine传ch * w。另外要保证 numpy 数组是连续的cv2.cvtColor返回的通常是连续的如果做过切片操作加一句np.ascontiguousarray。4.3 模型加载报 “No module named models”现象运行main.py时提示找不到models或utils。原因YOLOv5 的模块是相对项目根目录导入的你的入口脚本不在 YOLOv5 目录下Python 搜索路径里没有它。解决在detector.py开头用sys.path.append(./yolov5)把 YOLOv5 目录加进去注意路径要相对于你运行脚本的目录。更稳的做法是用os.path.dirname(__file__)拼绝对路径避免工作目录变化导致失效。4.4 打包成 exe 后闪退现象用 PyInstaller 打包后双击 exe 没反应或者一闪而过。原因YOLOv5 的权重文件和部分依赖没被打进去或者sys.path在打包后失效。解决用--add-data把weights/和yolov5/目录带上代码里用sys._MEIPASS判断是否在打包环境动态拼路径。另外 PyQt5 打包时建议加--hidden-import把 QtWebEngine 之类的隐式依赖显式声明。4.5 置信度阈值调了没效果现象把conf_thres从 0.25 改成 0.5检测框数量没变化。原因改的是detect方法的默认参数但调用时传了旧值或者模型输出的置信度本身就集中在高位。解决确认调用链上参数确实传到了non_max_suppression可以在detect里打印一下conf_thres。如果模型本身过拟合置信度普遍偏高光调阈值不够要回去看训练数据和超参数。5. 进阶技巧用 QSS 美化界面并接入摄像头实时检测界面能跑通之后下一步通常是两件事让它好看一点以及从单张图片扩展到摄像头实时流。QSS 是 Qt 的样式表语法类似 CSS能给按钮、文本框换配色和圆角不用重写控件。实时检测则要把DetectWorker改成循环读帧的模式同时控制帧率不然界面刷新跟不上推理速度。# 摄像头实时检测的 worker 改造 class CameraWorker(QThread): frame_ready pyqtSignal(object, list) def __init__(self, detector, camera_id0): super().__init__() self.detector detector self.camera_id camera_id self.running True def run(self): cap cv2.VideoCapture(self.camera_id) while self.running and cap.isOpened(): ret, frame cap.read() if not ret: break results self.detector.detect(frame, conf_thres0.4) for r in results: x1, y1, x2, y2 r[box] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) self.frame_ready.emit(frame, results) cap.release() def stop(self): self.running False self.wait()逻辑说明running标志控制循环退出stop方法里先置 False 再wait()等线程结束避免强制终止导致资源泄漏。frame_ready信号每帧发一次主线程收到后更新QLabel。参数上conf_thres在实时场景可以适当调高到 0.4 到 0.5减少误检带来的画面抖动。如果推理速度低于摄像头帧率可以在循环里加time.sleep或者跳帧处理别让队列堆积。QSS 美化可以直接在主窗口初始化时self.setStyleSheet(...)把按钮背景、圆角、字体大小统一改掉。一个实用的习惯是把 QSS 单独存成.qss文件用open读进来再设置改样式不用动 Python 代码。实时检测这块我踩过最深的坑是忘了在窗口关闭时调stop()结果线程还在跑程序退不干净任务管理器里留一堆僵尸进程。后来养成习惯在closeEvent里统一停掉所有 worker这个教训希望帮到你。本文还有配套的精品资源点击获取