前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >安卓端深度学习模型部署-以NCNN为例

安卓端深度学习模型部署-以NCNN为例

作者头像
带萝卜
发布2020-10-23 14:50:18
3.1K0
发布2020-10-23 14:50:18
举报

图已上传,对步骤不清楚的朋友可以留言,或者直接移步项目代码:

https://github.com/Arctanxy/DeepLearningDeployment/tree/master/SimplestNCNNExample

上一篇文章讲到了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,这个模拟器我没有用过,因为网速原因,我花了半天(字面意思)也没有把模拟器安装好。

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

逍遥模拟器

1.3 OpenCV源码

相比嵌入式环境来说,移动端的资源还是比较充足的,并且AndroidStudio中似乎有自动压缩库文件的功能,所以可以在安卓项目里面放心大胆地使用OpenCV。

1.4 NCNN源码

NCNN也可以选择下载预编译库。

2. 交叉编译

使用ndk的cmake toolchain进行交叉编译

2.1 编译opencv

代码语言:javascript
复制
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/zjk/tmp/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

代码语言:javascript
复制
mkdir build_arm;cd build_arm;
cmake \
-DCMAKE_TOOLCHAIN_FILE=\
/media/zjk/tmp/library/temp/android-ndk-r18b/build/cmake/android.toolchain.cmake \
-DANDROID_NDK=/media/zjk/tmp/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文件中添加如下内容:

代码语言:javascript
复制
android{
    defaultConfig{
        ndk{
            abiFilters 'armeabi-v7a'
        }
    }
}

4. 代码编写

4.1 Java与C++代码的衔接

创建完项目之后,可以看到src/main/cpp下有一个CMakeLists和native-lib.cpp,这个cpp文件里面有一个样例函数:

代码语言:javascript
复制
#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

代码语言:javascript
复制
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,内容如下:

代码语言:javascript
复制
cmake_minimum_required(VERSION 3.4.1)

## add ncnn prebuilt 0413
set(ncnn_path D:\\scripts\\ocr_android\\ncnn_0413)
include_directories(${ncnn_path}\\include\\ncnn)
link_directories(${ncnn_path}\\armeabi-v7a)
set(ncnn_lib ${ncnn_path}\\armeabi-v7a\\libncnn.a)
add_library (ncnn STATIC IMPORTED)
set_target_properties(ncnn PROPERTIES IMPORTED_LOCATION ${ncnn_lib})

# add opencv
set(OpenCV_DIR D:\\scripts\\ocr_android\\opencv_release_armeabi\\build)
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 头文件

代码语言:javascript
复制
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

代码语言:javascript
复制
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方法提取中间节点的运算结果
代码语言:javascript
复制
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()函数就需要改写成这样。

代码语言:javascript
复制
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对象),可以修改成如下形式:

代码语言:javascript
复制
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这个模型压缩的非常小,这个项目里面有效果更好的模型,只是模型尺寸更大,推理代码也更加复杂。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 踩坑概述
  • 1. 环境配置
    • 1.1 ncnn模型
      • 1.2 AndroidStudio和逍遥模拟器
        • 1.3 OpenCV源码
          • 1.4 NCNN源码
          • 2. 交叉编译
            • 2.1 编译opencv
              • 2.2 编译ncnn
              • 3. 安装项目创建
                • 3.1 创建Native C++项目
                  • 3.2 修改编译的目标平台
                  • 4. 代码编写
                    • 4.1 Java与C++代码的衔接
                      • 4.2 CMakeLists
                        • 4.3 头文件
                          • 4.3 模型加载
                            • 4.4 模型推理
                              • 4.5 模型文件与图片的加载
                                • 4.6 Java函数修改
                                • 5. 最终效果
                                相关产品与服务
                                AI 应用产品
                                文字识别(Optical Character Recognition,OCR)基于腾讯优图实验室的深度学习技术,将图片上的文字内容,智能识别成为可编辑的文本。OCR 支持身份证、名片等卡证类和票据类的印刷体识别,也支持运单等手写体识别,支持提供定制化服务,可以有效地代替人工录入信息。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档