使用MNN在Android上部署mnist模型

本文使用JNI技術在Android平臺部署深度學習模型,並使用MNN框架進行模型推理。java

模型及C++程序準備

mnist-mnnandroid

Android環境配置

  1. 打開Android studio, 建立一個Native C++工程,並配置OpenCV。
    在Android中使用OpenCV
    ios

  2. 在PC上編譯MNN-Android的動態連接庫
    MNN安裝和編譯
    c++

  3. CMakeLists.txt編寫
    在jni中編譯C/C++程序有兩種方法:一是使用ndk-build(須要配置.mk文件),二是使用CMake,本文使用CMake編譯的方法。
    後端

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
# opencv
set( OpenCV_DIR /home/yinliang/software/OpenCV-android-sdk/sdk/native/jni )
find_package(OpenCV REQUIRED)
# MNN_DIR爲本身安裝的MNN的路徑
set(MNN_DIR /home/yinliang/software/MNN)
# mnn的頭文件
include_directories(${MNN_DIR}/include)
include_directories(${MNN_DIR}/include/MNN)
include_directories(${MNN_DIR}/tools)
include_directories(${MNN_DIR}/tools/cpp)
include_directories(${MNN_DIR}/source)
include_directories(${MNN_DIR}/source/backend)
include_directories(${MNN_DIR}/source/core)
# 這個是本身定義的.h文件
include_directories(get_result.h)
# 連接mnn的動態庫,這裏編譯的是64位的,對應Android裏面的arm64-v8a架構
aux_source_directory(. SRCS)
add_library( # Sets the name of the library.
        native-lib
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        ${SRCS})

find_library( # Sets the name of the path variable.
        log-lib
        log)
# 須要把libMNN.so放到工程文件裏來,具體位置在 app/libs下,放在工程外好像不行
set(dis_DIR ../../../../libs)
add_library(
        MNN
        SHARED
        IMPORTED
)
set_target_properties(
        MNN
        PROPERTIES IMPORTED_LOCATION
        ${dis_DIR}/arm64-v8a/libMNN.so
)
# 代碼主要依賴opencv和mnn兩個庫,這裏連接一下
target_link_libraries( # Specifies the target library.
        native-lib
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib}
        MNN
        jnigraphics
        ${OpenCV_LIBS})
  1. 修改app下的build.gradle文件
    添加如下內容,否則沒法成功連接到libMNN.so
sourceSets {
            main{
                jniLibs.srcDirs=['libs']
            }
        }

完整的build.gradle爲:bash

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30
    defaultConfig {
        applicationId "com.mnn.mnist"
        minSdkVersion 25
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        sourceSets {
            main{
                jniLibs.srcDirs=['libs']
            }
        }
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14"
                arguments "-DANDROID_STL=c++_shared"
                abiFilters  "arm64-v8a"
            }
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"

        }
    }
}
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:runner:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

編寫native-lib.cpp

  1. 在src/main/cpp下新建一個get_result.cpp文件,實現MNN的前向推理過程。
//
// Created by yinliang on 20-8-17.
//
#include <jni.h>
#include <string>
#include <iostream>
#include <stdio.h>
#include <math.h>

#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgcodecs/imgcodecs.hpp>

#include "Backend.hpp"
#include "Interpreter.hpp"
#include "MNNDefine.h"
#include "Interpreter.hpp"
#include "Tensor.hpp"


using namespace MNN;
using namespace std;
using namespace cv;

int mnist(Mat image_src, const char* model_name){

    // const char* model_name = "/home/yinliang/works/C/MNN-APPLICATIONS/applications/mnist/onnx/jni/graphs/mnist.mnn";
    int forward = MNN_FORWARD_CPU;
    // int forward = MNN_FORWARD_OPENCL;

    int precision  = 2;
    int power      = 0;
    int memory     = 0;
    int threads    = 1;
    int INPUT_SIZE = 28;

    cv::Mat raw_image = image_src;
    cv::Mat image;
    cv::resize(raw_image, image, cv::Size(INPUT_SIZE, INPUT_SIZE));
    // cout<<"model_path:" << model_name<<endl;
    // 1. 建立Interpreter, 經過磁盤文件建立: static Interpreter* createFromFile(const char* file);
    std::shared_ptr<Interpreter> net(Interpreter::createFromFile(model_name));
    MNN::ScheduleConfig config;
    // 2. 調度配置,
    // numThread決定併發數的多少,但具體線程數和併發效率,不徹底取決於numThread
    // 推理時,主選後端由type指定,默認爲CPU。在主選後端不支持模型中的算子時,啓用由backupType指定的備選後端。
    config.numThread = threads;
    config.type      = static_cast<MNNForwardType>(forward);
    MNN::BackendConfig backendConfig;
    // 3. 後端配置
    // memory、power、precision分別爲內存、功耗和精度偏好
    backendConfig.precision = (MNN::BackendConfig::PrecisionMode)precision;
    backendConfig.power = (MNN::BackendConfig::PowerMode) power;
    backendConfig.memory = (MNN::BackendConfig::MemoryMode) memory;
    config.backendConfig = &backendConfig;
    // 4. 建立session
    auto session = net->createSession(config);
    net->releaseModel();

    clock_t start = clock();
    // preprocessing
    image.convertTo(image, CV_32FC3);
    image = image / 255.0f;
    // 5. 輸入數據
    // wrapping input tensor, convert nhwc to nchw
    std::vector<int> dims{1, INPUT_SIZE, INPUT_SIZE, 3};
    auto nhwc_Tensor = MNN::Tensor::create<float>(dims, NULL, MNN::Tensor::TENSORFLOW);
    auto nhwc_data   = nhwc_Tensor->host<float>();
    auto nhwc_size   = nhwc_Tensor->size();
    ::memcpy(nhwc_data, image.data, nhwc_size);

    std::string input_tensor = "data";
    // 獲取輸入tensor
    // 拷貝數據, 經過這類拷貝數據的方式,用戶只須要關注本身建立的tensor的數據佈局,
    // copyFromHostTensor會負責處理數據佈局上的轉換(如需)和後端間的數據拷貝(如需)。
    auto inputTensor  = net->getSessionInput(session, nullptr);
    inputTensor->copyFromHostTensor(nhwc_Tensor);

    // 6. 運行會話
    net->runSession(session);

    // 7. 獲取輸出
    std::string output_tensor_name0 = "dense1_fwd";
    // 獲取輸出tensor
    MNN::Tensor *tensor_scores  = net->getSessionOutput(session, output_tensor_name0.c_str());

    MNN::Tensor tensor_scores_host(tensor_scores, tensor_scores->getDimensionType());
    // 拷貝數據
    tensor_scores->copyToHostTensor(&tensor_scores_host);

    // post processing steps
    auto scores_dataPtr  = tensor_scores_host.host<float>();

    // softmax
    float exp_sum = 0.0f;
    for (int i = 0; i < 10; ++i)
    {
        float val = scores_dataPtr[i];
        exp_sum += val;
    }
    // get result idx
    int  idx = 0;
    float max_prob = -10.0f;
    for (int i = 0; i < 10; ++i)
    {
        float val  = scores_dataPtr[i];
        float prob = val / exp_sum;
        if (prob > max_prob)
        {
            max_prob = prob;
            idx      = i;
        }
    }

    // printf("the result is %d\n", idx);
    return idx;
}

函數的輸入爲一個Mat類型的圖像,const char*類型的模型地址,輸出爲識別結果。session

  1. 編寫對應的.h文件
    在相同目錄下建立get_result.cpp對應的頭文件。
#include <jni.h>
#include <string>
#include <iostream>
#include <stdio.h>
#include <math.h>

#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgcodecs/imgcodecs.hpp>

#include "Backend.hpp"
#include "Interpreter.hpp"
#include "MNNDefine.h"
#include "Interpreter.hpp"
#include "Tensor.hpp"

using namespace MNN;
using namespace std;
using namespace cv;

int mnist(Mat image_src, const char* model_name);
  1. 編寫native-lib.cpp
    定義jni接口函數,也就是咱們最後在Android端能夠調用的本地方法,函數的參數類型都是jni特有的類型,可參考jni技術簡介
#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <opencv2/opencv.hpp>
#include "get_result.h"
#include "stdio.h"
#include "stdlib.h"

extern "C" JNIEXPORT jstring JNICALL
Java_com_mnn_mnist_MainActivity_mnistJNI (JNIEnv *env, jobject obj, jobject bitmap, jstring jstr){

    AndroidBitmapInfo info;
    void *pixels;
    CV_Assert(AndroidBitmap_getInfo(env, bitmap, &info) >= 0);
    CV_Assert(info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 ||
              info.format == ANDROID_BITMAP_FORMAT_RGB_565);
    CV_Assert(AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0);
    CV_Assert(pixels);
    if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
        Mat temp(info.height, info.width, CV_8UC4, pixels);
        Mat temp2 = temp.clone();
		//將jstring類型轉換成C++裏的const char*類型
        const char *path = env->GetStringUTFChars(jstr, 0);

        Mat RGB;
        //先將圖像格式由BGRA轉換成RGB,否則識別結果不對
        cvtColor(temp2, RGB, COLOR_RGBA2RGB);
        //調用以前定義好的mnist()方法,識別文字圖像
        int result = mnist(RGB, path);
        //將圖像轉回RGBA格式,Android端才能夠顯示
        Mat show(info.height, info.width, CV_8UC4, pixels);
        cvtColor(RGB, temp, COLOR_RGB2RGBA);
        //將int類型的識別結果轉成jstring類型,並返回
        string re_reco = to_string(result);
        const char* ss = re_reco.c_str();
        char cap[12];
        strcpy(cap, ss);
        return (env)->NewStringUTF(cap);;

    } else {
        Mat temp(info.height, info.width, CV_8UC2, pixels);
        
    }
    AndroidBitmap_unlockPixels(env, bitmap);
}

Android端調用

因爲不會Android開發,這部分代碼很粗糙,能正確運行,可是不夠優雅。架構

package com.mnn.mnist;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;

import static android.content.pm.PackageManager.PERMISSION_GRANTED;


public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    //定義兩個控件,分別用來顯示圖像和文本
    private ImageView imageView;
    private TextView textView;
    // 加載生成的動態連接庫
    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }
    // 聲明JNI函數,對應native-lib.cpp裏定義的函數
    native String mnistJNI(Object bitmap, String str);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = findViewById(R.id.imageView);
        findViewById(R.id.show).setOnClickListener((View.OnClickListener) this);
        findViewById(R.id.process).setOnClickListener((View.OnClickListener) this);
        findViewById(R.id.gray).setOnClickListener((View.OnClickListener) this);

        textView = findViewById(R.id.textView);
        findViewById(R.id.textView).setOnClickListener((View.OnClickListener) this);

        myRequetPermission();
    }

    // 因爲我把.mnn模型用adb push放到手機的sd目錄下了,須要加權限才能訪問到
    private void myRequetPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
        } else {
            Toast.makeText(this, "您已經申請了權限!", Toast.LENGTH_SHORT).show();
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1) {
            for (int i = 0; i < permissions.length; i++) {
                if (grantResults[i] == PERMISSION_GRANTED) {//選擇了「始終容許」
                    Toast.makeText(this, "" + "權限" + permissions[i] + "申請成功", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }

    @Override
    public void onClick(View v) {
    // show爲一個button,只用來顯示一下圖像
        if (v.getId() == R.id.show) {
        //放一張圖像到res/drawable目錄下,並命名爲test.jpg
        //讀取圖像,在Android裏對應的類型爲Bitmap
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
            //顯示圖像
            imageView.setImageBitmap(bitmap);
        } else {
         // 
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
            //讀取sd卡下的mnist.mnn模型
            String model_path = Environment.getExternalStorageDirectory().getPath() + "/mnist.mnn";
            System.out.println("模型路徑:" + model_path);
            //顯示圖像
            imageView.setImageBitmap(bitmap);
            //顯示識別結果
            textView.setText(mnistJNI(bitmap, model_path));
        }
    }

    @Override
    public void onPointerCaptureChanged(boolean hasCapture) {
    }
}

對應的界面佈局文件併發

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

    <ImageView android:id="@+id/imageView" android:layout_width="match_parent" android:layout_height="match_parent" />

    <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:orientation="horizontal">

        <Button android:id="@+id/show" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="show" />

        <Button android:id="@+id/process" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="mnist" />

        <Button android:id="@+id/gray" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="gray" />
    </LinearLayout>

<TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textSize="24sp" android:textColor="#00ff00" android:text="result" />

</RelativeLayout>

TODO

  1. 目前是把.mnn文件事先放在手機裏面,在運行程序的時候從手機讀取模型,不知道怎麼放在項目裏面讀取;
  2. 輸入圖像爲res裏面存放的固定圖像,不知道怎麼從相冊裏選取一張圖識別或是拍照識別。
相關文章
相關標籤/搜索