はじめに
この Hello World! チュートリアルでは、MediaPipe フレームワークを使用して、Android で MediaPipe グラフを実行する Android アプリを開発します。
作業内容
Android デバイスでライブ動画ストリームに適用されたリアルタイムの Sobel エッジ検出用のシンプルなカメラアプリ。
セットアップ
- MediaPipe Framework をシステムにインストールします。詳しくは、フレームワークのインストール ガイドをご覧ください。
- Android Development SDK と Android NDK をインストールします。その方法については、[フレームワークのインストール ガイド]もご覧ください。
- Android デバイスで開発者向けオプションを有効にします。
- システムに 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"
}
グラフを図にまとめると、以下のようになります。
このグラフには、デバイスのカメラから提供されるすべての受信フレームについて、input_video
という名前の単一の入力ストリームがあります。
グラフの最初のノードである LuminanceCalculator
は、単一のパケット(画像フレーム)を受け取り、OpenGL シェーダーを使用して輝度の変更を適用します。生成された画像フレームは luma_video
出力ストリームに送信されます。
2 番目のノード SobelEdgesCalculator
は、luma_video
ストリームの受信パケットにエッジ検出を適用し、output_video
出力ストリームに結果を出力します。
Android アプリは、output_video
ストリームの出力画像フレームを表示します。
最小限のアプリケーションの初期セットアップ
まず、画面に「Hello World!」と表示するシンプルな Android アプリから始めます。bazel
を使用した Android アプリのビルドに精通している場合は、この手順をスキップできます。
Android アプリを作成する新しいディレクトリを作成します。たとえば、このチュートリアルの完全なコードは mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic
にあります。Codelab では、このパスを $APPLICATION_PATH
と呼びます。
アプリケーションのパスでは、次の点に注意してください。
- アプリケーションの名前は
helloworld
です。 - アプリの
$PACKAGE_PATH
はcom.google.mediapipe.apps.basic
です。このチュートリアルのコード スニペットで使用されるため、コード スニペットをコピーして使用する場合は、必ず独自の$PACKAGE_PATH
を使用してください。
activity_main.xml
ファイルを $APPLICATION_PATH/res/layout
に追加します。これにより、アプリの全画面に Hello
World!
という文字列とともに TextView
が表示されます。
<?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>
次のように、activity_main.xml
レイアウトのコンテンツを読み込むシンプルな MainActivity.java
を $APPLICATION_PATH
に追加します。
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
生成された APK ファイルを adb install
を使用してインストールします。次に例を示します。
adb install bazel-bin/$APPLICATION_PATH/helloworld.apk
デバイスでアプリを開きます。Hello World!
というテキストが画面に表示されます。
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 フレームワーク コンポーネントが提供するユーティリティ(PermissionHelper
)を使用します。これを使用するには、BUILD
の mediapipe_lib
ルールに依存関係 "//mediapipe/java/com/google/mediapipe/components:android_components"
を追加します。
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
オブジェクトに格納されます。これらを使用するには、まずアプリのレイアウトを変更する必要があります。
$APPLICATION_PATH/res/layout/activity_main.xml
から TextView
コードブロック全体を削除し、代わりに次のコードを追加します。
<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>
ユーザーがカメラの権限を付与しないと、画面は次のようになります。
次に、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
オブジェクトが作成され、そのオブジェクトに匿名リスナーが追加されます。cameraHelper
がカメラを開始したことを通知し、フレームを取得する surfaceTexture
が利用可能である場合、その surfaceTexture
を previewFrameTexture
として保存し、previewDisplayView
を可視化して、previewFrameTexture
からのフレームの表示を開始できるようにします。
ただし、カメラを起動する前に、使用するカメラを決定する必要があります。CameraXPreviewHelper
は CameraHelper
を継承し、FRONT
と BACK
の 2 つのオプションを提供します。別のカメラを使用して別のバージョンのアプリをビルドするためにコードを変更する必要がないように、BUILD
ファイルの決定をメタデータとして渡すことができます。
BACK
カメラを使用して、カメラから見たライブシーンでエッジ検出を実行する場合、メタデータを AndroidManifest.xml
に追加します。
...
<meta-data android:name="cameraFacingFront" android:value="${cameraFacingFront}"/>
</application>
</manifest>
manifest_values
の新しいエントリを使用して、helloworld
Android バイナリルールの BUILD
で選択範囲を指定します。
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);
この時点で、アプリケーションは正常にビルドされるはずです。ただし、デバイスでアプリを実行すると、(カメラの権限が付与されていても)黒い画面が表示されます。これは、CameraXPreviewHelper
によって提供された surfaceTexture
変数を保存しても、previewSurfaceView
はまだその出力を使用して画面に表示されないためです。
MediaPipe グラフでフレームを使用するため、このチュートリアルではカメラ出力を直接表示するコードは追加しません。代わりに、処理対象のカメラフレームを MediaPipe グラフに送信し、グラフの出力を画面に表示する方法に進みます。
ExternalTextureConverter
のセットアップ
SurfaceTexture
は、ストリームから画像フレームを OpenGL ES テクスチャとしてキャプチャします。MediaPipe グラフを使用するには、カメラからキャプチャしたフレームを通常の Open GL テクスチャ オブジェクトに保存する必要があります。フレームワークには、SurfaceTexture
オブジェクトに格納されている画像を通常の OpenGL テクスチャ オブジェクトに変換する ExternalTextureConverter
クラスが用意されています。
ExternalTextureConverter
を使用するには EGLContext
も必要です。これは、EglManager
オブジェクトによって作成および管理されます。EglManager
、"//mediapipe/java/com/google/mediapipe/glutil"
を使用するには、BUILD
ファイルに依存関係を追加します。
MainActivity
に次の宣言を追加します。
private EglManager eglManager;
private ExternalTextureConverter converter;
onCreate(Bundle)
関数に、カメラの権限をリクエストする前に eglManager
オブジェクトを初期化するステートメントを追加します。
eglManager = new EglManager(null);
カメラの権限が付与されていることを確認し、startCamera()
を呼び出すために、MainActivity
で onResume()
関数を定義したことを思い出してください。このチェックの前に、onResume()
に次の行を追加して、converter
オブジェクトを初期化します。
converter = new ExternalTextureConverter(eglManager.getContext());
この converter
は、eglManager
が管理する GLContext
を使用するようになりました。
また、アプリが一時停止状態になった場合に converter
を適切に閉じるように、MainActivity
の onPause()
関数をオーバーライドする必要もあります。
@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 グラフを使用するには、Android の 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,
)
BUILD
ファイルの mediapipe_lib
ビルドルールに依存関係 ":mediapipe_jni_lib"
を追加します。
次に、アプリケーションで使用する 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 モデルなどの他のアセットを追加することもできます。
さらに、後で MainActivity
で取得できるように、グラフ固有のプロパティに manifest_values
を追加します。
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
フィールドによって決定されるバイナリグラフのファイル名を示します。inputVideoStreamName
と outputVideoStreamName
は、それぞれグラフで指定された入力動画ストリーム名と出力動画ストリーム名です。
次に、MainActivity
が MediaPipe フレームワークを読み込む必要があります。また、フレームワークは OpenCV を使用するため、MainActvity
も OpenCV
を読み込む必要があります。両方の依存関係を読み込むには、MainActivity
で次のコードを使用します(クラス内のみ、関数内ではありません)。
static {
// Load all native libraries needed by the app.
System.loadLibrary("mediapipe_jni");
System.loadLibrary("opencv_java3");
}
MainActivity
でグラフを使用する
まず、グラフの .pbtxt
ファイルからコンパイルされた .binarypb
を含むアセットを読み込む必要があります。そのためには、MediaPipe ユーティリティの AndroidAssetUtil
を使用します。
eglManager
を初期化する前に、onCreate(Bundle)
でアセット マネージャーを初期化します。
// Initialize asset manager so that MediaPipe native libraries can access the app assets, e.g.,
// binary graphs.
AndroidAssetUtil.initializeNativeAssetManager(this);
ここで、converter
によって準備されたカメラフレームを MediaPipe グラフに送信してグラフを実行し、出力を準備して previewDisplayView
を更新して出力を表示する FrameProcessor
オブジェクトを設定する必要があります。次のコードを追加して 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
から変換されたフレームを使用する必要があります。converter
を初期化した後、次の行を onResume()
に追加します。
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
が作成されると、processor
の VideoSurfaceOutput
に Surface
が設定されました。破棄されると、processor
の VideoSurfaceOutput
から削除されます。
以上で終了です。これで、デバイス上でアプリを正常にビルドして実行できるようになり、ライブカメラフィードで Sobel エッジ検出が実行されていることを確認できます。おめでとうございます!
問題が発生した場合は、こちらのチュートリアルの完全なコードをご覧ください。