本文使用JNI技術在Android平臺部署深度學習模型,並使用MNN框架進行模型推理。java
模型及C++程序準備
mnist-mnnandroid
Android環境配置
-
打開Android studio, 建立一個Native C++工程,並配置OpenCV。
在Android中使用OpenCV
ios -
在PC上編譯MNN-Android的動態連接庫
MNN安裝和編譯
c++ -
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})
- 修改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
- 在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
- 編寫對應的.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);
- 編寫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
- 目前是把.mnn文件事先放在手機裏面,在運行程序的時候從手機讀取模型,不知道怎麼放在項目裏面讀取;
- 輸入圖像爲res裏面存放的固定圖像,不知道怎麼從相冊裏選取一張圖識別或是拍照識別。