Halo Dunia! di Android

Pengantar

Halo Dunia! menggunakan Kerangka Kerja MediaPipe untuk mengembangkan aplikasi Android yang menjalankan grafik MediaPipe di Android.

Yang akan Anda build

Aplikasi kamera sederhana untuk deteksi tepi Sobel secara real-time yang diterapkan ke video live melakukan streaming di perangkat Android.

edge_detection_android_gpu_gif

Penyiapan

  1. Instal Framework MediaPipe di sistem Anda, lihat Penginstalan framework untuk mengetahui detailnya.
  2. Instal Android Development SDK dan Android NDK. Lihat cara melakukannya juga di [Panduan penginstalan framework].
  3. Aktifkan opsi developer di perangkat Android Anda.
  4. Siapkan Bazel di sistem Anda untuk membangun dan men-deploy aplikasi Android.

Grafik untuk deteksi tepi

Kita akan menggunakan grafik berikut, 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"
}

Visualisasi grafik ditampilkan di bawah ini:

edge_detection_mobile_gpu

Grafik ini memiliki satu aliran input bernama input_video untuk semua frame yang masuk yang akan disediakan oleh kamera perangkat Anda.

Node pertama dalam grafik, LuminanceCalculator, mengambil satu paket (gambar ) dan menerapkan perubahan luminans menggunakan shader OpenGL. Hasil frame gambar dikirim ke aliran data output luma_video.

Node kedua, SobelEdgesCalculator, menerapkan deteksi edge ke peristiwa masuk paket di aliran data luma_video dan menghasilkan output berupa output output_video feed.

Aplikasi Android kita akan menampilkan bingkai gambar output dari Feed output_video.

Penyiapan aplikasi minimal awal

Pertama-tama, kita mulai dengan aplikasi Android sederhana yang menampilkan "Hello World!" (Halo Dunia) di layar. Anda dapat melewati langkah ini jika sudah terbiasa membangun Android aplikasi menggunakan bazel.

Buat direktori baru tempat Anda akan membuat aplikasi Android. Sebagai contoh, kode lengkap dari tutorial ini dapat ditemukan di mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic. Kami akan lihat jalur ini sebagai $APPLICATION_PATH di seluruh codelab.

Perhatikan bahwa dalam jalur ke aplikasi:

  • Aplikasinya bernama helloworld.
  • $PACKAGE_PATH aplikasi adalah com.google.mediapipe.apps.basic. Ini digunakan dalam cuplikan kode di tutorial ini, jadi harap ingat untuk menggunakan $PACKAGE_PATH Anda sendiri saat menyalin/menggunakan cuplikan kode.

Tambahkan file activity_main.xml ke $APPLICATION_PATH/res/layout. Ini menampilkan TextView di layar penuh aplikasi dengan string 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>

Menambahkan MainActivity.java sederhana ke $APPLICATION_PATH yang memuat konten dari tata letak activity_main.xml seperti yang ditunjukkan di bawah ini:

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

Menambahkan file manifes, AndroidManifest.xml ke $APPLICATION_PATH, yang meluncurkan MainActivity saat aplikasi dimulai:

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

Di aplikasi, kita menggunakan tema Theme.AppCompat di aplikasi sehingga kita perlu referensi tema yang tepat. Tambahkan colors.xml ke $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>

Tambahkan styles.xml ke $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>

Untuk membangun aplikasi, tambahkan file BUILD ke $APPLICATION_PATH, dan ${appName} dan ${mainActivity} dalam manifes akan diganti dengan string ditentukan di BUILD seperti yang ditunjukkan di bawah ini.

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

Aturan android_library menambahkan dependensi untuk MainActivity, file resource dan AndroidManifest.xml.

Aturan android_binary, menggunakan library Android basic_lib yang dibuat untuk membuat APK biner untuk diinstal di perangkat Android Anda.

Untuk membangun aplikasi, gunakan perintah berikut:

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

Instal file APK yang dihasilkan menggunakan adb install. Contoh:

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

Buka aplikasi di perangkat Anda. Layar akan ditampilkan dengan teks Hello World!.

bazel_hello_world_android

Menggunakan kamera melalui CameraX

Izin Akses Kamera

Untuk menggunakan kamera di aplikasi, kita perlu meminta pengguna untuk menyediakan akses ke kamera. Untuk meminta izin kamera, tambahkan hal berikut ke AndroidManifest.xml:

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

Ubah versi SDK minimum menjadi 21 dan versi SDK target ke 27 di file yang sama:

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

Hal ini memastikan bahwa pengguna diminta untuk meminta izin kamera dan mengaktifkan kami untuk menggunakan library CameraX untuk akses kamera.

Untuk meminta izin kamera, kita dapat menggunakan utilitas yang disediakan oleh MediaPipe Framework komponen, yaitu PermissionHelper. Untuk menggunakannya, tambahkan dependensi "//mediapipe/java/com/google/mediapipe/components:android_components" dalam mediapipe_lib aturan di BUILD.

Untuk menggunakan PermissionHelper di MainActivity, tambahkan baris berikut ke elemen Fungsi onCreate:

PermissionHelper.checkAndRequestCameraPermissions(this);

Tindakan ini meminta izin kepada pengguna dengan dialog di layar untuk menggunakan kamera di aplikasi ini.

Tambahkan kode berikut untuk menangani respons pengguna:

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

Biarkan metode startCamera() kosong untuk saat ini. Ketika pengguna merespons perintah tersebut, MainActivity akan dilanjutkan dan onResume() akan dipanggil. Kode itu akan mengonfirmasi bahwa izin akses untuk menggunakan kamera telah diberikan, kemudian kita akan memulai kamera.

Build ulang dan instal aplikasi. Anda akan melihat dialog yang meminta akses ke kamera untuk aplikasi.

Akses Kamera

Dengan izin kamera yang tersedia, kita dapat memulai dan mengambil {i>frame<i} dari kamera.

Untuk melihat frame dari kamera, kita akan menggunakan SurfaceView. Setiap frame dari kamera akan disimpan dalam objek SurfaceTexture. Untuk menggunakannya, kita kita perlu mengubah tata letak aplikasi kita terlebih dahulu.

Hapus seluruh blok kode TextView dari $APPLICATION_PATH/res/layout/activity_main.xml dan tambahkan kode berikut sebagai gantinya:

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

Blok kode ini memiliki FrameLayout baru bernama preview_display_layout dan TextView bersarang di dalamnya, bernama no_camera_access_preview. Saat kamera izin akses tidak diberikan, aplikasi kita akan menampilkan TextView dengan pesan string, disimpan dalam variabel no_camera_access. Tambahkan baris berikut di file $APPLICATION_PATH/res/values/strings.xml:

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

Jika pengguna tidak memberikan izin kamera, layar sekarang akan terlihat seperti ini:

missing_camera_permission_android

Sekarang, kita akan menambahkan objek SurfaceTexture dan SurfaceView ke MainActivity:

private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;

Pada fungsi onCreate(Bundle), tambahkan dua baris berikut sebelum meminta izin kamera:

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

Sekarang tambahkan kode yang menentukan setupPreviewDisplayView():

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

Kita menentukan objek SurfaceView baru dan menambahkannya ke preview_display_layout FrameLayout agar kita dapat menggunakannya untuk menampilkan frame kamera menggunakan objek SurfaceTexture bernama previewFrameTexture.

Untuk menggunakan previewFrameTexture guna mendapatkan bingkai kamera, kita akan menggunakan CameraX. Framework menyediakan utilitas bernama CameraXPreviewHelper untuk menggunakan CameraX. Class ini mengupdate pemroses saat kamera dimulai melalui onCameraStarted(@Nullable SurfaceTexture).

Untuk menggunakan utilitas ini, ubah file BUILD untuk menambahkan dependensi pada "//mediapipe/java/com/google/mediapipe/components:android_camerax_helper".

Sekarang impor CameraXPreviewHelper dan tambahkan baris berikut ke MainActivity:

private CameraXPreviewHelper cameraHelper;

Sekarang, kita dapat menambahkan implementasi kita ke 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);
    });
}

Tindakan ini akan membuat objek CameraXPreviewHelper baru dan menambahkan objek anonim pemroses pada objek. Saat cameraHelper memberikan sinyal bahwa kamera telah dimulai dan surfaceTexture untuk mengambil {i>frame<i} juga tersedia, kita menyimpannya surfaceTexture sebagai previewFrameTexture, dan buat previewDisplayView terlihat sehingga kita bisa mulai melihat frame dari previewFrameTexture.

Namun, sebelum memulai kamera, kita perlu memutuskan kamera mana gunakan. CameraXPreviewHelper mewarisi dari CameraHelper yang memberikan dua opsi, FRONT dan BACK. Kita dapat meneruskan keputusan dari file BUILD seperti metadata sehingga tidak diperlukan perubahan kode untuk membangun versi lain dari aplikasi menggunakan kamera yang berbeda.

Dengan asumsi kita ingin menggunakan kamera BACK untuk melakukan deteksi tepi pada adegan live yang dilihat dari kamera, tambahkan metadata ke AndroidManifest.xml:

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

dan tentukan pemilihan di BUILD dalam aturan biner android helloworld dengan entri baru di manifest_values:

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

Sekarang, di MainActivity untuk mengambil metadata yang ditentukan di manifest_values, tambahkan objek ApplicationInfo:

private ApplicationInfo applicationInfo;

Dalam fungsi onCreate(), tambahkan:

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

Sekarang tambahkan baris berikut di akhir fungsi startCamera():

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

Pada tahap ini, aplikasi akan berhasil dibuat. Namun, ketika Anda menjalankan aplikasi di perangkat, Anda akan melihat layar hitam (meskipun kamera izin telah diberikan). Hal ini karena meskipun kita menyimpan Variabel surfaceTexture disediakan oleh CameraXPreviewHelper, previewSurfaceView belum menggunakan outputnya dan menampilkannya di layar.

Karena kita ingin menggunakan {i>frame<i} dalam grafik MediaPipe, kita tidak akan menambahkan kode ke melihat output kamera langsung dalam tutorial ini. Sebagai gantinya, kita langsung membahas cara kita dapat mengirim {i>frame<i} kamera untuk diproses ke grafik MediaPipe dan menampilkan yang dihasilkan dari grafik di layar.

Penyiapan ExternalTextureConverter

SurfaceTexture menangkap frame gambar dari streaming sebagai OpenGL ES tekstur. Untuk menggunakan grafik MediaPipe, bingkai yang diambil dari kamera harus disimpan dalam objek tekstur Open GL biasa. Framework ini menyediakan kelas, ExternalTextureConverter untuk mengonversi gambar yang disimpan di SurfaceTexture ke objek tekstur OpenGL reguler.

Untuk menggunakan ExternalTextureConverter, kita juga memerlukan EGLContext, yang yang dibuat dan dikelola oleh objek EglManager. Menambahkan dependensi ke BUILD file ini untuk menggunakan EglManager, "//mediapipe/java/com/google/mediapipe/glutil".

Di MainActivity, tambahkan deklarasi berikut:

private EglManager eglManager;
private ExternalTextureConverter converter;

Dalam fungsi onCreate(Bundle), tambahkan pernyataan untuk melakukan inisialisasi pada Objek eglManager sebelum meminta izin kamera:

eglManager = new EglManager(null);

Ingat bahwa kita telah menentukan fungsi onResume() di MainActivity untuk mengonfirmasi izin kamera telah diberikan dan memanggil startCamera(). Sebelumnya centang, tambahkan baris berikut di onResume() untuk melakukan inisialisasi converter :

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

converter ini sekarang menggunakan GLContext yang dikelola oleh eglManager.

Kita juga perlu mengganti fungsi onPause() di MainActivity sehingga jika aplikasi berstatus dijeda, kita menutup converter dengan benar:

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

Untuk menyalurkan output previewFrameTexture ke converter, tambahkan blok kode berikut ke 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) {}
     });

Di blok kode ini, kita menambahkan SurfaceHolder.Callback kustom untuk previewDisplayView dan mengimplementasikan fungsi surfaceChanged(SurfaceHolder holder, int format, int width, int height) untuk menghitung ukuran tampilan yang sesuai frame kamera di layar perangkat dan untuk mengaitkan previewFrameTexture dan mengirim frame displaySize yang telah dihitung ke converter.

Sekarang kita siap menggunakan frame kamera dalam grafik MediaPipe.

Menggunakan grafik MediaPipe di Android

Menambahkan dependensi yang relevan

Untuk menggunakan grafik MediaPipe, kita perlu menambahkan dependensi ke framework MediaPipe. di Android. Pertama-tama, kita akan menambahkan aturan build untuk membangun cc_binary menggunakan kode JNI framework MediaPipe, lalu membuat aturan cc_library untuk menggunakan biner ini dalam aplikasi kita. Tambahkan blok kode berikut ke file BUILD Anda:

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

Tambahkan dependensi ":mediapipe_jni_lib" ke aturan build mediapipe_lib di file BUILD.

Selanjutnya, kita perlu menambahkan dependensi khusus untuk grafik MediaPipe yang ingin digunakan dalam aplikasi.

Pertama, tambahkan dependensi ke semua kode kalkulator di libmediapipe_jni.so aturan build:

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

Grafik MediaPipe adalah file .pbtxt, tetapi untuk menggunakannya dalam aplikasi, kita perlu untuk menggunakan aturan build mediapipe_binary_graph untuk menghasilkan file .binarypb.

Dalam aturan build biner Android helloworld, tambahkan mediapipe_binary_graph menargetkan khusus untuk grafik sebagai aset:

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

Di aturan build assets, Anda juga dapat menambahkan aset lain seperti TensorFlowLite yang digunakan dalam grafik Anda.

Selain itu, tambahkan manifest_values tambahan untuk properti khusus untuk grafik, yang nantinya dapat diambil di 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",
},

Perhatikan bahwa binaryGraphName menunjukkan nama file dari grafik biner, ditentukan oleh kolom output_name di target mediapipe_binary_graph. inputVideoStreamName dan outputVideoStreamName adalah input dan output nama streaming video yang ditentukan masing-masing dalam grafik.

Sekarang, MainActivity perlu memuat framework MediaPipe. Selain itu, framework menggunakan OpenCV, jadi MainActvity juga harus memuat OpenCV. Gunakan kode berikut di MainActivity (di dalam class, tetapi tidak di dalam fungsi apa pun) untuk memuat kedua dependensi:

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

Menggunakan grafik dalam MainActivity

Pertama, kita perlu memuat aset yang berisi .binarypb yang dikompilasi dari file .pbtxt dari grafik. Untuk melakukan ini, kita dapat menggunakan utilitas MediaPipe, AndroidAssetUtil

Melakukan inisialisasi pengelola aset di onCreate(Bundle) sebelum melakukan inisialisasi eglManager:

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

Sekarang, kita perlu menyiapkan objek FrameProcessor yang mengirim frame kamera disiapkan oleh converter ke grafik MediaPipe dan menjalankan grafik, menyiapkan output, lalu mengupdate previewDisplayView untuk menampilkan output. Tambah kode berikut untuk mendeklarasikan FrameProcessor:

private FrameProcessor processor;

dan lakukan inisialisasi di onCreate(Bundle) setelah melakukan inisialisasi eglManager:

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

processor perlu menggunakan frame yang dikonversi dari converter untuk diproses. Tambahkan baris berikut ke onResume() setelah melakukan inisialisasi converter:

converter.setConsumer(processor);

processor harus mengirimkan outputnya ke previewDisplayView. Untuk melakukannya, tambahkan definisi fungsi berikut ke SurfaceHolder.Callback kustom kita:

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

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

Saat SurfaceHolder dibuat, kita memiliki Surface ke VideoSurfaceOutput dari processor. Setelah dihancurkan, kita menghapusnya dari VideoSurfaceOutput dari processor.

Selesai. Sekarang Anda telah berhasil membangun dan menjalankan di perangkat dan melihat deteksi tepi Sobel yang berjalan di kamera live umpan! Selamat!

edge_detection_android_gpu_gif

Jika Anda mengalami masalah, lihat kode lengkap tutorial di sini.