Hello World! trên Android

Giới thiệu

Hướng dẫn Hello World! sử dụng Khung MediaPipe để phát triển một ứng dụng Android chạy biểu đồ MediaPipe trên Android.

Sản phẩm bạn sẽ tạo ra

Một ứng dụng máy ảnh đơn giản giúp phát hiện cạnh Sobel theo thời gian thực, áp dụng cho luồng video trực tiếp trên thiết bị Android.

edge_detection_android_gpu_gif

Thiết lập

  1. Cài đặt MediaPipe Framework trên hệ thống của bạn, xem phần Hướng dẫn cài đặt Frame để biết thông tin chi tiết.
  2. Cài đặt SDK phát triển Android và Android NDK. Hãy xem thêm cách thực hiện trong phần [Hướng dẫn cài đặt khung].
  3. Bật tuỳ chọn cho nhà phát triển trên thiết bị Android của bạn.
  4. Thiết lập Bazel trên hệ thống của bạn để xây dựng và triển khai ứng dụng Android.

Biểu đồ cho tính năng phát hiện cạnh

Chúng ta sẽ sử dụng biểu đồ sau đây, 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"
}

Hình ảnh của biểu đồ được hiển thị dưới đây:

edge_detection_mobile_gpu

Biểu đồ này có một luồng đầu vào duy nhất tên là input_video cho tất cả khung hình đến mà máy ảnh của thiết bị cung cấp.

Nút đầu tiên trong biểu đồ, LuminanceCalculator, lấy một gói duy nhất (khung hình ảnh) và áp dụng thay đổi về độ chói bằng chương trình đổ bóng OpenGL. Khung hình ảnh thu được sẽ được gửi đến luồng đầu ra luma_video.

Nút thứ hai, SobelEdgesCalculator áp dụng tính năng phát hiện cạnh cho các gói đến trong luồng luma_video và xuất kết quả là luồng đầu ra output_video.

Ứng dụng Android của chúng ta sẽ hiển thị các khung hình ảnh đầu ra của luồng output_video.

Thiết lập ứng dụng tối thiểu ban đầu

Trước tiên, chúng ta bắt đầu bằng một ứng dụng Android đơn giản hiển thị "Hello World!" trên màn hình. Bạn có thể bỏ qua bước này nếu đã quen với việc tạo ứng dụng Android bằng bazel.

Tạo thư mục mới để tạo ứng dụng Android. Ví dụ: bạn có thể tìm thấy mã đầy đủ của hướng dẫn này tại mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic. Chúng ta sẽ gọi đường dẫn này là $APPLICATION_PATH trong suốt lớp học lập trình này.

Lưu ý rằng trong đường dẫn đến ứng dụng:

  • Ứng dụng này có tên là helloworld.
  • $PACKAGE_PATH của ứng dụng là com.google.mediapipe.apps.basic. Mã này được sử dụng trong các đoạn mã ở hướng dẫn này, vì vậy, hãy nhớ sử dụng $PACKAGE_PATH của riêng bạn khi sao chép/sử dụng các đoạn mã.

Thêm tệp activity_main.xml vào $APPLICATION_PATH/res/layout. Thao tác này sẽ hiển thị một TextView trên toàn màn hình của ứng dụng cùng với chuỗi 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>

Thêm một MainActivity.java đơn giản vào $APPLICATION_PATH để tải nội dung của bố cục activity_main.xml như minh hoạ dưới đây:

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);
  }
}

Thêm tệp kê khai AndroidManifest.xml vào $APPLICATION_PATH. Tệp này sẽ chạy MainActivity khi khởi động ứng dụng:

<?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>

Trong ứng dụng này, chúng ta đang dùng giao diện Theme.AppCompat trong ứng dụng, vì vậy, chúng ta cần có các tham chiếu giao diện phù hợp. Thêm colors.xml vào $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>

Thêm styles.xml vào $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>

Để tạo ứng dụng, hãy thêm tệp BUILD vào $APPLICATION_PATH, đồng thời ${appName}${mainActivity} trong tệp kê khai sẽ được thay thế bằng các chuỗi được chỉ định trong BUILD như thể hiện dưới đây.

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",
    ],
)

Quy tắc android_library sẽ thêm các phần phụ thuộc cho MainActivity, tệp tài nguyên và AndroidManifest.xml.

Quy tắc android_binary sử dụng thư viện Android basic_lib được tạo để tạo một tệp APK nhị phân nhằm cài đặt trên thiết bị Android của bạn.

Để tạo ứng dụng, hãy dùng lệnh sau:

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

Cài đặt tệp APK đã tạo bằng adb install. Ví dụ:

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

Mở ứng dụng trên thiết bị. Ứng dụng sẽ hiển thị một màn hình có văn bản Hello World!.

bazel_hello_world_android

Đang sử dụng máy ảnh qua CameraX

Quyền truy cập máy ảnh

Để dùng máy ảnh trong ứng dụng, chúng ta cần yêu cầu người dùng cấp quyền truy cập vào máy ảnh. Để yêu cầu cấp quyền truy cập vào máy ảnh, hãy thêm đoạn mã sau vào AndroidManifest.xml:

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

Thay đổi phiên bản SDK tối thiểu thành 21 và phiên bản SDK mục tiêu thành 27 trong cùng một tệp:

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

Điều này đảm bảo rằng người dùng được nhắc yêu cầu quyền truy cập vào máy ảnh và cho phép chúng tôi sử dụng thư viện CameraX để truy cập vào máy ảnh.

Để yêu cầu quyền truy cập vào máy ảnh, chúng ta có thể sử dụng một tiện ích do các thành phần của MediaPipe Framework cung cấp, cụ thể là PermissionHelper. Để sử dụng đối tượng này, hãy thêm phần phụ thuộc "//mediapipe/java/com/google/mediapipe/components:android_components" vào quy tắc mediapipe_lib trong BUILD.

Để sử dụng PermissionHelper trong MainActivity, hãy thêm dòng sau vào hàm onCreate:

PermissionHelper.checkAndRequestCameraPermissions(this);

Thao tác này sẽ nhắc người dùng thông qua một hộp thoại trên màn hình để yêu cầu cấp quyền sử dụng máy ảnh trong ứng dụng này.

Thêm mã sau đây để xử lý phản hồi của người dùng:

@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() {}

Tạm thời, chúng ta sẽ để trống phương thức startCamera(). Khi người dùng phản hồi lời nhắc, MainActivity sẽ tiếp tục và onResume() sẽ được gọi. Mã này sẽ xác nhận rằng quyền sử dụng máy ảnh đã được cấp, sau đó sẽ khởi động máy ảnh.

Tạo lại và cài đặt ứng dụng. Bây giờ, bạn sẽ thấy một lời nhắc yêu cầu ứng dụng này được cấp quyền truy cập vào máy ảnh.

Quyền truy cập máy ảnh

Khi có các quyền truy cập vào máy ảnh, chúng ta có thể bắt đầu và tìm nạp khung hình từ máy ảnh.

Để xem các khung hình bằng máy ảnh, chúng ta sẽ sử dụng SurfaceView. Mỗi khung hình của máy ảnh sẽ được lưu trữ trong một đối tượng SurfaceTexture. Để sử dụng các bố cục này, trước tiên, chúng ta cần thay đổi bố cục của ứng dụng.

Xoá toàn bộ khối mã TextView khỏi $APPLICATION_PATH/res/layout/activity_main.xml rồi thêm mã sau:

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

Khối mã này có một FrameLayout mới tên là preview_display_layout và một TextView được lồng bên trong nó tên là no_camera_access_preview. Khi quyền truy cập vào máy ảnh không được cấp, ứng dụng của chúng tôi sẽ hiển thị TextView kèm theo một thông báo chuỗi, được lưu trữ trong biến no_camera_access. Thêm dòng sau vào tệp $APPLICATION_PATH/res/values/strings.xml:

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

Khi người dùng không cấp quyền truy cập vào camera, màn hình sẽ có dạng như sau:

missing_camera_permission_android

Bây giờ, chúng ta sẽ thêm các đối tượng SurfaceTextureSurfaceView vào MainActivity:

private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;

Trong hàm onCreate(Bundle), hãy thêm hai dòng sau trước khi yêu cầu quyền truy cập vào máy ảnh:

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

Giờ thì hãy thêm mã để xác định setupPreviewDisplayView():

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

Chúng ta xác định một đối tượng SurfaceView mới và thêm đối tượng đó vào đối tượng preview_display_layout FrameLayout để có thể sử dụng đối tượng này nhằm hiển thị khung máy ảnh bằng cách sử dụng đối tượng SurfaceTexture có tên là previewFrameTexture.

Nếu muốn dùng previewFrameTexture để lấy khung máy ảnh, chúng ta sẽ sử dụng CameraX. Khung cung cấp một tiện ích có tên CameraXPreviewHelper để sử dụng CameraX. Lớp này sẽ cập nhật một trình nghe khi máy ảnh được khởi động qua onCameraStarted(@Nullable SurfaceTexture).

Để sử dụng tiện ích này, hãy sửa đổi tệp BUILD để thêm phần phụ thuộc vào "//mediapipe/java/com/google/mediapipe/components:android_camerax_helper".

Giờ hãy nhập CameraXPreviewHelper rồi thêm dòng sau vào MainActivity:

private CameraXPreviewHelper cameraHelper;

Bây giờ, chúng ta có thể thêm phương thức triển khai vào 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);
    });
}

Thao tác này sẽ tạo một đối tượng CameraXPreviewHelper mới và thêm một trình nghe ẩn danh trên đối tượng đó. Khi cameraHelper báo hiệu rằng máy ảnh đã khởi động và có một surfaceTexture để lấy khung hình, chúng ta sẽ lưu surfaceTexture đó dưới dạng previewFrameTexture và hiển thị previewDisplayView để có thể bắt đầu xem các khung hình từ previewFrameTexture.

Tuy nhiên, trước khi khởi động máy ảnh, chúng ta cần quyết định máy ảnh mà mình muốn sử dụng. CameraXPreviewHelper kế thừa từ CameraHelper, cung cấp 2 tuỳ chọn là FRONTBACK. Chúng ta có thể truyền quyết định từ tệp BUILD dưới dạng siêu dữ liệu để không yêu cầu thay đổi mã để tạo một phiên bản khác của ứng dụng bằng máy ảnh khác.

Giả sử chúng ta muốn sử dụng camera BACK để thực hiện tính năng phát hiện cạnh trên cảnh trực tiếp mà chúng ta xem từ camera, hãy thêm siêu dữ liệu vào AndroidManifest.xml:

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

và chỉ định lựa chọn trong BUILD trong quy tắc nhị phân Android helloworld với một mục mới trong manifest_values:

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

Giờ đây, trong MainActivity để truy xuất siêu dữ liệu được chỉ định trong manifest_values, hãy thêm đối tượng ApplicationInfo:

private ApplicationInfo applicationInfo;

Trong hàm onCreate(), hãy thêm:

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

Bây giờ, hãy thêm dòng sau vào cuối hàm startCamera():

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

Tại thời điểm này, ứng dụng sẽ tạo bản dựng thành công. Tuy nhiên, khi chạy ứng dụng trên thiết bị, bạn sẽ thấy màn hình màu đen (mặc dù quyền máy ảnh đã được cấp). Điều này là do mặc dù chúng ta lưu biến surfaceTexture do CameraXPreviewHelper cung cấp, nhưng previewSurfaceView chưa sử dụng kết quả của nó và hiển thị biến đó trên màn hình.

Vì muốn sử dụng các khung hình trong biểu đồ MediaPipe, nên chúng ta sẽ không thêm mã để xem trực tiếp đầu ra máy ảnh trong hướng dẫn này. Thay vào đó, chúng ta chuyển sang phần cách gửi khung máy ảnh để xử lý đến biểu đồ MediaPipe và hiển thị kết quả của biểu đồ trên màn hình.

Thiết lập ExternalTextureConverter

SurfaceTexture chụp khung hình ảnh từ một luồng dưới dạng kết cấu OpenGL ES. Để sử dụng biểu đồ MediaPipe, khung hình đã chụp từ máy ảnh phải được lưu trữ trong đối tượng hoạ tiết Open GL thông thường. Khung này cung cấp một lớp ExternalTextureConverter để chuyển đổi hình ảnh được lưu trữ trong đối tượng SurfaceTexture thành đối tượng hoạ tiết OpenGL thông thường.

Để sử dụng ExternalTextureConverter, chúng ta cũng cần có EGLContext do đối tượng EglManager tạo và quản lý. Thêm phần phụ thuộc vào tệp BUILD để sử dụng EglManager, "//mediapipe/java/com/google/mediapipe/glutil".

Trong MainActivity, hãy thêm các nội dung khai báo sau:

private EglManager eglManager;
private ExternalTextureConverter converter;

Trong hàm onCreate(Bundle), hãy thêm một câu lệnh để khởi chạy đối tượng eglManager trước khi yêu cầu quyền truy cập vào máy ảnh:

eglManager = new EglManager(null);

Hãy nhớ rằng chúng ta đã xác định hàm onResume() trong MainActivity để xác nhận các quyền truy cập vào máy ảnh đã được cấp và gọi startCamera(). Trước bước kiểm tra này, hãy thêm dòng sau vào onResume() để khởi chạy đối tượng converter:

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

converter này hiện sử dụng GLContext do eglManager quản lý.

Chúng ta cũng cần ghi đè hàm onPause() trong MainActivity để nếu ứng dụng chuyển sang trạng thái tạm dừng, chúng ta sẽ đóng converter đúng cách:

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

Để chuyển đầu ra của previewFrameTexture sang converter, hãy thêm khối mã sau vào 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) {}
     });

Trong khối mã này, chúng ta thêm một SurfaceHolder.Callback tuỳ chỉnh vào previewDisplayView và triển khai hàm surfaceChanged(SurfaceHolder holder, int format, int width, int height) để tính toán kích thước hiển thị phù hợp của khung máy ảnh trên màn hình thiết bị, đồng thời liên kết đối tượng previewFrameTexture và gửi các khung của displaySize đã tính toán đến converter.

Chúng ta hiện đã sẵn sàng sử dụng khung máy ảnh trong biểu đồ MediaPipe.

Sử dụng biểu đồ MediaPipe trong Android

Thêm các phần phụ thuộc có liên quan

Để sử dụng biểu đồ MediaPipe, chúng ta cần thêm các phần phụ thuộc vào khung MediaPipe trên Android. Trước tiên, chúng ta sẽ thêm quy tắc xây dựng để tạo cc_binary bằng mã JNI của khung MediaPipe, sau đó tạo một quy tắc cc_library để dùng tệp nhị phân này trong ứng dụng. Thêm khối mã dưới đây vào tệp 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,
)

Thêm phần phụ thuộc ":mediapipe_jni_lib" vào quy tắc bản dựng mediapipe_lib trong tệp BUILD.

Tiếp theo, chúng ta cần thêm các phần phụ thuộc dành riêng cho biểu đồ MediaPipe mà chúng ta muốn sử dụng trong ứng dụng.

Trước tiên, hãy thêm phần phụ thuộc vào tất cả mã tính toán trong quy tắc xây dựng libmediapipe_jni.so:

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

Biểu đồ MediaPipe là các tệp .pbtxt, nhưng để sử dụng các tệp này trong ứng dụng, chúng ta cần sử dụng quy tắc bản dựng mediapipe_binary_graph để tạo tệp .binarypb.

Trong quy tắc tạo tệp nhị phân Android helloworld, hãy thêm mục tiêu mediapipe_binary_graph dành riêng cho biểu đồ dưới dạng tài sản:

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

Trong quy tắc tạo assets, bạn cũng có thể thêm các thành phần khác như mô hình TensorFlowLite dùng trong biểu đồ.

Ngoài ra, hãy thêm manifest_values bổ sung cho các thuộc tính cụ thể cho biểu đồ để truy xuất sau này trong 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",
},

Lưu ý rằng binaryGraphName cho biết tên tệp của biểu đồ nhị phân, được xác định bằng trường output_name trong mục tiêu mediapipe_binary_graph. inputVideoStreamNameoutputVideoStreamName lần lượt là tên luồng video đầu vào và đầu ra được chỉ định trong biểu đồ.

MainActivity cần tải khung MediaPipe. Ngoài ra, khung này sử dụng OpenCV, vì vậy, MainActvity cũng sẽ tải OpenCV. Hãy dùng đoạn mã sau trong MainActivity (bên trong lớp, nhưng không bên trong bất kỳ hàm nào) để tải cả hai phần phụ thuộc:

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

Sử dụng biểu đồ trong MainActivity

Trước tiên, chúng ta cần tải thành phần chứa .binarypb được biên dịch từ tệp .pbtxt của biểu đồ. Để thực hiện việc này, chúng ta có thể dùng tiện ích MediaPipe, AndroidAssetUtil.

Khởi chạy trình quản lý thành phần trong onCreate(Bundle) trước khi khởi chạy eglManager:

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

Bây giờ, chúng ta cần thiết lập một đối tượng FrameProcessor sẽ gửi các khung máy ảnh do converter chuẩn bị đến biểu đồ MediaPipe và chạy biểu đồ, chuẩn bị đầu ra rồi cập nhật previewDisplayView để hiển thị kết quả đầu ra. Thêm mã sau để khai báo FrameProcessor:

private FrameProcessor processor;

và khởi tạo trong onCreate(Bundle) sau khi khởi chạy eglManager:

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

processor cần sử dụng khung hình đã chuyển đổi từ converter để xử lý. Thêm dòng sau vào onResume() sau khi khởi chạy converter:

converter.setConsumer(processor);

processor sẽ gửi đầu ra của mình đến previewDisplayView. Để thực hiện việc này, hãy thêm các định nghĩa hàm sau vào SurfaceHolder.Callback tuỳ chỉnh của chúng ta:

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

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

Khi SurfaceHolder được tạo, chúng ta đã có Surface cho VideoSurfaceOutput của processor. Khi nó bị huỷ, chúng tôi sẽ xoá nó khỏi VideoSurfaceOutput của processor.

Chỉ vậy thôi! Giờ đây, bạn có thể tạo và chạy thành công ứng dụng trên thiết bị, cũng như thấy tính năng phát hiện cạnh Sobel chạy trên nguồn cấp dữ liệu camera trực tiếp! Xin chúc mừng!

edge_detection_android_gpu_gif

Nếu bạn gặp vấn đề, vui lòng xem mã đầy đủ của hướng dẫn tại đây.