Hello World! (Android)

简介

你好!世界教程使用 MediaPipe 框架来开发 Android 应用, 在 Android 上运行 MediaPipe 图。

构建内容

一款简单的相机应用,可对实时视频进行实时 Sobel 边缘检测 如何在 Android 设备上流式传输

edge_detection_android_gpu_gif

设置

  1. 在系统上安装 MediaPipe Framework,请参阅框架安装 指南了解详情。
  2. 安装 Android Development SDK 和 Android NDK。您也可以在 [框架安装指南]。
  3. 在 Android 设备上启用开发者选项
  4. 在系统上设置 Bazel,以构建和部署 Android 应用。

边缘检测图

我们将使用以下图表:edge_detection_mobile_gpu.pbtxt

# MediaPipe graph that performs GPU Sobel edge detection on a live video stream.
# Used in the examples in
# mediapipe/examples/android/src/java/com/mediapipe/apps/basic and
# mediapipe/examples/ios/edgedetectiongpu.

# Images coming into and out of the graph.
input_stream: "input_video"
output_stream: "output_video"

# Converts RGB images into luminance images, still stored in RGB format.
node: {
  calculator: "LuminanceCalculator"
  input_stream: "input_video"
  output_stream: "luma_video"
}

# Applies the Sobel filter to luminance images stored in RGB format.
node: {
  calculator: "SobelEdgesCalculator"
  input_stream: "luma_video"
  output_stream: "output_video"
}

图表的直观图示如下所示:

edge_detection_mobile_gpu

此图针对所有传入帧包含一个名为 input_video 的输入流 设备的相机提供的所有信息

图中的第一个节点 LuminanceCalculator 接受一个数据包(图片 帧),并使用 OpenGL 着色器应用亮度变化。生成的 图像帧发送到 luma_video 输出流。

第二个节点 SobelEdgesCalculator 对传入节点应用边缘检测 luma_video 流中的数据包,输出结果为 output_video 输出 。

我们的 Android 应用会显示 output_video 数据流。

初始最小应用设置

我们先从显示“Hello World!”的简单 Android 应用开始。 。如果您熟悉如何构建 Android 应用,则可以跳过此步骤。 使用 bazel 的应用。

创建一个将在其中创建 Android 应用的新目录。对于 本教程的完整代码可以在以下位置找到: mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic。我们将 在整个 Codelab 中,此路径称为 $APPLICATION_PATH

请注意,在应用路径中:

  • 该应用的名称为 helloworld
  • 应用的 $PACKAGE_PATHcom.google.mediapipe.apps.basic。 本教程的代码段中会用到它,因此请务必使用 您自己的 $PACKAGE_PATH

将文件 activity_main.xml 添加到 $APPLICATION_PATH/res/layout。系统会显示 应用全屏的 TextView,其中包含字符串 Hello World!

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

$APPLICATION_PATH 添加一个简单的 MainActivity.java,用于加载内容 的 activity_main.xml 布局,如下所示:

package com.google.mediapipe.apps.basic;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

/** Bare-bones main activity. */
public class MainActivity extends AppCompatActivity {

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

将清单文件 AndroidManifest.xml 添加到 $APPLICATION_PATH, 在应用启动时启动 MainActivity

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.mediapipe.apps.basic">

  <uses-sdk
      android:minSdkVersion="19"
      android:targetSdkVersion="19" />

  <application
      android:allowBackup="true"
      android:label="${appName}"
      android:supportsRtl="true"
      android:theme="@style/AppTheme">
      <activity
          android:name="${mainActivity}"
          android:exported="true"
          android:screenOrientation="portrait">
          <intent-filter>
              <action android:name="android.intent.action.MAIN" />
              <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
      </activity>
  </application>

</manifest>

在我们的应用中,我们将在应用中使用 Theme.AppCompat 主题,因此我们需要 适当的主题引用。将“colors.xml”添加到 $APPLICATION_PATH/res/values/

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
</resources>

styles.xml 添加到 $APPLICATION_PATH/res/values/ 中:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

如需构建应用,请将 BUILD 文件添加到 $APPLICATION_PATH, 清单中的 ${appName}${mainActivity} 将替换为字符串 在 BUILD 中指定,如下所示。

android_library(
    name = "basic_lib",
    srcs = glob(["*.java"]),
    manifest = "AndroidManifest.xml",
    resource_files = glob(["res/**"]),
    deps = [
        "//third_party:android_constraint_layout",
        "//third_party:androidx_appcompat",
    ],
)

android_binary(
    name = "helloworld",
    manifest = "AndroidManifest.xml",
    manifest_values = {
        "applicationId": "com.google.mediapipe.apps.basic",
        "appName": "Hello World",
        "mainActivity": ".MainActivity",
    },
    multidex = "native",
    deps = [
        ":basic_lib",
    ],
)

android_library 规则为 MainActivity 资源文件添加依赖项 和 AndroidManifest.xml

android_binary 规则,使用生成的 Android 库 basic_lib 构建二进制 APK,以便在您的 Android 设备上安装。

如需构建应用,请使用以下命令:

bazel build -c opt --config=android_arm64 $APPLICATION_PATH:helloworld

使用 adb install 安装生成的 APK 文件。例如:

adb install bazel-bin/$APPLICATION_PATH/helloworld.apk

在设备上打开该应用。它应该显示一个屏幕,上面有 Hello World!

bazel_hello_world_android

通过“CameraX”使用相机

相机权限

要在我们的应用中使用相机,我们需要请求用户提供 访问相机的权限。如需请求相机权限,请将以下代码添加到 AndroidManifest.xml:

<!-- For using the camera -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />

2127 同一个文件:

<uses-sdk
    android:minSdkVersion="21"
    android:targetSdkVersion="27" />

这样可以确保系统提示用户请求相机权限, 以使用 CameraX 库访问相机。

如需请求相机权限,我们可以使用 MediaPipe 框架提供的实用程序 组件,即 PermissionHelper。如需使用它,请添加依赖项 "//mediapipe/java/com/google/mediapipe/components:android_components"BUILD 中的mediapipe_lib规则。

如需在 MainActivity 中使用 PermissionHelper,请将以下代码行添加到 onCreate 函数:

PermissionHelper.checkAndRequestCameraPermissions(this);

这将在屏幕上显示一个对话框提示用户请求以下权限: 在此应用中使用相机。

添加以下代码以处理用户响应:

@Override
public void onRequestPermissionsResult(
    int requestCode, String[] permissions, int[] grantResults) {
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  PermissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

@Override
protected void onResume() {
  super.onResume();
  if (PermissionHelper.cameraPermissionsGranted(this)) {
    startCamera();
  }
}

public void startCamera() {}

我们暂时将 startCamera() 方法留空。当用户响应时 MainActivity 将恢复,并调用 onResume()。 该代码会确认用户已授予使用相机的权限 然后启动相机

重新构建并安装应用。您现在应该会看到一条请求 应用对相机的访问权限。

摄像头使用权限

有了相机权限后,我们就可以启动并从 摄像头。

为了从相机查看帧,我们将使用 SurfaceView。每一帧 将存储在 SurfaceTexture 对象中。为了使用这些功能, 首先需要更改应用的布局

从以下位置移除整个 TextView 代码块: $APPLICATION_PATH/res/layout/activity_main.xml 并添加以下代码 :

<FrameLayout
    android:id="@+id/preview_display_layout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1">
    <TextView
        android:id="@+id/no_camera_access_view"
        android:layout_height="fill_parent"
        android:layout_width="fill_parent"
        android:gravity="center"
        android:text="@string/no_camera_access" />
</FrameLayout>

此代码块有一个名为 preview_display_layout 的新 FrameLayout, 其中嵌套的 TextView 名为 no_camera_access_preview。开启摄像头时 那么我们的应用就会显示 带有字符串消息的 TextView,存储在变量 no_camera_access 中。 在 $APPLICATION_PATH/res/values/strings.xml 文件中添加以下代码行:

<string name="no_camera_access" translatable="false">Please grant camera permissions.</string>

当用户未授予相机权限时,屏幕现在将如下所示 :

missing_camera_permission_android

现在,我们将 SurfaceTextureSurfaceView 对象添加到 MainActivity:

private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;

onCreate(Bundle) 函数中,在前面添加以下两行代码 请求相机权限:

previewDisplayView = new SurfaceView(this);
setupPreviewDisplayView();

现在,添加定义 setupPreviewDisplayView() 的代码:

private void setupPreviewDisplayView() {
  previewDisplayView.setVisibility(View.GONE);
  ViewGroup viewGroup = findViewById(R.id.preview_display_layout);
  viewGroup.addView(previewDisplayView);
}

我们定义一个新的 SurfaceView 对象并将其添加到 preview_display_layout FrameLayout 对象,以便我们可以用它来显示 使用名为 previewFrameTextureSurfaceTexture 对象构建相机取景框。

如需使用 previewFrameTexture 获取相机帧,我们将使用 CameraX。 框架提供了一个名为 CameraXPreviewHelper 的实用程序,用于使用 CameraX。 当通过 onCameraStarted(@Nullable SurfaceTexture)

如需使用此实用程序,请修改 BUILD 文件以添加对 "//mediapipe/java/com/google/mediapipe/components:android_camerax_helper"

现在导入 CameraXPreviewHelper,并将以下代码行添加到 MainActivity:

private CameraXPreviewHelper cameraHelper;

现在,我们可以将实现添加到 startCamera() 中:

public void startCamera() {
  cameraHelper = new CameraXPreviewHelper();
  cameraHelper.setOnCameraStartedListener(
    surfaceTexture -> {
      previewFrameTexture = surfaceTexture;
      // Make the display view visible to start showing the preview.
      previewDisplayView.setVisibility(View.VISIBLE);
    });
}

这将创建一个新的 CameraXPreviewHelper 对象,并将一个匿名 对象的监听器。当 cameraHelper 指示摄像头已启动时 以及用于抓取帧的 surfaceTexture 可用,我们保存 surfaceTexture 作为 previewFrameTexture,并使 previewDisplayView 以便开始查看来自 previewFrameTexture 的帧。

不过,在启动摄像头之前,我们需要确定要使用哪个摄像头 。CameraXPreviewHelper 继承自 CameraHelper,后者提供两个 FRONTBACK 选项。我们可以从 BUILD 文件传入决定 这样无需更改代码即可构建 使用摄像头拍摄应用。

假设我们要使用 BACK 相机对实时场景执行边缘检测 将元数据添加到 AndroidManifest.xml 中:

      ...
      <meta-data android:name="cameraFacingFront" android:value="${cameraFacingFront}"/>
  </application>
</manifest>

并在 helloworld Android 二进制文件规则的 BUILD 中指定选择 并在 manifest_values 中生成一个新条目:

manifest_values = {
    "applicationId": "com.google.mediapipe.apps.basic",
    "appName": "Hello World",
    "mainActivity": ".MainActivity",
    "cameraFacingFront": "False",
},

现在,在 MainActivity 中检索 manifest_values 中指定的元数据, 添加一个 ApplicationInfo 对象:

private ApplicationInfo applicationInfo;

onCreate() 函数中,添加:

try {
  applicationInfo =
      getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
  Log.e(TAG, "Cannot find application info: " + e);
}

现在,在 startCamera() 函数的末尾添加以下代码行:

CameraHelper.CameraFacing cameraFacing =
    applicationInfo.metaData.getBoolean("cameraFacingFront", false)
        ? CameraHelper.CameraFacing.FRONT
        : CameraHelper.CameraFacing.BACK;
cameraHelper.startCamera(this, cameraFacing, /*unusedSurfaceTexture=*/ null);

此时,应用应该可以成功构建。但是,当您 应用,则会看到黑屏(即使摄像头已 权限)。这是因为虽然我们保存了 surfaceTexture 变量由 CameraXPreviewHelper 提供,即 previewSurfaceView 不使用其输出,尚未将其显示在屏幕上。

由于我们想使用 MediaPipe 图中的帧,因此无需向 直接在本教程中查看相机输出。我们将直接跳到 我们可以将相机帧发送到 MediaPipe 图进行处理,并显示 输出的图表。

ExternalTextureConverter设置

SurfaceTexture 会从数据流中捕获图像帧作为 OpenGL ES 纹理。要使用 MediaPipe 图,从相机捕获的帧应 存储在常规 OpenGL 纹理对象中框架提供了一个类, ExternalTextureConverter,用于转换存储在 SurfaceTexture 中的图片 对象映射到常规的 OpenGL 纹理对象。

如需使用 ExternalTextureConverter,我们还需要一个 EGLContext, 由 EglManager 对象创建和管理。向 BUILD 添加依赖项 文件以使用 EglManager"//mediapipe/java/com/google/mediapipe/glutil"

MainActivity 中,添加以下声明:

private EglManager eglManager;
private ExternalTextureConverter converter;

onCreate(Bundle) 函数中,添加一个语句来初始化 eglManager 对象,然后再请求相机权限:

eglManager = new EglManager(null);

回想一下,我们在 MainActivity 中定义 onResume() 函数来确认 已授予相机权限并调用 startCamera()。在此之前 请在 onResume() 中添加以下代码行,以初始化 converter 对象:

converter = new ExternalTextureConverter(eglManager.getContext());

converter 现在使用由 eglManager 管理的 GLContext

我们还需要替换 MainActivity 中的 onPause() 函数,以便 如果应用进入暂停状态,我们会正确关闭 converter

@Override
protected void onPause() {
  super.onPause();
  converter.close();
}

如需将 previewFrameTexture 的输出通过管道传送到 converter,请将 以下代码块添加到 setupPreviewDisplayView()

previewDisplayView
 .getHolder()
 .addCallback(
     new SurfaceHolder.Callback() {
       @Override
       public void surfaceCreated(SurfaceHolder holder) {}

       @Override
       public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
         // (Re-)Compute the ideal size of the camera-preview display (the area that the
         // camera-preview frames get rendered onto, potentially with scaling and rotation)
         // based on the size of the SurfaceView that contains the display.
         Size viewSize = new Size(width, height);
         Size displaySize = cameraHelper.computeDisplaySizeFromViewSize(viewSize);

         // Connect the converter to the camera-preview frames as its input (via
         // previewFrameTexture), and configure the output width and height as the computed
         // display size.
         converter.setSurfaceTextureAndAttachToGLContext(
             previewFrameTexture, displaySize.getWidth(), displaySize.getHeight());
       }

       @Override
       public void surfaceDestroyed(SurfaceHolder holder) {}
     });

在此代码块中,我们将自定义 SurfaceHolder.Callback 添加到 previewDisplayView,并实现 surfaceChanged(SurfaceHolder holder, int format, int width, int height) 函数以计算适当的显示屏尺寸 将相机帧显示在设备屏幕上,并将 previewFrameTexture 对象并将计算出的 displaySize 的帧发送到 converter

现在,我们可以在 MediaPipe 图中使用相机帧了。

在 Android 中使用 MediaPipe 图

添加相关依赖项

如需使用 MediaPipe 图,我们需要向 MediaPipe 框架添加依赖项 。我们首先添加一条构建规则,以便使用 JNI 代码构建 cc_binary 然后构建 cc_library 规则以使用此二进制文件 。将以下代码块添加到 BUILD 文件:

cc_binary(
    name = "libmediapipe_jni.so",
    linkshared = 1,
    linkstatic = 1,
    deps = [
        "//mediapipe/java/com/google/mediapipe/framework/jni:mediapipe_framework_jni",
    ],
)

cc_library(
    name = "mediapipe_jni_lib",
    srcs = [":libmediapipe_jni.so"],
    alwayslink = 1,
)

将依赖项 ":mediapipe_jni_lib" 添加到 mediapipe_lib 构建规则中 BUILD 文件。

接下来,我们需要添加特定于要使用的 MediaPipe 图的依赖项 应用场景

首先,将依赖项添加到 libmediapipe_jni.so 中的所有计算器代码 构建规则:

"//mediapipe/graphs/edge_detection:mobile_calculators",

MediaPipe 图是 .pbtxt 文件,但如需在应用中使用它们,我们需要 使用 mediapipe_binary_graph 构建规则生成 .binarypb 文件。

helloworld Android 二进制文件构建规则中,添加 mediapipe_binary_graph 作为资源的特定图表目标:

assets = [
  "//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
],
assets_dir = "",

assets 构建规则中,您还可以添加其他资源,例如 TensorFlowLite 。

此外,还要为特定于manifest_values 稍后在 MainActivity 中检索:

manifest_values = {
    "applicationId": "com.google.mediapipe.apps.basic",
    "appName": "Hello World",
    "mainActivity": ".MainActivity",
    "cameraFacingFront": "False",
    "binaryGraphName": "mobile_gpu.binarypb",
    "inputVideoStreamName": "input_video",
    "outputVideoStreamName": "output_video",
},

请注意,binaryGraphName 表示二进制图的文件名, 取决于 mediapipe_binary_graph 目标中的 output_name 字段。 inputVideoStreamNameoutputVideoStreamName 是输入和输出 分别在图表中指定的视频流名称。

现在,MainActivity 需要加载 MediaPipe 框架。此外, 框架使用 OpenCV,因此 MainActvity 也应加载 OpenCV。使用 MainActivity 中的以下代码(类内,但不在任何函数内) 来加载两个依赖项:

static {
  // Load all native libraries needed by the app.
  System.loadLibrary("mediapipe_jni");
  System.loadLibrary("opencv_java3");
}

使用 MainActivity 中的图表

首先,我们需要加载包含从命令行编译的 .binarypb 的资源。 图表的 .pbtxt 文件。为此,我们可以使用 MediaPipe 实用程序, AndroidAssetUtil

在初始化之前,先在 onCreate(Bundle) 中初始化资产管理器 eglManager

// Initialize asset manager so that MediaPipe native libraries can access the app assets, e.g.,
// binary graphs.
AndroidAssetUtil.initializeNativeAssetManager(this);

现在,我们需要设置一个用于发送相机帧的 FrameProcessor 对象 由 converter 准备到 MediaPipe 图并运行该图, 然后更新 previewDisplayView 以显示输出。将 以下代码来声明 FrameProcessor

private FrameProcessor processor;

并在初始化 eglManager 后在 onCreate(Bundle) 中对其进行初始化:

processor =
    new FrameProcessor(
        this,
        eglManager.getNativeContext(),
        applicationInfo.metaData.getString("binaryGraphName"),
        applicationInfo.metaData.getString("inputVideoStreamName"),
        applicationInfo.metaData.getString("outputVideoStreamName"));

processor 需要使用 converter 中已转换的帧 处理。在初始化onResume() converter

converter.setConsumer(processor);

processor 应将其输出发送至 previewDisplayView。为此,请将 将以下函数定义映射到自定义 SurfaceHolder.Callback

@Override
public void surfaceCreated(SurfaceHolder holder) {
  processor.getVideoSurfaceOutput().setSurface(holder.getSurface());
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
  processor.getVideoSurfaceOutput().setSurface(null);
}

创建 SurfaceHolder 后,我们就拥有了 Surface processorVideoSurfaceOutput。销毁后,我们会将其从以下位置移除: processorVideoSurfaceOutput

这样就大功告成了!现在,您应该能够成功构建并运行 并查看在实时摄像头上运行的 Sobel 边缘检测功能 Feed!恭喜!

edge_detection_android_gpu_gif

如果您遇到任何问题,请参阅本教程的完整代码 此处