สวัสดีชาวโลก! บน Android

เกริ่นนำ

บทแนะนำ Hello World! นี้ใช้ MediaPipe Framework ในการพัฒนาแอปพลิเคชัน Android ที่เรียกใช้กราฟ MediaPipe บน Android

สิ่งที่คุณจะสร้าง

แอปกล้องที่ใช้งานง่ายสำหรับการตรวจจับขอบ Sobel แบบเรียลไทม์ซึ่งใช้กับสตรีมวิดีโอสดในอุปกรณ์ Android

edge_detection_android_gpu_gif

ตั้งค่า

  1. ติดตั้งเฟรมเวิร์ก MediaPipe ในระบบของคุณ โปรดดูรายละเอียดที่คู่มือการติดตั้งเฟรมเวิร์ก
  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

โหนดที่ 2 SobelEdgesCalculator จะใช้การตรวจจับ Edge กับแพ็กเก็ตขาเข้าในสตรีม luma_video และเอาต์พุตจะทำให้มีสตรีมเอาต์พุต output_video รายการ

แอปพลิเคชัน Android ของเราจะแสดงเฟรมรูปภาพเอาต์พุตของสตรีม output_video

การตั้งค่าแอปพลิเคชันขั้นต่ำเริ่มต้น

เราเริ่มต้นด้วยแอปพลิเคชัน Android ง่ายๆ ที่แสดงคำว่า "สวัสดีชาวโลก" บนหน้าจอ คุณข้ามขั้นตอนนี้ได้หากคุ้นเคยกับการสร้างแอปพลิเคชัน Android โดยใช้ bazel

สร้างไดเรกทอรีใหม่ที่จะใช้สร้างแอปพลิเคชัน Android เช่น ดูโค้ดทั้งหมดของบทแนะนำนี้ได้ที่ mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic เราจะเรียกเส้นทางนี้ว่า $APPLICATION_PATH ใน Codelab

โปรดทราบว่าในเส้นทางไปที่แอปพลิเคชันนั้น:

  • แอปพลิเคชันชื่อ helloworld
  • $PACKAGE_PATH ของแอปพลิเคชันคือ com.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);
  }
}

เพิ่มไฟล์ Manifest 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} ในไฟล์ Manifest ด้วยสตริงที่ระบุใน 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 จะเพิ่มทรัพยากร Dependency สำหรับ MainActivity, ไฟล์ทรัพยากร และ AndroidManifest.xml

กฎ android_binary ใช้ไลบรารี Android basic_lib ที่สร้างขึ้นเพื่อสร้าง APK ไบนารีสำหรับการติดตั้งในอุปกรณ์ Android

หากต้องการสร้างแอป ให้ใช้คำสั่งต่อไปนี้

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

ติดตั้งไฟล์ APK ที่สร้างขึ้นโดยใช้ adb install เช่น

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 ได้ หากต้องการใช้งาน ให้เพิ่มทรัพยากร Dependency "//mediapipe/java/com/google/mediapipe/components:android_components" ในกฎ mediapipe_lib ใน BUILD

หากต้องการใช้ PermissionHelper ใน MainActivity ให้เพิ่มบรรทัดต่อไปนี้ลงในฟังก์ชัน 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>

โค้ดบล็อกนี้มี FrameLayout ใหม่ชื่อ preview_display_layout และ 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

ต่อไปเราจะเพิ่มออบเจ็กต์ 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 เพื่อให้เราใช้แสดงเฟรมกล้องโดยใช้ออบเจ็กต์ SurfaceTexture ชื่อ previewFrameTexture ได้

หากต้องการใช้ previewFrameTexture เพื่อหาเฟรมกล้อง เราจะใช้ CameraX เฟรมเวิร์กมียูทิลิตีชื่อ CameraXPreviewHelper เพื่อใช้ CameraX คลาสนี้จะอัปเดต Listener เมื่อกล้องเริ่มทำงานผ่าน onCameraStarted(@Nullable SurfaceTexture)

หากต้องการใช้ยูทิลิตีนี้ ให้แก้ไขไฟล์ BUILD เพื่อเพิ่มทรัพยากร Dependency บน "//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 ใหม่และเพิ่ม Listener แบบไม่ระบุชื่อให้กับออบเจ็กต์ เมื่อ cameraHelper มีสัญญาณว่ากล้องเริ่มทำงานและมี surfaceTexture เพื่อจับเฟรมพร้อมใช้งาน เราจะบันทึก surfaceTexture เป็น previewFrameTexture และแสดง previewDisplayView เพื่อให้เราเริ่มเห็นเฟรมจาก previewFrameTexture ได้

อย่างไรก็ตาม ก่อนเริ่มกล้อง เราต้องตัดสินใจว่าจะใช้กล้องตัวใด CameraXPreviewHelper รับค่ามาจาก CameraHelper ซึ่งมี 2 ตัวเลือก ได้แก่ FRONT และ BACK เราสามารถส่งผลการตัดสินจากไฟล์ BUILD เป็นข้อมูลเมตาได้โดยที่ไม่ต้องเปลี่ยนแปลงโค้ดเพื่อสร้างแอปเวอร์ชันอื่นโดยใช้กล้องอื่น

สมมติว่าเราต้องการใช้กล้อง BACK เพื่อตรวจจับขอบในฉากแบบเรียลไทม์ที่เราดูจากกล้อง ให้เพิ่มข้อมูลเมตาลงใน AndroidManifest.xml โดยทำดังนี้

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

และระบุการเลือกใน BUILD ในกฎไบนารี Android ของ helloworld ด้วยรายการใหม่ใน 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 เฟรมที่ถ่ายจากกล้องควรจัดเก็บในวัตถุพื้นผิว 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);

จำได้ไหมว่าเราได้กำหนดฟังก์ชัน onResume() ใน MainActivity เพื่อยืนยันว่าได้ให้สิทธิ์กล้องถ่ายรูปและเรียกใช้ startCamera() แล้ว ก่อนการตรวจสอบนี้ ให้เพิ่มบรรทัดต่อไปนี้ใน onResume() เพื่อเริ่มต้นออบเจ็กต์ converter

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

ตอนนี้ converter นี้ใช้ GLContext ที่จัดการโดย eglManager

นอกจากนี้เรายังต้องลบล้างฟังก์ชัน onPause() ใน MainActivity ด้วยเพื่อให้หากแอปพลิเคชันอยู่ในสถานะหยุดชั่วคราว เราจะปิด 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 แล้ว

การใช้กราฟ MediaPipe ใน Android

เพิ่มทรัพยากร Dependency ที่เกี่ยวข้อง

หากต้องการใช้กราฟ MediaPipe เราต้องเพิ่มทรัพยากร Dependency ไปยังเฟรมเวิร์ก MediaPipe ใน Android ก่อนอื่นเราจะเพิ่มกฎบิลด์เพื่อสร้าง cc_binary โดยใช้โค้ด JNI ของเฟรมเวิร์ก MediaPipe จากนั้นสร้างกฎ 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,
)

เพิ่มทรัพยากร Dependency ":mediapipe_jni_lib" ในกฎบิลด์ mediapipe_lib ในไฟล์ BUILD

ต่อไป เราจะต้องเพิ่มทรัพยากร Dependency ให้กับกราฟ MediaPipe ที่ต้องการใช้ในแอปพลิเคชันโดยเฉพาะ

ขั้นแรก ให้เพิ่มทรัพยากร Dependency ให้กับโค้ดเครื่องคำนวณทั้งหมดในกฎของบิลด์ libmediapipe_jni.so ดังนี้

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

กราฟ MediaPipe เป็นไฟล์ .pbtxt แต่การนำไปใช้ในแอปพลิเคชัน เราจำเป็นต้องใช้กฎบิลด์ mediapipe_binary_graph ในการสร้างไฟล์ .binarypb

ในกฎบิลด์ไบนารีของ helloworld ให้เพิ่มเป้าหมาย 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 จะระบุชื่อไฟล์ของกราฟไบนารี ซึ่งกำหนดโดยช่อง output_name ในเป้าหมาย mediapipe_binary_graph inputVideoStreamName และ outputVideoStreamName เป็นชื่อสตรีมวิดีโออินพุตและเอาต์พุตที่ระบุไว้ในกราฟตามลำดับ

ตอนนี้ MainActivity จะต้องโหลดเฟรมเวิร์ก MediaPipe นอกจากนี้ เฟรมเวิร์กใช้ OpenCV ด้วย ดังนั้น MainActvity จึงควรโหลด OpenCV ด้วย ใช้โค้ดต่อไปนี้ใน MainActivity (ภายในคลาส ไม่ใช่ในฟังก์ชันใดๆ) เพื่อโหลดทรัพยากร Dependency ทั้ง 2 รายการ

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;

และเริ่มต้นใน onCreate(Bundle) หลังจากเริ่มต้น eglManager:

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 ไปยัง VideoSurfaceOutput ของ processor เมื่อทำลายแล้ว เราจะนำรายการดังกล่าวออกจากVideoSurfaceOutputของprocessor

เพียงเท่านี้ก็เรียบร้อยแล้ว ตอนนี้คุณควรสร้างและเรียกใช้แอปพลิเคชันบนอุปกรณ์ได้สำเร็จแล้ว และจะเห็นการตรวจจับขอบของ Sobel ทำงานในฟีดกล้องแบบสด ยินดีด้วย!

edge_detection_android_gpu_gif

หากพบปัญหา โปรดดูโค้ดทั้งหมดของบทแนะนำที่นี่