由于工作原因,需要将一个基于 caffe 的深度学习检测工程转换成 exe 文件,在此过程中遇到了许多问题,现将其记录下来,以供参考。
fpn_ported 改动
Caffe related
1 caffe/pycaffe.py L13 from ._caffe
改成了 from caffe._caffe
。因为最后 dist\runcaffe_fpn\
里有一个 caffe._caffe.pyd
文件,因此需要 import 此文件。(个人解释见下文)
2 caffe/_init_.py L2 from._caffe
改成了 from caffe._caffe
caffe/_init_.py L3 from._caffe
改成了 from caffe._caffe
3 caffe 中关于图像读入的部分都是用的 skimage 进行实现,但是似乎 pyinstaller 对 skimage 的支持并不好,因此需要对 caffe 中的相关部分用 cv2 进行替代。
My Python project includes A Caffe module which run a simple image classification process. One basic function is Caffe calling skimage.io to load image:
https://github.com/BVLC/caffe/blob/master/python/caffe/io.py
1 | def load_image(filename, color=True): |
I wonder if PyInstaller currently has a good support for Python package skimage. But from what I know by now, it doesn’t.
Run from Python source code files, it works fine. But when I packed all things into one single binary file, it can not load image at all. And after debugging and googleing for a long time – I always thought maybe I did something wrong – I get rid of this. PyInstaller hates skimage! So at last I use cv2 instead. And it works smoothly.
1 | def cv2_load_image(filename, color=True): |
For all above details, please do check out PyInstaller Documentation: https://media.readthedocs.org/pdf/pyinstaller/latest/pyinstaller.pdf
具体参考这篇文章
Numpy related
1 numpy.core._init_.py 中 from .info import __doc__
改成 from numpy.core.info import __doc__
2 from . import multiarray
改成 from numpy.core import multiarray
3 将 numpy.core._init_.py 中其他的代表当前目录的 .
都替换成 numpy.core
4 将 numpy.fft.fftpack.py 中的 from . fft import fftpack_lite as fftpack
改成 from numpy.fft import fftpack_lite as fftpack
5.将 numpy.random._init_.py 中的 from .mtrand import *
改成 from numpy.random.mtrand import *
个人观点: 似乎无需像第3步一样将所有的 . 都改成绝对路径,貌似只有使用 .
导入 pyd 文件的导入语句会报找不到的错误。因此只需将导入 pyd 文件的语句更改即可。
os, os.path related
Problem
打包后运行时,在最外层的 runcaffe_fpn.py 中 import os 可以成功,但是在 models/rpn/proposal_layer.py
和 lib/config.py
中 import os
就会报错 no module named path
。
Analysis
由于 os 和 os.path 的特殊性,即 os.py 中根据平台不同,import 不同的文件作为 path 模块,例如,在 Windows 中,使用 import ntpath as path
并将其命名为 os.path 模块。此外,os.py 和 ntpath.py 文件存在相互 import 的情况,不知为什么,工程中一些文件中,如 config.py 中,import os 语句会报错 no module namded path。
Solution
由于 config.py 等文件中调用了 os.py 中的 makedirs 等函数以及 ntpath.py 中的一些函数。因此,尝试将 os.py 与 ntpath.py 文件解耦(其实是无法解耦的,因为 os.py 需要导入 ntpath.py 中的属性并供其函数使用),因此将 os.py 中 import ntpath as path
句注释,将 sys.modules['os.path'] = path
和 from os.path import (curdir, pardir, sep, pathsep, defpath, extsep, altsep, devnull)
注释掉,换成 from _ntpath import (curdir, pardir, sep, pathsep, defpath, extsep, altsep, devnull)
(from _ntpath
是因为如果直接使用 from ntpath
的话,它会自动调用 anaconda 下的 ntpath.py
文件,会导致前面的错误,因此,将 os.py 和 ntpath.py 文件拷贝至一个包下,并都在前面加上下划线进行重命名)。实际操作中,我是创建了一个名为 os_modified 的包,并将重命名后的 _os.py 和 _ntpath.py 文件放进去作为模块。在工程文件中调用 os 和 os.path 的地方,将 import os
和 import os.path as osp
改成 import modified_os._os as os
和 import modified_os._ntpath as osp
。工程文件中导入的其他一些包,如 genericpath,也会 import os,我也将这些文件拷贝进 modified_os 中重命名, 如 _genericpath,并将其中的 import os 改成 import _os as os。在工程文件中有 import genericpath
的地方改成 import modified_path._genericpath as genericpath
。其他有类似情况的也按此方法处理。总之,os_modified 模块下,都是在工程文件中使用到,且其有 import os
或 import os.path
的,我将所有这些文件集中到一个名为 modified_os 的模块下,当工程文件中调用到这些时,就调用此模块下的版本,而不是 anaconda 的原始版本。这样就解决了 no module named path
的错误。
Comment
上面所述的关于 os, os.path 的部分全部不需要,只需要将 os.py, ntpath.py 两个文件通过 --add-binary="/path/to/os.py;." --add-binary="/path/to/ntpath.py;."
添加进去即可。这样,在 dist/runcaffe_fpn/
下就会有 os.py
和 ntpath.py
两个文件。当一个 .py
文件是从 exe 文件中开始执行,即是 boundled 的时候,pyinstaller 的 bootloader 将 frozen
属性添加进 sys 模块中。因此,可以通过
1 | import sys |
来在工程文件中进行判断此工程文件是用 python 脚本调用执行的还是用 exe 文件调用执行的。如果是用 python 脚本调用,则根据其导入模块的结构进行 import。比如 fast_rcnn/nms_wrapper.py
需要导入 nms/gpu_nms.pyd
和 fast_rcnn/config.py
。在脚本模式下,需使用 from nms.gpu_nms import gpu_nms
和 from fast_rcnn.config import cfg
进行调用。而在 boundle 模式下,使用 from gpu_nms import gpu_nms
和 from config import cfg
进行调用即可。需要注意的是,在 boundle 模式下,还需使用 --add-binary
选项将 gpu_nms.pyd
和 config.py
两个文件添加进去,这样它的 import 语句才会生效。
上述做法有效的原因是:pyinstaller 似乎将工程中不同层次的脚本(即存在调用关系)所需的依赖都展开放在最后打包的依赖项所在的文件夹下,即 sys._MEIPASS
所指向路径下,是没有目录层次的。因此,当处于 boundle 模式下时,from gpu_nms import gpu_nms
语句会直接对 sys.__MEIPASS
目录下的 gpu_nms.pyd
文件进行 import。在解决 os, os.path 问题时,将 os.py 和 ntpath.py 文件都添加进去,就不会有找不到的问题。并且按照理论,import os 所导入的文件应该是 sys._MEIPASS
路径下的 os.py。(已验证)
工程文件中的示例如下:
1 | import sys |
解决了 os 和 pyd 文件导入的问题后,exe 可以成功运行了。但是美中不足的是,运行 exe 除了需要作为参数的那些文件外,还一直需要 `proposal_layer.py` 文件,这是不好的,因为不能向客户透露源码。因此,尝试将 `proposal_layer.py` 文件也用 `--add-binary` 选项添加进去。但是这样并没有用。究其原因是,`proposal_layer.py` 是在 `.prototxt` 文件中指定的(即 python 层中的 module 参数),原来为 `module: rpn.proposal_layer`。这是因为 prototxt 文件同级目录下有一个 rpn 文件夹,其下有一个 `proposal_layer.py` 文件。由于将 `proposal_layer.py` 文件添加进 `sys._MEIPASS` 后其名称仍为 `proposal_layer.py`,而 prototxt 中指定为 `rpn.proposal_layer`,不统一。因此,将 rpn 文件夹下的两个文件,`proposal_layer.py` 和 `generate_anchors.py`,放置到与 prototxt 文件同级目录下,并将 prototxt 中的 `rpn.proposal_layer` 改成 `proposal_layer`. 再将 `proposal_layer.py` 用 `--add-binary` 添加进去,就可以在 `sys._MEIPASS` 中调用 `proposal_layer.py` 了,这样再运行单个的 exe 文件时就不需带着 `proposal_layer.py` 文件了,因为它会调用 `sys._MEIPASS` 下的 `proposal_layer.py`。
Pyinstaller 打包命令如下:
1 | pyinstaller -F --add-binary="models/nms/gpu_nms.pyd;." ^ |
最后,如上面的代码所示,我是将所有的除 caffe 模块外的额外 python 脚本都添加进了 sys._MEIPASS
中,保证可以有效调用。
Pyinstaller 使用方法记录
Pyinstaller 运行时路径问题
pyinstaller 在打包后,会将 frozen
属性添加进 sys 变量。因此,可以使用 getattr(sys, 'frozen', False)
来判断当前执行的脚本是否是用 python 脚本执行的还是 exe 文件执行的。
pyinstaller 有两种打包模式,一种是 onefile 模式,即将所有的依赖文件都打包成一个单独的 exe 文件,在运行此 exe 文件时,将其所包含的依赖文件临时解压到某个目录下,并在此目录下进行执行。另一种是 one-folder 模式,将生成的 exe 文件和依赖文件都放在 dist 文件夹下与被打包的 script 同名的文件夹中。在运行 exe 文件时,不需要将依赖文件解压,因此速度更快一些。很显然,one-file 模式的 exe 文件会比较大。当运行 exe 文件时,pyinstaller 的 bootloader 将 bundle folder 的绝对路径添加进 sys._MEIPASS 中。其实,这就是 exe 文件执行时,依赖文件所在的位置。需要注意的是,one-folder 模式下的 exe 文件必须和依赖文件在同一路径下,否则会找不到依赖。
hidden-import
在一次测试中,我用 R.py
导入了 libs/rpn/proposal_layer.py
,在proposal_layer.py
中 from ..fast_rcnn.nms_wrapper import nms
,在 libs/fast_rcnn.nms_wrapper.py
中 from ..nms.gpu_nms import gpu_nms
,在 libs/nms/
下有一个 gpu_nms.pyd
文件。