图已上传,对步骤不清楚的朋友可以留言,或者直接移步项目代码:
https://github.com/Arctanxy/DeepLearningDeployment/tree/master/SimplestNCNNExample​github.com

上一篇文章讲到了NCNN的移动端部署,关于部署的步骤,很多人表示写得太抽象了,所以这篇文章是对上一篇文章的补充说明。

本文内容较长,面向的读者是有深度学习模型需要部署到安卓端,却对安卓开发相关知识一头雾水的朋友。

0. 踩坑概述

坑主要出现在安卓相关的部分,模型推理的接口很简单,没有遇到过什么难解决的问题。
一开始完全不懂安卓和java,遇到了不少问题。下面几个步骤花费了较多的时间:

  1. 解决AndroidStudio里面一些莫名其妙的错误
  2. 交叉编译
  3. 捣鼓Bitmap和AssetsManager

为了缩短篇幅,文中的代码是从完整项目里面抽离出来的,仅供参考

1. 环境配置

本文的交叉编译在Ubuntu18.04上进行,安卓项目开发在Win7上进行

首先需要准备

  1. 一个ncnn模型(包括param和bin)文件;
  2. AndroidStudio和逍遥模拟器;
  3. OpenCV最新源码;
  4. NCNN最新源码;
  5. AndroidNDK r18b(linux),最初选择的是r20b,因为和CMake之间的兼容问题,切换到了18b;

1.1 ncnn模型

我这里是直接拿了上次的chineseocr_lite中的crnn模型进行的测试,如果是其他模型写法也是类似的。

1.2 AndroidStudio和逍遥模拟器

AndroidStudio和JDK的安装请自行百度。

这里介绍一下模拟器的选择,Android开发比较麻烦的一点就是我们开发的apk是没法直接跑在PC上的,必须要有一个载体,这个载体可以是模拟器,也可以是连接到PC上的手机(也就是所谓的真机调试)。

在这里我给非专业安卓开发者的建议是:使用国产模拟器, 因为:

  1. AndroidStudio自带的模拟器非常卡、非常占内存;
  2. 真机调试老是掉线,这可能跟我的手机有关,可惜在安卓同事的帮助下最终也没有解决这个问题,所以也不建议;
  3. 在网上搜AndroidStudio模拟器选择,有很多博客都推荐Genymotion,这个模拟器我没有用过,因为网速原因,我花了半天(字面意思)也没有把模拟器安装好。

所以我最后的选择是这个:

86d3ea86c258d6b60f904231e02e45ac.png
逍遥模拟器

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++项目

2c31274b13ec2e3158f6de413f1204ed.png
创建项目的界面

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项目,确认项目配置没有问题之后再开始添加代码。

项目目录如下:

750fa0a43185966d12b53c934fce902a.png
项目目录

其中:

  • 模型文件放在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模型分三步:

  1. 创建一个ncnn::Extractor对象
  2. 设置输入;
  3. 提取输出节点,也可以使用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加入到模型中:

dc280716d7d8b8d1ec074f045827b2d0.png
这是一张图片

最终的结果如下:

4709c94c6ca0fb0ec2cdb99adffc10d3.png
crnn表示它已经尽力了

这里解释一下,效果不好的原因是因为crnn_lite_dw_dense这个模型压缩的非常小,这个项目里面有效果更好的模型,只是模型尺寸更大,推理代码也更加复杂。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐