模型部署之dll打包

       本文主要讲解模型训练好后,怎么封装成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的编译

       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 <protobuf-root-dir>
> 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 <ncnn-root-dir>
> mkdir build-static
> cd build-static
> cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%cd%/install -DProtobuf_INCLUDE_DIR=<protobuf-root-dir>/build-static/install/include -DProtobuf_LIBRARIES=<protobuf-root-dir>/build-static/install/lib/libprotobuf.lib -DProtobuf_PROTOC_EXECUTABLE=<protobuf-root-dir>/build-static/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是一个接口,可供其他语言调用,或者移植到另一台机器上使用。打包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的示例如下(文件: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接口进行图像识别,感兴趣的小伙伴可以留意下。

  • 9
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 23
    评论
将 C 语言编写的 DLL 文件打包进 EXE 文件,可以通过以下步骤实现: 1. 首先,需要创建一个用于打包 DLL 的 C 语言项目。在 Visual Studio 中选择新建项目,选择 C 语言项目模板,并添加你的功能代码。 2. 在项目中将功能代码封装为一个 DLL,可以使用动态链接库生成器来创建 DLL 文件。在 Visual Studio 创建项目后,选择“文件”->“新建”->“项目”->“Visual C++”->“动态链接库 (.dll)”,然后按照向导的步骤进行设置。 3. 在 DLL 项目中,将所有的功能代码写在 DLL 动态链接库的导出函数中,并且需要将这些函数在 .def 文件中进行导出声明。在导出函数中,可以通过动态链接库提供的接口来获取和使用 DLL 提供的功能。 4. 在主 EXE 项目中,添加对 DLL 的引用。在 Visual Studio 中选择主 EXE 项目,右键点击“引用”,选择“添加引用”,然后浏览到 DLL 项目的输出目录,选择 DLL 文件添加到引用中。这样主 EXE 项目就可以使用 DLL 提供的功能了。 5. 当主 EXE 项目构建并运行时,系统会自动加载和链接 DLL 文件,以便在主程序中使用 DLL 提供的功能。 通过上述步骤,我们可以将 C 语言编写的 DLL 文件打包进 EXE 文件,并且在主程序中使用 DLL 的功能。这样做的好处是可以将代码和资源集中在一个 EXE 文件中,方便分发和部署。另外,也可以通过将 DLL 文件打包进 EXE,提高一些信息的隐藏性和保密性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值