=======================================
Android 推理示例
=======================================


ShuffleNet V2 ARM-Android 示例快速入门
---------------------------------------

这是一个简单的图像分类应用，基于 MegEngine C++接口、Android JNI及Camera API，帮助大家快速在Android平台实现一个图像分类的App。
在这个例子中所使用的模型，为 MegEngine 官方预训练的 `ShuffleNet V2模型`_ ，用于做简单的图像分类任务。

1. 安装MegEngine python库
''''''''''''''''''''''''''

按照MegEngine的安装提示，完成python库的安装

* 通过包管理器 pip 安装 MegEngine，将MegEngine加入到python包中

  ::
    
    pip3 install megengine -f https://megengine.org.cn/whl/mge.html

2. 下载MegEngine的代码仓库
''''''''''''''''''''''''''

我们需要使用 C++ 环境进行最终的部署，所以这里还需要通过源文件来编译安装 C++ 库。

::

   git clone https://github.com/MegEngine/MegEngine.git

MegEngine的依赖组件都位于 third_party 目录下，请在有网络支持的条件下，使用如下脚本进行安装。

::

   ./third_party/prepare.sh
   ./third_party/install-mkl.sh

MegEngine可以支持多平台的交叉编译，可以根据官方指导文档选择不同目标的编译。
对这个例子来说，我们选择ARM-Android的交叉编译。

在ubuntu(16.04/18.04)上进行 ARM-Android的交叉编译:

1. 到Android的官网下载NDK的相关工具，这里推荐 *android-ndk-r21* 以上的版本： `NDK下载`_ 
2. 在bash中设置 NDK_ROOT 环境变量：export NDK_ROOT=NDK_DIR
3. 使用以下脚本进行ARM-Android的交叉编译：

   ::

      ./scripts/cmake-build/cross_build_android_arm_inference.sh

编译完成后，我们可以在 *build_dir/android/arm64-v8a/Release/install* 目录下找到编译生成的库文件和相关头文件。
这时，可以检查一下生成的库是否对应目标架构：

::

   file build_dir/android/arm64-v8a/Release/install/lib64/libmegengine.so
   #libmegengine.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=xxxxx, stripped

3. 准备预训练模型
'''''''''''''''''
想要使用MegEngine C++ API 来加载模型，我们还需要做一些准备工作

#. 获取基于python接口预训练好的神经网络
#. 将基于动态图的神经网络转换成静态图后，再转换成MegEngine C++ API可以加载的 mge文件

官方 `MegEngine ModelHub`_ 提供了多种预训练模型，以及基于python对这些模型进行训练、推理的指导文档。
通过这些指导文档，我们就可以大体了解训练和推理的基本过程。

接下来，通过以下python代码基于动态图的神经网络，实现动态图到静态图的转换并dump出可供C++调用的文件。

*代码片段:*

.. code-block:: python


   import megengine.module as M
   import megengine.functional as F
   import numpy as np

   if __name__ == '__main__':

      import megengine.hub
      import megengine.functional as F
      from megengine.jit import trace

      net = megengine.hub.load("megengine/models", "shufflenet_v2_x1_0", pretrained=True)
      net.eval()

      @trace(symbolic=True)
      def fun(data,*, net):
         pred = net(data)
         pred_normalized = F.softmax(pred)
         return pred_normalized

      data = np.random.random([1, 3, 224,
                              224]).astype(np.float32)
      

      fun.trace(data,net=net)
      fun.dump("shufflenet_deploy.mge", arg_names=["data"], optimize_for_inference=True)

执行脚本，并完成模型转换后，我们就获得了可以通过 MegEngine C++ API 加载的预训练模型文件 **shufflenet_deploy.mge**。

*这里需要注意，dump函数定义了input 为 "data"，在后续使用推理接口传入数据时，需要保持名称一致。*
*另外，dump参数 "optimize_for_inference=True" 可以对dump出的模型进行优化，具体信息可以参考* :meth:`dump optimize API <megengine.jit.trace.dump>` 

4. ShuffleNet V2 C++ 实现示例
''''''''''''''''''''''''''''''''
基于官方的 xor net C++ 案例 `xor net 部署`_ ，我们可以实现自己的基于 ShuffleNet V2 的推理代码。
代码的任务分成四步：

1. 参考官网对于 `ShuffleNet V2模型`_ 要求, 需要先将图像数据转换为指定格式的tensor
2. 将转换好的数据输入到模型的输入层
3. 调用MegEngine C++接口，实现推理过程
4. 将模型的预测结果进行解析，并打印出来

4.1. 将图像数据转换成tensor张量
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
在前面章节，我们在将PKL文件转换成mge模型的时候，为了计算图的全流程，我们是给模型的input层填充了一些随机数据。
现在需要将真实的图像数据填充到input层，以完成对图像的推理。在这个例子中，模型要求的输入数据为 **CHW:3*224*224**。
根据 `ShuffleNet V2模型`_ 的说明，我们需要对图像做以下的预处理

1. 将图像格式转换为 BGR
2. 先将图像缩放到 256*256，避免在后续的裁切中有更多的信息损失
3. 将图像中心裁切到 224*224 的大小，保留ROI区域，并适配模型输入要求
4. 将裁切后的图像做归一化处理, 根据 ModelHub 上的说明，这里用到的 mean 和 std 为：
  
   mean: [103.530, 116.280, 123.675]

   std: [57.375, 57.120, 58.395]

关于图像转换的步骤，可以参考 `inference.py`_ 中的原始代码片段：

.. code-block:: python


   transform = T.Compose(
      [
         T.Resize(256),
         T.CenterCrop(224),
         T.Normalize(
            mean=[103.530, 116.280, 123.675], std=[57.375, 57.120, 58.395]
         ),  # BGR
         T.ToMode("CHW"),
      ]
   )

具体到 C++ 代码的实现，也同样分成三步，我们以 OpenCV 为例：

1. 宽高 resize到 256*256
2. 中心裁切为 224*224
3. 对图像做归一化处理


*代码片段:*

.. code-block:: c++

   constexpr int RESIZE_WIDTH = 256;
   constexpr int RESIZE_HEIGHT = 256;
   constexpr int CROP_SIZE = 224;
   void image_transform(const cv::Mat& src, cv::Mat& dst){

      cv::Mat tmp;
      cv::Mat tmp2;
      // resize 
      cv::resize(src, tmp, cv::Size(RESIZE_WIDTH, RESIZE_HEIGHT), (0, 0), (0, 0), cv::INTER_LINEAR);

      //center crop
      const int offsetW = (tmp.cols - CROP_SIZE) / 2;
      const int offsetH = (tmp.rows - CROP_SIZE) / 2;
      const cv::Rect roi(offsetW, offsetH, CROP_SIZE, CROP_SIZE);
      tmp = tmp(roi).clone();
      //normalize
      tmp.convertTo(tmp2, CV_32FC1);
      cv::normalize(tmp2, dst, 0, 1,cv::NORM_MINMAX, CV_32F);
   }


4.2. 将转换好的图像数据传给 input 层
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

1. 原始图像数据格式是 'HWC', 需要转成模型需要的 'CHW' 数据格式。`HW表示宽高，C表示通道数`
2. 'CHW' 是 'NCHW' 的子集， `N表示batch size`
3. 以下是一个转换的参考示例代码：

*代码片段:*

.. code-block:: c++


      auto data = network.tensor_map.at("data");
      data->resize({1,3,224,224});
      
      auto iptr = data->ptr<float>();
      auto iptr2 = iptr + 224*224;
      auto iptr3 = iptr2 + 224*224;
      auto imgptr = dst.ptr<float>();
      // 给输入 Tensor 赋值
      for (size_t j =0; j< 224*224; j++){
         iptr[j] = imgptr[3*j];
         iptr2[j] = imgptr[3*j +1];
         iptr3[j] = imgptr[3*j +2];
      }

.. note::

    注意，此处网络的输入层名称为“data”，需要和第3节中dump时传入的名称保持一致。

完成数据格式转换后，调用MegEngine的推理接口，对输入图像数据进行预测。

4.3. 调用MegEngine 推理接口
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

*代码片段:*

.. code-block:: c++


   // 读取通过运行参数指定的模型文件,inp_file 需要输入的shufflenet_v2.mge文件
   std::unique_ptr<serialization::InputFile> inp_file = serialization::InputFile::make_fs(argv[1]);

   // 使用 GraphLoader 将模型文件转成 LoadResult，包括了计算图和输入等信息
   auto loader = serialization::GraphLoader::make(std::move(inp_file));
   serialization::GraphLoadConfig config;
   serialization::GraphLoader::LoadResult network =
      loader->load(config, false);

   // 参考上一节代码，将图像数据输入input layer

   // 将网络编译为异步执行函数
   // 输出output_var为一个字典的列表，second拿到键值对中的值，并存在 predict 中
   HostTensorND predict;
   std::unique_ptr<cg::AsyncExecutable> func =
         network.graph->compile({make_callback_copy(
            network.output_var_map.begin()->second, predict)});
   func->execute();
   func->wait();
   
   float* predict_ptr = predict.ptr<float>();

推理函数执行完毕后，会通过回调函数 make_callback_copy 将结果保存在 predict中，predict的类型为：

::

   HostTensorND predict;

我们可以通过打印函数来确认predict 的shape（1，1000）和dimension（2）:

::

   //shape
   predict.shape()
   //dimension
   predict.shape().ndim

对于 ShuffleNet V2 这个case来说，num_class 也即是 *类别数* 保存在：

::

   predict.shape(1)

根据类别数量，可以以此打印出每个类别的confidence，根据预设的阈值THRESHOLD，打印出高于阈值的类别。confidence最高的类别就是此次预测的 top1 结果：

*代码片段:*

.. code-block:: c++


   for (int i = 0; i < num_classes; i++){
      sum += predict_ptr[i];
      if (predict_ptr[i] > THRESHOLD)
         std::cout << " Predicted: " << predict_ptr[i] << " i: "<< i << std::endl;
   }

如果更进一步，我们还可以将label文件进行解析，并对照predict结果输出具体预测的类别。
对于这个示例，label信息保存在 `MegEngine Model`_ 的以下文件中：

   `imagenet_class_info.json`_

调用MegEngine 推理接口的完整代码可以参考：`C++ 推理代码`_ 。

接下来，我们来看看如何做ARM-Android的动态库封装，以使我们的Android应用程序可以正常调用推理接口。

5. C++ Shufflenet SDK封装
''''''''''''''''''''''''''''''''''''''''''
基本了解C++推理过程后，我们接着将相关通用过程封装为SDK动态库，提供API给主程序使用，方便后面通过JNI部署到Android APP上。
主要有如下过程：

* 设计API并实现API功能。
* 交叉编译动态库。
* 测试验证。

JNI 整体的目录结构设计如下：

::

   .
   inference_jni   //shufflenet 子模块，提供java 和jni interface，并包含megengine动态库
       ├── build.gradle
       └── src
           └─── main
               ├── AndroidManifest.xml
               ├── cpp
               │   ├── CMakeLists.txt
               │   ├── inference_jni.cpp
               │   └── native_interface
               │       ├── build_inference.sh
               │       ├── CMakeLists.txt
               │       ├── prebuilt    //构建native shuffletnet interface需要使用的动态库
               │       │   ├── megengine   //MegEngine 动态库及相关头文件
               │       │   └── opencv2 //图像处理需要使用的opencv库及相关头文件
               │       ├── src //Shufflenet SDK interface实现
               │       │   ├── inference_log.h
               │       │   ├── shufflenet_interface.cpp
               │       │   ├── shufflenet_interface.h
               │       │   └── shufflenet_run.cpp //shuffleNet可执行文件源码
               │       └── third_party
               │           └── cJSON-1.7.13    //解析json需要用到的cjson， 源码编译
               ├── java
               │   └── com
               │       └── example
               │           └── inference   //java shuffletnet interface定义和实现类
               │               └── ImageNetClassifier.java
               └── jniLibs //最终会打包到aar中的动态库

5.1. 设计API，提取公共流程代码为单独函数
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
推理过程主要有init, recognize和close三步，将其分别封装为API，其他函数则作为动态库的static函数内部使用。

*头文件shufflenet_interface.h代码片段:*

.. code-block:: c++


    typedef void *ShuffleNetContext_PTR;
    ShuffleNetContext_PTR PUBLIC_API shufflenet_init(const ModelInit &init);
    void PUBLIC_API shufflenet_recognize(ShuffleNetContext_PTR sc, const FrameData &frame, int number,
                                         FrameResult *results, int *output_size);
    void PUBLIC_API shufflenet_close(ShuffleNetContext_PTR sc);


*动态库主体shufflenet_interface.cpp 参考代码：* `shufflenet interface 代码`_
    
主程序的代码就相对比较简单了。

*测试程序shufflenet_loadrun.cpp代码片段:*

.. code-block:: c++

   
    #include "shufflenet_interface.h"

    using namespace std;

    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            std::cout << " Wrong argument" << std::endl;
            return 1;
        }

        //BGR
        cv::Mat bgr_ = cv::imread(argv[2], cv::IMREAD_COLOR);

        fprintf(stdout, "pic %dx%d c%d\n", bgr_.cols, bgr_.rows, bgr_.elemSize());
        vector<uint8_t> models;
        //读取模型文件
        readBufFromFile(models, argv[1]);
        fprintf(stdout, "======== model size %ld\n", models.size());
        int num_size = 5;
        int output_size = 0;
        FrameResult f_results[5];

        //初始化shufflenet interface
        ShuffleNetContext_PTR ptr = shufflenet_init({.model_data = models.data(), .model_size = models.size(), .json = IMAGENET_CLASS_INFOS, .limit_count = 1, .threshold=0.01f});
        if (ptr == nullptr)
        {
            fprintf(stderr, "fail to init model\n");
            return 1;
        }
        
        //调用识别接口
        shufflenet_recognize(ptr, FrameData{.data = bgr_.data, .size = static_cast<size_t>(bgr_.rows * bgr_.cols * bgr_.elemSize()), .width = bgr_.cols, .height = bgr_.rows, .rotation = ROTATION_0}, num_size, f_results, &output_size);
        for (int ii = 0; ii < output_size; ii++)
        {
            printf("output result[%d] Label:%s, Predict:%.2f\n", ii, (f_results + ii)->label,
                 (f_results + ii)->accuracy);
        }
        printf("test done!");

        //销毁shufflenet handle
        shufflenet_close(ptr);

        return 0;
    }


5.2. 交叉编译动态库和测试程序
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
代码准备好之后，我们使用CMake构建动态库和测试程序。

* 构建的启动脚本参考 `build inference 脚本`_
* CMake构建脚本参考 `libshufflenet_inference CMake 构建脚本`_

最终install目录下的文件

::

	install/
	├── cat.jpg
	├── libmegengine.so
	├── libshufflenet_inference.so
	├── shufflenet_deploy.mge
	└── shufflenet_loadrun


5.3. 测试验证
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
推送相关文件到手机运行验证功能。
::

    adb shell "rm -rf /data/local/tmp/mge_tests"
    adb shell "mkdir -p /data/local/tmp/mge_tests"
    files_=$(ls ${NATIVE_SRC_DIR}/install)
    for pf in $files_
    do
        adb push ${NATIVE_SRC_DIR}/install/$pf /data/local/tmp/mge_tests/
    done

执行命令行示例

::

    adb shell "chmod +x /data/local/tmp/mge_tests/shufflenet_loadrun" &&
    adb shell "cd /data/local/tmp/mge_tests/ && LD_LIBRARY_PATH=./ ./shufflenet_loadrun ./shufflenet_deploy.mge ./cat.jpg"

测试图片

.. image:: imgs/cat.jpg
   :align: center
   :scale: 50%

执行测试程序后，我们可以从标准输出获得predict的结果：
::

    # 阈值设置为0.01f
    ========output size 5
    ========output result[0] Label:Siamese_cat, Predict:0.55
    ========output result[1] Label:Persian_cat, Predict:0.05
    ========output result[2] Label:Siberian_husky, Predict:0.03
    ========output result[3] Label:tabby, Predict:0.03
    ========output result[4] Label:Eskimo_dog, Predict:0.03

6. Android Camera 预览实时推理
''''''''''''''''''''''''''''''''''''''''''
在这个章节，我们来看一下如何使用Android Camera做实时推理
我们可以基于 `Android Camera Example github`_ 修改，快速搭建我们的APP。

主要有如下过程：

* 将labels json文件和Model文件以assets方式打包到APK
* 将libmegengine.so和libshufflenet_inference.so作为动态库打包到APK
* 使用shufflenet interface实现JNI interface
* 获取Android Camera Preview数据, 经由JNI，最终送到MegEngine完成推理

app 的目录结构设计如下：

::

   .
   app //Android Camera APP 目录
   └── src
        └── main
            ├── AndroidManifest.xml
            ├── assets
            │   ├── imagenet_class_info.json
            │   └── shufflenet_deploy.mge
            └── java
                 └── com
                     └── example
                         └── android
                             └── camera2basic
                                 ├── AutoFitTextureView.java
                                 ├── Camera2BasicFragment.java
                                 └── CameraActivity.java

6.1. 打包APP使用的资源文件
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

这里我们只需要将json文件和model 文件直接放到app的assets 目录即可， APP在构建的时候会自动将该目录的文件打包到apk

6.2. 将APP依赖的JNI及动态库打包成aar module
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

我们将APP依赖的功能相关的逻辑抽离出来，作为一个独立module打包成aar并添加到app依赖项中。我们来看一下构建脚本
APP添加inference_jni依赖项
::

    implementation project(path: ':inference_jni')

在inference_jni gradle配置Java和jni的编译选项, 这里我们选择只是构建arm64-v8a,如需要armeabi-v7a, 可以在abiFilters添加即可

::
    
    defaultConfig {
        minSdkVersion 27
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        consumerProguardFiles 'consumer-rules.pro'

        externalNativeBuild {
            cmake {
                abiFilters 'arm64-v8a'
                arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_STL=c++_static"
                cppFlags "-frtti -fexceptions"
            }
        }

    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
    
inference jni构建脚本示例参考: `inference jni CMake 构建脚本`_
这里会生成Java interface会加载的动态库inference-jni。
inference-jni以动态链接方式链接前面章节实现的libshufflenet_inference.so(已经预置放到jniLibs目录)


6.3. 实现Java interface及JNI的调用
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
我们定义一个Java class：ImageNetClassifier。 
该类关键函数如下功能：

* Create为工厂函数，用来实例化ImageNetClassifier并初始化jni interface（对应前文的shufflenet_init）
* prepareRun里实现加载动态库libinference-jni.so
* recognizeYUV420Tp1，推理函数（对应前文的shufflenet_recognize），并返回Top1
* close，销毁jni handle（对应前文的shufflenet_close）及当前classifier对象

ImageNetClassifier 参考代码：`ImageNetClassifier`_

6.4. 实现JNI interface及libshufflenet_inference的调用
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
JNI interface主要是衔接Java interface和shufflenet interface， 
也就是将Java 传递到native的参数转成shufflenet interface 可以识别的参数，完成shufflenet interface的调用。
其中就包含了YUV420_888转BGR的逻辑.

JNI 参考代码：`inference jni 参考代码`_

6.5. 获取Camera Preview帧数据，完成推理
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
透过前面内容，我们已经封装出Java的上层API，也即可以将camera的preview 数据直接送到Java API即可将整个流程串通。
大家可以自行选择使用Camera API，还是Camera API2来获取预览数据，API使用上会有些许差异，本章节我们使用主流的API2来演示。

流程可以简化为：
* 创建一个格式为YUV420_888的ImageReader并设置为Camera Preview的Surface，然后开启预览。
* 在ImageReader收到预览帧数据后，我们就可以将帧数据post到后台线程并调用classifier.recognizeYUV420Tp1，
* 在jni完成YUV转BGR后送到Shufflenet interface，最终送到MegEngine完成推理。
* 在inference结果返回后，就可以在UI Thread 实时更新推理结果。

配置Camera预览的参考代码：`Camera preview 参考代码`_

6.6. 演示
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
经过前面实现，我们就可以build APP了。构建完成后， 我们就可以得到一个apk文件， 可以安装到手机来测试并继续优化了。

.. image:: imgs/inference_demo.png
   :align: center
   :height: 600px
   :scale: 50%


7. 量化部署
''''''''''''''''
MegEngine 也可以采用量化的模型在ARM-Android上进行部署，部署过程和本文的上述4-7章完全一致。
推理接口可以支持int8或fp32的模型部署。
具体量化模型的训练和dump方法可以参考github上的指导： `模型量化 Model Quantization`_


.. _`Android Camera Example github`: https://github.com/android/camera-samples/tree/master/Camera2Basic
.. _`MegEngine github`: https://github.com/MegEngine/MegEngine
.. _`MegEngine ModelHub`: https://megengine.org.cn/model-hub
.. _`MegEngine Model`: https://github.com/MegEngine/Models
.. _`xor net 部署`: https://megengine.org.cn/doc/latest/advanced/deployment.html
.. _`ShuffleNet V2模型`: https://megengine.org.cn/model-hub/megengine_vision_shufflenet_v2/
.. _`inference.py`: https://github.com/MegEngine/Models/blob/master/official/vision/classification/shufflenet/inference.py
.. _`imagenet_class_info.json`: https://github.com/MegEngine/Models/blob/master/official/assets/imagenet_class_info.json
.. _`模型量化 Model Quantization`: https://github.com/MegEngine/Models/tree/master/official/quantization
.. _`NDK下载`: https://developer.android.google.cn/ndk/downloads/ 

.. _`C++ 推理代码`: https://github.com/MegEngine/Inference-Demo/blob/master/native/shufflenet_interface/src/shufflenet_run.cpp
.. _`shufflenet interface 代码`: https://github.com/MegEngine/Inference-Demo/blob/master/native/shufflenet_interface/src/shufflenet_interface.cpp
.. _`build inference 脚本`: https://github.com/MegEngine/Inference-Demo/blob/master/native/shufflenet_interface/build_inference.sh
.. _`libshufflenet_inference CMake 构建脚本`: https://github.com/MegEngine/Inference-Demo/blob/master/native/shufflenet_interface/CMakeLists.txt
.. _`inference jni CMake 构建脚本`: https://github.com/MegEngine/Inference-Demo/blob/master/camera_app/Camera2Basic/inference_jni/src/main/cpp/CMakeLists.txt
.. _`inference jni 参考代码`: https://github.com/MegEngine/Inference-Demo/blob/master/camera_app/Camera2Basic/inference_jni/src/main/java/com/example/inference/ImageNetClassifier.java
.. _`Camera preview 参考代码`: https://github.com/MegEngine/Inference-Demo/blob/master/camera_app/Camera2Basic/inference_jni/src/main/cpp/inference_jni.cpp
.. _`ImageNetClassifier`: https://github.com/MegEngine/Inference-Demo/blob/master/camera_app/Camera2Basic/app/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java