Hello World! ב-Android

מבוא

במדריך הזה של Hello World! נעשה שימוש ב-MediaPipe Framework כדי לפתח אפליקציה ל-Android שמפעילה תרשים MediaPipe ב-Android.

מה תפַתחו

אפליקציית מצלמה פשוטה לזיהוי קצה Sobel בזמן אמת שמוחלת על שידור וידאו בשידור חי במכשיר Android.

edge_detection_android_gpu_gif

הגדרה

  1. לפרטים נוספים, קראו את המדריך להתקנת מסגרת כדי להתקין את MediaPipe Framework.
  2. התקן את Android Development SDK ו-Android NDK. קראו גם איך לעשות זאת [במדריך להתקנת מסגרת].
  3. מפעילים את האפשרויות למפתחים במכשיר Android.
  4. כדי לפתח את האפליקציה ל-Android ולפרוס אותה, צריך להגדיר את Bazel במערכת.

תרשים לזיהוי הקצה

נשתמש בתרשים הבא, 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, מחיל זיהוי קצה על חבילות נכנסות ב-stream של luma_video, ופלט הפלט גורם לזרם הפלט output_video.

האפליקציה שלנו ל-Android תציג את הפריימים של תמונות הפלט של הסטרימינג output_video.

הגדרה ראשונית של אפליקציה מינימלית

קודם כל, אנחנו מתחילים באפליקציית Android פשוטה שמציגה את "Hello World!" על המסך. אם אתם יודעים איך ליצור אפליקציות ל-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);
  }
}

מוסיפים ל-$APPLICATION_PATH קובץ מניפסט, AndroidManifest.xml, שיפעיל את 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

התקן את קובץ ה-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. כדי להשתמש בו, מוסיפים תלות "//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), מוסיפים את שתי השורות הבאות לפני בקשת הרשאות המצלמה:

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. Framework מספק כלי עזר בשם 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, שמספק שתי אפשרויות, FRONT ו-BACK. אנחנו יכולים להעביר את ההחלטה מהקובץ BUILD כמטא-נתונים, כך שלא נדרש שינוי בקוד כדי ליצור גרסה אחרת של האפליקציה באמצעות מצלמה אחרת.

בהנחה שאנחנו רוצים להשתמש במצלמה BACK כדי לבצע זיהוי קצה בסצנה בשידור חי שאנחנו צופים בה מהמצלמה, מוסיפים את המטא-נתונים ל-AndroidManifest.xml:

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

ולציין את הבחירה ב-BUILD בכלל הבינארי של helloworld ב-Android עם רשומה חדשה ב-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. ה-framework מספקת מחלקה, 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

מוסיפים יחסי תלות רלוונטיים

כדי להשתמש בתרשים MediaPipe, אנחנו צריכים להוסיף יחסי תלות ל-framework של MediaPipe ב-Android. קודם כול נוסיף כלל build כדי ליצור 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,
)

מוסיפים את התלות ":mediapipe_jni_lib" לכלל ה-build של mediapipe_lib בקובץ BUILD.

בשלב הבא צריך להוסיף יחסי תלות ספציפיים לתרשים MediaPipe שבו אנחנו רוצים להשתמש באפליקציה.

קודם כול, מוסיפים יחסי תלות לכל קוד המחשבון בכלל ה-build של libmediapipe_jni.so:

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

הגרפים של MediaPipe הם קובצי .pbtxt, אבל כדי להשתמש בהם באפליקציה אנחנו צריכים להשתמש בכלל ה-build mediapipe_binary_graph על מנת ליצור קובץ .binarypb.

בכלל ה-build הבינארי של helloworld ל-Android, מוסיפים את היעד mediapipe_binary_graph הספציפי לתרשים כנכס:

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

בכלל ה-build של 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 צריך לטעון את framework של MediaPipe. בנוסף, ה-framework משתמש ב-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;

ומפעילים אותו ב-onCreate(Bundle) אחרי האתחול של eglManager:

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 נוצר, היה לנו Surface עם VideoSurfaceOutput של processor. כשהוא מושמד, אנחנו מסירים אותו מה-VideoSurfaceOutput של processor.

זהו, סיימתם. עכשיו אמורה להיות אפשרות ליצור ולהפעיל את האפליקציה במכשיר ולראות את זיהוי הקצה של Sobel פועל בפיד המצלמה בשידור חי! איזה כיף!

edge_detection_android_gpu_gif

אם נתקלת בבעיות, אפשר לעיין בקוד המלא של המדריך כאן.