news 2026/11/2 13:36:11

PyQt5+YOLOv5桌面检测:QThread线程、图像转换与PyInstaller打包

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyQt5+YOLOv5桌面检测:QThread线程、图像转换与PyInstaller打包

简介:这是一套面向 PyQt5 与 YOLOv5 初学者的多目标检测 GUI 实战项目,适合刚接触界面开发或 YOLO 算法、希望用完整案例练手的人群。项目以 Python 和 PyTorch 为基础,重点演示界面设计与后端推理逻辑分离,并串联常见 PyQt5 控件的使用方式,同时提供 YOLOv5 源码与配置参考,便于理解检测流程与工程组织。压缩包共 112 个文件,约 83.46MB,包含 py 源码与 pyc 字节码、yaml 模型配置、pt 权重、ui 界面文件、sh 脚本,以及 mp4 演示和 jpg/png 图像素材,兼顾代码阅读、环境搭建与效果验证。已有 8884 人学习,读者可据此完成一个可运行的检测 GUI,并从目录结构与模块划分中掌握前后端解耦、控件联动和模型加载的思路,适合与 PyQt5 开发书籍配合实践。

1. 别把推理塞进按钮回调:PyQt5 + YOLOv5 的胶水层才是最难的部分

在 PyQt5 里点一下“开始检测”,界面白屏三秒,鼠标变成转圈,用户以为程序崩了——这是绝大多数人把 YOLOv5 塞进 GUI 的第一版。问题不在模型,在胶水层:推理是毫秒到秒级的阻塞调用,Qt 的事件循环是单线程的,两者硬塞在一起必然互相拖死。PyQt5 + YOLOv5 + PyTorch 这套组合的真正价值,不在于“能在桌面上跑检测”,而在于它把线程、图像格式转换、模型生命周期三件事拆得足够清楚,让你能在一个 Python 工程里同时管住界面和推理。适合手上有自训练权重、需要交付一个双击就能跑的桌面工具、又不想上前后端分离那一套的开发者。下面从一个能复现的最小闭环讲起,一直到打包成 exe。

2. 选型与环境:为什么是 PyQt5 而不是 PySide6,为什么是 YOLOv5 而不是更新的版本

2.1 三个部件各自承担什么工作

很多人一开始就搞混了职责边界,导致后面越写越乱。PyQt5 只负责两件事:控件布局和事件循环。它不知道什么是张量,也不该知道。它的核心是 QApplication 里那个不断转的 while 循环,负责派发鼠标、键盘、重绘事件;一旦某个槽函数里执行了耗时操作,这个循环就被卡住,所有控件停止响应,窗口变灰。这就是为什么所有推理代码必须离开主线程。

YOLOv5 负责的是从一张 BGR 的 ndarray 到一个检测结果对象。这个对象里有三样你要用的东西:xyxy[0]是 N×6 的张量(x1, y1, x2, y2, conf, cls),names是类别索引到名称的字典,render()能直接把框画回原图。前处理里的 letterbox 缩放、归一化、通道转置,全都封装在模型调用的__call__里了,你不用管。这一点是 YOLOv5 相对手写推理脚本最大的省事之处。

PyTorch 则是运行时。它决定了你能不能上 GPU、显存怎么分配、模型加载要多久。桌面端有个容易被忽略的事实:torch 的 import 本身就要 1~3 秒,模型加载再花 1~5 秒,如果把这些放在窗口构造函数里,用户点开程序看到的就是几秒钟的白窗口。常见做法是先把主窗口显示出来,再在后台线程里加载模型,或者做一个启动遮罩。

至于为什么选 PyQt5:不是因为它更先进,而是因为 Qt Designer 拖界面 +pyuic5生成代码这条链路最成熟,控件文档最多,遇到“窗口插件找不到”这类问题时能搜到的答案也最多。PySide6 的许可证更宽松,但 API 细节差异(信号槽写法、枚举路径)会让从旧教程抄来的代码报一堆 AttributeError。新手期少一个变量就少一次翻车。

2.2 版本固定:一份能复现的环境清单

这套组合最难受的地方在于依赖打架。PyQt5 对 Python 版本敏感,torch 对 numpy 版本敏感,opencv-python 又对 numpy 有要求。不锁版本,换台机器就是另一番景象。我自己验证过的一组组合如下,按这个装基本不会出问题:

组件版本说明
Python3.9.x3.10 以上 PyQt5 轮子偶发缺失
PyQt55.15.95.15.10 之后的 SIP 版本有兼容问题
torch1.13.1 / 2.0.1有卡选 cu117,无卡选 cpu 版
torchvision0.14.1 / 0.15.2必须与 torch 版本对齐
numpy1.23.5 / 1.24.4千万别上 2.x
opencv-python4.8.0.764.9 之后部分 API 有调整
pyinstaller5.13.26.x 打包 torch 需要额外 hook

numpy 这条最容易踩。装成 2.x 之后,import torch会直接抛A module that was compiled using NumPy 1.x cannot be run in NumPy 2.x,报错信息很长,但根因就一句话。装环境时用 conda 建独立环境,先装 torch,再装其余:

# 建环境,指定 Python 版本,避免污染全局 conda create -n yolo_gui python=3.9 -y conda activate yolo_gui # 先把 numpy 钉在 1.x,再装 torch,顺序反了会被自动升级 pip install "numpy==1.23.5" pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 \ --extra-index-url https://download.pytorch.org/whl/cu117 # 无独显改成: # pip install torch==1.13.1+cpu torchvision==0.14.1+cpu \ # --extra-index-url https://download.pytorch.org/whl/cpu pip install PyQt5==5.15.9 opencv-python==4.8.0.76 pyinstaller==5.13.2

命令里--extra-index-url指向的是 PyTorch 官方轮子源,cu117 表示 CUDA 11.7 编译版本,要和本机驱动支持的 CUDA 上限匹配。装完立刻做一次自检,把下面这段存成check_env.py跑一遍,比后面在 GUI 里排查省事十倍:

import sys, numpy, cv2, torch from PyQt5.QtCore import QT_VERSION_STR print("python :", sys.version.split()[0]) print("numpy :", numpy.__version__) # 必须是 1.x print("opencv :", cv2.__version__) print("torch :", torch.__version__) print("cuda :", torch.cuda.is_available()) # 有卡应为 True print("qt :", QT_VERSION_STR)

torch.cuda.is_available()返回 False 但你有显卡,通常是驱动版本不够或者装成了 cpu 版轮子,torch.__version__里带+cpu就是后者,别去改代码,重装。

2.3 先写一个纯命令行验证脚本,别急着开界面

跳过 GUI 直接验证模型,是省时间的关键。界面出问题时你无法判断是模型的问题还是 Qt 的问题,多一层变量就多一轮折腾。先把 YOLOv5 仓库目标准备好,权重放进去,然后跑一段最小推理:

import torch, cv2 # 第一个参数指向本地仓库目录,source="local" 禁止联网拉取 # 第二个参数 "custom" 表示用仓库里的 hubconf 加载自定义权重 model = torch.hub.load( "./yolov5", "custom", path="./weights/best.pt", # 换成你自己的权重 source="local", device="cuda:0", # 没卡写 "cpu" ) model.conf = 0.25 # 置信度阈值,低于此值的框直接丢弃 model.iou = 0.45 # NMS 的 IoU 阈值,重叠超过此值的框合并 model.eval() img = cv2.imread("test.jpg") # 读进来是 BGR with torch.no_grad(): # 推理不需要梯度,省显存 results = model(img, size=640) # size 是推理分辨率,须为 32 的倍数 print(results.xyxy[0].cpu().numpy()) # N×6:x1,y1,x2,y2,conf,cls cv2.imwrite("out.jpg", results.render()[0]) # render 返回 BGR 的标注图

几个参数值得说清楚。source="local"不加的话 torch.hub 会去联网 clone 仓库,内网环境直接卡死,这个坑很隐蔽,报错往往只显示超时。size不是越大越好,640 是 YOLOv5 训练时的默认值,改成 1280 精度可能涨一点但耗时翻数倍,桌面端实时场景一般守 640。model.eval()不调用的话 BN 层会走训练分支,结果和精度都会飘。

results.render()有个细节:它是在results.imgs上原地画框并返回列表,取[0]拿到的是第一张图的 BGR ndarray,尺寸和输入一致(letterbox 已经还原)。这也意味着你不能把原图重复传给它两次,第二次会画重影。

这段跑通,输出一张画了框的 out.jpg,说明模型、权重、CUDA 都没问题,可以开始接界面了。

3. 把推理搬进子线程:QThread 与 pyqtSignal 的完整写法

3.1 主线程到底被阻塞了什么

Qt 的事件循环是个死循环,所有槽函数都在这个循环里同步执行。你在“开始检测”按钮的槽里写results = model(frame),这一帧耗时 30ms,界面就卡 30ms;如果是从摄像头连续读帧,那就是每帧卡一次,窗口拖动、按钮点击全部排队。用户感知到的不是“慢”,是“卡死”。

更严重的是“窗口无响应”白屏。Windows 在窗口超过 5 秒不处理消息时会给标题栏加上“未响应”,这属于系统级判定,一旦出现,用户基本就认为程序挂了。推理大图或者首次加载模型时,这个时间很容易突破 5 秒。

所以正确做法只有一条:推理放进 QThread 的run(),结果通过信号发回主线程。Qt 的信号槽跨线程默认是队列连接,emit 出去的数据会被打包进事件队列,由主线程在事件循环里取出执行,天然线程安全。但这条安全规则有个前提——信号参数必须是可跨线程传递的类型,ndarray 这种 Python 对象要声明成object。

3.2 检测线程类的完整实现

下面这个类是我用了很多次的结构,覆盖图片、视频文件、摄像头三种源:

import time import cv2 import torch from PyQt5.QtCore import QThread, pyqtSignal class DetectWorker(QThread): # object 用于传 ndarray,Qt 会自动做引用计数管理 frame_ready = pyqtSignal(object) stats_ready = pyqtSignal(float, int) # fps, 本帧目标数 failed = pyqtSignal(str) def __init__(self, model, source, conf=0.25, iou=0.45, imgsz=640, parent=None): super().__init__(parent) self.model = model self.source = source # 0 表示摄像头,或文件路径 self.conf = conf self.iou = iou self.imgsz = imgsz self._running = False self._paused = False def run(self): self._running = True cap = cv2.VideoCapture(self.source) if not cap.isOpened(): self.failed.emit(f"视频源打不开: {self.source}") self._running = False return # 阈值每次启动时同步给模型,界面改了参数重开线程即可生效 self.model.conf = self.conf self.model.iou = self.iou prev = time.time() while self._running: if self._paused: self.msleep(30) # 暂停时让出 CPU,别空转 continue ok, frame = cap.read() if not ok: break # 视频读完或摄像头断开 with torch.no_grad(): results = self.model(frame, size=self.imgsz) annotated = results.render()[0] count = len(results.xyxy[0]) now = time.time() self.frame_ready.emit(annotated) self.stats_ready.emit(1.0 / max(now - prev, 1e-6), count) prev = now cap.release() self._running = False def pause(self, flag: bool): self._paused = flag def stop(self): self._running = False # 给循环 3 秒收尾,超时则不再等待,避免关窗时卡死 self.wait(3000)

run()里没有一行业务以外的代码,这是刻意的。cap.release()放在循环外,保证异常退出时也能释放设备,否则摄像头会被独占,下次启动报“无法打开”。self.msleep(30)而不是pass,是因为空 while 会把 CPU 打满,任务管理器里显示 100% 占用,用户第一反应是程序有病毒。

stop()里的wait(3000)是必需的收尾。QThread 对象如果在线程还在跑的时候被析构,Qt 会抛QThread: Destroyed while thread is still running并可能直接崩溃。先置标志位让循环自然退出,再等待,是最稳的顺序。

3.3 参数从界面传到线程

界面上的置信度滑块、IoU 输入框、设备下拉框,这些值不应该直接在线程运行中被改写,否则会出现读到一半参数被换掉的情况。常见做法是启动时一次性传进去,改参数就停掉重开。参数含义和取值区间如下:

参数典型值调大之后调小之后
conf0.25框变少、误检下降、漏检上升框变多、召回上升、误检上升
iou0.45密集目标保留更多重叠目标合并更狠
imgsz640小目标更准、耗时上升速度快、小目标丢失
devicecuda:0—cpu 兼容性最好

主窗口里启动线程的写法:

def on_start(self): if self.worker and self.worker.isRunning(): return # 防重复点击 self.worker = DetectWorker( model=self.model, source=self.current_source(), # 0 或文件路径 conf=self.ui.conf_slider.value() / 100, # 滑块 0-100 映射到 0-1 iou=self.ui.iou_slider.value() / 100, imgsz=int(self.ui.size_box.currentText()), ) self.worker.frame_ready.connect(self.on_frame) self.worker.stats_ready.connect(self.on_stats) self.worker.failed.connect(self.on_error) self.worker.start() # 非阻塞,立刻返回

滑块整数除以 100 得到浮点阈值,这一步别省。直接value()传进去会让 conf 变成 25,模型会认为阈值远超 1,一个框都不出,新手容易在这卡半天。start()是异步的,调用完立刻返回主线程,界面照常响应。

on_frame槽里只做显示相关的事,绝不能再做推理或大图运算:

def on_frame(self, bgr): self.ui.label.setPixmap(self.to_pixmap(bgr, self.ui.label.size()))

3.4 暂停、停止与窗口销毁

暂停用标志位而不terminate()。QThread 的 terminate 是强杀,模型可能正持有 CUDA 上下文,强杀之后显存不会释放,下一次推理直接报 out of memory。这个坑我踩过一次,重启程序才好。

窗口关闭时要覆写closeEvent,把线程停干净再接受事件:

def closeEvent(self, event): if self.worker and self.worker.isRunning(): self.worker.stop() # 内部已 wait(3000) event.accept()

如果不写这段,用户点右上角关闭,Python 进程可能残留在后台,任务管理器里能看到一堆同名进程,下次启动抢摄像头设备。

4. 图像链路:BGR 到 QImage 到 QPixmap 的三次转换

4.1 三种格式的对应关系

这条链路上有三个角色,搞混顺序就是一片红蓝互换或者花屏。OpenCV 读图和 YOLOv5 的render()输出都是 BGR 三通道 ndarray,内存布局是 H×W×3;QImage 需要 RGB888,同样是 H×W×3 但要按行对齐;QPixmap 是 Qt 的显示对象,构造之后就不再依赖原始内存。

阶段对象通道顺序归属
读图/推理输出np.ndarrayBGRPython
中间转换QImageRGBQt 管理,可引用外部内存
显示QPixmapRGBQt 管理,已拷贝

关键在第 2 到第 3 步。QImage 有一个构造函数可以接受外部 buffer 指针,不拷贝,性能好,但如果那个 ndarray 在 QImage 还在用的时候被回收,就会显示随机噪点或者直接崩溃。这就是后面要说的.copy()问题。

4.2 转换函数怎么写才不花屏

import cv2 import numpy as np from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt def bgr_to_qimage(bgr: np.ndarray) -> QImage: """BGR ndarray -> QImage,内部已拷贝,可安全脱离原数组""" if bgr is None or bgr.size == 0: return QImage() if bgr.ndim == 2: # 灰度图补通道 bgr = cv2.cvtColor(bgr, cv2.COLOR_GRAY2BGR) rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) # BGR -> RGB rgb = np.ascontiguousarray(rgb) # 保证内存连续 h, w, ch = rgb.shape bytes_per_line = ch * w # 无行填充 # .copy() 必须加:让 QImage 拥有自己的内存 return QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888).copy() def bgr_to_pixmap(bgr: np.ndarray, target_size) -> QPixmap: """按 QLabel 尺寸等比缩放,避免图片被拉伸变形""" img = bgr_to_qimage(bgr) pix = QPixmap.fromImage(img) return pix.scaled(target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)

np.ascontiguousarray不是可选项。经过 letterbox 还原、切片运算后的数组可能是不连续的视图,此时rgb.data拿到的指针 Qt 读起来会错位,表现出来就是画面上有几条斜纹,非常玄学,调半天还以为模型输出坏了。

bytes_per_line直接乘ch是因为我们已经是紧凑排列的数组,不需要 Qt 默认的 4 字节对齐。如果传了错误的对齐值,会出现图像被斜着切开的视觉效果。

scaled用KeepAspectRatio而不是IgnoreAspectRatio,后者会把 16:9 的视频压成 QLabel 的方形比例,人看着是扁的。SmoothTransformation比默认的FastTransformation慢一点,但缩小图像时锯齿少很多,视觉上更可信。

4.3 那个经典的内存坑

QImage(rgb.data, w, h, ...)这个构造函数是不拷贝的,它只是记住指针。如果后面没有.copy(),等 Python 的rgb被 GC 回收,QImage 里的指针就悬空了。表现是什么?图片显示几帧之后开始出现随机色块,或者时好时坏,换个机器又是另一种症状。

我在一个批量检测的项目上被这个坑拖了一下午,现象是预览正常但导出偶尔花屏,最后定位就是这里。加.copy()之后问题消失。代价是多一次内存拷贝,1080p 大概 6MB,几百帧每秒的循环里也就 1~2 毫秒,完全可以接受。省这个不划算。

还有一个相关的:不要把 QImage 或 QPixmap 在子线程里创建。Qt 的 GUI 对象只能在主线程构造,子线程 emit ndarray 是正确的分工,主线程负责转 QPixmap 和 setPixmap。反过来写,程序可能不报错但显示不刷新,排查起来很费劲。

4.4 大分辨率下的缩放与刷新节奏

4K 视频每帧 25MB 左右,即使只做转换和缩放,主线程也会被压得喘不过气。两个处理方向:

一是显示端降采样。检测用原图 640 推理,画框之后先把标注图缩到 QLabel 的尺寸再转 QPixmap,而不是转完再缩。cv2.resize在 numpy 层做比QPixmap.scaled走 Qt 的平滑算法快不少,尤其是每帧都执行的时候。

二是限制刷新频率。视频 30fps 时人眼其实分不出 25fps 和 30fps,如果 UI 处理不过来,可以在 worker 里做跳帧:每处理 2 帧 emit 1 帧,或者按时间戳节流,间隔小于 40ms 就不发信号。检测结果仍然按每一帧算,只是显示少刷几次,fps 和计数照常更新,用户感知不到差别,CPU 占用能降三成。

这里再提醒一句:label.setPixmap会触发重绘,如果 QLabel 设置了scaledContents=True,每次 setPixmap 都会额外拉伸一次,反而更慢。实际项目里我更倾向先算出 QLabel 的size(),在 numpy 层缩好再转,scaledContents关掉。

5. 排查清单:六个最常见的翻车现场

5.1 双击 exe 报 Qt platform plugin "windows" 找不到

现象:开发环境跑得好好的,PyInstaller 打包之后双击,弹窗提示could not find or load the Qt platform plugin "windows",程序直接退出。

原因:PyInstaller 的 PyQt5 hook 只识别显式 import 的模块,而 Qt 的插件(platforms/qwindows.dll)是按需动态加载的,静态分析扫不到。打包目录里platforms文件夹缺失或路径没被加进搜索路径,就会报这个。

解决:用--collect-all PyQt5让 PyInstaller 把整个包连同插件一起收进去,或者在 spec 的datas里手动加(qt_plugin_path, 'platforms')。实在头疼可以临时设环境变量QT_DEBUG_PLUGINS=1启动,控制台会打印它去哪些目录找过插件,路径一看就清楚。另外从 5.15 开始,QtWidgets是显式 import 的,别用from PyQt5 import *这种写法,静态分析会漏。

5.2 点了开始,界面转圈但一个框都不出

现象:线程明显在跑,fps 也有数字,但画面干净,没有任何检测框。

原因分两类。一类是参数传错,最常见的是 conf 传成了整数(25 而不是 0.25),模型把所有框都过滤掉了;另一类是模型加载的权重和当前类别不匹配,或者model.eval()没调用导致 BN 走训练分支,输出全是噪声。

解决:先在 worker 的run()里打印len(results.xyxy[0])和第一行的 conf 值,如果一直是 0,说明阈值过高。再确认model.names的长度和你的训练类别数是否一致,不一致就是权重文件拿错了。这两条占了我遇到的不出框问题的九成。

5.3 跑一段时间越来越卡,内存一路涨

现象:开始几十帧流畅,跑两三分钟后帧率明显下降,任务管理器里内存持续增长不回落。

原因:ndarray 通过信号传进事件队列后,如果主线程处理速度慢于 emit 速度,队列会堆积。Qt 的信号队列没有上限,堆到几万个 ndarray 就是几个 GB。另一种情况是 QImage 没有.copy(),Qt 内部持有悬空指针,某些平台上会触发重复分配。

解决:在 worker 里做节流,emit 之前判断上一次 emit 到现在的间隔,小于某个阈值就跳过(比如 33ms 对应 30fps)。同时确认转换函数里有.copy()。如果源是视频文件且需要保证处理每一帧,把显示和存储分开成两个信号,显示走节流,存储走同步落盘。

5.4 中文路径下读不到文件,或者中文标签显示成方块

现象:cv2.imread("D:/数据/测试.jpg")返回 None,或者检测框上的中文类别名显示成问号。

原因:OpenCV 的 imread 在 Windows 上走的是本地编码,对非 ASCII 路径支持不稳定。字体方面,Qt 默认字体不一定覆盖中文,画框时用cv2.putText更是完全没法写中文。

解决:读图改用np.fromfile(path, dtype=np.uint8)再cv2.imdecode(buf, cv2.IMREAD_COLOR),写图用cv2.imencode加tofile。中文标签不要用 cv2.putText,改成在转换完 QImage 之后用 QPainter 画文字,或者转成 RGB 后用 PIL 的 ImageDraw 配合指定中文字体文件。这两条都属于“踩过一次就记住”的类型。

5.5 关掉窗口,任务管理器里进程还在

现象:点右上角关闭,窗口消失了,但 Python 进程仍占着内存和摄像头,再启动报设备被占用。

原因:QThread 还在运行,主窗口析构时线程对象被销毁但线程本身没退出;或者closeEvent里没有等待线程结束就 accept 了。还有一种情况是cap.release()没执行到,摄像头句柄泄漏。

解决:按 3.4 节的写法,在closeEvent里先stop()再accept(),stop()内部置标志位加wait()。摄像头场景额外做一层保险,run()用 try/finally 包住,确保cap.release()一定执行。调试阶段可以用QApplication.setQuitOnLastWindowClosed(True)配合打印线程状态确认。

5.6 GPU 推理报 CUDA out of memory,或者和界面一起卡

现象:单跑命令行脚本正常,接进 GUI 之后报显存不足,或者界面卡顿明显加剧。

原因:多数是模型被加载了两份。常见的写法是构造窗口时加载一次用于预览,启动线程时又torch.hub.load一次,两份权重各占一块显存,小卡直接爆。另一个原因是推理分辨率调高之后,中间激活值占用上升,加上 Qt 本身也在占显存(如果有 OpenGL 加速)。

解决:模型只在主窗口初始化时加载一次,通过参数传进 worker,全程序共用一个实例。确认显存确实不够时,在推理前加torch.cuda.empty_cache()(会拖慢,别放进循环),或者把 imgsz 降到 512、batch 强制为 1。还有一种隐蔽情况:多次 start/stop 之后旧线程的 CUDA 上下文没有释放,可以把 worker 的模型引用在 stop 之后显式置为 None 再重建。

6. 交付前最后一公里:路径、打包与一处提速

PyInstaller 打包 torch 应用最容易被卡住的是路径。开发时./weights/best.pt相对于当前工作目录能读到,打成单文件 exe 后,运行时会被解压到一个临时目录,工作目录变成用户双击的位置,相对路径全部失效。判断基准要换成sys._MEIPASS:

import os, sys def resource_path(rel: str) -> str: """开发环境和 PyInstaller 单文件模式下的统一资源路径""" base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) return os.path.join(base, rel) WEIGHTS = resource_path("weights/best.pt")

打包命令上,torch 必须用--collect-all torch,否则运行时会缺torch._C或者部分 DLL;PyQt5 同理。完整命令大致是pyinstaller -F -w main.py --collect-all torch --collect-all PyQt5 --add-data "weights;weights",-w去掉控制台窗口,--add-data的分隔符在 Windows 上是分号、Linux 上是冒号,跨平台发布要分开打。体积会到 1.5~3GB,这是 torch 的固有成本,用--exclude-module剔掉matplotlib、pandas、scipy这些用不上的依赖能砍掉几百兆。

如果对启动速度有要求,值得把模型导成 ONNX,运行时只留 onnxruntime,进程启动从七八秒降到两秒左右,安装包也能小一个量级。导出时注意 opset 用 12,并且必须带--include onnx,动态 batch 设为 True 之后推理端就能按需传不同尺寸。代价是后处理要自己写一遍 NMS,如果只是想先交付、后面再优化,这一步可以缓一缓。

那次打包 deliver 给一个非技术同事,他在自己电脑上双击报插件缺失,我在工位上完全复现不了,最后发现是他的系统装了两个 Python 环境、PATH 顺序不同导致的搜索路径差异。从那以后我每次打包都会在一台干净机器上先跑一遍,只做双击一件事,跑通了再谈别的。希望这些经验能帮到你,需要完整工程结构的话,按上面几节的代码顺序拼起来就是可运行的骨架。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 13:36:01

SVM在电力负荷预测中的优化与应用实践

1. 电力负荷预测的技术背景与挑战电力系统调度部门每天都需要面对一个核心问题:明天、下周甚至下个月的电力负荷会是多少?这个问题看似简单,却直接影响着发电计划制定、机组组合优化和电力市场交易等关键决策。短期负荷预测通常指未来24小时到…

作者头像 李华
网站建设 2026/11/2 13:35:46

PyQt5+YOLOv5桌面检测器工程化拆解与避坑

简介:面向刚接触 PyQt5 与 YOLOv5 的初学者,这是一套可直接运行和练手的目标检测 GUI 项目源码,围绕多目标检测场景,重点处理界面设计、后端推理逻辑分离、常用控件使用以及 PyTorch 模型调用等问题。压缩包共 112 个文件&#xf…

作者头像 李华
网站建设 2026/11/2 13:35:18

全生命周期风控:智能闭环管理与技术实现

1. 项目概述:全生命周期风控范式的价值与挑战"全生命周期"风控范式正在重塑现代企业的风险管理体系。这个理念将传统割裂的雇前背景调查、在职行为监控和离职后管理整合为一个有机整体,形成从人才引进到持续管理的完整闭环。在金融、互联网、高…

作者头像 李华
网站建设 2026/11/2 13:33:23

从工具到管家:智能效率平台的技术架构与实践

1. 项目概述:从工具型应用到服务型平台的转型"从秘书到管家"这个产品定位转变,本质上反映了当代效率类应用从单一功能工具向综合服务平台的进化路径。五年前我刚入行时,市面上大多数效率应用还停留在"待办清单日历提醒"的…

作者头像 李华
网站建设 2026/11/2 13:33:21

华为MetaERP架构解析:事件驱动与实时数据处理

1. 华为MetaERP的架构革新与业务价值 华为MetaERP系统通过"交易即核算,核算即报告,报告即分析"的设计理念,彻底重构了传统ERP系统的数据处理流程。这套系统最显著的特点是实现了业务操作与财务处理的实时联动,让每个业务…

作者头像 李华
网站建设 2026/11/2 13:33:15

T6-WMS仓储管理系统的3+2+1架构解析

1. 什么是T6-WMS的"321"架构 在仓储管理领域,T6-WMS(Warehouse Management System)提出的"3条作业流2个异常池1套绩效塔"架构,正在成为提升仓储运营效率的新范式。这套架构不是简单的功能堆砌,而是…

作者头像 李华