Caffe转NCNN并移植Android配置记录

实验目的:

将caffe模型转成ncnn可以实现在移动端运行深度学习模型,主要使用:
https://github.com/Tencent/ncnn

实验环境:

1、系统环境

实验过程

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
2
3
4
5
6
7
8
9
10
vim ~/.bash_profile

# 在.bash_profile文件的最后添加上(路径根据自己的进行修改):
export PATH=$PATH:/Users/camlin_z/Data/Project/AndroidStudioProjects/android-ndk-r10e
# 或者想把环境变量添加成Android Studio配置的NDK的话:
export ANDROID_SDK="/Users/camlin_z/Library/Android/sdk"
export ANDROID_NDK="/Users/camlin_z/Library/Android/sdk/ndk-bundle"
export PATH="$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools:$ANDROID_NDK"

source ~/.bash_profile

或者想要替换Android Studio中的NDK环境为自己下的版本的话将上面下载的NDK压缩包重命名为ndk-bundle后放到sdk目录下即可

(2)编译libncnn.a
根据上面ncnn的github下的教程有:

1
2
3
4
5
6
7
8
$ cd <ncnn-root-dir>
$ mkdir -p build-android-armv7
$ cd build-android-armv7
$ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI="armeabi-v7a" -DANDROID_ARM_NEON=ON \
-DANDROID_PLATFORM=android-14 ..
$ make -j4
$ make install

即可“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
2
3
train.prototxt
deploy.prototxt
snapshot_10000.caffemodel

然后使用之前编译好的caffe中build/tools文件夹下的upgrade_net_proto_text和upgrade_net_proto_binary两个文件分别处理模型以及权重文件:

1
2
upgrade_net_proto_text [old prototxt] [new prototxt]
upgrade_net_proto_binary [old caffemodel] [new caffemodel]

同时要更改数据层的batchsize大小为1:

1
2
3
4
5
6
layer {
name: "data"
type: "Input"
top: "data"
input_param { shape: { dim: 1 dim: 3 dim: 227 dim: 227 } }
}

经过上面的步骤就准备好了需要转换的模型和权重文件。

接下来进入之前clone的 ncnn工程文件

1
2
3
4
cd tools/caffe
mkdir build
cmake ..
make -j4

就可以在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tencent.squeezencnn;

import android.graphics.Bitmap;
import android.content.Context;

public class SqueezeNcnn
{
// 我们自己定义的类方法,用于实现我们自己的功能(这里可以看到是java)
public native boolean Init(byte[] param, byte[] bin, byte[] words);

public native String Detect(Bitmap bitmap);

static {
System.loadLibrary("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
31
LOCAL_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
2
3
4
5
6
7
8
9
10
# APP_STL := stlport_static
APP_STL := gnustl_static
# APP_ABI := armeabi armeabi-v7a

# 注意此处哟啊对应你之前编译ncnn时的版本,比如我之前用的就是armeabi-v7a
# 下面就要指定为armeabi-v7a,不能再有后面的arm64-v8a
APP_ABI := armeabi-v7a #arm64-v8a

APP_PLATFORM := android-14
# NDK_TOOLCHAIN_VERSION := 4.9

写好上面的各个配置文件之后就可以在终端进入jni文件夹输入:

1
ndk-build

命令进行编译生成 libsqueezencnn. so动态链接库,经过了以上的所有步骤得到最后的动态链接库,Android中的函数就可以直接调用来实现对应的功能了

-------------本文结束感谢您的阅读-------------