实验目的:
将caffe模型转成ncnn可以实现在移动端运行深度学习模型,主要使用:
https://github.com/Tencent/ncnn
实验环境:
1、系统环境
- Mac OS Mojave系统
- 编译好的caffe源码(可以参考我之前的博客:https://blog.csdn.net/sinat_28731575/article/details/78958348)
2、软件
- Android Studio 3.2
- Genymotion虚拟机
(参考:http://www.open-open.com/lib/view/open1466430392743.html)
实验过程
1、实验准备
1、将 https://github.com/Tencent/ncnn clone到本地后解压,可以看到下面的组织结构:
其中
- examples是简单的在安卓上使用NCNN的例子,有一个根据这个例子编译好的Android Studio工程: https://github.com/dangbo/ncnn-mobile
- tools是后面需要用到的一些工具代码,包含了将各种网络转换到NCNN的代码
2、编译好的caffe源码用于后面转换模型使用
2、编译NCNN
(1)参照:https://github.com/Tencent/ncnn/wiki/how-to-build
中选择一个需要的环境编译,因为我需要在Android上面使用,所以选择了“Build for Android”:
这里首先需要安装NDK来编译Android项目,配置NDK环境有以下两种方式:
使用Android Studio来直接安装:
在偏好设置中进行如上图所示的配置,就可以配置NDK编译环境以及相关工具,安装好后NDK存放在上面的sdk目录下的ndk-bundle文件夹中自己到网站上面下载的方式:
下载网址为:http://developer.android.com/ndk/downloads/index.html
选择合适的版本下载(因为上面的第一种方法虽然简单,但是默认下载最新的NDK,在编译的时候可能会出现后面我会讲到的一些问题,所以这种方式可以根据实际需要选择合适的版本)
解压上面下载的NDK压缩包
使用下面的命令配置环境变量:
1 | vim ~/.bash_profile |
或者想要替换Android Studio中的NDK环境为自己下的版本的话将上面下载的NDK压缩包重命名为ndk-bundle后放到sdk目录下即可
(2)编译libncnn.a
根据上面ncnn的github下的教程有:
1 | $ cd <ncnn-root-dir> |
即可“build armv7 library”,之后便会在build-android-armv7/install/lib目录下生成libncnn.a,这样ncnn的编译工作就完成了
3、使用NCNN将caffemodel转换成NCNN中需要的格式
参照上面ncnn的github下第二个教程:
https://github.com/Tencent/ncnn/wiki/how-to-use-ncnn-with-alexnet
首先是下载模型以及权重文件:
1 | train.prototxt |
然后使用之前编译好的caffe中build/tools文件夹下的upgrade_net_proto_text和upgrade_net_proto_binary两个文件分别处理模型以及权重文件:
1 | upgrade_net_proto_text [old prototxt] [new prototxt] |
同时要更改数据层的batchsize大小为1:
1 | layer { |
经过上面的步骤就准备好了需要转换的模型和权重文件。
接下来进入之前clone的 ncnn工程文件:
1 | cd tools/caffe |
就可以在build文件夹中生成caffe2ncnn.cpp对应的可执行文件caffe2ncnn,最后执行:
1 | caffe2ncnn deploy.prototxt bvlc_alexnet.caffemodel alexnet.param alexnet.bin |
就可以得到最后转化的权重以及模型文件:alexnet.param alexnet.bin
4、编译jni生成了.so库文件
进入刚刚ncnn工程下的examples中,这是一个用squeeze net作为例子来生成动态链接库的例子,可以看到examples下面有已经按照3中步骤生成好的squeeze net对应的权重和模型文件,
进入的squeezencnn/jni文件夹中,可以看到如下文件架结构:
其中的cpp和h就是我们需要编写的C++文件和头文件,其中包含以下几个部分:
- 我们需要的C++功能函数以及对应的头文件
- C++和java之间的jni接口函数,用于两者之间的信息互通
然后在终端使用
1 | ndk-build |
命令就可以将上面的文件打包成一个 .so动态链接库供Android调用,可以参考:
https://blog.csdn.net/CrazyMo_/article/details/52804896 中的讲解,下面我以squeeze net这个例子简单说明一下安卓调用的过程:
首先是Android Studio工程中的结构为:
实际上上图中的工程顺序也就是我们建立我们工程的顺序:
- 按照上面3中的步骤转换的模型就放在assets目录下
- 然后我们除了MainActivity.java,就可以定义一个自己需要的函数接口类代码,比如这里的SqueezeNcnn.java,里面的内容为:
1 | package com.tencent.squeezencnn; |
然后可以参考:https://blog.csdn.net/createchance/article/details/53783490
来自动生成jni文件夹下的squeezenet_v1.1.id.h和squeezencnn_jni.cpp,然后在其中进一步编写我们需要实现的功能函数
- 接着就是JNI代码了,这个部分实际上包含了实现功能的C/C++代码以及jni接口函数两部分,通过上面的生成,我们得到了squeezenet_v1.1.id.h和squeezencnn_jni.cpp,对应于上面SqueezeNcnn.java中的类方法,squeezencnn_jni.cpp中有对应的JNI接口函数:
(函数具体内容大家可以到ncnn工程中查看,这里为了说明方便隐去内容)
可以看到jni接口函数是在java类函数的前面加上了
1 | Java_com_tencent_squeezencnn_SqueezeNcnn_ |
部分,将java的native方法转换成C函数声明的规则是这样的:Java_{package_and_classname}_{function_name}(JNI arguments)。包名中的点换成单下划线。需要说明的是生成函数中的两个参数:
JNIEnv *:这是一个指向JNI运行环境的指针,后面我们会看到,我们通过这个指针访问JNI函数
jobject:这里指代java中的this对象
而对于一些不是接口的功能函数,我们就可以使用C++或者C来编写,而不需要考虑jni
- 最后就是将上面的代码编译成libsqueezencnn.so动态库
这里我们首先需要编写jni目录下的编译配置文件 Android.mk 和 Application.mk ,类似于C++编译中的CMakeLists.txt:
Android. mk :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31LOCAL_PATH := $(call my-dir)
# change this folder path to yours
NCNN_INSTALL_PATH := /Users/camlin_z/Data/Project/AndroidStudioProjects/ncnn-master/build-android-armv7/install
include $(CLEAR_VARS)
LOCAL_MODULE := ncnn
# LOCAL_SRC_FILES := $(NCNN_INSTALL_PATH)/$(TARGET_ARCH_ABI)/libncnn.a
LOCAL_SRC_FILES := $(NCNN_INSTALL_PATH)/lib/libncnn.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := squeezencnn
LOCAL_SRC_FILES := squeezencnn_jni.cpp
LOCAL_C_INCLUDES := $(NCNN_INSTALL_PATH)/include
LOCAL_STATIC_LIBRARIES := ncnn
LOCAL_CFLAGS := -O2 -fvisibility=hidden -fomit-frame-pointer -fstrict-aliasing -ffunction-sections -fdata-sections -ffast-math
LOCAL_CPPFLAGS := -O2 -fvisibility=hidden -fvisibility-inlines-hidden -fomit-frame-pointer -fstrict-aliasing -ffunction-sections -fdata-sections -ffast-math
LOCAL_LDFLAGS += -Wl,--gc-sections
LOCAL_CFLAGS += -fopenmp
LOCAL_CPPFLAGS += -fopenmp
LOCAL_LDFLAGS += -fopenmp
LOCAL_LDLIBS := -lz -llog -ljnigraphics
include $(BUILD_SHARED_LIBRARY)
具体里面的配置方法可以参考:
http://www.cnblogs.com/wainiwann/p/3837936.html
Application. mk:
1 | # APP_STL := stlport_static |
写好上面的各个配置文件之后就可以在终端进入jni文件夹输入:
1 | ndk-build |
命令进行编译生成 libsqueezencnn. so动态链接库,经过了以上的所有步骤得到最后的动态链接库,Android中的函数就可以直接调用来实现对应的功能了