简介:这套项目把PyQt5界面开发与YOLOv5目标检测算法结合起来,定位为入门级完整示例,特别适合刚接触PyQt5和YOLO算法的初学者,也适合想通过现成项目练手并快速跑通GUI应用的开发者。压缩包共112个文件,大小83.46MB,除26个Python源码和33个pyc编译文件外,还包含25个YAML配置、6个Shell脚本、3个权重文件、测试图片、演示视频以及界面ui、说明文本、Docker环境文件等,从模型配置到界面运行所需的文件基本齐全。目前已有8875人学习下载,参考热度不错。通过这套项目,可以学习PyQt5常见控件的布局与信号槽用法,理解界面展示与后端逻辑如何分离,并对照YOLOv5源码梳理目标检测的完整流程;示例中还带有图片、视频等不同输入方式的演示,配合权重文件与Docker环境文件,能快速在本地复现多目标检测效果,尤其适合课程设计或毕业设计起步阶段使用。与PyQt5开发书籍搭配阅读,可进一步实现理论与实践同步提升。
1. 为什么说 PyQt5+YOLOv5 是桌面端目标检测最稳的组合
如果你已经用命令行跑通了 YOLOv5 的 detect.py,大概率会陷入一种“既兴奋又憋屈”的状态:模型能框出目标,但每次都要去改 XML 配置、敲 python detect.py --source xxx.mp4,检测结果只能留在控制台和输出目录里。而业务方或导师要的是一个能双击打开、拖个文件进去就能看结果的工具。这时候你需要的是一个桌面壳子,把 YOLOv5 的推理能力包进去,PyQt5 就是这个壳子最成熟的选项。
PyQt5 和 YOLOv5 的组合之所以是桌面端目标检测的常见做法,原因有三:PyQt5 的信号槽机制天然适合做“界面按钮触发后台推理”这种异步交互;YOLOv5 的模型封装足够干净, torch.load 之后拿到 model() 就能直接推理,不需要改模型结构;Python 胶水层让两者衔接几乎没有额外开销。这套组合适合三类人:要做课程设计或毕业设计的在校生、要给公司内部做标注或质检工具的工程师、以及想把算法 demo 包装成可用产品的独立开发者。
这篇文章从环境搭建讲到打包发布,中间的推理线程设计、模型转换、性能调优都是实际项目中翻过车才总结出来的,直接照着做能省掉至少一周的摸索时间。
2. 环境与工程骨架:装在哪个环境里、代码怎么组织
2.1 版本选型:Python 版本、PyQt5 和 YOLOv5 的兼容矩阵
环境选型是最容易翻车的地方,因为 PyQt5 和 YOLOv5 对 Python 版本的要求会打架。YOLOv5 官方在 v6.0 之后要求 Python 3.7 以上,但 PyQt5 在 Python 3.10 以上偶尔会遇到 sip 编译异常。我实际用下来的稳妥组合是 Python 3.8 或 3.9 + PyQt5 5.15.x + YOLOv5 v6.0/v7.0,这个组合在 Windows 和 Ubuntu 上都跑过,稳定性最好。
# 创建虚拟环境,避免污染系统 Python conda create -n yolo_gui python=3.9 conda activate yolo_gui # 安装 PyQt5,5.15 系列的最后一个版本兼容性最好 pip install PyQt5==5.15.10 PyQt5-sip==12.13.0 # 安装 YOLOv5 依赖,注意 requirements.txt 里不需要全部装 pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install opencv-python numpy==1.23.5参数说明:torch 版本不用追最新,1.13.1 配合 CUDA 11.7 在 YOLOv5 v6.0/v7.0 下表现稳定;numpy 版本必须锁 1.23.5,因为新版 numpy 移除了 np.int 等别名,会导致 YOLOv5 的通用检测代码在类型转换时报错。conda 不是必须的,但虚拟环境能让你在打包时少处理一堆版本冲突。
2.2 工程目录结构:推理与界面分离是底线
界面代码和推理代码揉在同一个文件里是新手最容易犯的错。一个只有屏幕截图功能的 demo 还好说,一旦要加视频检测、多线程、模型热切换,单文件的维护成本会指数级上升。我维护的项目一般按这个结构组织:
yolo_gui/ ├── main.py # 程序入口,只负责启动 QApplication ├── ui/ │ ├── __init__.py │ ├── main_window.py # 主窗口类,负责布局和信号连接 │ └── view_widget.py # 自定义画布控件,用于显示检测结果 ├── core/ │ ├── __init__.py │ ├── detector.py # 模型加载与推理封装 │ ├── thread_worker.py # QThread 工作线程封装 │ └── config.py # 模型路径、置信度阈值、类别过滤等常量 ├── models/ # 放训练好的 .pt 或转换后的 .onnx └── resources/ # 图标、qss 样式文件这个结构的核心逻辑是:ui 目录只认识 core 目录提供的接口,比如 detector.detect_frame(frame) 输入一帧图像返回标注后的图像和检测结果列表,core 目录完全不感知界面的存在。这样做的好处是你可以先把 detector.py 用命令行测试,再接入界面,排查问题时能准确定位是推理环节出错还是界面显示环节出错。
# core/detector.py 的接口设计示例 class Detector: def __init__(self, weights: str, conf_thres=0.25, iou_thres=0.45): self.model = torch.load(weights, map_location='cpu')['model'].float() self.model.eval() self.conf_thres = conf_thres self.iou_thres = iou_thres def detect_frame(self, frame: np.ndarray) -> tuple: # 返回 (标注后图像, [检测结果列表]) # 接口只接受 numpy 数组,不接受文件路径 # 这样视频流和单张图片共用同一个推理入口 pass逻辑说明:detect_frame 的入参设计成 np.ndarray 而不是文件路径,是因为摄像头帧、视频帧、拖拽图片读出来的都是 ndarray,统一接口后上层调用只有一行代码。这个设计看着简单,但能让你后续加视频检测时少写一套重复逻辑。
2.3 跑通最小骨架:先验证 PyQt5 窗口能弹出来
不要一上来就接模型,先把 PyQt5 的窗口骨架跑通,确认环境没问题。
# main.py 最小可运行版本 import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PyQt5 + YOLOv5 检测工具") self.resize(1280, 800) self.label = QLabel("窗口已启动,等待接入模型推理") self.setCentralWidget(self.label) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())这个骨架验证两件事:PyQt5 的库依赖是否完整,窗口能否正常弹出。如果运行报错,先看 PyQt5.sip 版本是否匹配,这是最常见的翻车点。确认窗口能弹出来后,再往里面加按钮、文本框、画布控件。
3. 界面布局与交互设计:排队等待是桌面工具的门面
3.1 主界面布局:左右分栏 + 底部状态栏是最实用的结构
桌面检测工具用左右分栏布局是实际项目里磨合出来的结果:左侧是操作区和检测结果列表,右侧是大画布用来显示原始图像和标注结果。底部状态栏显示推理耗时和当前帧率。这种布局的优点是操作逻辑从上到下、视觉焦点在右侧大屏,符合使用习惯。
# ui/main_window.py 布局核心代码 from PyQt5.QtWidgets import (QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, QListWidget, QFileDialog, QComboBox, QSpinBox, QSplitter) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PyQt5 + YOLOv5 桌面检测工具") self.resize(1440, 900) # 右侧画布用 QLabel 充当,后续替换为自定义控件 self.canvas_label = QLabel("检测结果将显示在这里") self.canvas_label.setStyleSheet("background-color: #2b2b2b; color: #cccccc;") self.canvas_label.setAlignment(Qt.AlignCenter) # 左侧操作面板 left_widget = QWidget() left_layout = QVBoxLayout() self.open_img_btn = QPushButton("打开图片") self.open_video_btn = QPushButton("打开视频") self.open_camera_btn = QPushButton("开启摄像头") self.model_combo = QComboBox() # 模型选择下拉框 self.conf_spin = QSpinBox() # 置信度调节 self.result_list = QListWidget() # 检测结果列表 left_layout.addWidget(self.open_img_btn) left_layout.addWidget(self.open_video_btn) left_layout.addWidget(self.open_camera_btn) left_layout.addWidget(QLabel("模型选择:")) left_layout.addWidget(self.model_combo) left_layout.addWidget(QLabel("置信度阈值:")) left_layout.addWidget(self.conf_spin) left_layout.addWidget(QLabel("检测结果:")) left_layout.addWidget(self.result_list) left_layout.addStretch() left_widget.setLayout(left_layout) # 左右分栏 splitter = QSplitter(Qt.Horizontal) splitter.addWidget(left_widget) splitter.addWidget(self.canvas_label) splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 3) self.setCentralWidget(splitter)布局逻辑说明:QSplitter 比直接 setCentralWidget 一个固定布局要好,因为用户能自己拖动分割条调整左右比例,屏幕小的笔记本上可以把左侧收窄。setStretchFactor 设置为 1:3 能让画布区域默认占据三分之二的宽度,这是视觉上最舒适的比例。底部状态栏用 QStatusBar 就行,这里先省略,后面接上推理耗时显示。
3.2 信号槽连接:按钮、下拉框、阈值调节怎么协同
界面搭好后,要把控件信号连接到对应的处理函数。核心原则是:所有耗时操作必须在子线程执行,主线程只负责界面刷新。如果直接在主线程里跑模型推理,界面会卡死到系统提示“无响应”。
# 信号槽连接核心代码 from PyQt5.QtCore import QThread, pyqtSignal class InferenceThread(QThread): frame_ready = pyqtSignal(object, object, float) # 画面, 结果列表, 耗时 error_occurred = pyqtSignal(str) def __init__(self, detector, source_type, source_path): super().__init__() self.detector = detector self.source_type = source_type # 'image' / 'video' / 'camera' self.source_path = source_path self.running = True def run(self): try: if self.source_type == 'image': self.process_image() elif self.source_type == 'video': self.process_video() else: self.process_camera() except Exception as e: self.error_occurred.emit(str(e)) finally: self.running = False信号槽参数说明:frame_ready 这个信号携带三个参数:处理后的图像对象、检测结果列表、单帧推理耗时。界面收到信号后只做两件事——往画布上贴图和刷新状态栏,绝不在这里做任何图像处理或文件操作。error_occurred 信号用来把子线程里的异常抛回主线程弹出错误框,避免子线程崩溃后界面毫无反应。
3.3 图片/视频/摄像头三种输入路径的统一处理
三种输入源的共同归宿是逐帧调用 detector.detect_frame(),差别只在于帧的来源不同。视频和摄像头需要循环读帧,图片只需要处理一次。这里要小心一个坑:OpenCV 读出来的 BGR 格式和 PyQt5 显示用的 RGB 格式必须做转换,否则颜色会偏蓝。
# 图片检测路径代码 def process_image(self): frame = cv2.imread(self.source_path) annotated, results, t_infer = self.detector.detect_frame(frame) # OpenCV 默认 BGR,PyQt5 显示需要 RGB rgb_image = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.frame_ready.emit(qt_image, results, t_infer)这段代码的坑点在于 QImage 构造时传入的 bytes_per_line 必须等于 width × channels,很多人在图像宽度不是 4 的倍数时会出现图像错位的诡异现象,就是因为没算对每行字节数。另外 rgb_image.data 必须保证在信号发出前不被垃圾回收,emit 之后再无引用才会安全释放,所以这里直接传递 QImage 对象而不是原始 ndarray。
4. 模型加载与推理线程:YOLOv5 接入 PyQt5 的核心封装
4.1 YOLOv5 的两种加载方式:TorchScript 与裸权重
YOLOv5 的模型加载有两种主流路径:直接加载 .pt 权重后用 torch.load 解析,或者导出为 TorchScript 再用 torch.jit.load 加载。我推荐后者。原因有三条:TorchScript 模型不依赖原始模型定义代码,部署时只需要一个 .pt 文件;加载速度比裸权重快十倍以上;避免因为 YOLOv5 仓库代码版本变动导致的兼容性失效。
# core/detector.py 完整推理封装 import torch import cv2 import numpy as np from pathlib import Path class Detector: def __init__(self, weights_path: str, device: str = 'cpu', conf_thres: float = 0.25, iou_thres: float = 0.45): self.device = torch.device(device) # 优先加载 TorchScript 版本,退化到裸权重 if str(weights_path).endswith('.torchscript'): self.model = torch.jit.load(weights_path, map_location=self.device) else: ckpt = torch.load(weights_path, map_location=self.device) self.model = ckpt['model'].float().to(self.device) self.model.eval() self.conf_thres = conf_thres self.iou_thres = iou_thres # YOLOv5 模型自带类别名列表 self.names = self.model.names if hasattr(self.model, 'names') else \ self.model.module.names if hasattr(self.model, 'module') else [] def detect_frame(self, frame_bgr: np.ndarray): t0 = time.time() # 预处理:BGR 转 RGB,resize 到 640x640,归一化 img = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) img_tensor = torch.from_numpy(img).float().to(self.device).permute(2, 0, 1) img_tensor = img_tensor / 255.0 img_tensor = img_tensor.unsqueeze(0) # 推理 with torch.no_grad(): pred = self.model(img_tensor)[0] # 后处理:NMS 过滤冗余框 pred = self.non_max_suppression(pred, self.conf_thres, self.iou_thres) t_infer = time.time() - t0 # 绘制结果 annotated = frame_bgr.copy() results = [] for det in pred[0]: x1, y1, x2, y2, conf, cls = det.tolist() x1, y1, x2, y2 = int(x1*640/frame_bgr.shape[1]), int(y1*640/frame_bgr.shape[0]), \ int(x2*640/frame_bgr.shape[1]), int(y2*640/frame_bgr.shape[0]) label = f"{self.names[int(cls)]} {conf:.2f}" cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(annotated, label, (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) results.append({"class": self.names[int(cls)], "conf": conf, "bbox": [x1, y1, x2, y2]}) return annotated, results, t_infer逻辑说明:这里的关键是把 YOLOv5 原始输出的坐标映射回原始图像尺寸。YOLOv5 的推理统一在 640×640 下进行,输出框坐标是相对 640×640 的,必须按原图比例缩放回去,否则框的位置会错位。non_max_suppression 是 YOLOv5 仓库自带的函数,建议直接 import 而不是自己重写,自己写的 NMS 在边界框重叠时容易漏检。
4.2 设备选择与推理性能:GPU 显存不够时的正确玩法
模型加载时设备选择要注意显存容量。YOLOv5s 模型在 GPU(显存 4GB 以上)上能跑到 60 FPS 以上,但小显存卡比如 2GB 的笔记本独显,连续推理视频时会出现 CUDA out of memory 崩溃。
# 设备自动选择逻辑 def auto_select_device(): if torch.cuda.is_available(): free_mem = torch.cuda.mem_get_info()[0] / 1024**3 if free_mem > 2.0: return 'cuda:0' else: print(f"显存不足 ({free_mem:.1f}GB),回退到 CPU") return 'cpu' return 'cpu'这段逻辑解决的是“有 GPU 但显存不够”的尴尬场景。检测 GPU 可用显存而不是只看 torch.cuda.is_available(),能避免推理跑起来几秒后才崩的体验。CPU 推理也不是不能用:YOLOv5s 在 CPU 上单帧约 80~150ms(取决于 CPU 型号),对于图片检测和低速视频检测完全够用,只是视频帧率会被拉低到 10 FPS 以下。这个延迟对互动性要求不高的场景可以接受。
4.3 模型导出为 TorchScript:一条命令解决部署难题
模型导出是部署前必须做的一步,因为 .pt 裸权重在换机器运行时需要安装 YOLOv5 整个仓库才能加载,TorchScript 格式不存在这个问题。
# 在 YOLOv5 仓库目录下执行导出 python export.py --weights runs/train/exp/weights/best.pt \ --include torchscript \ --img 640 \ --batch-size 1参数说明:--include torchscript 表示只导出 TorchScript 格式,不需要同时导出 ONNX 等其他格式,导出速度更快。--img 必须和训练时的输入尺寸一致,YOLOv5 默认是 640,如果你训练时改过 imgsz,导出也要改成对应尺寸。导出成功后会生成 best.torchscript 文件,这个文件就是 4.1 节代码里 detector 直接加载的对象。
4.4 视频推理的帧率控制:别让推理线程把 CPU 打满
视频推理和摄像头推理有一个隐蔽的问题:如果不对读取帧做间隔控制,推理线程会把 CPU 或 GPU 跑满,导致整台电脑卡顿,界面也响应迟钝。
# 视频推理帧率控制 def process_video(self): cap = cv2.VideoCapture(self.source_path) fps = cap.get(cv2.CAP_PROP_FPS) interval = 1.0 / min(fps, 30) # 最多 30 FPS,避免资源耗尽 while self.running: t_start = time.time() ret, frame = cap.read() if not ret: break annotated, results, t_infer = self.detector.detect_frame(frame) rgb_image = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qt_image = QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(qt_image.copy(), results, t_infer) # 控制帧率:保证实际处理速度不超过设定值 elapsed = time.time() - t_start sleep_time = interval - elapsed if sleep_time > 0: time.sleep(sleep_time) cap.release()这段代码值得注意的地方是 qt_image.copy(),如果不复制,连续 emit 之后同一块内存会被重复使用,画面会出现闪烁或花屏。帧率上限设 30 FPS 是因为桌面检测工具不是游戏,人眼对超过 30 FPS 的检测标注画面差异感知很弱,而压低帧率能显著降低 CPU 和散热压力。
5. 避坑指南:PyQt5 和 YOLOv5 结合时的 5 个常见问题
5.1 界面卡死:信号槽用错线程
现象:点击“打开视频”按钮后,窗口标题栏显示“无响应”,拖不动窗口,几分钟后系统提示进程已停止工作。
原因:推理循环直接写在了按钮的 clicked 信号处理函数里,主线程被 while 循环阻塞,没法处理窗口绘制和鼠标消息。这是新手最常见的翻车。
解决:所有推理逻辑放进 QThread 子类的 run() 方法,主线程只负责创建线程、启动线程、接收信号更新界面。线程生命周期要管理好,关闭窗口时必须线程退出,否则进程残留。
5.2 摄像头打不开但 OpenCV 单独测试正常
现象:单独运行 cv2.VideoCapture(0) 能出画面,但放进 PyQt5 界面里就报错或黑屏。
原因:PyQt5 的 QApplication 初始化后,Qt 的事件循环和 OpenCV 的摄像头调用在某些平台上有冲突,尤其是 Windows 上 PyQt5 5.15.x 和 OpenCV 旧版本存在兼容性问题。
解决:把摄像头初始化也放进子线程,并且先调用 cv2.CAP_DSHOW 后端初始化摄像头,代码写法是 cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)。另外注意关闭摄像头时不要直接 del cap,先调用 cap.release() 再等几毫秒,否则下次打开会报“设备被占用”。
5.3 检测框位置偏移到左上角
现象:模型检测出目标了,但标注框画的区域和真实目标位置完全对不上,框总是偏向图像的左上角区域。
原因:检测结果坐标没有从 640×640 映射回原图尺寸。YOLOv5 把输入缩放到 640×640 后输出框坐标,如果不按原图宽高比例还原,框的位置必然错乱。而且这种错乱在图像不是正方形时特别明显。
解决:在绘制前用缩放比例换算坐标,注意宽高要分别算,不能用一个统一比例。
scale_x = frame.shape[1] / 640 scale_y = frame.shape[0] / 640 x1, y1, x2, y2 = int(x1 * scale_x), int(y1 * scale_y), int(x2 * scale_x), int(y2 * scale_y)5.4 打包后模型加载失败
现象:PyInstaller 打包出来的 exe 在别的电脑上双击运行报错,提示找不到 .pt 文件或 torch 相关模块缺失。
原因:PyInstaller 不会自动收集 YOLOv5 动态导入的模块和模型文件。torch 在运行时有很多动态加载的库,默认打包配置会漏掉。
解决:打包时用 --collect-all torch 收集完整依赖,模型文件用 --add-data 打进去或者放在 exe 同目录。
pyinstaller main.py --name YoloDetector --windowed --collect-all torch --add-data "models/best.torchscript;models" --hidden-import cv25.5 QImage 显示图像颜色不对
现象:图像显示出来了,但整体颜色发蓝发红,和 OpenCV 保存出来的结果不一致。
原因:OpenCV 的颜色通道顺序是 BGR,而 QImage 的 Format_RGB888 期望的是 RGB 格式。直接拿 BGR 数据放进去,红色和蓝色会被交换。
解决:用 cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 转换后再构造 QImage,这是所有涉及图像显示的场景都必须做的操作。写着简单,但几乎每个人都会漏掉一次。
6. 打包发布与性能监控:把工具交给别人的正确姿势
6.1 PyInstaller 打包参数详解与体积控制
打包是桌面工具落地的最后一步,也是门槛最高的一步。PyInstaller 的默认参数打出来的 exe 通常有 1GB 以上,因为 torch 和 CUDA 库都被完整收集了。体积优化有几个实际手段:只打包 CPU 版本的 torch 推理代码,体积能降一半;用 UPX 压缩可执行文件;模型放在 exe 外部而不是打进包里。但体积不是最终目标,稳定才是。我见过只追求小体积把 USB 摄像头支持库漏掉的翻车案例,那比体积大更难受。
# 推荐的打包命令 pyinstaller main.py \ --name YoloDetector \ --windowed \ --onefile \ --collect-all torch \ --hidden-import cv2 \ --add-data "models/best.torchscript;models" \ --exclude-module matplotlib \ --exclude-module pandas \ --log-level WARNING参数说明:--onefile 生成单个 exe,便于分发但启动时解压慢,首次启动可能等待 5~10 秒。--exclude-module 排除用不到的重量级库可以减少体积。注意 --add-data 在 Windows 下用分号分隔源路径和目标目录,在 Linux/macOS 下用冒号,这个写错会导致运行时找不到模型文件。打包完成后要在干净的虚拟机或另一台电脑上测试,因为开发机上跑得通不代表换台机器没问题。
6.2 推理耗时的可视化监控
工具交给使用者之后,最常被问的问题是“为什么这么慢”。与其口头解释,不如把性能数据直接显示在界面上。
# 在状态栏显示实时性能参数 self.statusBar().showMessage( f"推理耗时: {t_infer*1000:.0f} ms | " f"FPS: {1/max(t_infer, 0.001):.1f} | " f"检测目标: {len(results)}" )这段代码放在 frame_ready 信号的接收函数里。推理耗时用毫秒显示比秒更直观,FPS 用单帧耗时的倒数计算。加上检测目标数量能让使用者确认模型确实在干活。状态栏实时刷新的开销可以忽略,但注意不要在状态栏消息里做字符串拼接以外的任何操作。
6.3 日志记录与异常兜底
桌面工具最怕的场景是:用户双击打开后黑屏或闪退,而且不反馈任何报错。给工具加日志写入是必须的习惯。设置环境变量 PYTHONUNBUFFERED=1 和写入日志文件的两段逻辑能让问题定位时间从几天缩短到几分钟。
import logging logging.basicConfig( filename="yolo_gui.log", level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" ) # 在关键节点写入日志 logging.info(f"模型加载完成: {weights_path}") logging.info(f"开始推理: source={source_path}, device={device}")这里要强调一点:日志文件路径必须设置为当前工作目录,不要去写绝对路径,因为用户可能把工具放在任意目录下。如果用户反馈工具打不开,先让他把 yolo_gui.log 发过来,九成的问题看一眼日志末尾的异常栈就能定位。
另外给窗口关闭事件加一个兜底逻辑,确保视频推理线程在窗口关闭时能正确退出。我的习惯是维护一个全局的线程引用列表,窗口关闭时逐个请求终止线程并等待其退出,避免关窗后进程残留在任务管理器里无法结束。
def closeEvent(self, event): # 窗口关闭时请求推理线程退出 if hasattr(self, 'inference_thread') and self.inference_thread.isRunning(): self.inference_thread.running = False self.inference_thread.wait(3000) # 最多等 3 秒 event.accept()从第一个能弹窗的骨架到交付给别人用的工具,这套方案的坑基本都在前面五章踩过了。我自己的教训是:永远不要相信“在开发机上跑通了就能用”,把打包后的 exe 拿到一台没有 Python 环境的干净机器上测一遍,才是真的完成。希望帮到你。
本文还有配套的精品资源,点击获取