图已上传,对步骤不清楚的朋友可以留言,或者直接移步项目代码:
https://github.com/Arctanxy/DeepLearningDeployment/tree/master/SimplestNCNNExamplegithub.com
上一篇文章讲到了NCNN的移动端部署,关于部署的步骤,很多人表示写得太抽象了,所以这篇文章是对上一篇文章的补充说明。
本文内容较长,面向的读者是有深度学习模型需要部署到安卓端,却对安卓开发相关知识一头雾水的朋友。
0. 踩坑概述
坑主要出现在安卓相关的部分,模型推理的接口很简单,没有遇到过什么难解决的问题。
一开始完全不懂安卓和java,遇到了不少问题。下面几个步骤花费了较多的时间:
- 解决AndroidStudio里面一些莫名其妙的错误
- 交叉编译
- 捣鼓Bitmap和AssetsManager
为了缩短篇幅,文中的代码是从完整项目里面抽离出来的,仅供参考
1. 环境配置
本文的交叉编译在Ubuntu18.04上进行,安卓项目开发在Win7上进行
首先需要准备
- 一个ncnn模型(包括param和bin)文件;
- AndroidStudio和逍遥模拟器;
- OpenCV最新源码;
- NCNN最新源码;
- AndroidNDK r18b(linux),最初选择的是r20b,因为和CMake之间的兼容问题,切换到了18b;
1.1 ncnn模型
我这里是直接拿了上次的chineseocr_lite中的crnn模型进行的测试,如果是其他模型写法也是类似的。
1.2 AndroidStudio和逍遥模拟器
AndroidStudio和JDK的安装请自行百度。
这里介绍一下模拟器的选择,Android开发比较麻烦的一点就是我们开发的apk是没法直接跑在PC上的,必须要有一个载体,这个载体可以是模拟器,也可以是连接到PC上的手机(也就是所谓的真机调试)。
在这里我给非专业安卓开发者的建议是:使用国产模拟器, 因为:
- AndroidStudio自带的模拟器非常卡、非常占内存;
- 真机调试老是掉线,这可能跟我的手机有关,可惜在安卓同事的帮助下最终也没有解决这个问题,所以也不建议;
- 在网上搜AndroidStudio模拟器选择,有很多博客都推荐Genymotion,这个模拟器我没有用过,因为网速原因,我花了半天(字面意思)也没有把模拟器安装好。
所以我最后的选择是这个:
逍遥模拟器
1.3 OpenCV源码
相比嵌入式环境来说,移动端的资源还是比较充足的,并且AndroidStudio中似乎有自动压缩库文件的功能,所以可以在安卓项目里面放心大胆地使用OpenCV。
1.4 NCNN源码
NCNN也可以选择下载预编译库。
2. 交叉编译
使用ndk的cmake toolchain进行交叉编译
2.1 编译opencv
mkdir build_arm;cd build_arm;
cmake
-DCMAKE_TOOLCHAIN_FILE=
/media/dailuobo/library/temp/android-ndk-r18b/build/cmake/android.toolchain.cmake
-DANDROID_NDK=/media/luobodai/library/temp/android-ndk-r18b
-DCMAKE_BUILD_TYPE=Release
-DBUILD_ANDROID_PROJECTS=OFF
-DBUILD_ANDROID_EXAMPLES=OFF
-DANDROID_ABI=armeabi-v7a
-DANDROID_NATIVE_API_LEVEL=21 ..
make -j4
2.2 编译ncnn
mkdir build_arm;cd build_arm;
cmake
-DCMAKE_TOOLCHAIN_FILE=
/media/luobodai/library/temp/android-ndk-r18b/build/cmake/android.toolchain.cmake
-DANDROID_NDK=/media/luobodai/library/temp/android-ndk-r18b
-DCMAKE_BUILD_TYPE=Release
-DANDROID_ABI=armeabi-v7a
-DANDROID_NATIVE_API_LEVEL=21 ..
make -j4
这样编译完成之后就可以得到OpenCV和NCNN的静态库。
3. 安装项目创建
3.1 创建Native C++项目
创建项目的界面
C++ standard 选择 C++11
创建完成之后,可能会看见报错:
Unable to resolve dependency for ':app@debug/compileClasspath': Could not find any version that matches com.android.support:appcompat-v7:29.+.
把app/build.gradle文件中的implementation 'com.android.support:appcompat-v7:29.+'
修改成implementation 'com.android.support:appcompat-v7:+'
即可。
可以先编译运行一下这个helloworld项目,确认项目配置没有问题之后再开始添加代码。
项目目录如下:
项目目录
其中:
- 模型文件放在assets目录下(需要自建)
- cpp代码放在cpp目录下
- java代码放在java目录下
- 界面的xml文件放在res/layout目录下
3.2 修改编译的目标平台
默认情况下会面向四个平台编译:x86、x64、armeabi-v7a、arm64-v8a,这里我们只希望编译armeabi-v7a,可以在app/build.gradle文件中添加如下内容:
android{
defaultConfig{
ndk{
abiFilters 'armeabi-v7a'
}
}
}
4. 代码编写
4.1 Java与C++代码的衔接
创建完项目之后,可以看到src/main/cpp下有一个CMakeLists和native-lib.cpp,这个cpp文件里面有一个样例函数:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cardocrapp_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
函数名称与对应的java函数代码路径有关,比如这个函数名叫Java_com_example_cardocrapp_MainActivity_stringFromJNI
,而它对应的函数位于java/com/example/cardocrapp/MainActivity.java
中,名为stringFromJNI
:
package com.example.cardocrapp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
我们的自定义函数也需要参照这种命名规则。
另外这个函数有两个默认参数,JNIEnv *env 和 jobject, 可以看到这两个参数在对应的java函数中是没有的,应该是环境默认参数。我们自定义函数的参数可以加在这两个参数的后面。
4.2 CMakeLists
cmake中需要导入Opencv、NCNN和Openmp,内容如下:
cmake_minimum_required(VERSION 3.4.1)
## add ncnn prebuilt 0413
set(ncnn_path D:scriptsocr_androidncnn_0413)
include_directories(${ncnn_path}includencnn)
link_directories(${ncnn_path}armeabi-v7a)
set(ncnn_lib ${ncnn_path}armeabi-v7alibncnn.a)
add_library (ncnn STATIC IMPORTED)
set_target_properties(ncnn PROPERTIES IMPORTED_LOCATION ${ncnn_lib})
# add opencv
set(OpenCV_DIR D:scriptsocr_androidopencv_release_armeabibuild)
find_package(OpenCV REQUIRED)
# openmp
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fopenmp")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fopenmp")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fopenmp")
add_library( # Sets the name of the library.
native-lib
SHARED
native-lib.cpp model.cpp)
find_library(
log-lib
log
android)
target_link_libraries( # Specifies the target library.
native-lib
ncnn
${OpenCV_LIBS}
android
jnigraphics
${log-lib})
4.3 头文件
class model {
public:
model(){};
~model(){};
int init(AAssetManager *mgr, const std::string crnn_param, const std::string crnn_bin);
int forward(const cv::Mat image, std::string &result);
int forward(const std::string image_path, std::string &result);
private:
ncnn::Net crnn;
int decode(const ncnn::Mat score, const std::string alphabetChinese, std::string &result);
const float mean_vals_crnn[1] = { 127.5};
const float norm_vals_crnn[1] = { 1.0 /127.5};
std::string utf8_substr2(const std::string &str,int start, int length=INT_MAX);
std::string alphabetChinese = 此处省略5000+字符;
};
因为decode函数和utf8_substr2函数与本文内容不太相关,为了节省篇幅,可以去chineseocr_lite项目查看。
4.3 模型加载
关于AAssetsManager的解释请看4.5
int model::init(AAssetManager *mgr, const std::string crnn_param, const std::string crnn_bin)
{
int ret1 = crnn.load_param(mgr, crnn_param.c_str());
int ret2 = crnn.load_model(mgr, crnn_bin.c_str());
LOGI("ret1 is %d, ret2 is %d", ret1, ret2);
return (ret1||ret2);
}
4.4 模型推理
运行ncnn模型分三步:
- 创建一个ncnn::Extractor对象
- 设置输入;
- 提取输出节点,也可以使用extract方法提取中间节点的运算结果
int model::forward(const cv::Mat image, std::string &result){
ncnn::Mat in = ncnn::Mat::from_pixels(image.data, ncnn::Mat::PIXEL_BGR2GRAY, image.cols, image.rows);
in.substract_mean_normalize(mean_vals_crnn, norm_vals_crnn);
LOGI("input size : %d, %d, %d", in.w, in.h, in.c);
ncnn::Extractor ex = crnn.create_extractor();
ex.input("input",in);
ncnn::Mat preds;
ex.extract("out",preds);
LOGI("output size : %d, %d, %d", preds.w, preds.h, preds.c);
decode(preds, alphabetChinese, result);
return 0;
}
4.5 模型文件与图片的加载
项目生成apk之后,我们就没办法直接获取到模型文件的绝对路径了,所以也就不能通过路径来读取,为了解决这个问题,有三种思路:
1. 在app启动的时候,把模型文件移动到存储卡中一个有权限的文件夹下面,比如Download文件夹,然后通过绝对路径来读取模型文件;
2. 在Java端使用AssetsManager读取到assets下的模型文件,以二进制数据的形式传输到C++函数中;
3. 在C++端利用AssetsManager直接读取模型文件。
这里选择的是第三种,但是AssetsManager对象还是需要Java端传入。
同理,我们放在项目里面的图片也读不到了,需要在java端使用Bitmap读取,然后传入C++函数,转换成cv::Mat之后才能用。
那么,nativa-lib.cpp文件中的stringFromJNI()函数就需要改写成这样。
model *ocr = new model();
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cardocrapp_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */,jobject assetManager, jobject bitmap) {
LOGI("loading assetmanager");
static AAssetManager * mgr = NULL;
mgr = AAssetManager_fromJava( env, assetManager);
LOGI("convert bitmap to cv::Mat");
// convert bitmap to mat
int *data = NULL;
AndroidBitmapInfo info = {0};
AndroidBitmap_getInfo(env, bitmap, &info);
AndroidBitmap_lockPixels(env, bitmap, (void **) &data);
// 这里偷懒只写了RGBA格式的转换
LOGI("info format RGBA ? %d", info.format == ANDROID_BITMAP_FORMAT_RGBA_8888);
cv::Mat test(info.height, info.width, CV_8UC4, (char*)data); // RGBA
cv::Mat img_bgr;
cvtColor(test, img_bgr, CV_RGBA2BGR);
LOGI("loading model");
std::string crnn_param = "crnn_lite_dw_dense.param";
std::string crnn_bin = "crnn_lite_dw_dense.bin";
int ret = ocr->init(mgr, crnn_param, crnn_bin);
std::string result;
if(ret){
result = "Model loading failed";
return env->NewStringUTF(result.c_str());
}
LOGI("running model");
ocr->forward(img_bgr, result);
return env->NewStringUTF(result.c_str());
}
4.6 Java函数修改
对应的Java这边也需要给StringFromJNI()函数提供素材(AssetsManager和Bitmap对象),可以修改成如下形式:
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
AssetManager am = getAssets();
// Bitmap
String filename = "test.png";
Bitmap bitmap = null;
try
{
InputStream is = am.open(filename);
bitmap = BitmapFactory.decodeStream(is);
is.close();
}catch (IOException e)
{
e.printStackTrace();
}
Log.i(TAG, "java forwarding ...");
tv.setText(stringFromJNI(am, bitmap));
}
public native String stringFromJNI(AssetManager am, Bitmap bitmap);
}
5. 最终效果
我把下面这张图片命名成test.png加入到模型中:
这是一张图片
最终的结果如下:
crnn表示它已经尽力了
这里解释一下,效果不好的原因是因为crnn_lite_dw_dense这个模型压缩的非常小,这个项目里面有效果更好的模型,只是模型尺寸更大,推理代码也更加复杂。