基于PaddleMobile的Android图像识别应用开发

项目概述

本文档详细介绍了如何使用百度PaddleMobile框架在Android平台上实现图像识别功能。通过将训练好的深度学习模型集成到移动应用中,我们可以在本地设备上完成图像分类任务,无需依赖云端服务,从而提升识别速度和用户隐私安全性。

环境准备

安装必要工具

  • Android Studio (推荐3.0以上版本)
  • Java Development Kit (JDK 8)
  • Python 3.x (用于模型转换和量化)

下载PaddleMobile源码

git clone https://github.com/PaddlePaddle/paddle-mobile.git

编译PaddleMobile库

使用Docker编译

  1. 安装Docker
apt-get install docker.io
  1. 构建Docker镜像
cd paddle-mobile
docker build -t paddle-mobile:dev - < Dockerfile
  1. 运行容器并编译
docker run -it -v $PWD:/paddle-mobile paddle-mobile:dev
cd paddle-mobile
cmake -DCMAKE_TOOLCHAIN_FILE=tools/toolchains/arm-android-neon.cmake
make
exit
  1. 获取编译产物
ls build/libpaddle-mobile.so

使用Ubuntu交叉编译

  1. 安装NDK
wget https://dl.google.com/android/repository/android-ndk-r17b-linux-x86_64.zip
unzip android-ndk-r17b-linux-x86_64.zip
export NDK_ROOT="/path/to/android-ndk-r17b"
  1. 编译PaddleMobile
cd paddle-mobile/tools
sh build.sh android

创建Android项目

项目结构设置

  1. 创建新项目
    - 包名: com.example.paddlemobile1
    - 最低SDK版本: API 16+

  2. 添加权限

<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" />
  1. 创建目录结构
    - 在main目录下创建assets/paddle_models文件夹,存放模型文件
    - 添加jniLibs/armeabi-v7a文件夹,放入编译好的libpaddle-mobile.so

核心Java代码实现

1. ImageRecognition.java (JNI接口)

package com.example.paddlemobile1;

public class ImageRecognition {
    // 设置线程数
    public static native void setThread(int threadCount);

    // 加载模型
    public static native boolean load(String modelDir);

    // 预测图像
    public static native float[] predictImage(float[] buf, int[] dims);

    // 加载优化模型
    public static native boolean loadQualified(String modelDir);

    // 加载组合模型
    public static native boolean loadCombined(String modelPath, String paramPath);

    static {
        System.loadLibrary("paddle-mobile");
    }
}

2. PhotoUtil.java (图像处理工具)

package com.example.paddlemobile1;

import android.app.Activity;
import android.content.Context;
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.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class PhotoUtil {
    // 启动相机
    public static Uri startCamera(Activity activity, int requestCode) {
        // 实现相机启动逻辑
    }

    // 打开相册
    public static void usePhoto(Activity activity, int requestCode) {
        // 实现相册打开逻辑
    }

    // 获取图片路径
    public static String getPathFromUri(Context context, Uri uri) {
        // 实现Uri转路径逻辑
    }

    // 图片压缩与预处理
    public static float[] getScaledMatrix(Bitmap bitmap, int desWidth, int desHeight) {
        // 实现图片缩放和数据转换
        float[] data = new float[3 * desWidth * desHeight];
        // ... 数据处理逻辑 ...
        return data;
    }
}

3. MainActivity.java (主界面逻辑)

package com.example.paddlemobile1;

import android.Manifest;
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.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.request.RequestOptions;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {
    private static final int USE_PHOTO = 1001;
    private static final int START_CAMERA = 1002;
    private ImageView showImage;
    private TextView resultText;
    private String assetsPath = "paddle_models";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 初始化视图
        Button usePhotoBtn = findViewById(R.id.use_photo);
        Button startCameraBtn = findViewById(R.id.start_camera);
        showImage = findViewById(R.id.show_image);
        resultText = findViewById(R.id.result_text);

        // 请求权限
        requestPermissions();

        // 复制模型文件到SD卡
        copyModelFromAssets();

        // 加载模型
        loadModel();

        // 按钮点击事件
        usePhotoBtn.setOnClickListener(v -> PhotoUtil.usePhoto(this, USE_PHOTO));
        startCameraBtn.setOnClickListener(v -> {
            Uri uri = PhotoUtil.startCamera(this, START_CAMERA);
            Glide.with(this).load(uri).into(showImage);
        });
    }

    // 从assets复制模型文件到SD卡
    private void copyModelFromAssets() {
        // 实现模型文件复制逻辑
    }

    // 加载PaddleMobile模型
    private void loadModel() {
        String modelPath = Environment.getExternalStorageDirectory() + 
                          "/paddle_models/googlenet";
        boolean loadSuccess = ImageRecognition.load(modelPath);
        if (loadSuccess) {
            Log.d("PaddleMobile", "模型加载成功");
        } else {
            Log.e("PaddleMobile", "模型加载失败");
        }
    }

    // 处理图片选择结果
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            if (requestCode == USE_PHOTO) {
                Uri uri = data.getData();
                String imagePath = PhotoUtil.getPathFromUri(this, uri);
                predictImage(imagePath);
            } else if (requestCode == START_CAMERA) {
                Uri uri = PhotoUtil.startCamera(this, START_CAMERA);
                String imagePath = PhotoUtil.getPathFromUri(this, uri);
                predictImage(imagePath);
            }
        }
    }

    // 预测图像
    private void predictImage(String imagePath) {
        Bitmap bitmap = PhotoUtil.getScaledBitmap(imagePath);
        float[] inputData = PhotoUtil.getScaledMatrix(bitmap, 224, 224);
        int[] dims = {1, 3, 224, 224};

        long start = System.currentTimeMillis();
        float[] result = ImageRecognition.predictImage(inputData, dims);
        long end = System.currentTimeMillis();

        // 处理预测结果
        int maxIndex = getMaxIndex(result);
        String resultText = "预测结果: " + maxIndex + "\n概率: " + result[maxIndex] + 
                           "\n耗时: " + (end - start) + "ms";
        this.resultText.setText(resultText);
    }

    // 获取概率最大的结果索引
    private int getMaxIndex(float[] result) {
        int maxIndex = 0;
        float maxValue = result[0];
        for (int i = 1; i < result.length; i++) {
            if (result[i] > maxValue) {
                maxValue = result[i];
                maxIndex = i;
            }
        }
        return maxIndex;
    }

    // 请求权限
    private void requestPermissions() {
        // 实现权限请求逻辑
    }
}

布局文件 (activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/show_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@id/btn_ll" />

    <TextView
        android:id="@+id/result_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@id/btn_ll"
        android:layout_margin="16dp"
        android:text="预测结果将显示在这里" />

    <LinearLayout
        android:id="@+id/btn_ll"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal">

        <Button
            android:id="@+id/use_photo"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="相册" />

        <Button
            android:id="@+id/start_camera"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="拍照" />
    </LinearLayout>
</RelativeLayout>

模型准备与优化

模型转换

使用PaddleMobile提供的工具将训练好的模型转换为移动端格式:

python tools/convert.py --model ./models --output ./paddle_models

模型量化 (可选)

cd paddle-mobile/tools/quantification
python quantify.py --model ./googlenet --output ./googlenet_quantized

编译与运行

  1. 添加依赖
implementation 'com.github.bumptech.glide:glide:4.3.1'
  1. 配置Gradle
    - 添加NDK支持
    - 设置CPU架构为armeabi-v7a

  2. 运行应用
    - 选择”相册”或”拍照”功能
    - 应用会自动加载模型并显示预测结果

性能优化建议

  1. 模型优化
    - 使用模型量化减小模型体积
    - 选择轻量级网络结构(如MobileNet)
    - 移除不必要的层和连接

  2. 图像处理优化
    - 采用适当的图像压缩策略
    - 预加载常用模型到内存
    - 使用多线程加速预测

  3. 代码优化
    - 异步处理预测任务
    - 实现模型缓存机制
    - 合理释放资源

参考资料

  1. PaddleMobile官方文档
  2. Android NDK开发指南
  3. PaddlePaddle模型部署教程

注意事项

  • 确保模型文件路径正确
  • 注意权限请求和动态权限管理
  • 处理不同设备上的硬件差异
  • 优化预测速度和内存使用

通过以上步骤,你可以构建一个高效的Android图像识别应用,实现本地深度学习模型的部署与推理。

Xiaoye