Android で Hello World!

はじめに

この Hello World!このチュートリアルでは、MediaPipe Framework を使用して、 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 は、1 つのパケット(画像 フレームなど)を指定し、OpenGL シェーダーを使用して輝度の変化を適用します。結果 画像フレームが luma_video 出力ストリームに送信されます。

2 番目のノード SobelEdgesCalculator は、受信インスタンスにエッジ検出を適用します。 パケットが luma_video ストリームで出力され、結果が output_video 出力に出力されます。 。

出力された画像フレームが Android アプリに表示され、 output_video ストリーム。

最小限のアプリケーションの初期設定

最初に、「Hello World!」と表示するシンプルな Android アプリケーションから始めます。 表示されます。Android のビルドに慣れている場合は、この手順をスキップしてもかまいません。 bazel を使用するアプリケーション。

Android アプリを作成する新しいディレクトリを作成します。対象 このチュートリアルのコードの全文は mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic。Google は 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>

コンテンツを読み込むシンプルな MainActivity.java$APPLICATION_PATH に追加します。 これを 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>

$APPLICATION_PATH/res/values/styles.xml を追加します。

<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 ルールは、生成された basic_lib Android ライブラリを使用して、 Android デバイスにインストールするバイナリ APK をビルドします。

アプリをビルドするには、次のコマンドを使用します。

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" />

最小 SDK バージョンを 21 に、ターゲット SDK バージョンを 27 に変更します。 作成します。

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

これにより、カメラへのアクセス許可を求めるメッセージがユーザーに表示され、 カメラへのアクセスに CameraX ライブラリを使用します。

カメラへのアクセス許可をリクエストするには、MediaPipe Framework が提供するユーティリティを使用します。 つまり PermissionHelper です。これを使用するには、依存関係を "//mediapipe/java/com/google/mediapipe/components:android_components"( 「BUILD」の「mediapipe_lib」ルール。

MainActivityPermissionHelper を使用するには、次の行を 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 と、 その中にネストされ、no_camera_access_preview という名前の TextView。カメラ 付与されていない場合、アプリケーションには 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

次に、SurfaceTexture オブジェクトと SurfaceView オブジェクトを MainActivity:

private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;

onCreate(Bundle) 関数の、前に次の 2 行を追加します。 カメラへのアクセス権限をリクエストしています。

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 オブジェクトを作成し、これを使用して previewFrameTexture という名前の SurfaceTexture オブジェクトを使用して、カメラ フレームの位置を指定します。

カメラフレームの取得に previewFrameTexture を使用するには、CameraX を使用します。 フレームワークには、CameraX を使用するための CameraXPreviewHelper という名前のユーティリティが用意されています。 このクラスは、カメラを起動するとリスナーを更新します。 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 オブジェクトが作成されて、匿名 ID が追加されます。 設定されます。cameraHelper がカメラの起動を通知したとき フレームを取得するための surfaceTexture が使用でき、 surfaceTexturepreviewFrameTexture として、previewDisplayView にします。 previewFrameTexture からフレームを確認できるようにしています。

ただし、カメラを起動する前に、使用するカメラを決定する必要があります あります。CameraXPreviewHelper は、次の 2 つを提供する 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);

この時点で、アプリケーションが正常にビルドされるはずです。ただし、 アプリがアップデートされていなければ、カメラがカメラを離れても、 付与されています)。これは、予測されたデータセットを CameraXPreviewHelpersurfaceTexture 変数、 previewSurfaceView は、まだその出力を使用して画面上に表示しません。

MediaPipe グラフではフレームを使用するので、ここでは このチュートリアルでは直接カメラ出力を確認できます。その代わりに 事前構築済み API の 処理するカメラフレームを MediaPipe グラフに送信し、 画面上でのグラフの出力です

ExternalTextureConverterのセットアップ

SurfaceTexture は、OpenGL ES としてストリームから画像フレームをキャプチャします。 テクスチャです。MediaPipe グラフを使用するには、カメラからキャプチャしたフレームを 標準 Open GL テクスチャ オブジェクトに格納されます。このフレームワークには、 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);

確認のため、MainActivityonResume() 関数を定義したことを思い出してください。 カメラへのアクセスを許可し、startCamera() を呼び出してください。これまで 確認します。onResume() に次の行を追加して converter を初期化する オブジェクト:

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

この converter は、eglManager が管理する GLContext を使用するようになりました。

また、MainActivityonPause() 関数をオーバーライドして、 アプリが一時停止状態になった場合は、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.CallbackpreviewDisplayView を実行し、適切な表示サイズを計算する 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 は入力と出力です。 グラフで指定された動画ストリーム名を 1 つ作成します。

次に、MainActivity が MediaPipe フレームワークを読み込む必要があります。また、 フレームワークは OpenCV を使用するため、MainActvityOpenCV を読み込む必要があります。こちらの 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 の作成時には、SurfaceprocessorVideoSurfaceOutput。破壊されると processorVideoSurfaceOutput

これで完了です。Cloud Shell をビルドし、正常に実行できるはずです。 Sobel Edge Detection アプリがライブカメラで実行されていることを確認する フィード!おめでとうございます!

edge_detection_android_gpu_gif

問題が発生した場合は、チュートリアルの完全なコードをご覧ください。 こちらをご覧ください。