基于Paddle Lite在Android手机上实现图像分类
原文博客:Doi技術(shù)團隊
鏈接地址:https://blog.doiduoyi.com/authors/1584446358138
初心:記錄優(yōu)秀的Doi技術(shù)團隊學(xué)習(xí)經(jīng)歷
本文鏈接:基于Paddle Lite在Android手機上實現(xiàn)圖像分類
前言
Paddle Lite是飛槳基于Paddle Mobile全新升級推出的端側(cè)推理引擎,在多硬件、多平臺以及硬件混合調(diào)度的支持上更加完備,為包括手機在內(nèi)的端側(cè)場景的AI應(yīng)用提供高效輕量的推理能力,有效解決手機算力和內(nèi)存限制等問題,致力于推動AI應(yīng)用更廣泛的落地。
本教程源碼地址:https://github.com/yeyupiaoling/ClassificationForAndroid/tree/master/PaddleLiteClassification
模型轉(zhuǎn)換
Paddle Lite使用的是PaddlePaddle保存的預(yù)測模型,如果不了解PaddlePaddle的模型保存,可以參考《模型的保存與使用》這篇文章。下面簡單介紹一下保存模型的方式。通過使用fluid.io.save_inference_model()接口可以保存預(yù)測模型,預(yù)測模型值保存推所需的網(wǎng)絡(luò),不會保存損失函數(shù)等。當(dāng)使用model_filename和params_filename指定參數(shù)之后,保存的預(yù)測模型只有兩個文件,這種稱為合并模型,否則會以網(wǎng)絡(luò)結(jié)構(gòu)命名將大量的參數(shù)文件保存在dirname指定的路徑下,這種叫做非合并模型。例如通過以下的代碼片段保存的預(yù)測模型為model和params,這兩個模型將會用于下一步的模型轉(zhuǎn)換。
import paddle.fluid as fluid# 定義網(wǎng)絡(luò) image = fluid.layers.data(name='img', shape=[1, 28, 28], dtype='float32') label = fluid.layers.data(name='label', shape=[1], dtype='int64') feeder = fluid.DataFeeder(feed_list=[image, label], place=fluid.CPUPlace()) predict = fluid.layers.fc(input=image, size=10, act='softmax')loss = fluid.layers.cross_entropy(input=predict, label=label) avg_loss = fluid.layers.mean(loss)exe = fluid.Executor(fluid.CPUPlace()) exe.run(fluid.default_startup_program())# 數(shù)據(jù)輸入及訓(xùn)練過程# 保存預(yù)測模型 fluid.io.save_inference_model(dirname="mobilenet_v2/",feeded_var_names=[image.name],target_vars=[predict],executor=exe,model_filename="model", params_filename="params")opt轉(zhuǎn)換
使用fluid.io.save_inference_model()接口可以保存預(yù)測模型并不能直接使用,還需要通過opt工具轉(zhuǎn)換,這個工具可以下載Paddle Lite預(yù)編譯的,或者通過源碼編譯,opt下載地址:https://paddle-lite.readthedocs.io/zh/latest/user_guides/release_lib.html#opt,關(guān)于如何編譯opt請看下一部分。
通過以下命令即即可把預(yù)測模型轉(zhuǎn)變成Paddle Lite使用的模型,其中輸出的mobilenet_v2.nb就是所需的模型文件,因為轉(zhuǎn)換之后,模型可以在valid_targets指定的環(huán)境上加速預(yù)測,所以變得非常牛B,因此后綴名為nb(開個玩笑)。
./opt \--model_file=mobilenet_v2/model \--param_file=mobilenet_v2/params \--optimize_out_type=naive_buffer \--optimize_out=mobilenet_v2 \--valid_targets=arm opencl \--record_tailoring_info=false上面參數(shù)的說明如下表所示,其中需要關(guān)注的是valid_targets參數(shù),要看模型用著上面設(shè)備上,通過指定backend可以使用更好的加速方式。有些讀取可能會出現(xiàn)這樣的疑問,上面使用的是合并的模型,沒合并的模型怎樣用呢,其實很簡單,只有設(shè)置--model_dir,忽略--model_file和--param_file就可以了。
| –model_dir | 待優(yōu)化的PaddlePaddle模型(非combined形式)的路徑 |
| –model_file | 待優(yōu)化的PaddlePaddle模型(combined形式)的網(wǎng)絡(luò)結(jié)構(gòu)文件路徑。 |
| –param_file | 待優(yōu)化的PaddlePaddle模型(combined形式)的權(quán)重文件路徑。 |
| –optimize_out_type | 輸出模型類型,目前支持兩種類型:protobuf和naive_buffer,其中naive_buffer是一種更輕量級的序列化/反序列化實現(xiàn)。若您需要在mobile端執(zhí)行模型預(yù)測,請將此選項設(shè)置為naive_buffer。默認為protobuf。 |
| –optimize_out | 優(yōu)化模型的輸出路徑。 |
| –valid_targets | 指定模型可執(zhí)行的backend,默認為arm。目前可支持x86、arm、opencl、npu、xpu,可以同時指定多個backend(以空格分隔),Model Optimize Tool將會自動選擇最佳方式。如果需要支持華為NPU(Kirin 810/990 Soc搭載的達芬奇架構(gòu)NPU),應(yīng)當(dāng)設(shè)置為npu, arm。 |
| –record_tailoring_info | 當(dāng)使用 根據(jù)模型裁剪庫文件 功能時,則設(shè)置該選項為true,以記錄優(yōu)化后模型含有的kernel和OP信息,默認為false。 |
源碼編譯opt
上面所使用的opt工具是通過下載得到的,如果讀者喜歡折騰,可以嘗試自行源碼編譯編譯,首先是環(huán)境搭建,環(huán)境搭建有兩種方式,第一種是使用Docker,第二種是本地搭建環(huán)境。
在以上的環(huán)境中編譯opt工具,執(zhí)行以下命令即可完成編譯,編譯完成之后,在build.opt/lite/api/下的可執(zhí)行文件opt。
cd Paddle-Lite && ./lite/tools/build.sh build_optimize_toolPaddle Lite的Android預(yù)測庫
Paddle Lite的Android預(yù)測庫也可以通過下載預(yù)編譯的,或者通過源碼編譯。下載地址為:,注意本教程使用的是靜態(tài)庫的方式,而且使用的是圖像識別的,所以需要選擇的下載庫為with_extra=ON,arm_stl=c++_static,with_cv=ON的armv7和armv8庫。下載解壓之后得到的目錄結(jié)構(gòu)如下,其中我們所需的在java的jar和so動態(tài)庫,注意32位的so動態(tài)庫放在Android的armeabi-v7a目錄,64位的so動態(tài)庫放在Android的arm64-v8a目錄,jar包只取一個就好。
inference_lite_lib.android.armv8/ |-- cxx C++ 預(yù)測庫和頭文件 | |-- include C++ 頭文件 | | |-- paddle_api.h | | |-- paddle_image_preprocess.h | | |-- paddle_lite_factory_helper.h | | |-- paddle_place.h | | |-- paddle_use_kernels.h | | |-- paddle_use_ops.h | | `-- paddle_use_passes.h | `-- lib C++預(yù)測庫 | |-- libpaddle_api_light_bundled.a C++靜態(tài)庫 | `-- libpaddle_light_api_shared.so C++動態(tài)庫 |-- java Java預(yù)測庫 | |-- jar | | `-- PaddlePredictor.jar | |-- so | | `-- libpaddle_lite_jni.so | `-- src |-- demo C++和Java示例代碼 | |-- cxx C++ 預(yù)測庫demo | `-- java Java 預(yù)測庫demo同樣如果讀者喜歡折騰,可以嘗試自行源碼編譯編譯,在上面編譯opt工具時搭建的環(huán)境上編譯Paddle Lite的Android預(yù)測庫。在Paddle Lite源碼的根目錄下執(zhí)行以下兩條命令編譯Paddle Lite的Android預(yù)測庫。
./lite/tools/build_android.sh --arch=armv7 --with_extra=ON ./lite/tools/build_android.sh --arch=armv8 --with_extra=ON完成編譯之后,會在Paddle-Lite/build.lite.android.armv7.gcc/inference_lite_lib.android.armv7和Paddle-Lite/build.lite.android.armv8.gcc/inference_lite_lib.android.armv8目錄生成所以的jar和動態(tài)庫,所在位置和使用查看上面的下載Android預(yù)測庫的介紹。
開發(fā)Android項目
創(chuàng)建一個Android項目,在app/libs目錄下存放上一步編譯得到的PaddlePredictor.jar,并添加到app庫中,添加方式可以是選擇這個jar包,右鍵選擇add as Librarys,或者在app/build.gradle添加以下代碼結(jié)果都是一樣的。
implementation files('libs\\PaddlePredictor.jar')然后在app/src/main/jniLibs下存放下載或者編譯得到的動態(tài)庫,最好把32位和64為的動態(tài)庫libpaddle_lite_jni.so都添加進去,分別是armeabi-v7a目錄和arm64-v8a目錄。
復(fù)制轉(zhuǎn)換的預(yù)測模型到app/src/main/assets目錄下,還有類別的標(biāo)簽,每一行對應(yīng)一個標(biāo)簽名稱。
Paddle Lite工具
編寫一個PaddleLiteClassification工具類,關(guān)于Paddle Lite的操作都在這里完成,如加載模型、預(yù)測。在構(gòu)造方法中,通過參數(shù)傳遞的模型路徑加載模型,在加載模型的時候配置預(yù)測信息,如預(yù)測時使用的線程數(shù)量,使用計算資源的模式,要注意的是圖像預(yù)處理的縮放比例scale,均值inputMean和標(biāo)準(zhǔn)差inputStd,因為在訓(xùn)練的時候圖像預(yù)處理可能不一樣的,有些讀者出現(xiàn)在電腦上準(zhǔn)確率很高,但在手機上準(zhǔn)確率很低,多數(shù)情況下就是這個圖像預(yù)處理做得不對。
public class PaddleLiteClassification {private static final String TAG = PaddleLiteClassification.class.getName();private PaddlePredictor paddlePredictor;private Tensor inputTensor;private long[] inputShape = new long[]{1, 3, 224, 224};private static float[] scale = new float[]{1.0f / 255.0f, 1.0f / 255.0f, 1.0f / 255.0f};private static float[] inputMean = new float[]{0.485f, 0.456f, 0.406f};private static float[] inputStd = new float[]{0.229f, 0.224f, 0.225f};private static final int NUM_THREADS = 4;/*** @param modelPath model path*/public PaddleLiteClassification(String modelPath) throws Exception {File file = new File(modelPath);if (!file.exists()) {throw new Exception("model file is not exists!");}try {MobileConfig config = new MobileConfig();config.setModelFromFile(modelPath);config.setThreads(NUM_THREADS);config.setPowerMode(PowerMode.LITE_POWER_HIGH);paddlePredictor = PaddlePredictor.createPaddlePredictor(config);inputTensor = paddlePredictor.getInput(0);inputTensor.resize(inputShape);} catch (Exception e) {e.printStackTrace();throw new Exception("load model fail!");}}為了兼容圖片路徑和Bitmap格式的圖片預(yù)測,這里創(chuàng)建了兩個重載方法,它們都是通過調(diào)用predict()
public float[] predictImage(String image_path) throws Exception {if (!new File(image_path).exists()) {throw new Exception("image file is not exists!");}FileInputStream fis = new FileInputStream(image_path);Bitmap bitmap = BitmapFactory.decodeStream(fis);float[] result = predictImage(bitmap);if (bitmap.isRecycled()) {bitmap.recycle();}return result;}public float[] predictImage(Bitmap bitmap) throws Exception {return predict(bitmap);}這里創(chuàng)建一個獲取最大概率值,并把下標(biāo)返回的方法,其實就是獲取概率最大的預(yù)測標(biāo)簽。
public static int getMaxResult(float[] result) {float probability = 0;int r = 0;for (int i = 0; i < result.length; i++) {if (probability < result[i]) {probability = result[i];r = i;}}return r;}在數(shù)據(jù)輸入之前,需要對數(shù)據(jù)進行預(yù)處理,輸入的數(shù)據(jù)是一個浮點數(shù)組,但是目前輸入的是一個Bitmap的圖片,所以需要把Bitmap轉(zhuǎn)換為浮點數(shù)組,在轉(zhuǎn)換過程中需要對圖像做相應(yīng)的預(yù)處理,如乘比例,減均值,除以方差。為了避免輸入的圖像過大,圖像預(yù)處理變慢,通常在元數(shù)據(jù)預(yù)處理之前,需要對圖像進行壓縮,使用getScaleBitmap()方法可以壓縮等比例壓縮圖像。
private static float[] getScaledMatrix(Bitmap bitmap, int desWidth, int desHeight) {float[] dataBuf = new float[3 * desWidth * desHeight];int rIndex;int gIndex;int bIndex;int[] pixels = new int[desWidth * desHeight];Bitmap bm = Bitmap.createScaledBitmap(bitmap, desWidth, desHeight, false);bm.getPixels(pixels, 0, desWidth, 0, 0, desWidth, desHeight);int j = 0;int k = 0;for (int i = 0; i < pixels.length; i++) {int clr = pixels[i];j = i / desHeight;k = i % desWidth;rIndex = j * desWidth + k;gIndex = rIndex + desHeight * desWidth;bIndex = gIndex + desHeight * desWidth;// 轉(zhuǎn)成RGB通道順序dataBuf[rIndex] = (((clr & 0x00ff0000) >> 16) * scale[0] - inputMean[0]) / inputStd[0];dataBuf[gIndex] = (((clr & 0x0000ff00) >> 8) * scale[1] - inputMean[1]) / inputStd[1];dataBuf[bIndex] = (((clr & 0x000000ff)) * scale[2] - inputMean[2]) / inputStd[2];}if (bm.isRecycled()) {bm.recycle();}return dataBuf;}private Bitmap getScaleBitmap(Bitmap bitmap) {int bmpWidth = bitmap.getWidth();int bmpHeight = bitmap.getHeight();int size = (int) inputShape[2];float scaleWidth = (float) size / bitmap.getWidth();float scaleHeight = (float) size / bitmap.getHeight();Matrix matrix = new Matrix();matrix.postScale(scaleWidth, scaleHeight);return Bitmap.createBitmap(bitmap, 0, 0, bmpWidth, bmpHeight, matrix, true);}這個方法就是Paddle Lite執(zhí)行預(yù)測的最后一步,使用inputTensor.setData(inputData)輸入預(yù)測圖像數(shù)據(jù),通過執(zhí)行paddlePredictor.run()對輸入的數(shù)據(jù)進行預(yù)測并得到預(yù)測結(jié)果,預(yù)測結(jié)果通過paddlePredictor.getOutput(0)提前出來,最后通過解析獲取到最大的概率的預(yù)測標(biāo)簽。到這里Paddle Lite的工具就完成了。
private float[] predict(Bitmap bmp) throws Exception {Bitmap b = getScaleBitmap(bmp);float[] inputData = getScaledMatrix(b, (int) inputShape[2], (int) inputShape[3]);b.recycle();bmp.recycle();inputTensor.setData(inputData);try {paddlePredictor.run();} catch (Exception e) {throw new Exception("predict image fail! log:" + e);}Tensor outputTensor = paddlePredictor.getOutput(0);float[] result = outputTensor.getFloatData();Log.d(TAG, Arrays.toString(result));int l = getMaxResult(result);return new float[]{l, result[l]};}選擇圖片預(yù)測
本教程會有兩個頁面,一個是選擇圖片進行預(yù)測的頁面,另一個是使用相機實時預(yù)測并顯示預(yù)測結(jié)果。以下為activity_main.xml的代碼,通過按鈕選擇圖片,并在該頁面顯示圖片和預(yù)測結(jié)果。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".MainActivity"><ImageViewandroid:id="@+id/image_view"android:layout_width="match_parent"android:layout_height="400dp" /><TextViewandroid:id="@+id/result_text"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_below="@id/image_view"android:text="識別結(jié)果"android:textSize="16sp" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:orientation="horizontal"><Buttonandroid:id="@+id/select_img_btn"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="選擇照片" /><Buttonandroid:id="@+id/open_camera"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="實時預(yù)測" /></LinearLayout></RelativeLayout>在MainActivity.java中,進入到頁面我們就要先加載模型,我們是把模型放在Android項目的assets目錄的,但是Tensorflow Lite并不建議直接在assets讀取模型,所以我們需要把模型復(fù)制到一個緩存目錄,然后再從緩存目錄加載模型,同時還有讀取標(biāo)簽名,標(biāo)簽名稱按照訓(xùn)練的label順序存放在assets的label_list.txt,以下為實現(xiàn)代碼。
classNames = Utils.ReadListFromFile(getAssets(), "label_list.txt"); String classificationModelPath = getCacheDir().getAbsolutePath() + File.separator + "mobilenet_v2.nb"; Utils.copyFileFromAsset(MainActivity.this, "mobilenet_v2.nb", classificationModelPath); try {paddleLiteClassification = new PaddleLiteClassification(classificationModelPath);Toast.makeText(MainActivity.this, "模型加載成功!", Toast.LENGTH_SHORT).show(); } catch (Exception e) {Toast.makeText(MainActivity.this, "模型加載失敗!", Toast.LENGTH_SHORT).show();e.printStackTrace();finish(); }添加兩個按鈕點擊事件,可以選擇打開相冊讀取圖片進行預(yù)測,或者打開另一個Activity進行調(diào)用攝像頭實時識別。
Button selectImgBtn = findViewById(R.id.select_img_btn); Button openCamera = findViewById(R.id.open_camera); imageView = findViewById(R.id.image_view); textView = findViewById(R.id.result_text); selectImgBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 打開相冊Intent intent = new Intent(Intent.ACTION_PICK);intent.setType("image/*");startActivityForResult(intent, 1);} }); openCamera.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 打開實時拍攝識別頁面Intent intent = new Intent(MainActivity.this, CameraActivity.class);startActivity(intent);} });當(dāng)打開相冊選擇照片之后,回到原來的頁面,在下面這個回調(diào)方法中獲取選擇圖片的Uri,通過Uri可以獲取到圖片的絕對路徑。如果Android8以上的設(shè)備獲取不到圖片,需要在AndroidManifest.xml配置文件中的application添加android:requestLegacyExternalStorage="true"。拿到圖片路徑之后,調(diào)用PaddleLiteClassification類中的predictImage()方法預(yù)測并獲取預(yù)測值,在頁面上顯示預(yù)測的標(biāo)簽、對應(yīng)標(biāo)簽的名稱、概率值和預(yù)測時間。
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {super.onActivityResult(requestCode, resultCode, data);String image_path;if (resultCode == Activity.RESULT_OK) {if (requestCode == 1) {if (data == null) {Log.w("onActivityResult", "user photo data is null");return;}Uri image_uri = data.getData();image_path = getPathFromURI(MainActivity.this, image_uri);try {// 預(yù)測圖像FileInputStream fis = new FileInputStream(image_path);imageView.setImageBitmap(BitmapFactory.decodeStream(fis));long start = System.currentTimeMillis();float[] result = paddleLiteClassification.predictImage(image_path);long end = System.currentTimeMillis();String show_text = "預(yù)測結(jié)果標(biāo)簽:" + (int) result[0] +"\n名稱:" + classNames.get((int) result[0]) +"\n概率:" + result[1] +"\n時間:" + (end - start) + "ms";textView.setText(show_text);} catch (Exception e) {e.printStackTrace();}}} }上面獲取的Uri可以通過下面這個方法把Url轉(zhuǎn)換成絕對路徑。
// get photo from Uri public static String getPathFromURI(Context context, Uri uri) {String result;Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);if (cursor == null) {result = uri.getPath();} else {cursor.moveToFirst();int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);result = cursor.getString(idx);cursor.close();}return result; }攝像頭實時預(yù)測
在調(diào)用相機實時預(yù)測我就不再介紹了,原理都差不多,具體可以查看https://github.com/yeyupiaoling/ClassificationForAndroid/tree/master/PaddleLiteClassification中的源代碼。核心代碼如下,創(chuàng)建一個子線程,子線程中不斷從攝像頭預(yù)覽的AutoFitTextureView上獲取圖像,并執(zhí)行預(yù)測,并在頁面上顯示預(yù)測的標(biāo)簽、對應(yīng)標(biāo)簽的名稱、概率值和預(yù)測時間。每一次預(yù)測完成之后都立即獲取圖片繼續(xù)預(yù)測,只要預(yù)測速度夠快,就可以看成實時預(yù)測。
private Runnable periodicClassify =new Runnable() {@Overridepublic void run() {synchronized (lock) {if (runClassifier) {// 開始預(yù)測前要判斷相機是否已經(jīng)準(zhǔn)備好if (getApplicationContext() != null && mCameraDevice != null && tfLiteClassificationUtil != null) {predict();}}}if (mInferThread != null && mInferHandler != null && mCaptureHandler != null && mCaptureThread != null) {mInferHandler.post(periodicClassify);}}};// 預(yù)測相機捕獲的圖像 private void predict() {// 獲取相機捕獲的圖像Bitmap bitmap = mTextureView.getBitmap();try {// 預(yù)測圖像long start = System.currentTimeMillis();float[] result = paddleLiteClassification.predictImage(bitmap);long end = System.currentTimeMillis();String show_text = "預(yù)測結(jié)果標(biāo)簽:" + (int) result[0] +"\n名稱:" + classNames.get((int) result[0]) +"\n概率:" + result[1] +"\n時間:" + (end - start) + "ms";textView.setText(show_text);} catch (Exception e) {e.printStackTrace();} }本項目中使用的了讀取圖片的權(quán)限和打開相機的權(quán)限,所以不要忘記在AndroidManifest.xml添加以下權(quán)限申請。
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>如果是Android 6 以上的設(shè)備還要動態(tài)申請權(quán)限。
// check had permissionprivate boolean hasPermission() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {return checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;} else {return true;}}// request permissionprivate void requestPermission() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {requestPermissions(new String[]{Manifest.permission.CAMERA,Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);}}選擇圖片識別效果圖:
相機實時識別效果圖:
總結(jié)
以上是生活随笔為你收集整理的基于Paddle Lite在Android手机上实现图像分类的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 手机串号IMEI的国际查询网站及说明
- 下一篇: 骗子广告联盟_骗子把我的脸变成了Goog