# 前言
Paddle Lite是飛槳基於Paddle Mobile全新升級推出的端側推理引擎,在多硬件、多平臺以及硬件混合調度的支持上更加完備,爲包括手機在內的端側場景的AI應用提供高效輕量的推理能力,有效解決手機算力和內存限制等問題,致力於推動AI應用更廣泛的落地。
本教程源碼地址:https://github.com/yeyupiaoling/ClassificationForAndroid/tree/master/PaddleLiteClassification
模型轉換¶
Paddle Lite使用的是PaddlePaddle保存的預測模型,如果不瞭解PaddlePaddle的模型保存,可以參考《模型的保存與使用》這篇文章。下面簡單介紹一下保存模型的方式。通過使用fluid.io.save_inference_model()接口可以保存預測模型,預測模型值保存推所需的網絡,不會保存損失函數等。當使用model_filename和params_filename指定參數之後,保存的預測模型只有兩個文件,這種稱爲合併模型,否則會以網絡結構命名將大量的參數文件保存在dirname指定的路徑下,這種叫做非合併模型。例如通過以下的代碼片段保存的預測模型爲model和params,這兩個模型將會用於下一步的模型轉換。
import paddle.fluid as fluid
# 定義網絡
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())
# 數據輸入及訓練過程
# 保存預測模型
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轉換¶
使用fluid.io.save_inference_model()接口可以保存預測模型並不能直接使用,還需要通過opt工具轉換,這個工具可以下載Paddle Lite預編譯的,或者通過源碼編譯,opt下載地址:https://paddle-lite.readthedocs.io/zh/latest/user_guides/release_lib.html#opt,關於如何編譯opt請看下一部分。
通過以下命令即即可把預測模型轉變成Paddle Lite使用的模型,其中輸出的mobilenet_v2.nb就是所需的模型文件,因爲轉換之後,模型可以在valid_targets指定的環境上加速預測,所以變得非常牛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
上面參數的說明如下表所示,其中需要關注的是valid_targets參數,要看模型用着上面設備上,通過指定backend可以使用更好的加速方式。有些讀取可能會出現這樣的疑問,上面使用的是合併的模型,沒合併的模型怎樣用呢,其實很簡單,只有設置--model_dir,忽略--model_file和--param_file就可以了。
| 參數 | 說明 |
|---|---|
| –model_dir | 待優化的PaddlePaddle模型(非combined形式)的路徑 |
| –model_file | 待優化的PaddlePaddle模型(combined形式)的網絡結構文件路徑。 |
| –param_file | 待優化的PaddlePaddle模型(combined形式)的權重文件路徑。 |
| –optimize_out_type | 輸出模型類型,目前支持兩種類型:protobuf和naive_buffer,其中naive_buffer是一種更輕量級的序列化/反序列化實現。若您需要在mobile端執行模型預測,請將此選項設置爲naive_buffer。默認爲protobuf。 |
| –optimize_out | 優化模型的輸出路徑。 |
| –valid_targets | 指定模型可執行的backend,默認爲arm。目前可支持x86、arm、opencl、npu、xpu,可以同時指定多個backend(以空格分隔),Model Optimize Tool將會自動選擇最佳方式。如果需要支持華爲NPU(Kirin 810/990 Soc搭載的達芬奇架構NPU),應當設置爲npu, arm。 |
| –record_tailoring_info | 當使用 根據模型裁剪庫文件 功能時,則設置該選項爲true,以記錄優化後模型含有的kernel和OP信息,默認爲false。 |
源碼編譯opt¶
上面所使用的opt工具是通過下載得到的,如果讀者喜歡折騰,可以嘗試自行源碼編譯編譯,首先是環境搭建,環境搭建有兩種方式,第一種是使用Docker,第二種是本地搭建環境。
- 使用Docker容器,只要3條命令即可搭建環境,這個也是最簡單的方式。
# 拉取Paddle Lite鏡像
docker pull paddlepaddle/paddle-lite:2.0.0_beta
# 克隆源碼
git clone https://github.com/PaddlePaddle/Paddle-Lite.git
# 進行Paddle Lite容器
docker run -it \
--name paddlelite_docker \
-v $PWD/Paddle-Lite:/Paddle-Lite \
--net=host \
paddlepaddle/paddle-lite /bin/bash
- 在Ubuntu本地搭建Paddle Lite編譯環境,通過執行以下命令在Ubuntu本地完成環境搭建。
# 1. 安裝基本環境
apt update
apt-get install -y --no-install-recommends \
gcc g++ git make wget python unzip adb curl
# 2. 安裝Java環境
apt-get install -y default-jdk
# 3. 安裝Cmake
wget -c https://mms-res.cdn.bcebos.com/cmake-3.10.3-Linux-x86_64.tar.gz && \
tar xzf cmake-3.10.3-Linux-x86_64.tar.gz && \
mv cmake-3.10.3-Linux-x86_64 /opt/cmake-3.10 && \
ln -s /opt/cmake-3.10/bin/cmake /usr/bin/cmake && \
ln -s /opt/cmake-3.10/bin/ccmake /usr/bin/ccmake
# 4. 安裝NDK
cd /tmp && curl -O https://dl.google.com/android/repository/android-ndk-r17c-linux-x86_64.zip
cd /opt && unzip /tmp/android-ndk-r17c-linux-x86_64.zip
# 5. 添加環境變量
echo "export NDK_ROOT=/opt/android-ndk-r17c" >> ~/.bashrc
source ~/.bashrc
在以上的環境中編譯opt工具,執行以下命令即可完成編譯,編譯完成之後,在build.opt/lite/api/下的可執行文件opt。
cd Paddle-Lite && ./lite/tools/build.sh build_optimize_tool
Paddle Lite的Android預測庫¶
Paddle Lite的Android預測庫也可以通過下載預編譯的,或者通過源碼編譯。下載地址爲:,注意本教程使用的是靜態庫的方式,而且使用的是圖像識別的,所以需要選擇的下載庫爲with_extra=ON,arm_stl=c++_static,with_cv=ON的armv7和armv8庫。下載解壓之後得到的目錄結構如下,其中我們所需的在java的jar和so動態庫,注意32位的so動態庫放在Android的armeabi-v7a目錄,64位的so動態庫放在Android的arm64-v8a目錄,jar包只取一個就好。
inference_lite_lib.android.armv8/
|-- cxx C++ 預測庫和頭文件
| |-- 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++預測庫
| |-- libpaddle_api_light_bundled.a C++靜態庫
| `-- libpaddle_light_api_shared.so C++動態庫
|-- java Java預測庫
| |-- jar
| | `-- PaddlePredictor.jar
| |-- so
| | `-- libpaddle_lite_jni.so
| `-- src
|-- demo C++和Java示例代碼
| |-- cxx C++ 預測庫demo
| `-- java Java 預測庫demo
同樣如果讀者喜歡折騰,可以嘗試自行源碼編譯編譯,在上面編譯opt工具時搭建的環境上編譯Paddle Lite的Android預測庫。在Paddle Lite源碼的根目錄下執行以下兩條命令編譯Paddle Lite的Android預測庫。
./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和動態庫,所在位置和使用查看上面的下載Android預測庫的介紹。
開發Android項目¶
創建一個Android項目,在app/libs目錄下存放上一步編譯得到的PaddlePredictor.jar,並添加到app庫中,添加方式可以是選擇這個jar包,右鍵選擇add as Librarys,或者在app/build.gradle添加以下代碼結果都是一樣的。
implementation files('libs\\PaddlePredictor.jar')
然後在app/src/main/jniLibs下存放下載或者編譯得到的動態庫,最好把32位和64爲的動態庫libpaddle_lite_jni.so都添加進去,分別是armeabi-v7a目錄和arm64-v8a目錄。
複製轉換的預測模型到app/src/main/assets目錄下,還有類別的標籤,每一行對應一個標籤名稱。
Paddle Lite工具¶
編寫一個PaddleLiteClassification工具類,關於Paddle Lite的操作都在這裏完成,如加載模型、預測。在構造方法中,通過參數傳遞的模型路徑加載模型,在加載模型的時候配置預測信息,如預測時使用的線程數量,使用計算資源的模式,要注意的是圖像預處理的縮放比例scale,均值inputMean和標準差inputStd,因爲在訓練的時候圖像預處理可能不一樣的,有些讀者出現在電腦上準確率很高,但在手機上準確率很低,多數情況下就是這個圖像預處理做得不對。
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格式的圖片預測,這裏創建了兩個重載方法,它們都是通過調用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);
}
這裏創建一個獲取最大概率值,並把下標返回的方法,其實就是獲取概率最大的預測標籤。
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;
}
在數據輸入之前,需要對數據進行預處理,輸入的數據是一個浮點數組,但是目前輸入的是一個Bitmap的圖片,所以需要把Bitmap轉換爲浮點數組,在轉換過程中需要對圖像做相應的預處理,如乘比例,減均值,除以方差。爲了避免輸入的圖像過大,圖像預處理變慢,通常在元數據預處理之前,需要對圖像進行壓縮,使用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;
// 轉成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執行預測的最後一步,使用inputTensor.setData(inputData)輸入預測圖像數據,通過執行paddlePredictor.run()對輸入的數據進行預測並得到預測結果,預測結果通過paddlePredictor.getOutput(0)提前出來,最後通過解析獲取到最大的概率的預測標籤。到這裏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]};
}
選擇圖片預測¶
本教程會有兩個頁面,一個是選擇圖片進行預測的頁面,另一個是使用相機即時預測並顯示預測結果。以下爲activity_main.xml的代碼,通過按鈕選擇圖片,並在該頁面顯示圖片和預測結果。
<?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">
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="400dp" />
<TextView
android:id="@+id/result_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/image_view"
android:text="識別結果"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<Button
android:id="@+id/select_img_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="選擇照片" />
<Button
android:id="@+id/open_camera"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="即時預測" />
</LinearLayout>
</RelativeLayout>
在MainActivity.java中,進入到頁面我們就要先加載模型,我們是把模型放在Android項目的assets目錄的,但是Tensorflow Lite並不建議直接在assets讀取模型,所以我們需要把模型複製到一個緩存目錄,然後再從緩存目錄加載模型,同時還有讀取標籤名,標籤名稱按照訓練的label順序存放在assets的label_list.txt,以下爲實現代碼。
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();
}
添加兩個按鈕點擊事件,可以選擇打開相冊讀取圖片進行預測,或者打開另一個Activity進行調用攝像頭即時識別。
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() {
@Override
public void onClick(View v) {
// 打開相冊
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, 1);
}
});
openCamera.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 打開即時拍攝識別頁面
Intent intent = new Intent(MainActivity.this, CameraActivity.class);
startActivity(intent);
}
});
當打開相冊選擇照片之後,回到原來的頁面,在下面這個回調方法中獲取選擇圖片的Uri,通過Uri可以獲取到圖片的絕對路徑。如果Android8以上的設備獲取不到圖片,需要在AndroidManifest.xml配置文件中的application添加android:requestLegacyExternalStorage="true"。拿到圖片路徑之後,調用PaddleLiteClassification類中的predictImage()方法預測並獲取預測值,在頁面上顯示預測的標籤、對應標籤的名稱、概率值和預測時間。
@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 {
// 預測圖像
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 = "預測結果標籤:" + (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轉換成絕對路徑。
// 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;
}
攝像頭即時預測¶
在調用相機即時預測我就不再介紹了,原理都差不多,具體可以查看https://github.com/yeyupiaoling/ClassificationForAndroid/tree/master/PaddleLiteClassification中的源代碼。核心代碼如下,創建一個子線程,子線程中不斷從攝像頭預覽的AutoFitTextureView上獲取圖像,並執行預測,並在頁面上顯示預測的標籤、對應標籤的名稱、概率值和預測時間。每一次預測完成之後都立即獲取圖片繼續預測,只要預測速度夠快,就可以看成即時預測。
private Runnable periodicClassify =
new Runnable() {
@Override
public void run() {
synchronized (lock) {
if (runClassifier) {
// 開始預測前要判斷相機是否已經準備好
if (getApplicationContext() != null && mCameraDevice != null && tfLiteClassificationUtil != null) {
predict();
}
}
}
if (mInferThread != null && mInferHandler != null && mCaptureHandler != null && mCaptureThread != null) {
mInferHandler.post(periodicClassify);
}
}
};
// 預測相機捕獲的圖像
private void predict() {
// 獲取相機捕獲的圖像
Bitmap bitmap = mTextureView.getBitmap();
try {
// 預測圖像
long start = System.currentTimeMillis();
float[] result = paddleLiteClassification.predictImage(bitmap);
long end = System.currentTimeMillis();
String show_text = "預測結果標籤:" + (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();
}
}
本項目中使用的了讀取圖片的權限和打開相機的權限,所以不要忘記在AndroidManifest.xml添加以下權限申請。
<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 以上的設備還要動態申請權限。
// check had permission
private 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 permission
private 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);
}
}
選擇圖片識別效果圖:

相機即時識別效果圖:
