基于SAM2的眼动数据跟踪3——python转exe

基于SAM2的眼动数据跟踪3——python转exe 目录一、前言二、如何将python转exe问题Python→exe 到底要写哪些东西问题为什么onnxruntime_libs [ (str(site_packages / onnxruntime/capi/libonnxruntime_providers_cuda.so), onnxruntime/capi), (str(site_packages / onnxruntime/capi/libonnxruntime_providers_shared.so), onnxruntime/capi) ] 原来是.so是不行的改成.dll就行跟系统有关系问题hiddenimports里面到底要写啥问题stripFalse, upxFalse, 这什么意思问题为什么datas里面写了路径后面还要跟一个名字问题为什么打包后生成的是一个_internal文件夹和exe_internal这个名称是什么时候确定的其实不重要问题为什么Windows里需要在.spec中加sys.setrecursionlimit(5000) 问题假设我现在有个python项目我想把它转为exe我该怎么做一、前言我下载的X-AnyLabeling是3.1.1版本如果你用最新版本可能会遇到一些需要自己解决的问题我把它放在了D:\zero_track\X-AnyLabeling-3.1.1如果你跟我保持一致则你可以跟我采用完全一致的步骤方法进行否则你需要根据你的项目路径进行更改。此外我使用的conda环境是(X_AnyLabeling_win_env)这个环境要基于SAM2的眼动数据跟踪1介绍的那样根据源码Readme去配置这个环境位置我是放在D:\miniforge3\envs\X_AnyLabeling_win_env如果你装的是conda而不是miniforge3或者是名字有所不同则需要根据你的路径进行更改下面x-anylabeling-linux-gpu.spec的修改内容中不再赘述这些。建议是跟我保持一致因为我之所以将X-AnyLabeling-3.1.1放在D:\zero_track是因为在zero_track项目中我打算对SAM算法各种改进项目以及各种目标跟踪算法进行一个性能比较和研究测试或者是与其他研究联动等等而X-AnyLabeling-3.1.1只是其中一个。不过这只是计划。二、如何将python转exe我们之前都是通过这样一句python语句打开X_AnyLabeling程序的(X_AnyLabeling_win_env) D:\zero_track\X-AnyLabeling-3.1.1 python anylabeling/app.py现在我们要将python转exe。在scripts/build_executable.sh中可以看到有不同的系统选择也可以选择cpu还是gpu。#!/bin/bash system$1 if [ $system win-cpu ]; then echo Building Windows CPU version... export X_ANYLABELING_DEVICECPU pyinstaller --noconfirm x-anylabeling-win-cpu.spec elif [ $system win-gpu ];then echo Building Windows GPU version... export X_ANYLABELING_DEVICEGPU pyinstaller --noconfirm x-anylabeling-win-gpu.spec elif [ $system linux-cpu ];then echo Building Linux CPU version... export X_ANYLABELING_DEVICECPU pyinstaller --noconfirm x-anylabeling-linux-cpu.spec elif [ $system linux-gpu ];then echo Building Linux GPU version... export X_ANYLABELING_DEVICEGPU pyinstaller --noconfirm x-anylabeling-linux-gpu.spec elif [ $system macos ];then echo Building macOS version... export X_ANYLABELING_DEVICECPU pyinstaller --noconfirm x-anylabeling-macos.spec else echo System value $system is not recognized. fi这里我们选择的是win-gpu因为我们是在windows上进行python转exe的同时我们需要的是gpu版本的如果你要跑SAM2视频跟踪不要想着cpu能跑那将会慢到卡死。在转exe之前我们需要先确保依赖已经正确安装如果是gpu版本(X_AnyLabeling_win_env) D:\zero_track\X-AnyLabeling-3.1.1 pip install -r requirements-gpu-dev.txt 如果是cpu版本则是pip install -r requirements-dev.txt然后打开 anylabeling/app_info.py将__preferred_device__ CPU 改为__preferred_device__ GPU然后我们只需要打开git bash 终端这是windows系统下能用的linux终端然后输入先不要运行看完下面bash scripts/build_executable.sh win-gpu如果选择win-gpu的话实际上执行的是下面这句所以说如果在windows系统下没有装git终端则可以直接在cmd命令终端下使用这句先不要运行看完下面pyinstaller --noconfirm x-anylabeling-win-gpu.spec在此之前我们需要改一个文件。在x-anylabeling-win-gpu.spec中# -*- mode: python -*- # vim: ftpython import sys import sysconfig from pathlib import Path site_packages Path(sysconfig.get_path(purelib)) # 找到当前虚拟环境的 site-packages onnxruntime_libs [ # 准备两张 CUDA 动态库 (str(site_packages / onnxruntime/capi/libonnxruntime_providers_cuda.so), onnxruntime/capi), (str(site_packages / onnxruntime/capi/libonnxruntime_providers_shared.so), onnxruntime/capi) ] a Analysis( [anylabeling/app.py], # 入口脚本 pathex[anylabeling], # 额外模块搜索路径 binaries[], # 无额外二进制 datas[ # 需要拷贝的数据文件 (anylabeling/configs/auto_labeling/*.yaml, anylabeling/configs/auto_labeling), (anylabeling/configs/*.yaml, anylabeling/configs), (anylabeling/views/labeling/widgets/auto_labeling/auto_labeling.ui, anylabeling/views/labeling/widgets/auto_labeling), (anylabeling/services/auto_labeling/configs/bert/*, anylabeling/services/auto_labeling/configs/bert), (anylabeling/services/auto_labeling/configs/clip/*, anylabeling/services/auto_labeling/configs/clip), (anylabeling/services/auto_labeling/configs/ppocr/*, anylabeling/services/auto_labeling/configs/ppocr), (anylabeling/services/auto_labeling/configs/ram/*, anylabeling/services/auto_labeling/configs/ram), *onnxruntime_libs # 把上面两张 .so 也当数据拷 ], hiddenimports[], # 无隐藏导入 hookspath[], runtime_hooks[], excludes[], ) pyz PYZ(a.pure, a.zipped_data) # 把纯 Python 代码压成 zlib exe EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, # ← 所有东西一起打进一个文件 nameX-Anylabeling-Linux-GPU, debugFalse, # 不输出调试 stripFalse, upxFalse, runtime_tmpdirNone, consoleFalse, # 不弹控制台 iconanylabeling/resources/images/icon.icns, ) # 包成 .app app BUNDLE( exe, nameX-AnyLabeling.app, iconanylabeling/resources/images/icon.icns, bundle_identifierNone, info_plist{NSHighResolutionCapable: True}, )上面的结果就是生成单文件 exeLinux 下无后缀双击即可运行所有依赖都在 exe 内部。但是我觉得这样不好因为我们有要修改的配置文件全封装进exe不但运行的时候非常慢每次运行exe要把依赖复制到tmp文件夹很耗时而且什么都修改不了。所以我们修改为下面的单文件夹模式这样会在D:\zero_track\X-AnyLabeling-3.1.1\dist生成X-AnyLabeling-GPU文件夹里面是一个_internal文件夹和一个X-AnyLabeling-GPU.exe。可以看到我们下面把参数a.binaries, a.zipfiles, a.datas从EXE那里转移到了BUNDLE这就是将一些东西从原本的单个exe抽出来放在_internal文件夹# -*- mode: python -*- # vim: ftpython import sys import sysconfig from pathlib import Path sys.setrecursionlimit(5000) # required on Windows # site_packages Path(sysconfig.get_path(purelib)) a Analysis( [anylabeling/app.py], # [新增] pathex[anylabeling, rD:\zero_track\X-AnyLabeling-3.1.1\segment-anything-2-camera,D:/zero_track/X-AnyLabeling-3.1.1/xanylabeling_data], binaries[], datas[ (anylabeling/configs/auto_labeling/*.yaml, anylabeling/configs/auto_labeling), (anylabeling/configs/*.yaml, anylabeling/configs), (anylabeling/views/labeling/widgets/auto_labeling/auto_labeling.ui, anylabeling/views/labeling/widgets/auto_labeling), (anylabeling/services/auto_labeling/configs/bert/*, anylabeling/services/auto_labeling/configs/bert), (anylabeling/services/auto_labeling/configs/clip/*, anylabeling/services/auto_labeling/configs/clip), (anylabeling/services/auto_labeling/configs/ppocr/*, anylabeling/services/auto_labeling/configs/ppocr), (anylabeling/services/auto_labeling/configs/ram/*, anylabeling/services/auto_labeling/configs/ram), # [新增] (D:/miniforge3/envs/X_AnyLabeling_win_env/Lib/site-packages/onnxruntime/capi/onnxruntime_providers_cuda.dll, onnxruntime/capi), (D:/miniforge3/envs/X_AnyLabeling_win_env/Lib/site-packages/onnxruntime/capi/onnxruntime_providers_shared.dll, onnxruntime/capi), # sam2配置文件地址 (D:/zero_track/X-AnyLabeling-3.1.1/segment-anything-2-camera/sam2_configs, sam2_configs), # 模型地址 (D:/zero_track/X-AnyLabeling-3.1.1/xanylabeling_data/models/*, xanylabeling_data/models), # 模型路径配置文件地址 (D:/zero_track/X-AnyLabeling-3.1.1/xanylabeling_data/models.json, xanylabeling_data/models.json) ], # [新增] hiddenimports[ sam2, sam2.build_sam, sam2.sam2_image_predictor, sam2_configs, ], hookspath[], runtime_hooks[], excludes[], ) pyz PYZ(a.pure, a.zipped_data) # [修改] exe EXE( pyz, a.scripts, # a.binaries, # a.zipfiles, # a.datas, exclude_binariesTrue, # 关键不打包二进制留给 COLLECT nameX-Anylabeling-Linux-GPU, debugTrue, # 打开调试信息 stripFalse, upxFalse, # runtime_tmpdirNone, consoleTrue, # 是否带控制台 iconanylabeling/resources/images/icon.icns, ) # [修改] coll BUNDLE( exe, a.binaries, a.zipfiles, a.datas, stripFalse, upxFalse, nameX-AnyLabeling.app, )问题Python→exe 到底要写哪些东西PyInstaller 的核心工作只有 3 步Analysis把入口脚本app.py里import到的所有模块、动态库、数据文件、隐藏导入……统统“抓”出来。需要告诉它入口脚本是谁额外二进制.dll/.so在哪额外数据文件.yaml/.ui/.json在哪哪些包是“动态 import”必须手工加hiddenimports哪些包不要打进去excludesEXE把 Analysis 的结果变成“可执行文件”。需要决定单文件模式one-fileexclude_binariesFalse还是单文件夹模式one-folderexclude_binariesTrue是否带控制台consoleTrue/False是否开调试信息debugTrue/False图标、名字、UPX 压缩等COLLECT / BUNDLE单文件夹模式下再把“剩下的二进制 数据”收集到一个目录macOS 下用 BUNDLE 生成.app包。写完这三段保存成.spec然后pyinstaller --noconfirm xxx.spec就出来 exe或 app。问题为什么onnxruntime_libs [ (str(site_packages / onnxruntime/capi/libonnxruntime_providers_cuda.so), onnxruntime/capi), (str(site_packages / onnxruntime/capi/libonnxruntime_providers_shared.so), onnxruntime/capi) ] 原来是.so是不行的改成.dll就行跟系统有关系是的.so与.dll的区别纯粹是“操作系统约定”跟 Python 本身无关表格复制操作系统动态库后缀环境变量/搜索路径Linux.soLD_LIBRARY_PATHWindows.dllPATHmacOS.dylibDYLD_LIBRARY_PATHONNX Runtime 的 GPU 版在安装时会根据当前平台复制同名不同后缀的文件Linux 下只有libonnxruntime_providers_cuda.solibonnxruntime_providers_shared.soWindows 下只有onnxruntime_providers_cuda.dllonnxruntime_providers_shared.dll因此site_packages / onnxruntime/capi/libonnxruntime_providers_cuda.so在 Windows 机器上根本找不到同名文件PyInstaller 会报“文件不存在”或运行时报Could not locate onnxruntime_providers_cuda.dll把后缀改成.dll只是让路径字符串与真实文件一致没有别的魔法。跨平台写 spec 时可以用一个小技巧import sys, sysconfig, os from pathlib import Path site_packages Path(sysconfig.get_path(purelib)) ext {win32: .dll, linux: .so, darwin: .dylib}[sys.platform] onnxruntime_libs [ (str(site_packages / fonnxruntime/capi/onnxruntime_providers_cuda{ext}), onnxruntime/capi), (str(site_packages / fonnxruntime/capi/onnxruntime_providers_shared{ext}), onnxruntime/capi) ]这样同一份.spec在 Linux/Windows/macOS 都能自动拿到正确文件。问题hiddenimports里面到底要写啥hiddenimports里只写**“PyInstaller 静态分析抓不到但运行时一定会 import”的模块名。一句话“让 exe 在运行时不报 ModuleNotFoundError” 的最小集合。**一、先搞清为什么需要它PyInstaller 靠文本扫描import xxx语句来收集依赖但遇到下面 4 种情况就瞎了动态__import__()/importlib.import_module()插件式加载字符串拼接名字二进制扩展.pyd/.so被其它扩展调用运行时按条件才 importif flag: import y这些模块不会出现在分析结果里可执行文件里也就没打包进去于是双击 exe 直接ModuleNotFoundError: No module named xxx二、到底写什么——“运行时一定出现”的顶级包名或完整子模块名写法就是纯字符串一行一个不要路径、不要文件后缀、不要写.py场景正确写法错误写法顶级包sam2sam2.py、D:\\sam2子模块sam2.build_samsam2/build_sam.py包内动态 importmmcv.opsmmcv/ops扩展模块onnxruntime.capi._pybind_state_pybind_state.pyd三、快速定位该写谁跑一遍 exe看 traceback缺谁就把谁名字加进去 → 最靠谱。全局搜索代码里的字符串拼接例for name in [sam2, yolov5, ppocr]: importlib.import_module(fmodels.{name})那么models.sam2、models.yolov5、models.ppocr都要写。用社区钩子很多库已有官方钩子hook-xxx.py装了钩子就不用手写没有钩子的库才手动补。四、常见真实例子hiddenimports [ # SAM2 动态 import sam2, sam2.build_sam, sam2.sam2_image_predictor, # 插件式 OCR ppocr.data, ppocr.modeling, # 二进制扩展 onnxruntime.capi._pybind_state, # CUDA 后端 onnxruntime.providers.cuda, # 第三方库延迟加载 sklearn.utils._typedefs, sklearn.neighbors._partition_nodes, ]五、一句话总结hiddenimports就是**“PyInstaller 漏掉但程序实际会 import 的模块名清单”**缺啥写啥不要多写不要写路径写完重新打包 → 运行 → 再缺再加直到 exe 不再报ModuleNotFoundError为止。问题stripFalse, upxFalse, 这什么意思这两行是PyInstaller 在打包阶段对二进制文件做的“瘦身”与“压缩”开关——默认都是False不动原文件一旦设为True就可能让体积变小但也可能把程序弄崩。stripFalse只影响 Linux / macOS 下的.so/.dylib对应命令行工具strip把动态库里的调试符号表、重定位信息等全部剃掉。体积能再小 5 %–30 %但– 崩溃时gdb/lldb看不到函数名堆栈全是???– 极少数库CUDA、PyTorch被strip后会直接加载失败。Windows 的.pyd/.dll不受该选项影响PE 文件格式不同没有strip概念。upxFalseUPX “Ultimate Packer for eXecutables”把可执行文件/动态库压成自解压格式。体积可再小 20 %–60 %但– 启动时会先解压到内存冷启动变慢– Windows Defender / 杀毒软件常把 UPX 压过的文件当成可疑加壳容易误报– 部分库opencv_world*.dll、onnxruntime_providers_cuda.dll、torch*.dll被 UPX 后加载失败内部自校验不通过。实战建议场景推荐值理由本地调试都False出问题能看完整符号不会被压缩干扰正式发布且充分测试过可stripTrueLinux/macOS 体积再小一圈想发到全网下载保持upxFalse杀毒误报、加载失败代价更高真在乎体积用 7z 分发即可一句话总结stripFalse不剥符号upxFalse不用 UPX 压缩两者都是“求稳”做法体积换兼容性只要出现过“打包后 exe 打不开 / 杀毒报毒 / CUDA 初始化失败”第一时间把这两个开关关掉再试。问题为什么datas里面写了路径后面还要跟一个名字datas 里每一项是二元组(源路径, 打包后在 exe 内部的目录名)第 1 个元素告诉 PyInstaller去哪里找文件第 2 个元素告诉 PyInstaller在生成的 bundle 里把文件放到哪个目录也就是运行时sys._MEIPASS下的相对路径。例子 1把单个文件放进去datas[ (conf/settings.yaml, conf) ]打包后目录结构运行时临时目录sys._MEIPASS/ ├─ conf/ │ └─ settings.yaml代码里就可以import os, sys yaml_path os.path.join(sys._MEIPASS, conf, settings.yaml)例子 2把整个文件夹映射进去datas[ (models/*.pt, models) ]所有匹配的.pt文件会被复制到sys._MEIPASS/models/xxx.pt例子 3换内部名字datas[ (assets/icon.png, res/img) ]运行时sys._MEIPASS/res/img/icon.png与原始目录结构无关内部路径完全由第 2 个字符串决定。一句话记忆第 1 段是“硬盘上去哪儿拿”第 2 段是“拿进来以后放哪儿”不写第 2 段PyInstaller 就不知道在 bundle 里建什么目录程序就找不到文件。问题为什么打包后生成的是一个_internal文件夹和exe_internal这个名称是什么时候确定的其实不重要_internal这个名字不是.spec文件里写的而是PyInstaller 在单文件夹模式one-folder下的硬编码默认子目录名。只要满足下面两个条件它就会自动出现打包时用了单文件夹模式即EXE(..., exclude_binariesTrue)然后COLLECT(...)或BUNDLE(...)把二进制再收集回来没有通过命令行--name或COLLECT(name...)显式指定目录名。出现时机在PyInstaller/building/api.py的COLLECT类里写死# PyInstaller 5.x 源码摘录 class COLLECT(Target): def __init__(self, *args, **kws): ... # 如果没给 name就用脚本名 _internal if self.name is None: self.name os.path.splitext(os.path.basename(self.scripts[0]))[0] _internal因此当你用默认.spec或由pyinstaller your.py自动生成时PyInstaller 会生成dist\your.exe再生成dist\your_internal\并把所有.dll/.pyd和数据塞进去启动时your.exe先把_internal解压到临时目录再把该路径加入sys.path/sys.dllhandle最后调真正的入口。想改名怎么办命令行pyinstaller your.spec --name MyApp目录会变成dist\MyApp\和dist\MyApp_internal\。手动改.spec把COLLECT或BUNDLE的name参数写死coll COLLECT(exe, a.binaries, a.zipfiles, a.datas, nameruntime) # 会生成 dist\runtime\一句话总结_internal是 PyInstaller单文件夹模式默认的后缀目录名源码里写死不出现它的办法只有用单文件模式或者自己指定目录名。问题为什么Windows里需要在.spec中加sys.setrecursionlimit(5000) 在 Windows 下用 PyInstaller 打包超大项目或深目录长路径时经常会在.spec开头看到import sys sys.setrecursionlimit(5000)这不是 PyInstaller 的“官方要求”而是规避 Python 默认递归深度限制的临时 workaround。触发场景PyInstaller 在Analysis 阶段会递归地扫描import链解析hook文件处理datas/binaries路径如果项目很大、目录层级深尤其放在D:\very\long\path\to\project\...Python 的路径解析模块遍历就会深递归最终抛出RecursionError: maximum recursion depth exceeded默认限制CPython 的默认递归深度 sys.getrecursionlimit() 1000对普通代码够用但对 PyInstaller 的“巨型 AST 长路径”可能不够。为什么 Windows 更常见Windows 路径长度上限 260 字符PyInstaller 内部会把长路径拆成多级拼接递归层数比 Linux/macOS 更高。很多 Windows 用户习惯把项目放在桌面/下载文件夹路径本身就很深。解决方式在.spec文件最顶部任何函数调用前把限制临时调高即可import sys sys.setrecursionlimit(5000) # 或 10000够用就行这只是运行打包脚本时生效不会影响最终 exe 的运行性能也不改变 Python 解释器本身。官方态度PyInstaller 文档把这条写进 FAQIf you encounterRecursionErrorduring analysis, increase the limit in your.specfile.一句话总结Windows 长路径 大项目 → PyInstaller 递归深度爆表 →.spec里手动sys.setrecursionlimit(5000)只是让打包过程不崩溃对最终程序无任何副作用。问题假设我现在有个python项目我想把它转为exe我该怎么做下面给你一条**“从 0 到拿到可运行 exe”**的最短路线分三步先能跑再优化全程在虚拟环境里做避免把系统 Python 搅乱准备阶段在虚拟环境里安装依赖# 1. 创建并激活虚拟环境Python 3.8~3.11 最稳 python -m venv venv venv\Scripts\activate # Linux/Mac 用 source venv/bin/activate # 如果是conda环境 # 指定 Python 版本环境名随意 conda create -n exepack python3.10 -y conda activate exepack # 2. 安装你的项目 PyInstaller pip install -r requirements.txt # 你的依赖 pip install pyinstaller先“裸跑”PyInstaller看能不能启动进入项目入口脚本所在目录假设入口是myapp\main.py# 最简单命令生成单文件夹模式 pyinstaller myapp\main.py --name MyApp --clean -y解释--name MyApp让 exe 叫MyApp.exe目录叫MyApp而不是默认的main.exe/main_internal。--clean强制清空缓存。-y不询问直接覆盖。跑完会出来dist\ ├─ MyApp\ │ ├─ MyApp.exe # 启动器 │ └─ …一堆 dll/pyd 文件双击MyApp.exe如果能正常打开 → 恭喜直接跳到第 3 步微调。如果闪退/报错 → 看弹出的控制台日志缺啥补啥见下方“常见补缺”。微调写一份最小.spec把缺的东西加进去第 2 步只是“快速验证”。真正交付要写.spec文件否则每次命令行都得敲一长串。让 PyInstaller 先生成模板pyi-makespec myapp\main.py --name MyApp --onefile得到MyApp.spec。用文本编辑器打开MyApp.spec按需要补 3 处# ① 数据文件ui、yaml、图标等 datas[ (myapp/config/*.yaml, myapp/config), (myapp/resources/icon.png, myapp/resources), ], # ② 隐藏导入运行时报 ModuleNotFoundError 就加 hiddenimports[ sklearn.utils._typedefs, onnxruntime.capi._pybind_state, ], # ③ 单文件还是单文件夹示例保持单文件 exe EXE( ... consoleFalse, # 正式发布用 False调试阶段可 True iconmyapp/resources/icon.ico, )重新打包pyinstaller MyApp.spec --clean -y产物dist\MyApp.exe # 单文件双击即可运行常见补缺速查表报错现象解决手段ModuleNotFoundError: xxx把xxx加到hiddenimportsFileNotFoundError: config\a.yaml把该 yaml 加到datas代码里用sys._MEIPASS拼接路径见下双击无窗口闪退先--consoleTrue看日志解决后再改回False杀毒软件报毒关掉UPX默认就关用数字签名工具签一下 exe读取打包后资源的标准代码import sys, os def resource_path(rel): 获取打包后资源绝对路径 base getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base, rel) config resource_path(myapp/config/settings.yaml)一句话总结venv或者conda 装依赖先裸跑pyinstaller main.py --name MyApp验证能否启动用pyi-makespec生成.spec把数据/隐藏导入加进去再pyinstaller MyApp.spec出最终 exe按这个流程10 分钟就能拿到可运行的 exe后续再慢慢优化体积、图标、自动更新等高级需求。