本文主要讲解模型训练好后,怎么封装成dll接口,以供其他语言调用。神经网络框架以ncnn为例,其他框架大体思想都差不多,可以参考本文的思想,或者将模型转成ncnn,直接使用本文的教程亦可。
在打包前,首先需要明白打包的目标,如下:
1. 打包的文件。我们的目标是生成dll文件,如果仅仅将代码打包成dll,那么模型文件将会独立出来,从而打包好后的dll内仅仅包含代码,部署时,还需将模型文件一起发布,即dll+model的组合发布,这样是极其不便利的,也不符合简约美。本文采用另一种方式,将模型文件读进内存,和代码一起整合进dll中,进而发布时,仅需dll文件即可。此处涉及到ncnn模型的加载方式问题,可以参考上一篇博客:https://blog.csdn.net/Enchanted_ZhouH/article/details/106063552,本文以mobilenet_v2为示例讲解打包的过程,关于mobilenet_v2对应的ncnn模型的获取,只需将这篇博客的resnet18换成mobilenet_v2即可。
2. 打包的方式。静态打包or动态打包?静态打包是指将所有依赖的库一起打包进dll中,这样dll不管在哪个环境下均可以运行。动态打包是指dll在运行时,自动根据依赖关系在当前设备上寻找依赖库,如果两台设备(打包设备/部署设备)环境不一致,那么调用dll时将会报错,提示找不到xxx.dll。比较关键的是vc runtime库,不同的设备下,不一定安装了此库,或者版本不同等。为了得到更好的可移植性,本文将采用静态打包,需要注意的一点是,静态打包时,所有的依赖库均需要静态编译。
3. 打包的位数。操作系统分为32位和64位,32位的dll在32位环境中运行,64位的dll在64位环境中运行。实际测试中,32位的dll只能在32位的python下运行,64位同理。本文以常用的64位进行打包,并用python调用dll进行测试。
综上,本文在64位环境下,采用静态打包的方式,将代码和模型一起整合进dll。
全文的代码、读进内存的模型和打包好的dll等放在了github上,地址在这:https://github.com/PigTS/model-package-dll
ncnn的编译参考官网:https://github.com/Tencent/ncnn/wiki/how-to-build#build-for-windows-x64-using-visual-studio-community-2017,主要改动在于打开静态编译选项。
打开VS编译工具,本文以VS2015为例,其他版本操作步骤基本一样,如下:开始 -> 项目 -> Visual Studio 2015 -> VS2015 x64 本机工具命令提示符。
如若需要打包32位的dll,此处的工具选择x86即可,后续所有库均在x86下编译,本文后续均在x64下进行编译。
首先,编译protobuf库,如下:
download protobuf-3.4.0 from https://github.com/google/protobuf/archive/v3.4.0.zip
> cd
> mkdir build-static
> cd build-static
> cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%cd%/install -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_MSVC_STATIC_RUNTIME=ON ../cmake
> nmake
> nmake install
重要改变在于-Dprotobuf_MSVC_STATIC_RUNTIME=ON,即打开静态编译,官方编译选项默认是关闭的,在protobuf的CMakeLists.txt文件中,该选项的内容如下(124~132行):
if (MSVC AND protobuf_MSVC_STATIC_RUNTIME)
foreach(flag_var
CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
if(${flag_var} MATCHES "/MD")
string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
endif(${flag_var} MATCHES "/MD")
endforeach(flag_var)
endif (MSVC AND protobuf_MSVC_STATIC_RUNTIME)
主要就是将/MD全部替换成/MT,其中,/MD为动态编译,/MT为静态编译,且均在release下,如果后缀加上d,如/MDd和/MTd,那么就是对应的debug版本。
接下来,编译ncnn,官方ncnn的CMakeLists.txt文件中没有静态编译这个选项,那么我们按照protobuf的CMakeLists.txt文件加上即可,定义一个NCNN_MSVC_STATIC_RUNTIME编译选项,更新后的CMakeLists.txt文件已经在上面给出的github地址中,文件放在ncnn目录下,编译命令如下:
cd
> mkdir build-static
> cd build-static
> cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%cd%/install -DProtobuf_INCLUDE_DIR=/build-vs2017/install/include -DProtobuf_LIBRARIES=/build-vs2017/install/lib/libprotobuf.lib -DProtobuf_PROTOC_EXECUTABLE=/build-vs2017/install/bin/protoc.exe -DNCNN_MSVC_STATIC_RUNTIME=ON -DNCNN_VULKAN=OFF ..
> nmake
> nmake install
注意加上-DNCNN_MSVC_STATIC_RUNTIME=ON,打开ncnn的静态编译。
至此,ncnn编译完成,我们只需要build-static/install里面的头文件和库文件即可。
dll是一个接口,可供其他语言调用,或者移植到另一台机器上使用。打包dll,首先需要明白要暴露出来的接口是什么,本文设计的接口头文件是src/interface.h,主要接口如下:
//init model
void initModel();
//identify
const char* identify(const char* img_base64);
//free model
void freeModel();
主要实现如下功能:1. 初始化模型;2. 识别,输入为图像的base64编码流,方便传输,返回为类别+概率的char指针;3. 释放模型内存。
关于代码的实现部分,直接看src/interface.cpp即可。读取图像这块,使用了stb库(https://github.com/nothings/stb),使用该库仅用来读取图像,小巧轻便,方便打包。
接下来,准备打包dll,步骤如下:1. 在VS中创建一个空项目,将ncnn的头文件和库导入;2. 将github中src下的代码(.h/.cpp)导入对应的头文件和源文件中;3. 添加配置文件,即源文件中添加src/interface.def。
interface.def文件为模板定义文件,其中定义了输出的方法,即dll中的接口,内容如下:
LIBRARY ImageRecognitionEngine
EXPORTS
initModel
identify
freeModel
最后,在项目的属性页中,运行库选择多线程(/MT),和前面保持一致。操作步骤为:右键项目 -> 属性 -> C/C++ -> 代码生成 -> 运行库 -> 选择多线程(/MT)。
编译整个项目即可生成dll文件,生成的dll文件见github的dll目录,dll/static目录下为静态编译的dll文件,dll/dynamic目录下为动态编译的dll文件。
python调用dll的示例如下(文件:python/dll_test.py):
import ctypes
import base64
import time
#test img
img_path = "../img/test.jpg"
with open(img_path, 'rb') as f:
img_base64 = base64.b64encode(f.read())
#load dll
IREngine = ctypes.CDLL("../dll/static/ImageRecognitionEngine.dll")
#IREngine = ctypes.CDLL("../dll/dynamic/ImageRecognitionEngine.dll")
#config interface argtypes and restypes
IREngine.initModel.argtypes = []
IREngine.initModel.restype = ctypes.c_void_p
IREngine.identify.argtypes = [ctypes.c_char_p]
IREngine.identify.restype = ctypes.c_char_p
IREngine.freeModel.argtypes = []
IREngine.freeModel.restypes = ctypes.c_void_p
#init model
IREngine.initModel()
#indentify
count = 0
while count < 100:
res = IREngine.identify(img_base64).decode()
cls, value = res.split()
print("[dll]--->predicted class: %s, predicted value: %s" % (cls, value))
count += 1
time.sleep(150/1000) #sleep 150ms
#free model
IREngine.freeModel()
dll预测结果如下:
[dll]--->predicted class: 920, predicted value: 19.291550
...
和pytorch运行的结果进行对比,pytorch测试的代码如下(python/pytorch_test.py):
import torch
import torchvision
import numpy as np
import cv2
#test image
img_path = "../img/test.jpg"
img = cv2.imread(img_path)
img = cv2.resize(img, (224, 224))
img = np.transpose(img, (2, 0, 1)).astype(np.float32)
img = torch.from_numpy(img)
img = img.unsqueeze(0)
#pytorch test
model = torchvision.models.mobilenet_v2(pretrained=True)
model.eval()
output = model.forward(img)
val, cls = torch.max(output.data, 1)
print("[pytorch]--->predicted class: %d, predicted value: %.6f" % (cls.item(), val.item()))
pytorch预测结果如下:
[pytorch]--->predicted class: 920, predicted value: 19.230936
由此可见,dll和pytorch预测类别保持一致,由于计算库的不同,预测值有些许偏差。
文末做个小结,如下:
1. 打包模型时,如果要将模型和代码一起打包,那么可将模型读进内存,这样打包出来只有一个dll文件,方便部署的同时,又加密了模型。
2. 静态编译将vc runtime等库一起打包进dll,使得dll的可移植性更好,动态编译需要部署机器和打包机器环境一致才能运行dll。静态编译的dll会比动态编译的dll的体积略大一些,具体可见dll目录。
使用VS自带的dumpbin工具分析dll依赖关系,静态编译的dll依赖关系如下:
dumpbin /dependents static/ImageRecognitionEngine.dll
运行结果:
Dump of file ImageRecognitionEngine.dll
File Type: DLL
Image has the following dependencies:
VCOMP140.DLL
KERNEL32.dll
可见,静态编译的dll仅仅依赖VCOMP140.DLL和KERNEL32.dll这两个库,KERNEL32.dll是win系统自带的,VCOMP140.DLL一般win上也有,查看VCOMP140.DLL的依赖关系,结果如下:
dumpbin /dependents VCOMP140.DLL
运行结果:
Dump of file VCOMP140.DLL
File Type: DLL
Image has the following dependencies:
KERNEL32.dll
USER32.dll
可见,VCOMP140.DLL依赖的库都是系统自带库,若部署机器上没有VCOMP140.DLL,将打包机器上的VCOMP140.DLL同ImageRecognitionEngine.dll一起复制到部署机器上即可。
接着,分析动态编译的dll依赖关系,如下:
dumpbin /dependents dynamic/ImageRecognitionEngine.dll
运行结果:
Dump of file ImageRecognitionEngine.dll
File Type: DLL
Image has the following dependencies:
MSVCP140.dll
VCOMP140.DLL
VCRUNTIME140.dll
api-ms-win-crt-string-l1-1-0.dll
api-ms-win-crt-runtime-l1-1-0.dll
api-ms-win-crt-stdio-l1-1-0.dll
api-ms-win-crt-heap-l1-1-0.dll
api-ms-win-crt-convert-l1-1-0.dll
api-ms-win-crt-math-l1-1-0.dll
KERNEL32.dll
可见,动态编译的dll依赖的库有些多,主要有个VCRUNTIME140.dll,还有MSVCP140.dll和一系列api-ms-win-xxx等库,这些库还动态依赖了一些其他库,导致部署起来很麻烦,如果部署机器上没有安装vc runtime等一系列库,那么调用动态编译的dll很容易报错,提示找不到xxx.dll。
所以,强烈推荐采用静态编译的方式部署dll。
3. 打包位数根据自己的需求进行选择,32位or64位。
4. 一般图像都会进行一些预处理,如:归一化等,然后再送进网络进行识别,数据预处理在python和C++中需要统一。
至此,dll打包的核心内容就介绍完毕了,dll主要是部署在Windows机器上,如若打算部署在移动端,如:Android,则需要打包成so接口,下一篇博客将介绍使用本文的代码如何打包成so接口,使用Android机器调用so接口进行图像识别,感兴趣的小伙伴可以留意下。