使用MNN在Android上部署mnist模型_mnn android-程序员宅基地

技术标签: 深度学习  Android  

本文使用JNI技术在Android平台部署深度学习模型,并使用MNN框架进行模型推理。

模型及C++程序准备

mnist-mnn

Android环境配置

  1. 打开Android studio, 创建一个Native C++工程,并配置OpenCV。
    在Android中使用OpenCV

  2. 在PC上编译MNN-Android的动态链接库
    MNN安装和编译

  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为:

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*类型的模型地址,输出为识别结果。

  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);

    }

    @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);
            //把模型从Assets拷贝到Cache,再获取模型的路径,供C++读取
            copyAssetAndWrite("mnist.mnn");
            File dataFile = new File(getCacheDir(),"mnist.mnn");
            //System.out.println("模型路径:" + dataFile.getAbsolutePath());
            imageView.setImageBitmap(bitmap);
            textView.setText(mnistJNI(bitmap, dataFile.getAbsolutePath()));
        }
    }

    @Override
    public void onPointerCaptureChanged(boolean hasCapture) {
    
    }
    private boolean copyAssetAndWrite(String fileName){
    
        try {
    
            File cacheDir=getCacheDir();
            if (!cacheDir.exists()){
    
                cacheDir.mkdirs();
            }
            File outFile =new File(cacheDir,fileName);
            if (!outFile.exists()){
    
                boolean res=outFile.createNewFile();
                if (!res){
    
                    return false;
                }
            }else {
    
                if (outFile.length()>10){
    //表示已经写入一次
                    return true;
                }
            }
            InputStream is=getAssets().open(fileName);
            FileOutputStream fos = new FileOutputStream(outFile);
            byte[] buffer = new byte[1024];
            int byteCount;
            while ((byteCount = is.read(buffer)) != -1) {
    
                fos.write(buffer, 0, byteCount);
            }
            fos.flush();
            is.close();
            fos.close();
            return true;
        } catch (IOException e) {
    
            e.printStackTrace();
        }

        return false;
    }
}

对应的界面布局文件

<?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. 输入图像为res里面存放的固定图像,不知道怎么从相册里选取一张图识别或是拍照识别。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_37546267/article/details/108111949

智能推荐

深入理解内存映射:mmap映射的背后原理以及和共享内存的差异_共享库 和内存映射区-程序员宅基地

文章浏览阅读926次,点赞10次,收藏10次。`mmap` 是实现内存映射的关键系统调用。它创建了文件内容和进程地址空间之间的直接映射,使得文件的一部分或全部可以直接映射到进程的地址空间中。这样,文件的读写就变得像内存访问一样高效。_共享库 和内存映射区

数仓实战 作业_select campseg_id, s_start_date,count(1) from i_h_-程序员宅基地

文章浏览阅读464次。作业题:在会员分析中计算最近七天连续三天活跃会员数。 项目的数据采集过程中,有哪些地方能够优化,如何实现?连续值求解问题对表 dws_member_start_day 中的数据使用 row_number()函数进行排序,并且根据设备号 device_id分组,登录日期dt排序,用日期减去排名得到groupId 并限定如期最近7天,根据设备号和groupId分组,求出统计值cnt 大于等于 3的记录。WITH tmp as( SELECT ..._select campseg_id, s_start_date,count(1) from i_h_iop_2_93001_day_dm group b

nagios用NsClient自定义windows监控-程序员宅基地

文章浏览阅读76次。Nagios用NsClient自定义windows监控NsClient++来监控windows主机有三种方式:check_nt,check_nrpe,nsca.相对而言,check_nrpe已经可以满足大部分的监控任务要求了。这三种方式在nsclient的PDF中说的很详细,但全是E文,根据我自己的部署过程,边做边写下自己理解:check_nt的使用方法:这是NsClient++默认的使用方法,但..._nagios自定义监控windows

docker- 构建 oracle2c-r2(12.2.0.1) 的镜像-程序员宅基地

文章浏览阅读423次。需求由于公司的数据库需要使用新的oracle版本(12c-r2 -->12.2.0.1),从之前的oracle11g迁移到12c。所以,便有了我们今天的内容。首先,我们就先来介绍一下如何构建oracle12c的镜像(docker image)。如果大家有使用的需求而又不是正式的项目,可以直接到docker hub 上面 pull 一个别人家的。在这里附上链接:https://hu..._/opt/database/install/unzip -qqqo ../stage/components/oracle.jdk/1.8.0.91.0/

寻(光阴的故事)-程序员宅基地

文章浏览阅读101次。寻寻一束芬芳的鲜花插于窗前在我疲劳的时候 看它一眼寻一把眷恋的故土带在身边在我思乡的时候 捧于胸前寻一段纯真的恋情藏在心间在我寂寞的时候 把她思念 ...

ZStack实践汇 | ZStack平台的使用心得-程序员宅基地

文章浏览阅读2.1k次。作者:ZStack 社区 王彬Iaas云服务的普及,让我们在使用服务器的时候享受了飞一般的感觉,新兴企业在构建自己的系统时,往往都会选择购买云厂商的云服务器(虚拟机)进行使用,使用这样的虚拟机企业不需要购置任何硬件,不需要考虑机房、网络、服务器维护相关的工作便可以获取到一个低成本、安全免维护、伸缩性强、可灵活迁移的云服务器。在这个云服务器上我们可以快速的构建企业的业务系统。随着企业的不断发展,...

随便推点

Linux检查空口令_linux空口令账户-程序员宅基地

文章浏览阅读6.5k次,点赞5次,收藏11次。Linux加固:检查空口令账号创建空口令账号useradd test 添加test用户passwd -d test 为test设置空密码​ 查看空口令账号​ /etc/shadow 存放密码位置​ 冒号分割​ $1 用户名​ $2 密码​ $3 UID​使用awk命令 检查$2位是否为空,为空则print输出$1位用户名awk -F “:” ‘($2==""){print $1}’ /etc/shadow​..._linux空口令账户

面试题 10.09. 排序矩阵查找_排序矩阵查找,给定m*n矩阵 每一行每一列-程序员宅基地

文章浏览阅读193次。给定M×N矩阵,每一行、每一列都按升序排列,请编写代码找出某元素。_排序矩阵查找,给定m*n矩阵 每一行每一列

在Linux运行LaTeX_latex linux-程序员宅基地

文章浏览阅读649次,点赞9次,收藏10次。文件比较大,这步花的时间多一点,不过也不会太多,感觉5分钟十分钟的样子吧。这一步是安装一个类似在windows系统下的TaTeX GUI软件。下载对应版本安装包安装。_latex linux

s3c6410 jpeg编码 linux,立宇泰ARMSYS6410开发板推出三个linux系统版本-程序员宅基地

文章浏览阅读91次。ARMSYS6410采用了Linux-2.6.28作为标准版的linux内核,其中集成了丰富的驱动资源,充分展现S3C6410的各项特性,包括硬件编解码、2D/3D加速、显示协处理、TVOUT输出、视频采集和编码、4路串口、2路SD/MMC接口、1路10/100M以太网接口、1路USB host接口等等,使ARMSYS6410成为目前linux配置最为强劲和最完整的开发板之一。ARMSYS6410..._s3c6410可以刷那个版本linux

java参数-xmn1g_假如某个JAVA进程的JVM参数配置如下:-Xms1G&nb-程序员宅基地

文章浏览阅读618次。Xms 起始内存Xmx 最大内存Xmn 新生代内存Xss 栈大小。 就是创建线程后,分配给每一个线程的内存大小-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor..._-xmn1g

LKD:中断_neil中断-程序员宅基地

文章浏览阅读238次。中断请求(IRQ)线:不同设备对应的中断不同,而每个中断都通过一个唯一的数字标志。重点在于特定的中断总是与特定的设备相关联,并且内核要知道这些信息。 异常:常常也称为同步中断。如处理器执行到由于编程失误导致的错误指令(如被0除),或者执行期间出现特殊情况(如缺页),处理器就会产生一个异常。 中断处理程序(ISR):上半部——接收到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接收的..._neil中断

推荐文章

热门文章

相关标签