MACE(3)-----工程化


作者:十歲的小男孩

QQ:929994365

能下者,上。

前言

  本文是MACE的第三步即MACE環境編譯出來的庫在Android工程中的使用。在第一篇博文中通過mace官方提供的安卓工程進行調試,本文將其精簡,只關心其數據流的邏輯過程。該工程功能是mace的demo物體識別,即傳入一張圖片,模型識別預測將結果在桌面顯示。本人萌新一枚,學習安卓有一月多了,工程漏洞百出,望相互學習。下一篇博文會對mace做一個全面的總結。本部分的學習需要掌握JNI/NDK技術,若有問題瀏覽前面文章。

  MACE(1)-----環境搭建:https://www.cnblogs.com/missidiot/p/9480033.html

  MACE(2)-----模型編譯:https://www.cnblogs.com/missidiot/p/9509831.html

  JNI/NDK:https://www.cnblogs.com/missidiot/p/9716902.html

經過mace環境編譯出文件如下:

此處應該有圖!!! 

給Android studio配置NDK版本,其版本與編譯版本應一致,本文是r-16b。

新建工程,勾選c++支持選項框。

 

其c++標准庫選擇c++11。

 以上工程構建完畢。

Android studio2.2版本以上采用的是CMake編譯,加載庫在CMakeLists.txt文件中配置。在MainActivity中加載庫,如下:

庫的名稱自己更改:比如本文改為"mace_jni",相對應的在CMakeLists.txt文件中修改名稱和創建對應的cpp文件,即mace_jni.cpp,如下圖:

在MainActivity中加載庫;

static {
        System.loadLibrary("mace_jni");
    }

在CMakeLists.txt文件中修改;(剛開始文件如下)

cmake_minimum_required(VERSION 3.4.1)

add_library(
             mace_jni

             SHARED

             src/main/cpp/mace_jni.cpp )

find_library(
              log-lib

              log )

target_link_libraries(
                       mace_jni

                       ${log-lib} )

CMakeLists最終文件如下:(加載編譯出的a庫)

cmake_minimum_required(VERSION 3.4.1)

#set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../app/libs/${ANDROID_ABI})

# 這塊路徑有點問題
include_directories(${CMAKE_SOURCE_DIR}/)
include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/public)

# libmace.a是mace編譯生成的,這塊以后要改的在這塊
set(mace_lib ${CMAKE_SOURCE_DIR}/src/main/cpp/lib/arm64-v8a/libmace.a)
# set(mace_lib ${CMAKE_SOURCE_DIR}/src/main/cpp/lib/armeabi-v7a/libmace.a)

# mobilneet.a也是mace環境編譯生成的,后期如果生成的話要改在這里
set(mobilenet_lib ${CMAKE_SOURCE_DIR}/src/main/cpp/model/arm64-v8a/mobilenet.a)
# set(mobilenet_lib ${CMAKE_SOURCE_DIR}/src/main/cpp/model/armeabi-v7a/mobilenet.a)

add_library (mace_lib STATIC IMPORTED)
set_target_properties(mace_lib PROPERTIES IMPORTED_LOCATION ${mace_lib})

add_library (mobilenet_lib STATIC IMPORTED)
set_target_properties(mobilenet_lib PROPERTIES IMPORTED_LOCATION ${mobilenet_lib})


add_library( # Sets the name of the library.
             mace_jni

             SHARED

             src/main/cpp/mace_jni.cpp


             )
find_library(
              log-lib
              log )

target_link_libraries( # Specifies the target library.
                        mace_jni
                        mace_lib
                        mobilenet_lib
                       ${log-lib} )

 

創建mace_jni.cpp文件,實現native方法。

在java包下新建JniUtils.java,將native方法的聲明放在其中,當然也將MainActivity中的加載庫代碼剪切放在JniUtils文件中。並聲明三個方法,會自動在mace_jni.cpp下自動被聲明。新建方法會顯示紅色報錯,在AS中快捷鍵是alt+enter創建方法在mace_jni.cpp文件中。

package com.tcl.weilong.mace;

public class JniUtils {
    static {
        System.loadLibrary("mace_jni");
    }

    /**
     * 給模型設置參數
     * @param ompNumThreads
     * @param cpuAffinityPolicy
     * @param gpuPerfHint
     * @param gpuPriorityHint
     * @param kernelPath
     * @return
     */

    public   native int maceMobilenetSetAttrs(int ompNumThreads, int cpuAffinityPolicy, int gpuPerfHint, int gpuPriorityHint, String kernelPath);
    /**
     * 給模型創建運行環境
     * @param model
     * @param device
     * @return
     */
    public  native int maceMobilenetCreateEngine(String model, String device);

    /**
     * 模型具體核心功能,識別圖片
     * @param input
     * @return
     */
    public  native float[] maceMobilenetClassify(float[] input);
}

 在mace_jni.cpp文件中自動生成對聲明方法的實現聲明:

在build.gradle文件中更改為如下代碼:

externalNativeBuild {
            cmake {
                cppFlags "-std=c++11 -fopenmp"
                abiFilters "arm64-v8a"
            }
        }

將mace附帶的example工程中的lib,model,public三個文件拷貝到cpp文件夾下面並在加載時候修改路徑,按如下目錄結構。

最為該工程核心的是在java聲明的native方法在底層c/c++中具體實現,以下代碼是以上三個方法在mace_jni.cpp文件中的具體實現:

#include <jni.h>
#include <algorithm>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <numeric>

#include "public/mace.h"
#include "public/mace_runtime.h"
#include "public/mace_engine_factory.h"

namespace {

    struct ModelInfo {
        std::string input_name;
        std::string output_name;
        std::vector<int64_t> input_shape;
        std::vector<int64_t> output_shape;
    };
//有的地方叫.cc也可以叫.cpp其實一個意思,實質為區別c文件的
    struct MaceContext {
        std::shared_ptr<mace::MaceEngine> engine;
        std::shared_ptr<mace::KVStorageFactory> storage_factory;
        std::string model_name;
        mace::DeviceType device_type = mace::DeviceType::CPU;
        //模型的輸入輸出在這里改
        std::map<std::string, ModelInfo> model_infos = {
                {"mobilenet_v1", {"input", "MobilenetV1/Predictions/Reshape_1",
                                         {1, 224, 224, 3}, {1, 1001}}},
                {"mobilenet_v2", {"input", "MobilenetV2/Predictions/Reshape_1",
                                         {1, 224, 224, 3}, {1, 1001}}}
        };
    };

    mace::DeviceType ParseDeviceType(const std::string &device) {
        if (device.compare("CPU") == 0) {
            return mace::DeviceType::CPU;
        } else if (device.compare("GPU") == 0) {
            return mace::DeviceType::GPU;
        } else if (device.compare("HEXAGON") == 0) {
            return mace::DeviceType::HEXAGON;
        } else {
            return mace::DeviceType::CPU;   //默認是返回CPU
        }
    }

    MaceContext& GetMaceContext() {
        static auto *mace_context = new MaceContext;

        return *mace_context;
    }

}   //namcspace
/**
 * java+包名+類名+函數名
 * Java+com.tcl.weilong.mace+JniUtils+maceMobilenetSetAttrs
 */
extern "C"
jint Java_com_tcl_weilong_mace_JniUtils_maceMobilenetSetAttrs(JNIEnv *env, jobject instance,
                                                         jint ompNumThreads, jint cpuAffinityPolicy,
                                                         jint gpuPerfHint, jint gpuPriorityHint,
                                                         jstring kernelPath_) {
    MaceContext &mace_context = GetMaceContext();
    mace::MaceStatus status;
    // openmp ??
    status = mace::SetOpenMPThreadPolicy(
            ompNumThreads,
            static_cast<mace::CPUAffinityPolicy>(cpuAffinityPolicy));
    //  gpu
    mace::SetGPUHints(
            static_cast<mace::GPUPerfHint>(gpuPerfHint),
            static_cast<mace::GPUPriorityHint>(gpuPriorityHint));
    //  opencl cache
    const char *kernel_path_ptr = env->GetStringUTFChars(kernelPath_, nullptr);
    if (kernel_path_ptr == nullptr) return JNI_ERR;
    const std::string kernel_file_path(kernel_path_ptr);
    mace_context.storage_factory.reset(
            new mace::FileStorageFactory(kernel_file_path));
    mace::SetKVStorageFactory(mace_context.storage_factory);
    env->ReleaseStringUTFChars(kernelPath_, kernel_path_ptr);
    return JNI_OK;
}
extern "C"
jint Java_com_tcl_weilong_mace_JniUtils_maceMobilenetCreateEngine(JNIEnv *env, jobject instance,
                                                             jstring model_, jstring device_) {
    MaceContext &mace_context = GetMaceContext();
    //  parse model name
    const char *model_name_ptr = env->GetStringUTFChars(model_, nullptr);
    if (model_name_ptr == nullptr) return JNI_ERR;
    mace_context.model_name.assign(model_name_ptr);
    env->ReleaseStringUTFChars(model_, model_name_ptr);

    //  load model input and output name
//    auto model_info_iter = mace_context.model_infos.find(mace_context.model_name);
    auto model_info_iter = mace_context.model_infos.find(mace_context.model_name);
    if (model_info_iter == mace_context.model_infos.end()) {

        return JNI_ERR;
    }
    std::vector<std::string> input_names = {model_info_iter->second.input_name};

    std::vector<std::string> output_names = {model_info_iter->second.output_name};

    // get device
    const char *device_ptr = env->GetStringUTFChars(device_, nullptr);
    if (device_ptr == nullptr) return JNI_ERR;
    mace_context.device_type = ParseDeviceType(device_ptr);
    env->ReleaseStringUTFChars(device_, device_ptr);

    mace::MaceStatus create_engine_status =
            CreateMaceEngineFromCode(mace_context.model_name,
                                     std::string(),
                                     input_names,
                                     output_names,
                                     mace_context.device_type,
                                     &mace_context.engine);


    return create_engine_status == mace::MaceStatus::MACE_SUCCESS ?
           JNI_OK : JNI_ERR;
}extern "C"
jfloatArray Java_com_tcl_weilong_mace_JniUtils_maceMobilenetClassify(JNIEnv *env, jobject instance,
                                                         jfloatArray input_) {
    MaceContext &mace_context = GetMaceContext();
    //  prepare input and output
    auto model_info_iter =
            mace_context.model_infos.find(mace_context.model_name);
    if (model_info_iter == mace_context.model_infos.end()) {

        return nullptr;
    }
    const ModelInfo &model_info = model_info_iter->second;
    const std::string &input_name = model_info.input_name;
    const std::string &output_name = model_info.output_name;
    const std::vector<int64_t> &input_shape = model_info.input_shape;
    const std::vector<int64_t> &output_shape = model_info.output_shape;
    const int64_t input_size =
            std::accumulate(input_shape.begin(), input_shape.end(), 1,
                            std::multiplies<int64_t>());
    const int64_t output_size =
            std::accumulate(output_shape.begin(), output_shape.end(), 1,
                            std::multiplies<int64_t>());

    //  load input
    jfloat *input_data_ptr = env->GetFloatArrayElements(input_, nullptr);
    if (input_data_ptr == nullptr) return nullptr;
    jsize length = env->GetArrayLength(input_);
    if (length != input_size) return nullptr;

    std::map<std::string, mace::MaceTensor> inputs;
    std::map<std::string, mace::MaceTensor> outputs;
    // construct input
    auto buffer_in = std::shared_ptr<float>(new float[input_size],
                                            std::default_delete<float[]>());
    std::copy_n(input_data_ptr, input_size, buffer_in.get());
    env->ReleaseFloatArrayElements(input_, input_data_ptr, 0);
    inputs[input_name] = mace::MaceTensor(input_shape, buffer_in);

    // construct output
    auto buffer_out = std::shared_ptr<float>(new float[output_size],
                                             std::default_delete<float[]>());
    outputs[output_name] = mace::MaceTensor(output_shape, buffer_out);

    // run model
    mace_context.engine->Run(inputs, &outputs);

    // transform output
    jfloatArray jOutputData = env->NewFloatArray(output_size);  // allocate
    if (jOutputData == nullptr) return nullptr;
    env->SetFloatArrayRegion(jOutputData, 0, output_size,
                             outputs[output_name].data().get());  // copy

    return jOutputData;
}

  以上步驟將模型底層運行環境准備好了,接下來繼續封裝為模型准備數據及其數據結果展示表層工作。

建立AppModel.java為模型准備輸入數據和接受模型的預測結果數據作為MainActivity和底層native方法的橋梁。

說明:這個文件有四個方法,首先是對模型的設置參數,第二個是對模型創建一個運行環境實在cpu還是在gpu下運行,第三個是對識別功能准備數據將其轉換為rgb,第四個是核心功能即物體識別。

package com.tcl.weilong.mace;

import android.graphics.Bitmap;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.nio.FloatBuffer;

public class AppModel {
    JniUtils jniUtils = new JniUtils();
    //這些參數的值具體代表什么尚未搞清楚
    int ompNumThreads = 2;  //線程個數
    int cpuAffinityPolicy = 0;  //
    int gpuPerfHint = 3;    //
    int gpuPriorityHint = 3;    //
    String kernelPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "mace";  //內核路徑

    public static final String[] MODELS = new String[]{"mobilenet_v1", "mobilenet_v2"}; //模型名稱,這塊不要de
    public static final String[] DEVICES = new String[]{"CPU", "GPU"};  //設備

    String model = MODELS[1]; //mobilenet_v2
    String device = DEVICES[0]; //CPU

    private int[] colorValues; //存儲RGB顏色數據
    private FloatBuffer floatBuffer; //輸入緩存

    /**
     * 給模型設置屬性
     */
    public void maceMobilenetSetAttrs(){
        int attrs;
        attrs = jniUtils.maceMobilenetSetAttrs(ompNumThreads,cpuAffinityPolicy,gpuPerfHint,gpuPriorityHint,kernelPath);
        Log.d("idiot","attrs="+attrs);
    }

    /**
     * 給模型創建運行引擎,cpu或者gpu
     */
    public void maceMobilenetCreateEngine(){
        int engine;
        engine = jniUtils.maceMobilenetCreateEngine(model,device);
        Log.d("idiot","engin="+engine);
    }

    /**
     * 輸入數據處理,將照片轉換成數組 float[],bitmap是224*224的大小
     * @param bitmap 要處理的原始圖片
     * @return 將圖片處理后轉換成像素點進行返回
     */
    public FloatBuffer dealPic(Bitmap bitmap){
        //這塊要創建一個224*224的圖片是針對輸入原始圖片創建的,每一行有224個像素點,共有224行
        colorValues = new int[224 * 224];   //存儲像素
        float[] floatValues = new float[224 * 224 * 3]; //RGB像素*3
        floatBuffer = FloatBuffer.wrap(floatValues, 0, 224 * 224 * 3);
        //這個函數要深究,從(0,0)點開始平移,平移尺度是單元尺寸的一個寬度。
        bitmap.getPixels(colorValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
        floatBuffer.rewind();   //??
        //最核心的圖像像素點的變化,根本沒懂
            for (int i = 0; i < colorValues.length; i++) {
                int value = colorValues[i];
                floatBuffer.put((((value >> 16) & 0xFF) - 128f) / 128f);
                floatBuffer.put((((value >> 8) & 0xFF) - 128f) / 128f);
                floatBuffer.put(((value & 0xFF) - 128f) / 128f);
            }
            return floatBuffer;
    }
    /**
     * 模型的分類函數
     * @param input 緩存的像素點
     * @return  識別結果數據,在類標中匹配識別
     */
    public float[] maceMobilenetClassify(FloatBuffer input){
        float[] result;
        result = jniUtils.maceMobilenetClassify(input.array());
        return result;
    }
}

  以上文件即可實現對模型輸入圖片的功能,下面的文件是對識別結果進行匹配。創建LableCache.java文件實現:

package com.tcl.weilong.mace;

import android.content.Context;
import android.content.res.AssetManager;
import android.util.Log;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class LabelCache {

    private List<Float> floatList = new ArrayList<>();  //每張識別結果的概率
    private List<String> resultLabel = new ArrayList<>();   //識別結果緩存列表
    private ResultData mResultData; //

    /**
     * 加載類標的資源文件
     * @param context 上下文
     */
    public void readCacheLabelFromLocalFile(Context context) {
        try {
            AssetManager assetManager = context.getAssets();    //獲取資源管理器
            //從資源管理器中加載標簽文件,所有的結果在文本文件中,識別的結果在label中去匹配
            BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open("cacheLabel.txt")));
            String readLine = null;
            while ((readLine = reader.readLine()) != null) {
                Log.d("labelCache", "readLine = " + readLine);
                resultLabel.add(readLine);  //原文是移動識別,這樣一張大的圖片會識別很多個,將所有識別的都放在該列表里,該最簡工程只識別了最小識別單元
            }
            reader.close();
        } catch (Exception e) {
            Log.e("labelCache", "error " + e);
        }
    }

    /**
     *  獲取所有識別結果中概率最高的一個值進行返回
     * @param floats 識別結果值: result = appModel.maceMobilenetClassify(input);
     * @return 將識別的結果值存儲到data中返回
     */
    public ResultData getResultFirst(float[] floats) {
        floatList.clear();
        for (float f : floats) {
            floatList.add(f);
        }
        float maxResult = Collections.max(floatList);   //在所有識別結果中獲取最大的概率即為最終識別結果

        int indexResult = floatList.indexOf(maxResult);
        if (indexResult < resultLabel.size()) {
            String result = resultLabel.get(indexResult);
            if (result != null) {
                if (mResultData == null) {
                    mResultData = new ResultData(result, maxResult);
                } else {
                    mResultData.updateData(result, maxResult);
                }
                return mResultData;
            }
        }
        return null;
    }
}

  所需的資源文件為cacheLabel.txt文件在assets資源文件夾下面。

  

以上工作將識別的結果已經拿到,由於結果包括識別名稱,識別的時間和相對應的概率,我們需要data包來封裝這些數據,創建ResultData.java文件封裝。

package com.tcl.weilong.mace;

public class ResultData {
    public String name; //識別結果內容
    public float probability;   //識別的可能性,概率值
    public long costTime;  // 運行時間

    /**
     * 構造函數,只有名稱和識別概率值
     * @param name
     * @param probability
     */
    public ResultData(String name, float probability) {
        this.name = name;
        this.probability = probability;
    }

    /**
     * 構造函數
     * @param name
     */
    public ResultData(String name) {
        this.name = name;
    }

    /**
     * 更新名稱和概率值,這個函數原工程中使用用於不斷的識別,更新名稱顯示
     * @param name
     * @param probability
     */
    public void updateData(String name, float probability) {
        this.name = name;
        this.probability = probability;
    }

}

  接下來對拿到的數據進行展示,展示比較簡單。其activity.xml文件如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_height="match_parent"
    android:layout_width="match_parent">
    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:layout_gravity="center_horizontal" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/tv"/>
</LinearLayout>

  最后MainActivity.java文件進行調度:

package com.tcl.weilong.mace;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import java.nio.FloatBuffer;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = findViewById(R.id.iv);
        TextView tv =  findViewById(R.id.tv);
        float[] result;
        //獲取圖片,圖片的大小是224 * 224,由於模型的識別輸入是該大小,如果是一張大圖片就會順次平移類似於CNN一樣
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.mouse,options);
        Log.d("idiot","width:"+bitmap.getWidth()+";height:"+bitmap.getHeight());
        iv.setImageBitmap(bitmap);  //只顯示了圖片,這塊沒有意義,原意該處為圖片的獲取及其展示

        LabelCache labelCache = new LabelCache();       //模型結果label
        Context context = getApplicationContext();
        labelCache.readCacheLabelFromLocalFile(context);    //加載label資源文件

        AppModel appModel = new AppModel();
        appModel.maceMobilenetSetAttrs();      //為模型設置環境
        appModel.maceMobilenetCreateEngine();

        FloatBuffer input = appModel.dealPic(bitmap);   //加載輸入識別照片,返回緩存的像素點單元
        long start = System.currentTimeMillis();
        result = appModel.maceMobilenetClassify(input); //該工程的核心功能塊,識別圖片
        long end = System.currentTimeMillis();
        long costTime = end - start;    //識別一張圖片的時間
        Toast.makeText(this,"CostTime: "+costTime+" ms",Toast.LENGTH_LONG).show();

        ResultData data;
        data = labelCache.getResultFirst(result);   //將返回的結果對照label處理
        data.costTime = costTime;
        String resultt = data.name  + "\n" + data.probability + "\ncost time(ms): " + data.costTime;
        tv.setText(resultt);    //展示結果
    }

}

注意:

1.傳入的圖片是224*224大小,這個問題以后會更改。

2.運行的cpu架構是armv-8的,這個問題沒有解決。

模擬器運行結果如下:采用的模擬器時間很慢。

真機測試運行時間和內存消耗結果:

CPU:

GPU:

MACE采用GPU加速,時間和硬件消耗縮減一半左右。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM