安卓调用第三方带模型的so文件教程
百度上的教程基本都是复制黏贴,针对的是简单的demo.而项目中都比较复杂,做AI的都需要加载模型,而正常的编译生成so文件是不带模型文件的.故需要特殊处理.
一、准备工作
1.1 具有ncnn模型文件,如果没有可以把mxnet,onnx跟caffe模型通过ncnn工具进行转换
1.2 电脑已经安装了cmake, 编译了protoobuf、ncnn(利用vs2019编译,查看cmake_protoobuf_ncnn_opecv安装教程)
二、不把模型打包进so的调用方法
2.1 原项目编译
原项目新建时需选择C++安卓项目
在编译原始项目时,在app/src/mian下新建一个assets的文件夹,不能右键-新建文件夹,这不不会被识别为资源文件.必须在android studio里右键-New-Folder-Assets Folder,把模型文件放入该文件夹中.
编译完成后在\app\build\intermediates下的cmake, merged_native_libs,stripped_native_libs下的子文件夹下产生相应的so文件,文件格式为lib库名.so,其中库名在CmakeLists.txt里设置.
./app/build/intermediates/cmake/debug/obj/arm64-v8a/libarc_sateis.so
./app/build/intermediates/cmake/debug/obj/armeabi-v7a/libarc_sateis.so
./app/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libtest.so
./app/build/intermediates/merged_native_libs/debug/out/lib/armeabi-v7a/libtest.so
./app/build/intermediates/stripped_native_libs/debug/out/lib/arm64-v8a/libtest.so
./app/build/intermediates/stripped_native_libs/debug/out/lib/armeabi-v7a/libtest.so
其中obj是中间生成so,用的话可以用merged_native_libs和stripped_native_libs下的;
区别:cmake, merged_native_libs的so文件一样大,stripped_native_libs的so文件更小.,应该是merged下包含一些map信息、地址等,调试用比较合适;stripped移除了这部分,release比较合适
2.2 新安卓项目
2.2.1 拷贝so文件
拷贝出任意一个(拷贝出带安卓cpu的文件夹,如armeabi-v7a), 在新安卓项目中(不需要C++安卓项目),在app下新建文件夹libs,把带so的文件夹拷贝进这里:
image-20210715100207195.png在app下的build.gradle中增加ndk 跟sourceSets
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.ylz.javatest"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
// moduleName "ncnn"
ldLibs "log"
abiFilters "armeabi-v7a", "armeabi", "x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs'] //寻找库文件地址添加进library
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
这样设置后会在app下自动生成jniLibs文件夹,含了刚刚添加进去的文件
image-20210715102114669.png2.2.2 设置资源文件
同源项目一样,在app/src/main下新建assets资源文件夹,并把模型文件放入
2.2.3 建立类文件
需要建立与so文件功能对应的类文件,里面含有他的功能,并且文件的名称跟地址都要跟源so一模一样.
image-20210715104837582.png点击运行即可生成apk,可使用so文件里的功能
三、模型文件加密
原始ncnn模型文件有:.param参数文件跟.bin模型文件
mnet.25-opt.param mnet.25-opt.bin
由于后面生成头文件 .跟-都会变成下划线,我干脆对上面文件重命名
mnet25.param mnet25.bin
把模型文件复制到ncnn/build-vs2019/tools下.有一个ncnn2mem.exe文件,对参数文件和模型文件进行加密会生成param.bin、id.h跟mem.h头文件, 命令行中不需要写param.bin
$./ncnn2mem mnet25.param mnet25.bin mnet25.id.h mnet25.mem.h
mnet25.param.bin //二进制的模型结构文件
mnet25.id.h //模型结构头文件
mnet25.mem.h //模型参数头文件
读取文件
//load非加密的ncnn模型
ncnn::Net net;
net.load_param_bin("mnet25.param");
net.load_model("mnet25.bin");
//load加密的ncnn模型
ncnn::Net net;
net.load_param_bin("mnet25.param.bin");
net.load_model("mnet25.bin");
四、源文件改造
4.1 导入头文件
由于param.bin窥探不到模型结构,因此,需要导入id.h头文件来获取模型的输入和输出
这种方式虽然给模型文件加密了,但还是没办法把模型文件加入到so文件,其实两个头文件已经涵盖了模型的所有内容,所以我们只需要把两个头文件赋值到源项目中的app/src/main/cpp下,就不再需要assets文件夹了.
image-20210715110328657.png打开mnet25.id.h查看内容如下:
#ifndef NCNN_INCLUDE_GUARD_mnet25_id_h
#define NCNN_INCLUDE_GUARD_mnet25_id_h
namespace mnet25_param_id {
const int LAYER_data = 0;
const int BLOB_data = 0;
const int LAYER_mobilenet0_conv0_fwd = 1;
const int BLOB_mobilenet0_relu0_fwd = 1;
const int LAYER_mobilenet0_conv1_fwd = 2;
const int BLOB_mobilenet0_relu1_fwd = 2;
...
...
const int BLOB_face_rpn_cls_prob_reshape_stride8 = 106;
const int LAYER_face_rpn_bbox_pred_stride8 = 89;
const int BLOB_face_rpn_bbox_pred_stride8 = 107;
const int LAYER_face_rpn_landmark_pred_stride8 = 90;
const int BLOB_face_rpn_landmark_pred_stride8 = 108;
} // namespace mnet25_param_id
#endif // NCNN_INCLUDE_GUARD_mnet25_id_h
可以看到输入层输入数据命名为mnet25_param_id::BLOB_data
在原始cpp代码中需要修改输入跟输出,以及加载方式:
4.2 增加导入头文件
#include "mnet25.id.h"#include "mnet25.mem.h"
导入mmet25.mem.h 会报文件大小限制问题,根据提示修改设置Help-Edit Custom Properties
# custom Android Studio propertiesidea.max.intellisense.filesize=5000000 //我设置为5M
4.3 修改加载模型方式
从内存引用加载网络和模型,没有可见字符串,模型数据全在代码里头,没有任何外部文件 另外,android apk 打包的资源文件读出来也是内存块
当加载模型文件时,可传入文件管理器AAssetManager,但如果加载为模型的头文件时,不支持该项.
//加载模型方式//AAssetManager *mgr = AAssetManager_fromJava(env, assetManager); //int ret = retinaface.load_param(mgr, "mnet.25-opt.param"); //int ret = retinaface.load_param_bin(mgr,"mnet25.param.bin") //加载二进制模型//ret = retinaface.load_model(mgr,"mnet25.bin")
<font color='red'>这种方式ret=0,表明加载模型/参数成功</font>
//加载头文件方式int ret =retinaface.load_param(mnet25_param_bin); //在官方说明里用的是load_param_bin,但是测试发现报错, int ret =retinaface.load_model(mnet25_bin)
<font color='red'>这种方式 加载成功,ret并不是==0, 在我这个测试中加载成功参数ret=4984,bin=853632</font>
安卓c++查看log信息方法
注意:log打印信息必须为char* 类型,所以其他类型都要转为char*类型
#include <android/log.h>#define TAG "projectname" // 这个是自定义的LOG的标识#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型//或者#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, fmt, ##args)//其他类似
//打印信息//原始方法__android_log_print(ANDROID_LOG_DEBUG, "RetinaFace", "load_param failed") //固定内容__android_log_print(ANDROID_LOG_DEBUG, "RetinaFace", "加载模型参数=%d",ret); //根据参数ret变化//简写方法LOGI("ret= %d \n",ret) //必须有有%d//如果参数为char*时,采用以下方法int ret;string ret_info = to_string(ret);const char * ret_infos = ret_info.c_str(); //或者用.data()LOGI("ret= %s \n",ret_infos)
<font color='red'>注意:打印信息的内容格式只能为char*,必须要加%,如%d,%f,%s, 不能像python的print()或者c++的cout功能.</font>
解决中文乱码问题:
cpp文件必须在UTF-8下编写,如果之前为GBK, 则点击后选择utf-8 点convert
image-20210715195245963.png
4.4 修改输入与输出
修改的内容为把字符串的输入输出层名称根据mnet25.id.h 修改为指引方式
输入修改
// ex.input("data", in); ex.input(mnet25_param_id::BLOB_data, in);
输出修改(其中一个示意)
// stride 32 { ncnn::Mat score_blob, bbox_blob, landmark_blob;// ex.extract("face_rpn_cls_prob_reshape_stride32", score_blob);// ex.extract("face_rpn_bbox_pred_stride32", bbox_blob);// ex.extract("face_rpn_landmark_pred_stride32", landmark_blob); ex.extract(mnet25_param_id::BLOB_face_rpn_cls_prob_reshape_stride32, score_blob); ex.extract(mnet25_param_id::BLOB_face_rpn_bbox_pred_stride32, bbox_blob); ex.extract(mnet25_param_id::BLOB_face_rpn_landmark_pred_stride32, landmark_blob);
这样运行编译后即可得到so包含了两个头文件,即含有了模型文件
另外,可进行如下设置
ncnn::Extractor ex = retinaface.create_extractor(); ex.set_light_mode(true); //使用light模式,intermediate blob will be recycled when enabled ex.set_num_threads(4); //使用4线程
4.5 其他项目调用第三方so文件
新建一个安卓空项目,设置同2.2一致,唯一不同就是不需要新建assets文件.
这样我们就只需要提供一个so文件给别人,而不需要提供模型文件.别人就可以在他们项目中调用我们的模型及函数了.
参考:use ncnn with alexnet.zh · Tencent/ncnn Wiki (github.com)
网友评论