做者:十歲的小男孩html
QQ:929994365java
能下者,上。python
前言android
本文是MACE的第三步即MACE環境編譯出來的庫在Android工程中的使用。在第一篇博文中經過mace官方提供的安卓工程進行調試,本文將其精簡,只關心其數據流的邏輯過程。該工程功能是mace的demo物體識別,即傳入一張圖片,模型識別預測將結果在桌面顯示。本人萌新一枚,學習安卓有一月多了,工程漏洞百出,望相互學習。下一篇博文會對mace作一個全面的總結。本部分的學習須要掌握JNI/NDK技術,如有問題瀏覽前面文章。c++
MACE(1)-----環境搭建:http://www.javashuo.com/article/p-agfoihkl-ch.html數組
MACE(2)-----模型編譯:http://www.javashuo.com/article/p-edzhmnkd-cu.html緩存
JNI/NDK:http://www.javashuo.com/article/p-pklnhrvb-bt.html架構
通過mace環境編譯出文件以下:app
此處應該有圖!!! ide
給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加速,時間和硬件消耗縮減一半左右。