原文博客:Doi技术团队
链接地址:https://blog.doiduoyi.com/authors/1584446358138
初心:记录优秀的Doi技术团队学习经历
现在越来越多的手机要使用到深度学习了,比如一些图像分类,目标检测,风格迁移等等,之前都是把数据提交给服务器完成的。但是提交给服务器有几点不好,首先是速度问题,图片上传到服务器需要时间,客户端接收结果也需要时间,这一来回就占用了一大半的时间,会使得整体的预测速度都变慢了,再且现在手机的性能不断提高,足以做深度学习的预测。其二是隐私问题,如果只是在本地预测,那么用户根本就不用上传图片,安全性也大大提高了。现在的手机深度学习会计有很多,比如百度的paddle-mobile、小米的MACNE、腾讯的NCNN、谷歌的TensorFlow lite,而我们在本章使用的是百度的paddle-mobile。
PaddleMobile的GitHub地址:https://github.com/PaddlePaddle/paddle-mobile
想要使用paddle-mobile,就要编译Android能够使用的CPP库,在这一部分中,我们介绍两种编译Android的paddle-mobile库,分别是使用Docker编译paddle-mobile库、使用Ubuntu交叉编译paddle-mobile库。
在Android项目中有使用过CPP的读者都知道,想要让Java代码能够调用CPP代码,那么CPP的函数明明就要按照一定的规范:Java_包名_类名_对应的Java的方法名
,目前官方提供了5个可以给Java调用的函数,该代码在:paddle-mobile/src/jni/paddle_mobile_jni.cpp
,如果想要让这些函数能够在自己的包名下的类调用,就要修改CPP的函数名称修改如下:
修改之前的load
函数:
JNIEXPORT jboolean JNICALL Java_com_baidu_paddle_PML_load(JNIEnv *env,
jclass thiz,
jstring modelPath) {
ANDROIDLOGI("load invoked");
bool optimize = true;
return getPaddleMobileInstance()->Load(jstring2cppstring(env, modelPath),
optimize);
}
笔者项目的包名为com.example.paddlemobile1
,在这个包下有一个ImageRecognition.java
的程序来对应这个CPP程序,那么修改load
函数如下:
JNIEXPORT jboolean JNICALL Java_com_example_paddlemobile1_ImageRecognition_load(JNIEnv *env,
jclass thiz,
jstring modelPath) {
ANDROIDLOGI("load invoked");
bool optimize = true;
return getPaddleMobileInstance()->Load(jstring2cppstring(env, modelPath),
optimize);
}
如果读者觉得这样比较麻烦的话,可以不用修改,只要创建一个com.baidu.paddle
包,在这个包下创建一个PML.java
的Java程序来对应这个CPP程序。
为了方便操作,以下的操作都是在root用户的执行的:
1、安装Docker,以下是在Ubuntu下安装的的方式,只要一条命令就可以了:
apt-get install docker.io
2、克隆paddle-mobile源码:
git clone https://github.com/PaddlePaddle/paddle-mobile.git
3、进入到paddle-mobile根目录下编译docker镜像:
cd paddle-mobile
# 编译生成进行,编译时间可能要很长
docker build -t paddle-mobile:dev - < Dockerfile
编译完成可以使用docker images
命令查看是否已经生成进行:
root@test:/home/test# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
paddle-mobile dev fffbd8779c68 20 hours ago 3.76 GB
4、运行镜像并进入到容器里面,当前目录还是在paddle-mobile根目录下:
docker run -it -v $PWD:/paddle-mobile paddle-mobile:dev
5、在容器里面执行以下两条命令:
root@fc6f7e9ebdf1:/# cd paddle-mobile/
root@fc6f7e9ebdf1:/paddle-mobile# cmake -DCMAKE_TOOLCHAIN_FILE=tools/toolchains/arm-android-neon.cmake
6、(可选)可以使用命令ccmake .
配置一些信息,比如可以设置NET
仅支持googlenet
,这样便于得到的paddle-mobile库会更小一些,修改完成之后,使用c
命令保存,使用g
退出。笔者一般跳过这个步骤。
Page 1 of 1
CMAKE_ASM_FLAGS
CMAKE_ASM_FLAGS_DEBUG
CMAKE_ASM_FLAGS_RELEASE
CMAKE_BUILD_TYPE
CMAKE_INSTALL_PREFIX /usr/local
CMAKE_TOOLCHAIN_FILE /paddle-mobile/tools/toolchains/arm-android-neon.cmake
CPU ON
DEBUGING ON
FPGA OFF
LOG_PROFILE ON
MALI_GPU OFF
NET defult
USE_EXCEPTION ON
USE_OPENMP ON
7、最后执行一下make
就可以了,到这一步就完成了paddle-mobile的编译。
root@fc6f7e9ebdf1:/paddle-mobile# make
8、使用exit
命令退出容器,回到Ubuntu本地上。
root@fc6f7e9ebdf1:/paddle-mobile# exit
9、在paddle-mobile根目录下,有一个build目录,我们编译好的paddle-mobile库就在这里。
root@test:/home/test/paddle-mobile/build# ls
libpaddle-mobile.so
libpaddle-mobile.so
就是我们在开发Android项目的时候使用到的paddle-mobile库。
1、首先要下载和解压NDK。
wget https://dl.google.com/android/repository/android-ndk-r17b-linux-x86_64.zip
unzip android-ndk-r17b-linux-x86_64.zip
2、设置NDK环境变量,目录是NDK的解压目录。
export NDK_ROOT="/home/test/paddlepaddle/android-ndk-r17b"
设置好之后,可以使用以下的命令查看配置情况。
root@test:/home/test/paddlepaddle# echo $NDK_ROOT
/home/test/paddlepaddle/android-ndk-r17b
3、安装cmake,需要安装较高版本的,笔者的cmake版本是3.11.2。
wget https://cmake.org/files/v3.11/cmake-3.11.2.tar.gz
tar -zxvf cmake-3.11.2.tar.gz
bootstrap
。cd cmake-3.11.2
./bootstrap
make
make install
cmake --version
是否安装成功.root@test:/home/test/paddlepaddle# cmake --version
cmake version 3.11.2
CMake suite maintained and supported by Kitware (kitware.com/cmake).
4、克隆paddle-mobile源码。
git clone https://github.com/PaddlePaddle/paddle-mobile.git
5、进入到paddle-mobile的tools目录下,执行编译。
cd paddle-mobile/tools/
sh build.sh android
(可选)如果想编译针对某一个网络编译更小的库时,可以在命令后面加上相应的参数,如下:
sh build.sh android googlenet
6、最后会在paddle-mobile/build/release/arm-v7a/build
目录下生产paddle-mobile库。
root@test:/home/test/paddlepaddle/paddle-mobile/build/release/arm-v7a/build# ls
libpaddle-mobile.so
libpaddle-mobile.so
就是我们在开发Android项目的时候使用到的paddle-mobile库。
首先使用Android Studio创建一个普通的Android项目,包名为com.example.paddlemobile1
,我们可以不用选择CPP的支持,因为我们已经编译好了CPP。之后按照以下的步骤开始执行:
1、在main
目录下创建l两个assets/paddle_models
文件夹,这个文件夹我们将会使用它来存放PaddleFluid训练好的预测模型,官方也提供了一些训练好的模型和预测图像,可以在这里下载。预测有两种,一种是合并的模型,另一种是非合并的模型,在本项目中,我们使用的是非合并的模型,下面就是笔者使用的一个googlenet神经网络训练102中花卉数据集得到的预测模型,可以到这里下载笔者训练好的模型。
PS: PaddleMobile支持量化模型,使用模型量化可以把模型缩小至原来的四分之一,比如笔者的模型是20多M,量化之后就是5M多了。这个量化是把模型的float32经过量化变成int8,所以模型会是原来的四分之一,当然精度也有一点点损失,但影响不大。
cd paddle-mobile/tools/quantification/
cmake .
make
# 命令格式./quantify (0:seperated. 1:combined ) (输入路径) (输出路径)
./quantify 0 googlenet/ ./googlenet_min/
如果使用量化模型,那加载模型的接口也有修改一下,使用以下的接口加载模型:
public static native boolean loadQualified(String modelDir);
2、在main
目录下创建一个jniLibs
文件夹,这个文件夹是存放CPP编译库的,在本项目中就存放上一部分编译的libpaddle-mobile.so
,这里可以下载笔者编译好的模型。
3、在Android项目的配置文件夹中加上权限声明,因为我们要使用到读取相册和使用相机,所以加上以下的权限声明:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
4、修改activity_main.xml
界面,修改成如下:
<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"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/btn_ll"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/use_photo"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="相册" />
<Button
android:id="@+id/start_camera"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="拍照" />
LinearLayout>
<TextView
android:layout_above="@id/btn_ll"
android:id="@+id/result_text"
android:textSize="16sp"
android:layout_width="match_parent"
android:hint="预测结果会在这里显示"
android:layout_height="100dp" />
<ImageView
android:layout_alignParentTop="true"
android:layout_above="@id/result_text"
android:id="@+id/show_image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
RelativeLayout>
5、创建一个ImageRecognition.java
的Java程序,这个程序的作用就是调用paddle-mobile/src/jni/paddle_mobile_jni.cpp
的函数,对应的是里面的函数。目前支持一下几个接口。
package com.example.paddlemobile1;
public class ImageRecognition {
// set thread num
public static native void setThread(int threadCount);
//Load seperated parameters
public static native boolean load(String modelDir);
// load qualified model
public static native boolean loadQualified(String modelDir);
// Load combined parameters
public static native boolean loadCombined(String modelPath, String paramPath);
// load qualified model
public static native boolean loadCombinedQualified(String modelPath, String paramPath);
// object detection
public static native float[] predictImage(float[] buf, int[]ddims);
// predict yuv image
public static native float[] predictYuv(byte[] buf, int imgWidth, int imgHeight, int[] ddims, float[]meanValues);
// clear model
public static native void clear();
}
6、然后编写一个PhotoUtil.java
的工具类。
package com.example.paddlemobile1;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
public class PhotoUtil {
// start camera
public static Uri start_camera(Activity activity, int requestCode) {
Uri imageUri;
// save image in cache path
File outputImage = new File(activity.getExternalCacheDir(), "out_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= 24) {
// compatible with Android 7.0 or over
imageUri = FileProvider.getUriForFile(activity,
"com.example.paddlemobile1", outputImage);
} else {
imageUri = Uri.fromFile(outputImage);
}
// set system camera Action
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// set save photo path
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// set photo quality, min is 0, max is 1
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
activity.startActivityForResult(intent, requestCode);
return imageUri;
}
// get picture in photo
public static void use_photo(Activity activity, int requestCode){
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
activity.startActivityForResult(intent, requestCode);
}
// get photo from Uri
public static String get_path_from_URI(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;
}
// Compress the image to the size of the training image,and change RGB
public 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;
dataBuf[rIndex] = (float) ((clr & 0x00ff0000) >> 16) - 148;
dataBuf[gIndex] = (float) ((clr & 0x0000ff00) >> 8) - 148;
dataBuf[bIndex] = (float) ((clr & 0x000000ff)) - 148;
}
if (bm.isRecycled()) {
bm.recycle();
}
return dataBuf;
}
// compress picture
public static Bitmap getScaleBitmap(String filePath) {
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, opt);
int bmpWidth = opt.outWidth;
int bmpHeight = opt.outHeight;
int maxSize = 500;
// compress picture with inSampleSize
opt.inSampleSize = 1;
while (true) {
if (bmpWidth / opt.inSampleSize < maxSize || bmpHeight / opt.inSampleSize < maxSize) {
break;
}
opt.inSampleSize *= 2;
}
opt.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filePath, opt);
}
}
start_camera()
方法是启动相机并返回图片的URI。use_photo()
方法是打开相册,获取到的图片URI在回到函数中获取。get_path_from_URI()
方法是把图片的URI转换成绝对路径。getScaledMatrix()
方法是把图片压缩成跟训练时的大小,并转换成预测需要用的数据格式浮点数组。getScaleBitmap()
方法是对图片进行等比例压缩,减少内存的支出。7、最后修改MainActivity.java
,修改如下:
package com.example.paddlemobile1;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.RequestOptions;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getName();
private static final int USE_PHOTO = 1001;
private static final int START_CAMERA = 1002;
private Uri image_uri;
private ImageView show_image;
private TextView result_text;
private String assets_path = "paddle_models";
private boolean load_result = false;
private int[] ddims = {1, 3, 224, 224};
private static final String[] PADDLE_MODEL = {
"lenet",
"alexnet",
"vgg16",
"resnet",
"googlenet",
"mobilenet_v1",
"mobilenet_v2",
"inception_v1",
"inception_v2",
"squeezenet"
};
// load paddle-mobile api
static {
try {
System.loadLibrary("paddle-mobile");
} catch (SecurityException e) {
e.printStackTrace();
} catch (UnsatisfiedLinkError e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
// initialize view
private void init() {
request_permissions();
show_image = (ImageView) findViewById(R.id.show_image);
result_text = (TextView) findViewById(R.id.result_text);
Button use_photo = (Button) findViewById(R.id.use_photo);
Button start_photo = (Button) findViewById(R.id.start_camera);
// use photo click
use_photo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
PhotoUtil.use_photo(MainActivity.this, USE_PHOTO);
// load_model();
}
});
// start camera click
start_photo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
image_uri = PhotoUtil.start_camera(MainActivity.this, START_CAMERA);
}
});
// copy file from assets to sdcard
String sdcard_path = Environment.getExternalStorageDirectory()
+ File.separator + assets_path;
copy_file_from_asset(this, assets_path, sdcard_path);
// load model
load_model();
}
// load infer model
private void load_model() {
String model_path = Environment.getExternalStorageDirectory()
+ File.separator + assets_path + File.separator + PADDLE_MODEL[4];
Log.d(TAG, model_path);
load_result = ImageRecognition.load(model_path);
if (load_result) {
Log.d(TAG, "model load success");
} else {
Log.d(TAG, "model load fail");
}
}
// clear infer model
private void clear_model() {
ImageRecognition.clear();
Log.d(TAG, "model is clear");
}
// copy file from asset to sdcard
public void copy_file_from_asset(Context context, String oldPath, String newPath) {
try {
String[] fileNames = context.getAssets().list(oldPath);
if (fileNames.length > 0) {
// directory
File file = new File(newPath);
if (!file.exists()) {
file.mkdirs();
}
// copy recursivelyC
for (String fileName : fileNames) {
copy_file_from_asset(context, oldPath + "/" + fileName, newPath + "/" + fileName);
}
Log.d(TAG, "copy files finish");
} else {
// file
File file = new File(newPath);
// if file exists will never copy
if (file.exists()) {
return;
}
// copy file to new path
InputStream is = context.getAssets().open(oldPath);
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int byteCount;
while ((byteCount = is.read(buffer)) != -1) {
fos.write(buffer, 0, byteCount);
}
fos.flush();
is.close();
fos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
String image_path;
RequestOptions options = new RequestOptions().skipMemoryCache(true).diskCacheStrategy(DiskCacheStrategy.NONE);
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case USE_PHOTO:
if (data == null) {
Log.w(TAG, "user photo data is null");
return;
}
image_uri = data.getData();
Glide.with(MainActivity.this).load(image_uri).apply(options).into(show_image);
// get image path from uri
image_path = PhotoUtil.get_path_from_URI(MainActivity.this, image_uri);
// show result
result_text.setText(image_path);
// predict image
predict_image(PhotoUtil.get_path_from_URI(MainActivity.this, image_uri));
break;
case START_CAMERA:
// show photo
Glide.with(MainActivity.this).load(image_uri).apply(options).into(show_image);
// get image path from uri
image_path = PhotoUtil.get_path_from_URI(MainActivity.this, image_uri);
// show result
result_text.setText(image_path);
// predict image
predict_image(PhotoUtil.get_path_from_URI(MainActivity.this, image_uri));
break;
}
}
}
@SuppressLint("SetTextI18n")
private void predict_image(String image_path) {
// picture to float array
Bitmap bmp = PhotoUtil.getScaleBitmap(image_path);
float[] inputData = PhotoUtil.getScaledMatrix(bmp, ddims[2], ddims[3]);
try {
long start = System.currentTimeMillis();
// get predict result
float[] result = ImageRecognition.predictImage(inputData, ddims);
Log.d(TAG, "origin predict result:" + Arrays.toString(result));
long end = System.currentTimeMillis();
long time = end - start;
Log.d("result length", String.valueOf(result.length));
// show predict result and time
int r = get_max_result(result);
String show_text = "result:" + r + "\nprobability:" + result[r] + "\ntime:" + time + "ms";
result_text.setText(show_text);
} catch (Exception e) {
e.printStackTrace();
}
}
private int get_max_result(float[] result) {
float probability = result[0];
int r = 0;
for (int i = 0; i < result.length; i++) {
if (probability < result[i]) {
probability = result[i];
r = i;
}
}
return r;
}
// request permissions
private void request_permissions() {
List<String> permissionList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.CAMERA);
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
// if list is not empty will request permissions
if (!permissionList.isEmpty()) {
ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), 1);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 1:
if (grantResults.length > 0) {
for (int i = 0; i < grantResults.length; i++) {
int grantResult = grantResults[i];
if (grantResult == PackageManager.PERMISSION_DENIED) {
String s = permissions[i];
Toast.makeText(this, s + " permission was denied", Toast.LENGTH_SHORT).show();
}
}
}
break;
}
}
@Override
protected void onDestroy() {
// clear model before destroy app
clear_model();
super.onDestroy();
}
}
load_model()
方法是加载预测模型的。clear_model()
方法是清空预测模型的。copy_file_from_asset()
方法是把预测模型复制到内存卡上。predict_image()
方法是预测图片的。get_max_result()
方法是获取概率最大的预测结果。request_permissions()
方法是动态请求权限的。因为使用到图像加载框架Glide,所以要在build.gradle
加入以下的引用。
implementation 'com.github.bumptech.glide:glide:4.3.1'
源码下载: 上面已经提供了全部代码,为了方便读者使用,这个提供了整个项目的下载。