YOLOv7与PyQt5迁移PySide6:构建图像视频检测GUI的完整实践

发布时间:2026/9/13 9:46:02
YOLOv7与PyQt5迁移PySide6:构建图像视频检测GUI的完整实践 简介面向有图像处理与目标检测需求的开发者这份资源提供了一套基于YOLOv7与PySide6/PyQt5的可视化检测工具支持图像和视频两种输入方式。程序启动后会自动加载模型目录下的YOLOv7预训练权重无需手动配置即可快速开展检测适合希望快速搭建GUI检测原型或学习目标检测界面开发的初中级Python程序员。压缩包共88个文件包含28个Python脚本与27个编译后的pyc文件另有UI设计文件、图标/背景图、字体、模型配置与Dockerfile等辅助内容整体约138.37MB。Python源码覆盖主程序、YOLO模型封装、类别定义、鼠标标注等模块便于阅读与二次修改模型目录中提供两个预训练权重文件可直接运行体验。已有810人浏览学习对于想要结合YOLOv7与Qt界面做检测应用的人来说是一份结构完整、上手门槛较低的可直接运行参考。1. 老界面配新模型YOLOv7 和 PyQt5-YOLOv5 项目迁移到 PySide6 的常见路线把 YOLOv7、PyQt5、YOLOv5、PySide6 放在同一个标题里通常是这样的现场你手里已经有一套能跑的 PyQt5 YOLOv5 检测工具另一边刚调通了 YOLOv7 的推理脚本现在要把它们整合成一个 yolov7-Pyside6 可视化界面并且同时支持图像和视频检测。这个需求的难点不在模型而在界面代码和推理代码怎么解耦。如果直接把两个模型的预处理、坐标缩放、画框逻辑塞进按钮事件里后面每换一个权重都要改一次界面。下面按我改造的实际路线来讲先把 YOLOv7 和 YOLOv5 的推理抽成同一个 Detector 后端再用 QThread 处理视频帧循环最后把画框、保存和半自动标注接上让界面只碰一个统一的检测结果格式。2. 统一模型后端把 YOLOv7 和 YOLOv5 的加载与推理参数对准我不建议把 YOLOv7 或 YOLOv5 官方仓库里的 detect.py 直接复制到 GUI 工程里因为这两个仓库的入口参数、输入预处理、输出格式都不一致。YOLOv5 的 hub 模型封装得很完整YOLOv7 则更接近“手动挡”。界面层如果同时面对两套调用方式后期维护成本会很高。常见做法是先写一个 Detector 类让上层只认一个detect(frame_bgr, conf, iou, imgsz)方法返回值统一成(N, 6)的 NumPy 数组每行是x1, y1, x2, y2, conf, cls。2.1 输入输出的差异以及统一推理层的边界YOLOv5 的torch.hub.load(ultralytics/yolov5, custom, weights)返回的模型自带 letterbox、归一化和 NMS直接调用model(frame, size640, conf_thres0.25, iou_thres0.45)然后从results.xyxy[0]拿到的就是xyxy conf cls。YOLOv7 不同它的自定义模型是原始检测头输入要先手动缩放填充推理输出还要自己过non_max_suppression。两边的公共点只有一个最终都可以整理成统一的检测数组。所以我把统一层的边界定在“坐标回到原图坐标系之后”再往后就是界面的事了。把边界划在这里还有一个好处部署 YOLOv7 时如果显存紧张只需要在 Detector 内部调整half()和imgsz界面不用感知以后换 TensorRT 或 ONNX 时也只要给 Detector 增加一个engine分支。2.2 一个可复用的 Detector 类加载 .pt 和统一 detect 方法下面的代码会调用 YOLOv7 仓库里的utils.general.non_max_suppression。最省事的做法是让 YOLOv7 的源码目录在项目里的相对路径固定比如sys.path.append(yolov7)。import sys import cv2 import torch import numpy as np sys.path.append(yolov7) # 改成你实际的 YOLOv7 仓库路径 def resize_and_pad(img, size640): h, w img.shape[:2] r min(size / h, size / w) new_w, new_h int(round(w * r)), int(round(h * r)) resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) top (size - new_h) // 2 bottom size - new_h - top left (size - new_w) // 2 right size - new_w - left padded cv2.copyMakeBorder( resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value(114, 114, 114) ) return padded, r, left, top class Detector: def __init__(self, archyolov7, weights, device): self.arch arch.lower() self.device device if device else (cuda:0 if torch.cuda.is_available() else cpu) if self.arch yolov7: self.model torch.hub.load( WongKinYiu/yolov7, custom, weights, force_reloadFalse ).to(self.device).eval() if self.device ! cpu: self.model.half() elif self.arch yolov5: self.model torch.hub.load( ultralytics/yolov5, custom, pathweights, force_reloadFalse ).to(self.device).eval() self.names getattr(self.model, names, []) def detect(self, frame_bgr, conf0.25, iou0.45, imgsz640): if self.arch yolov7: from utils.general import non_max_suppression img, ratio, pad_left, pad_top resize_and_pad(frame_bgr, imgsz) rgb img[:, :, ::-1].copy() tensor torch.from_numpy(rgb).permute(2, 0, 1).float() / 255.0 tensor tensor.unsqueeze(0).to(self.device) if self.device ! cpu: tensor tensor.half() with torch.no_grad(): outputs self.model(tensor) if isinstance(outputs, (tuple, list)): outputs outputs[0] pred non_max_suppression(outputs, conf, iou) if not pred or pred[0] is None: return np.empty((0, 6), dtypenp.float32) det pred[0].float() boxes det[:, :4].clone() boxes[:, [0, 2]] - pad_left boxes[:, [1, 3]] - pad_top boxes / ratio result torch.cat((boxes, det[:, 4:6]), dim1).cpu().numpy() return result.astype(np.float32) else: results self.model( frame_bgr, sizeimgsz, conf_thresconf, iou_thresiou ) det results.xyxy[0].cpu().numpy() return det.astype(np.float32) if len(det) else np.empty((0, 6), dtypenp.float32)这个类里没有把类别过滤写死因为界面上通常会有一个“只显示指定类别”的选项在调用处过滤更灵活。YOLOv7 分支在 GPU 上开启半精度后显存占用差不多能减半如果跑 CPU就不要调用half()tensor 保持 float32 即可。YOLOv5 分支不需要手动预处理官方 hub 内部会处理。提示不要在界面里反复实例化 Detector。模型加载一次往往要几百毫秒放在程序启动时做失败时用 QMessageBox 弹窗提示让用户去检查权重路径。2.3 调用检测器时的超参数设置和调试入口界面中最常暴露给用户的参数是conf、iou、imgsz它们对速度影响很大。尤其imgsz从 640 提到 1280推理耗时可能涨三到四倍。参数YOLOv7 里对应YOLOv5 里对应建议范围说明confconfconf_thres0.15 ~ 0.5越小框越多误检也越多iouiouiou_thres0.35 ~ 0.7越大重叠框越不会被合并imgsz640size416 / 512 / 640决定延迟和显存的主要参数half模型 half()由仓库自动GPU 固定开启半精度推理classesnon_max_suppression 的 classesmodel.classes不填仅保留指定类别 id我在界面上一般用滑块控制confimgsz做成下拉框而不是滑块因为resize_and_pad对输入尺寸有严格约束常见档位就 416、512、640、1280。调试时先拿单张图跑一次Detector.detect确认输出坐标都是正数且没有 NaN再接入视频否则问题会混在一起。这一步最常见的报错是ModuleNotFoundError: utils.general。不是模型坏了而是sys.path.append(yolov7)没指向真正的 YOLOv7 源码目录。另一个高频报错是 CUDA out of memory把imgsz降到 512或者打开half()基本能解决。3. PySide6 与 PyQt5 的界面骨架线程、信号槽和视频帧循环PyQt5-YOLOv5 项目迁移到 PySide6 时大部分代码可以直接复用要改的主要是 import 名称和少量 API。PySide6 中信号叫Signal槽叫SlotPyQt5 里叫pyqtSignal和pyqtSlot。别以为只是换名字QAction的包位置和主循环的exec写法也有变化。3.1 迁移 PyQt5 到 PySide6 时最容易踩的 3 处语法差异功能PyQt5 写法PySide6 写法备注信号声明change pyqtSignal(int)change Signal(int)语义几乎一致槽声明pyqtSlot()Slot()不影响功能主循环退出app.exec_()app.exec()PyQt5 中 exec_ 是兼容写法QAction 位置QtWidgets.QAction()QtGui.QAction()大工程里很容易漏工程里如果混装 PyQt5 和 PySide6import 冲突很常见。安装时也是两套独立包名PyQt5 用 pip 安装PyQt5PySide6 用 pip 安装PySide6。我建议在正式工程里写一个qt_compat.py统一的入口后续切换时只改一个文件。try: from PySide6.QtCore import Signal, Slot, QThread, QObject, Qt from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QFileDialog from PySide6.QtGui import QImage, QPixmap, QAction QT_BINDING PySide6 except ImportError: from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot, QThread, QObject, Qt from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QFileDialog from PyQt5.QtGui import QImage, QPixmap, QAction QT_BINDING PyQt5这样过渡期可以两套环境都跑。正式发布我建议固定 PySide6两个包同时装进同一个虚拟环境Qt 插件目录会互相干扰轻则样式不对重则启动崩溃。这是我的真实教训。3.2 用 QThread 跑视频帧循环界面标题栏才不会转圈视频检测的耗时点有两个cap.read()和detect()。如果直接在界面主线程里写 while 循环窗口会变成“无响应”系统会提示用户强行关闭。常见做法是把两个操作都放进 QThread用信号把画好框的 QImage 发给主窗口。为了降低跨线程开销我只在信号里传QImage不传原始 numpy 数组。import time import cv2 from qt_compat import QThread, QObject, Signal, QImage class VideoWorker(QObject): frame_ready Signal(object) # 里面放 QImage这里是 PyQt5/PySide6 通用写法 metrics Signal(float, int) # fps, 当前目标数 finished Signal() def __init__(self, detector, source, conf, iou, imgsz640): super().__init__() self.detector detector self.source source self.conf conf self.iou iou self.imgsz imgsz self._stop False def stop(self): self._stop True def run(self): cap cv2.VideoCapture(self.source) if not cap.isOpened(): self.finished.emit() return prev time.time() while not self._stop: ok, frame cap.read() if not ok: break dets self.detector.detect(frame, self.conf, self.iou, self.imgsz) frame draw_boxes(frame, dets, self.detector.names) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy() self.frame_ready.emit(qimg) now time.time() fps 1.0 / (now - prev) prev now self.metrics.emit(round(fps, 1), len(dets)) cap.release() self.finished.emit()启动线程的代码固定在主窗口里看起来像这样self.thread QThread() self.worker VideoWorker(self.detector, video_path, conf, iou) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.frame_ready.connect(self.show_frame) self.button_stop.clicked.connect(self.worker.stop) self.thread.start()这里moveToThread决定 worker 里的代码在哪个线程执行。frame_ready信号连接主窗口的槽函数Qt 会自动在接收者线程里执行所以显示 QLabel 的操作不会阻塞 worker。stop()只是把循环标志置位线程会在读完当前帧后退出比强行 terminate 安全得多。4. 图像和视频检测的可视化画框、显示置信度与结果保存界面最后展示的是一张画好框的画面。画框代码不要散落在各个按钮事件里我一般抽成draw_boxes让图像检测、视频 worker 和保存结果共用同一个函数这样字体粗细、颜色和标签格式才能统一。4.1 画检测框和置信度标签的统一入口def draw_boxes(img, dets, names, colorsNone): if dets is None or len(dets) 0: return img if colors is None: colors {} for d in dets: x1, y1, x2, y2 int(d[0]), int(d[1]), int(d[2]), int(d[3]) conf float(d[4]) cls_id int(d[5]) label names[cls_id] if names and cls_id len(names) else str(cls_id) label f {conf:.2f} color colors.get(cls_id) if color is None: color tuple(int(x) for x in np.random.randint(0, 255, 3)) colors[cls_id] color cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) (tw, th), _ cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) cv2.rectangle(img, (x1, y1 - th - 6), (x1 tw, y1), color, -1) cv2.putText(img, label, (x1, y1 - 3), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA) return imgdets的列顺序在第 2 章统一成x1, y1, x2, y2, conf, cls这里直接按位置索引。names来自Detector.names一般是和训练时 class 顺序完全一致的列表。颜色用 BGR 传给 OpenCVworker 最后会转成 RGB 给 Qt不要提前转否则画出来的框颜色全错。4.2 图像检测的界面操作选择文件、显示、保存单张图片检测比视频简单不需要长循环。但如果模型在 CPU 上跑耗时有几百毫秒界面还是会短时间无响应。我把可接受的范围定在 100ms 以内超过的话最好再套一层 QThread。def on_open_image(self): path, _ QFileDialog.getOpenFileName( self, 选择检测图片, , Images (*.jpg *.png *.bmp)) if not path: return frame cv2.imread(path) if frame is None: return dets self.detector.detect(frame, self.conf, self.iou, self.imgsz) drawn draw_boxes(frame.copy(), dets, self.detector.names) rgb cv2.cvtColor(drawn, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy() self.label_result.setPixmap(QPixmap.fromImage(qimg).scaled( self.label_result.width(), self.label_result.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation )) self.current_result drawn self.result_count len(dets) def on_save_image(self): if self.current_result is None: return path, _ QFileDialog.getSaveFileName( self, 保存结果, , PNG(*.png);;JPG(*.jpg)) if path: cv2.imwrite(path, self.current_result)显示时用scaled把大图压缩到 QLabel 尺寸否则 4K 图片会把界面撑到只剩一块。保存时保存的是原始分辨率的画框图跟 QLabel 上显示的那张缩放图无关。QImage.Format_RGB888要求数据是 RGB所以一定要先cvtColor否则颜色会明显偏蓝。4.3 视频结果保存编码器选择和保存流程视频不要等全部检完再写文件内存扛不住。正确姿势是在 worker 里创建VideoWriter每帧检测完直接写盘。保存文件前先从视频流读取帧率和尺寸避免最后写出的文件时长不对或黑屏。fourcc cv2.VideoWriter_fourcc(*mp4v) fps cap.get(cv2.CAP_PROP_FPS) or 25 w int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer cv2.VideoWriter(output_path, fourcc, fps, (w, h)) # 在 worker 的 while 循环里每帧做完 detect 和 draw_boxes 后 # writer.write(frame)编码器字符串直接影响文件能不能在普通播放器里播放。我整理过几个常用组合编码器后缀说明mp4v.mp4兼容性最好多数环境直接可用XVID.avi老设备兼容文件体积偏大avc1.mp4H.264 编码清晰度高部分系统不支持这里最容易被坑的是尺寸不一致。VideoWriter的宽高必须和实际写入的帧完全一致如果你保存的是 QLabel 缩放后的 QPixmap写出来的视频十有八九是黑屏。所以保存流程要在 worker 内部共用画框后的原始帧界面只拿 QImage 做显示。5. 收尾技巧验证、调优与用界面做半自动标注界面能跑起来只是开始。我在 YOLOv7 和 PyQt5-YOLOv5 这类工程里还会加三个小工具模型预热、检测结果导出成 YOLO 格式、以及快速定位崩溃点。它们能省下大量排错时间。5.1 模型预热和显存占用确认def warmup(detector, imgsz640): dummy np.zeros((imgsz, imgsz, 3), dtypenp.uint8) for _ in range(2): detector.detect(dummy, conf0.25, iou0.45, imgszimgsz)第一次推理会把参数搬运到 GPU、建立 cuDNN 缓存耗时会比后续推理高很多。在界面初始化时跑两次 dummy能让视频第一帧不突然后掉帧。同时这个操作也能提前暴露显存不足如果连一张 640 的零值图都跑不过那就先降低imgsz。dummy 的通道顺序要和真实画面一样用 BGR否则 YOLOv7 分支里的img[:, :, ::-1]没有被完整执行到预热效果会打折扣。5.2 把检测结果导出成 YOLO 标注文件这是界面做半自动标注最快的方式。把第 2 章统一的检测结果从x1,y1,x2,y2,conf,cls转成归一化的center_x, center_y, width, height按图片同名写入 txt再交给 YOLOv5 训练自己的数据集。def to_yolo_line(det, img_w, img_h): x1, y1, x2, y2, conf, cls det xc ((x1 x2) / 2) / img_w yc ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h return f{int(cls)} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}我在界面里给这个功能加了一个min_conf输入框默认 0.5。低于这个值的检测框不写入标注文件避免把误检框也变成训练样本。如果是视频抽帧标注记得每帧图片单独命名不要覆盖同名文件。5.3 三个高频故障的表现和解决方向故障现象根因处理方向打开界面后拖动窗口卡死推理执行在 UI 线程所有 detect 放进 QThreadworker 只发 QImage视频跑几分钟内存持续上涨信号里传原始 numpy 大数组转成 QImage 后.copy()不要长期持有大数组退出程序崩溃线程还没退出就销毁窗口先stop()再thread.quit(); thread.wait(2000)PyQt5 和 PySide6 同时存在import 报错插件目录互相覆盖用独立 venv按QT_BINDING固定一套界面的“酷炫”样式可以放到这些基础验证之后。PySide6 用qdarkstyle或自绘 QSS 都能换皮肤但换皮不会替你做线程规划。一个更隐蔽的坑是保存视频的帧率来源帧率要从cap.get(cv2.CAP_PROP_FPS)读不要用检测耗时反推否则导出视频的播放节奏会不对。把这个值在 worker 启动时读一次和VideoWriter的 fps 参数对齐最后落盘的文件时长才准确。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询