制作使用预构建 PyTorch 库的原生 Android 应用程序¶
作者: Ivan Kobzarev
在本食谱中,您将学习
如何制作一个使用原生代码 (C++) 中的 LibTorch API 的 Android 应用程序。
如何在该应用程序中使用带有自定义运算符的 TorchScript 模型。
您可以在 PyTorch Android 演示应用程序存储库 中找到此应用程序的完整设置。
设置¶
您将需要一个具有以下已安装包(及其依赖项)的 Python 3 环境
PyTorch 1.6
对于 Android 开发,您需要安装
Android NDK
wget https://dl.google.com/android/repository/android-ndk-r19c-linux-x86_64.zip
unzip android-ndk-r19c-linux-x86_64.zip
export ANDROID_NDK=$(pwd)/android-ndk-r19c
Android SDK
wget https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
unzip sdk-tools-linux-3859397.zip -d android_sdk
export ANDROID_HOME=$(pwd)/android_sdk
Gradle 4.10.3
Gradle 是 Android 应用程序最常用的构建系统,我们需要它来构建我们的应用程序。下载它并添加到路径中以便在命令行中使用 gradle
。
wget https://services.gradle.org/distributions/gradle-4.10.3-bin.zip
unzip gradle-4.10.3-bin.zip
export GRADLE_HOME=$(pwd)/gradle-4.10.3
export PATH="${GRADLE_HOME}/bin/:${PATH}"
JDK
Gradle 需要 JDK,您需要安装它并设置环境变量 JAVA_HOME
指向它。例如,您可以按照 说明 安装 OpenJDK。
OpenCV SDK for Android
我们自定义的操作符将使用 OpenCV 库实现。要在 Android 上使用它,我们需要下载包含预编译库的 OpenCV SDK for Android。从 OpenCV 发布页面 下载。解压缩并设置环境变量 OPENCV_ANDROID_SDK
指向它。
使用自定义 C++ 操作符准备 TorchScript 模型¶
TorchScript 允许使用自定义 C++ 操作符,您可以阅读 专门的教程 了解更多信息。
因此,您可以编写使用自定义操作符的模型,该操作符使用 OpenCV 的 cv::warpPerspective
函数。
import torch
import torch.utils.cpp_extension
print(torch.version.__version__)
op_source = """
#include <opencv2/opencv.hpp>
#include <torch/script.h>
torch::Tensor warp_perspective(torch::Tensor image, torch::Tensor warp) {
cv::Mat image_mat(/*rows=*/image.size(0),
/*cols=*/image.size(1),
/*type=*/CV_32FC1,
/*data=*/image.data_ptr<float>());
cv::Mat warp_mat(/*rows=*/warp.size(0),
/*cols=*/warp.size(1),
/*type=*/CV_32FC1,
/*data=*/warp.data_ptr<float>());
cv::Mat output_mat;
cv::warpPerspective(image_mat, output_mat, warp_mat, /*dsize=*/{64, 64});
torch::Tensor output =
torch::from_blob(output_mat.ptr<float>(), /*sizes=*/{64, 64});
return output.clone();
}
static auto registry =
torch::RegisterOperators("my_ops::warp_perspective", &warp_perspective);
"""
torch.utils.cpp_extension.load_inline(
name="warp_perspective",
cpp_sources=op_source,
extra_ldflags=["-lopencv_core", "-lopencv_imgproc"],
is_python_module=False,
verbose=True,
)
print(torch.ops.my_ops.warp_perspective)
@torch.jit.script
def compute(x, y):
if bool(x[0][0] == 42):
z = 5
else:
z = 10
x = torch.ops.my_ops.warp_perspective(x, torch.eye(3))
return x.matmul(y) + z
compute.save("compute.pt")
此代码片段生成 compute.pt
文件,它是使用自定义操作符 my_ops.warp_perspective
的 TorchScript 模型。
您需要安装用于开发的 OpenCV 才能运行它。对于 Linux 系统,可以使用以下命令:CentOS
yum install opencv-devel
Ubuntu
apt-get install libopencv-dev
创建 Android 应用程序¶
在成功获得 compute.pt
后,我们希望在 Android 应用程序中使用此 TorchScript 模型。在 Android 上使用通用 TorchScript 模型(没有自定义操作符),使用 Java API,您可以在 这里 找到。我们不能将此方法用于我们的情况,因为我们的模型使用自定义操作符 (my_ops.warp_perspective
),默认的 TorchScript 执行将无法找到它。
操作符的注册未公开给 PyTorch Java API,因此我们需要使用原生部分(C++)构建 Android 应用程序,并使用 LibTorch C++ API 为 Android 实现和注册相同的自定义操作符。由于我们的操作符使用 OpenCV 库,我们将使用预编译的 OpenCV Android 库,并使用 OpenCV 中的相同函数。
让我们开始在 NativeApp
文件夹中创建 Android 应用程序。
mkdir NativeApp
cd NativeApp
Android 应用程序构建设置¶
Android 应用程序构建由主要的 gradle 部分和原生构建 CMake 部分组成。这里的所有代码清单都是完整的文件清单,如果要重新创建整个结构,您将能够构建和安装生成的 Android 应用程序,而无需任何代码添加。
Gradle 构建设置¶
我们需要添加 gradle 设置文件:build.gradle、gradle.properties、settings.gradle。有关 Android Gradle 构建配置的更多信息,您可以在 这里 找到。
NativeApp/settings.gradle
include ':app'
NativeApp/gradle.properties
android.useAndroidX=true
android.enableJetifier=true
NativeApp/build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
在 NativeApp/build.gradle
中,我们指定 Android gradle 插件版本为 3.5.0。此版本不是最新的。但是,我们使用它,因为 PyTorch android gradle 构建使用此版本。
NativeApp/settings.gradle
显示我们的项目只包含一个模块 - app
,它将是我们的 Android 应用程序。
mkdir app
cd app
NativeApp/app/build.gradle
apply plugin: 'com.android.application'
repositories {
jcenter()
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
}
}
android {
configurations {
extractForNativeBuild
}
compileSdkVersion 28
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "org.pytorch.nativeapp"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
}
}
}
buildTypes {
release {
minifyEnabled false
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'org.pytorch:pytorch_android:1.6.0-SNAPSHOT'
extractForNativeBuild 'org.pytorch:pytorch_android:1.6.0-SNAPSHOT'
}
task extractAARForNativeBuild {
doLast {
configurations.extractForNativeBuild.files.each {
def file = it.absoluteFile
copy {
from zipTree(file)
into "$buildDir/$file.name"
include "headers/**"
include "jni/**"
}
}
}
}
tasks.whenTaskAdded { task ->
if (task.name.contains('externalNativeBuild')) {
task.dependsOn(extractAARForNativeBuild)
}
}
此 gradle 构建脚本注册了对 pytorch_android 快照的依赖关系,这些快照发布在夜间通道上。
由于它们发布到 nexus sonatype 存储库,因此我们需要注册该存储库:https://oss.sonatype.org/content/repositories/snapshots
。
在我们的应用程序中,我们需要在应用程序的原生构建部分使用 LibTorch C++ API。为此,我们需要访问预编译的二进制文件和头文件。它们预先打包在 PyTorch Android 构建中,该构建发布在 Maven 存储库中。
要从 gradle 依赖项(即 aar 文件)中使用 PyTorch Android 预编译库,我们应该为配置 extractForNativeBuild
添加注册,在依赖项中添加此配置,并将它的定义放在最后。
extractForNativeBuild
任务将调用 extractAARForNativeBuild
任务,该任务将 pytorch_android aar 解压缩到 gradle 构建目录。
Pytorch_android aar 包含 LibTorch 头文件在 headers
文件夹中,以及针对不同 Android ABI 的预编译库在 jni
文件夹中:$ANDROID_ABI/libpytorch_jni.so
、$ANDROID_ABI/libfbjni.so
。我们将在原生构建中使用它们。
原生构建在此 build.gradle
中用以下行注册
android {
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
...
defaultConfig {
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
}
}
}
我们将使用 CMake
配置进行原生构建。这里我们还指定我们将动态链接 STL,因为我们有几个库。有关此内容的更多信息,您可以在 这里 找到。
原生构建 CMake 设置¶
原生构建将在 NativeApp/app/CMakeLists.txt
中配置
cmake_minimum_required(VERSION 3.4.1)
set(TARGET pytorch_nativeapp)
project(${TARGET} CXX)
set(CMAKE_CXX_STANDARD 14)
set(build_DIR ${CMAKE_SOURCE_DIR}/build)
set(pytorch_testapp_cpp_DIR ${CMAKE_CURRENT_LIST_DIR}/src/main/cpp)
file(GLOB pytorch_testapp_SOURCES
${pytorch_testapp_cpp_DIR}/pytorch_nativeapp.cpp
)
add_library(${TARGET} SHARED
${pytorch_testapp_SOURCES}
)
file(GLOB PYTORCH_INCLUDE_DIRS "${build_DIR}/pytorch_android*.aar/headers")
file(GLOB PYTORCH_LINK_DIRS "${build_DIR}/pytorch_android*.aar/jni/${ANDROID_ABI}")
target_compile_options(${TARGET} PRIVATE
-fexceptions
)
set(BUILD_SUBDIR ${ANDROID_ABI})
find_library(PYTORCH_LIBRARY pytorch_jni
PATHS ${PYTORCH_LINK_DIRS}
NO_CMAKE_FIND_ROOT_PATH)
find_library(FBJNI_LIBRARY fbjni
PATHS ${PYTORCH_LINK_DIRS}
NO_CMAKE_FIND_ROOT_PATH)
# OpenCV
if(NOT DEFINED ENV{OPENCV_ANDROID_SDK})
message(FATAL_ERROR "Environment var OPENCV_ANDROID_SDK is not set")
endif()
set(OPENCV_INCLUDE_DIR "$ENV{OPENCV_ANDROID_SDK}/sdk/native/jni/include")
target_include_directories(${TARGET} PRIVATE
"${OPENCV_INCLUDE_DIR}"
${PYTORCH_INCLUDE_DIRS})
set(OPENCV_LIB_DIR "$ENV{OPENCV_ANDROID_SDK}/sdk/native/libs/${ANDROID_ABI}")
find_library(OPENCV_LIBRARY opencv_java4
PATHS ${OPENCV_LIB_DIR}
NO_CMAKE_FIND_ROOT_PATH)
target_link_libraries(${TARGET}
${PYTORCH_LIBRARY}
${FBJNI_LIBRARY}
${OPENCV_LIBRARY}
log)
这里我们只注册一个源文件 pytorch_nativeapp.cpp
。
在之前的步骤中,在 NativeApp/app/build.gradle
中,任务 extractAARForNativeBuild
将头文件和原生库提取到构建目录中。我们将 PYTORCH_INCLUDE_DIRS
和 PYTORCH_LINK_DIRS
设置为它们。
之后,我们找到库 libpytorch_jni.so
和 libfbjni.so
,并将它们添加到目标的链接中。
由于我们计划使用 OpenCV 函数来实现我们的自定义操作符 my_ops::warp_perspective
- 我们需要链接到 libopencv_java4.so
。它打包在 OpenCV SDK for Android 中,该 SDK 在设置步骤中下载。在此配置中,我们通过环境变量 OPENCV_ANDROID_SDK
找到它。
我们还链接 log
库以便能够将我们的结果记录到 Android LogCat。
由于我们链接到 OpenCV Android SDK 的 libopencv_java4.so
,我们应该将它复制到 NativeApp/app/src/main/jniLibs/${ANDROID_ABI}
cp -R $OPENCV_ANDROID_SDK/sdk/native/libs/* NativeApp/app/src/main/jniLibs/
将模型文件添加到应用程序¶
要将 TorschScript 模型 compute.pt
包含在我们的应用程序中,我们应该将它复制到 assets 文件夹
mkdir -p NativeApp/app/src/main/assets
cp compute.pt NativeApp/app/src/main/assets
Android 应用程序清单¶
每个 Android 应用程序都有一个清单。这里我们指定应用程序名称、包名、主活动。
NativeApp/app/src/main/AndroidManifest.xml
:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.pytorch.nativeapp">
<application
android:allowBackup="true"
android:label="PyTorchNativeApp"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
源代码¶
Java 代码¶
现在我们准备在
NativeApp/app/src/main/java/org/pytorch/nativeapp/MainActivity.java
:
package org.pytorch.nativeapp;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "PyTorchNativeApp";
public static String assetFilePath(Context context, String assetName) {
File file = new File(context.getFilesDir(), assetName);
if (file.exists() && file.length() > 0) {
return file.getAbsolutePath();
}
try (InputStream is = context.getAssets().open(assetName)) {
try (OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[4 * 1024];
int read;
while ((read = is.read(buffer)) != -1) {
os.write(buffer, 0, read);
}
os.flush();
}
return file.getAbsolutePath();
} catch (IOException e) {
Log.e(TAG, "Error process asset " + assetName + " to file path");
}
return null;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final String modelFileAbsoluteFilePath =
new File(assetFilePath(this, "compute.pt")).getAbsolutePath();
NativeClient.loadAndForwardModel(modelFileAbsoluteFilePath);
}
}
在之前的步骤中,当我们将 compute.pt
复制到 NativeApp/app/src/main/assets
时,该文件成为 Android 应用程序资产,它将被打包到应用程序中。Android 系统仅提供对其的流访问。要从 LibTorch 使用此模块,我们需要将它物化到磁盘上的一个文件中。 assetFilePath
函数从资产输入流中复制数据,将其写入磁盘,并返回它的绝对文件路径。
OnCreate
方法在活动创建后立即被调用。在此方法中,我们调用 assertFilePath
并调用 NativeClient
类,该类将通过 JNI 调用将其分派到原生代码。
NativeClient
是一个辅助类,它有一个内部私有类 NativePeer
,负责处理应用程序的原生部分。它有一个静态块,它将加载 libpytorch_nativeapp.so
,该库是用我们之前添加的 CMakeLists.txt
构建的。静态块将在 NativePeer
类的第一次引用时执行。这发生在 NativeClient#loadAndForwardModel
中。
NativeApp/app/src/main/java/org/pytorch/nativeapp/NativeClient.java
:
package org.pytorch.nativeapp;
public final class NativeClient {
public static void loadAndForwardModel(final String modelPath) {
NativePeer.loadAndForwardModel(modelPath);
}
private static class NativePeer {
static {
System.loadLibrary("pytorch_nativeapp");
}
private static native void loadAndForwardModel(final String modelPath);
}
}
NativePeer#loadAndForwardModel
声明为 native
,它在 Java 中没有定义。对此方法的调用将通过 JNI 重新分派到我们 libpytorch_nativeapp.so
中的 C++ 方法,在 NativeApp/app/src/main/cpp/pytorch_nativeapp.cpp
中。
原生代码¶
现在我们准备编写应用程序的原生部分。
NativeApp/app/src/main/cpp/pytorch_nativeapp.cpp
:
#include <android/log.h>
#include <cassert>
#include <cmath>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#define ALOGI(...) \
__android_log_print(ANDROID_LOG_INFO, "PyTorchNativeApp", __VA_ARGS__)
#define ALOGE(...) \
__android_log_print(ANDROID_LOG_ERROR, "PyTorchNativeApp", __VA_ARGS__)
#include "jni.h"
#include <opencv2/opencv.hpp>
#include <torch/script.h>
namespace pytorch_nativeapp {
namespace {
torch::Tensor warp_perspective(torch::Tensor image, torch::Tensor warp) {
cv::Mat image_mat(/*rows=*/image.size(0),
/*cols=*/image.size(1),
/*type=*/CV_32FC1,
/*data=*/image.data_ptr<float>());
cv::Mat warp_mat(/*rows=*/warp.size(0),
/*cols=*/warp.size(1),
/*type=*/CV_32FC1,
/*data=*/warp.data_ptr<float>());
cv::Mat output_mat;
cv::warpPerspective(image_mat, output_mat, warp_mat, /*dsize=*/{8, 8});
torch::Tensor output =
torch::from_blob(output_mat.ptr<float>(), /*sizes=*/{8, 8});
return output.clone();
}
static auto registry =
torch::RegisterOperators("my_ops::warp_perspective", &warp_perspective);
template <typename T> void log(const char *m, T t) {
std::ostringstream os;
os << t << std::endl;
ALOGI("%s %s", m, os.str().c_str());
}
struct JITCallGuard {
torch::autograd::AutoGradMode no_autograd_guard{false};
torch::AutoNonVariableTypeMode non_var_guard{true};
torch::jit::GraphOptimizerEnabledGuard no_optimizer_guard{false};
};
} // namespace
static void loadAndForwardModel(JNIEnv *env, jclass, jstring jModelPath) {
const char *modelPath = env->GetStringUTFChars(jModelPath, 0);
assert(modelPath);
JITCallGuard guard;
torch::jit::Module module = torch::jit::load(modelPath);
module.eval();
torch::Tensor x = torch::randn({4, 8});
torch::Tensor y = torch::randn({8, 5});
log("x:", x);
log("y:", y);
c10::IValue t_out = module.forward({x, y});
log("result:", t_out);
env->ReleaseStringUTFChars(jModelPath, modelPath);
}
} // namespace pytorch_nativeapp
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass c = env->FindClass("org/pytorch/nativeapp/NativeClient$NativePeer");
if (c == nullptr) {
return JNI_ERR;
}
static const JNINativeMethod methods[] = {
{"loadAndForwardModel", "(Ljava/lang/String;)V",
(void *)pytorch_nativeapp::loadAndForwardModel},
};
int rc = env->RegisterNatives(c, methods,
sizeof(methods) / sizeof(JNINativeMethod));
if (rc != JNI_OK) {
return rc;
}
return JNI_VERSION_1_6;
}
此代码清单相当长,并且一些东西在这里混合在一起,我们将遵循控制流来了解这段代码是如何工作的。控制流到达的第一个位置是 JNI_OnLoad
。此函数在加载库后被调用。它负责注册原生方法,该方法在调用 NativePeer#loadAndForwardModel
时被调用,这里它是 pytorch_nativeapp::loadAndForwardModel
函数。
pytorch_nativeapp::loadAndForwardModel
将模型路径作为参数。首先,我们提取它的 const char*
值,并使用 torch::jit::load
加载模块。
要加载移动设备的 TorchScript 模型,我们需要设置这些保护措施,因为移动设备构建不支持自动梯度等功能,以便实现更小的构建大小,放置在该示例中的 struct JITCallGuard
中。它可能在将来发生变化。您可以通过关注 PyTorch GitHub 中的源代码 来跟踪最新变化。
方法 warp_perspective
的实现和注册完全与 桌面构建的教程 相同。
构建应用程序¶
要指定 Gradle 找到 Android SDK 和 Android NDK 的位置,我们需要填写 NativeApp/local.properties
文件。
cd NativeApp
echo "sdk.dir=$ANDROID_HOME" >> NativeApp/local.properties
echo "ndk.dir=$ANDROID_NDK" >> NativeApp/local.properties
要构建生成 apk
文件,我们运行
cd NativeApp
gradle app:assembleDebug
要将应用程序安装到已连接的设备上
cd NativeApp
gradle app::installDebug
之后,您可以通过点击 PyTorchNativeApp 图标在设备上运行应用程序。或者,您也可以从命令行进行操作
adb shell am start -n org.pytorch.nativeapp/.MainActivity
如果检查 Android 日志 cat
adb logcat -v brief | grep PyTorchNativeApp
您应该会看到带有标签 'PyTorchNativeApp' 的日志,它会打印 x、y 和模型前向传播的结果,这些结果使用 log
函数在 NativeApp/app/src/main/cpp/pytorch_nativeapp.cpp
中打印。
I/PyTorchNativeApp(26968): x: -0.9484 -1.1757 -0.5832 0.9144 0.8867 1.0933 -0.4004 -0.3389
I/PyTorchNativeApp(26968): -1.0343 1.5200 -0.7625 -1.5724 -1.2073 0.4613 0.2730 -0.6789
I/PyTorchNativeApp(26968): -0.2247 -1.2790 1.0067 -0.9266 0.6034 -0.1941 0.7021 -1.5368
I/PyTorchNativeApp(26968): -0.3803 -0.0188 0.2021 -0.7412 -0.2257 0.5044 0.6592 0.0826
I/PyTorchNativeApp(26968): [ CPUFloatType{4,8} ]
I/PyTorchNativeApp(26968): y: -1.0084 1.8733 0.5435 0.1087 -1.1066
I/PyTorchNativeApp(26968): -1.9926 1.1047 0.5311 -0.4944 1.9178
I/PyTorchNativeApp(26968): -1.5451 0.8867 1.0473 -1.7571 0.3909
I/PyTorchNativeApp(26968): 0.4039 0.5085 -0.2776 0.4080 0.9203
I/PyTorchNativeApp(26968): 0.3655 1.4395 -1.4467 -0.9837 0.3335
I/PyTorchNativeApp(26968): -0.0445 0.8039 -0.2512 -1.3122 0.6543
I/PyTorchNativeApp(26968): -1.5819 0.0525 1.5680 -0.6442 -1.3090
I/PyTorchNativeApp(26968): -1.6197 -0.0773 -0.5967 -0.1105 -0.3122
I/PyTorchNativeApp(26968): [ CPUFloatType{8,5} ]
I/PyTorchNativeApp(26968): result: 16.0274 9.0330 6.0124 9.8644 11.0493
I/PyTorchNativeApp(26968): 8.7633 6.9657 12.3469 10.3159 12.0683
I/PyTorchNativeApp(26968): 12.4529 9.4559 11.7038 7.8396 6.9716
I/PyTorchNativeApp(26968): 8.5279 9.1780 11.3849 8.4368 9.1480
I/PyTorchNativeApp(26968): 10.0000 10.0000 10.0000 10.0000 10.0000
I/PyTorchNativeApp(26968): 10.0000 10.0000 10.0000 10.0000 10.0000
I/PyTorchNativeApp(26968): 10.0000 10.0000 10.0000 10.0000 10.0000
I/PyTorchNativeApp(26968): 10.0000 10.0000 10.0000 10.0000 10.0000
I/PyTorchNativeApp(26968): [ CPUFloatType{8,5} ]
您可以在 PyTorch Android 演示应用程序存储库 中找到此应用程序的完整设置。