Witaj świecie na Androidzie

Wstęp

Ten samouczek Hello World! wykorzystuje platformę MediaPipe Framework do utworzenia aplikacji na Androida, która uruchamia wykres MediaPipe na urządzeniu z tym systemem.

Co utworzysz

Prosta aplikacja kamery do wykrywania krawędzi Sobel w czasie rzeczywistym w transmisji wideo na żywo na urządzeniu z Androidem.

edge_detection_android_gpu_gif

Konfiguracja

  1. Zainstaluj MediaPipe Framework w systemie. Szczegółowe informacje znajdziesz w przewodniku instalacji ramek.
  2. Zainstaluj pakiet Android Development SDK i pakiet NDK dla Androida. Zobacz, jak to zrobić, także w przewodniku po instalacji struktur
  3. Włącz opcje programisty na urządzeniu z Androidem.
  4. Skonfiguruj Bazel w swoim systemie, aby skompilować i wdrożyć aplikację na Androida.

Wykres wykrywania krawędzi

Będziemy używać tego wykresu: 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"
}

Wizualizacja wykresu znajduje się poniżej:

edge_detection_mobile_gpu

Ten wykres zawiera pojedynczy strumień wejściowy o nazwie input_video dla wszystkich przychodzących klatek, które dostarczy aparat Twojego urządzenia.

Pierwszy węzeł na wykresie – LuminanceCalculator – pobiera 1 pakiet (ramkę obrazu) i stosuje zmianę luminancji za pomocą cieniowania OpenGL. Otrzymana ramka obrazu jest wysyłana do strumienia wyjściowego luma_video.

Drugi węzeł, SobelEdgesCalculator, stosuje wykrywanie brzegów do pakietów przychodzących w strumieniu luma_video i zwraca wynik strumienia wyjściowego output_video.

Aplikacja na Androida wyświetla wyjściowe ramki obrazów ze strumienia output_video.

Wstępna minimalna konfiguracja aplikacji

Zaczynamy od prostej aplikacji na Androida, która wyświetla na ekranie komunikat „Hello World”. Możesz pominąć ten krok, jeśli potrafisz tworzyć aplikacje na Androida za pomocą bazel.

Utwórz nowy katalog, w którym utworzysz aplikację na Androida. Na przykład pełny kod tego samouczka znajdziesz tutaj: mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic. W trakcie ćwiczeń w programie będziemy używać tej ścieżki jako $APPLICATION_PATH.

Zwróć uwagę, że na ścieżce do aplikacji:

  • Aplikacja nosi nazwę helloworld.
  • $PACKAGE_PATH aplikacji to com.google.mediapipe.apps.basic. Jest on używany we fragmentach kodu w tym samouczku, więc pamiętaj, aby podczas kopiowania i używania fragmentów kodu używać własnych elementów $PACKAGE_PATH.

Dodaj plik activity_main.xml do folderu $APPLICATION_PATH/res/layout. Na pełnym ekranie aplikacji wyświetli się TextView z ciągiem 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>

Dodaj prosty element MainActivity.java do elementu $APPLICATION_PATH, który wczytuje zawartość układu activity_main.xml, jak poniżej:

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

Dodaj plik manifestu (AndroidManifest.xml) do $APPLICATION_PATH, który uruchamia aplikację MainActivity przy uruchomieniu aplikacji:

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

W naszej aplikacji używamy motywu Theme.AppCompat, dlatego potrzebujemy odpowiednich odwołań. Dodaj colors.xml do $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>

Dodaj styles.xml do $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>

Aby skompilować aplikację, dodaj do $APPLICATION_PATH plik BUILD. ${appName} i ${mainActivity} w pliku manifestu zostaną zastąpione ciągami określonymi w BUILD, jak pokazano poniżej.

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

Reguła android_library dodaje zależności dla MainActivity, plików zasobów i AndroidManifest.xml.

Reguła android_binary wykorzystuje wygenerowaną bibliotekę basic_lib na Androida do kompilowania binarnego pliku APK, który można zainstalować na urządzeniu z Androidem.

Aby utworzyć aplikację, użyj tego polecenia:

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

Zainstaluj wygenerowany plik APK za pomocą programu adb install. Na przykład:

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

Otwórz aplikację na urządzeniu. Powinien wyświetlić się ekran z tekstem Hello World!.

bazel_hello_world_android

Używanie aparatu na urządzeniu CameraX

Uprawnienia do aparatu

Aby użyć aparatu w naszej aplikacji, musimy poprosić użytkownika o dostęp do kamery. Aby poprosić o uprawnienia do korzystania z aparatu, dodaj do AndroidManifest.xml te elementy:

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

Zmień minimalną wersję pakietu SDK na 21 i ustaw docelową wersję pakietu SDK na 27 w tym samym pliku:

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

Dzięki temu użytkownik zostanie poproszony o zgodę na dostęp do aparatu, a my będziemy mogli używać biblioteki CameraX do uzyskiwania dostępu do kamery.

Aby poprosić o uprawnienia do korzystania z aparatu, możemy skorzystać z narzędzia dostępnego w komponentach MediaPipe Framework o nazwie PermissionHelper. Aby go użyć, dodaj zależność "//mediapipe/java/com/google/mediapipe/components:android_components" do reguły mediapipe_lib w regionie BUILD.

Aby użyć PermissionHelper w MainActivity, dodaj ten wiersz do funkcji onCreate:

PermissionHelper.checkAndRequestCameraPermissions(this);

Spowoduje to wyświetlenie na ekranie okna z prośbą o pozwolenie na korzystanie z aparatu w tej aplikacji.

Dodaj ten kod, aby obsługiwać odpowiedź użytkownika:

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

Na razie pozostawimy metodę startCamera() pustą. Gdy użytkownik odpowie na prompt, MainActivity zostanie wznowiony, a metoda onResume() zostanie wywołana. Kod potwierdzi, że uprawnienia do korzystania z kamery zostały przyznane, a następnie ją uruchomi.

Skompiluj ponownie i zainstaluj aplikację. Wyświetli się prośba o dostęp do aparatu dla aplikacji.

Dostęp do aparatu

Mając uprawnienia do aparatu, możemy uruchamiać i pobierać klatki z kamery.

Do wyświetlania klatek z aparatu użyjemy SurfaceView. Każda klatka z kamery będzie przechowywana w obiekcie SurfaceTexture. Aby z nich korzystać, musimy najpierw zmienić układ aplikacji.

Usuń cały blok kodu TextView z $APPLICATION_PATH/res/layout/activity_main.xml i zamiast tego dodaj ten kod:

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

Ten blok kodu zawiera nowy blok kodu FrameLayout o nazwie preview_display_layout i zagnieżdżony w nim element TextView o nazwie no_camera_access_preview. Jeśli nie przyznasz uprawnień dostępu do aparatu, aplikacja wyświetli TextView z komunikatem tekstowym przechowywanym w zmiennej no_camera_access. Dodaj do pliku $APPLICATION_PATH/res/values/strings.xml ten wiersz:

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

Jeśli użytkownik nie przyzna uprawnień do korzystania z aparatu, ekran będzie wyglądał tak:

missing_camera_permission_android

Teraz dodamy obiekty SurfaceTexture i SurfaceView do MainActivity:

private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;

W funkcji onCreate(Bundle) dodaj te 2 wiersze przed wysłaniem prośby o uprawnienia do korzystania z kamery:

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

A teraz dodaj kod definiujący setupPreviewDisplayView():

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

Definiujemy nowy obiekt SurfaceView i dodajemy go do obiektu preview_display_layout FrameLayout, abyśmy mogli używać go do wyświetlania klatek z aparatu za pomocą obiektu SurfaceTexture o nazwie previewFrameTexture.

Aby uzyskać klatki aparatu za pomocą previewFrameTexture, użyjemy CameraX. Framework udostępnia narzędzie o nazwie CameraXPreviewHelper umożliwiające korzystanie z CameraX. Ta klasa aktualizuje odbiornik po włączeniu kamery za pomocą onCameraStarted(@Nullable SurfaceTexture).

Aby użyć tego narzędzia, zmodyfikuj plik BUILD, aby dodać zależność "//mediapipe/java/com/google/mediapipe/components:android_camerax_helper".

Teraz zaimportuj CameraXPreviewHelper i dodaj ten wiersz do MainActivity:

private CameraXPreviewHelper cameraHelper;

Teraz możemy dodać naszą implementację do interfejsu 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);
    });
}

Spowoduje to utworzenie nowego obiektu CameraXPreviewHelper i dodanie do niego anonimowego odbiornika. Gdy cameraHelper zasygnalizuje, że kamera się uruchomiła i że dostępny jest element surfaceTexture do pobrania klatek, zapiszemy ten element (surfaceTexture) jako previewFrameTexture i udostępnimy parametr previewDisplayView, abyśmy mogli wyświetlać klatki z previewFrameTexture.

Najpierw jednak musimy zdecydować, której z nich będziemy używać. CameraXPreviewHelper dziedziczy dziedziczenie z elementu CameraHelper, które udostępnia 2 opcje: FRONT i BACK. Możemy przekazać decyzję z pliku BUILD w postaci metadanych, dzięki czemu do skompilowania kolejnej wersji aplikacji przy użyciu innego aparatu nie trzeba wprowadzać żadnych zmian w kodzie.

Zakładając, że chcemy używać kamery BACK do wykrywania krawędzi na scenie na żywo oglądanej z kamery, dodaj metadane do pliku AndroidManifest.xml:

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

i określ wybór w BUILD w regule binarnej Androida helloworld z nowym wpisem w kolumnie manifest_values:

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

Teraz w MainActivity, aby pobrać metadane określone w manifest_values, dodaj obiekt ApplicationInfo:

private ApplicationInfo applicationInfo;

Do funkcji onCreate() dodaj:

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

Teraz dodaj następujący wiersz na końcu funkcji startCamera():

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

Na tym etapie aplikacja powinna się skompilować. Jednak po uruchomieniu aplikacji na urządzeniu pojawi się czarny ekran (chociaż przyznano uprawnienia do korzystania z aparatu). Dzieje się tak, ponieważ mimo zapisywania zmiennej surfaceTexture dostarczonej przez CameraXPreviewHelper, previewSurfaceView nie używa jeszcze danych wyjściowych i nie wyświetla ich jeszcze na ekranie.

Chcemy używać klatek na wykresie MediaPipe, dlatego w tym samouczku nie dodamy kodu, który pozwala wyświetlać dane wyjściowe kamery. Zamiast tego przejdziemy dalej do sposobu przesyłania klatek z aparatu do przetworzenia na wykres MediaPipe i wyświetlania wyników wykresu na ekranie.

Konfiguracja urządzenia ExternalTextureConverter

SurfaceTexture przechwytuje klatki obrazu ze strumienia jako tekstura OpenGL ES. Aby można było użyć wykresu MediaPipe, klatki zarejestrowane przez kamerę powinny być przechowywane w zwykłym obiekcie tekstury Open GL. Platforma udostępnia klasę ExternalTextureConverter służącą do konwertowania obrazu zapisanego w obiekcie SurfaceTexture na zwykły obiekt tekstury OpenGL.

Aby można było używać ExternalTextureConverter, potrzebny jest też obiekt EGLContext, który jest tworzony i zarządzany przez obiekt EglManager. Dodaj zależność do pliku BUILD, aby użyć właściwości EglManager, "//mediapipe/java/com/google/mediapipe/glutil".

W MainActivity dodaj te deklaracje:

private EglManager eglManager;
private ExternalTextureConverter converter;

W funkcji onCreate(Bundle) dodaj instrukcję inicjowania obiektu eglManager przed zażądaniem uprawnień do aparatu:

eglManager = new EglManager(null);

Przypomnijmy, że zdefiniowaliśmy funkcję onResume() w polu MainActivity, aby potwierdzić przyznanie uprawnień do kamery i wywołać startCamera(). Przed sprawdzeniem dodaj w pliku onResume() ten wiersz, aby zainicjować obiekt converter:

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

Ten element converter korzysta teraz z konta GLContext zarządzanego przez eglManager.

Musimy też zastąpić funkcję onPause() w MainActivity, aby jeśli aplikacja została wstrzymana, prawidłowo zamknęliśmy converter:

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

Aby przenieść dane wyjściowe funkcji previewFrameTexture do converter, dodaj ten blok kodu do funkcji 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) {}
     });

W tym bloku kodu dodajemy niestandardowy SurfaceHolder.Callback do previewDisplayView i implementujemy funkcję surfaceChanged(SurfaceHolder holder, int format, int width, int height), która pozwala obliczyć odpowiedni rozmiar wyświetlanych klatek na ekranie urządzenia oraz powiązać obiekt previewFrameTexture i wysyłać klatki obliczonych elementów displaySize do converter.

Teraz możesz użyć ramek aparatu na wykresie MediaPipe.

Korzystanie z wykresu MediaPipe na Androidzie

Dodaj odpowiednie zależności

Aby użyć wykresu MediaPipe, musimy dodać zależności do platformy MediaPipe na Androidzie. Najpierw dodamy regułę kompilacji, aby utworzyć cc_binary za pomocą kodu JNI platformy MediaPipe, a następnie utworzymy regułę cc_library, która będzie wykorzystywała ten plik binarny w naszej aplikacji. Dodaj do pliku BUILD ten blok kodu:

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

Dodaj zależność ":mediapipe_jni_lib" do reguły kompilacji mediapipe_lib w pliku BUILD.

Następnie musimy dodać zależności specyficzne do wykresu MediaPipe, którego chcesz użyć w aplikacji.

Najpierw dodaj zależności do całego kodu kalkulatora w regule kompilacji libmediapipe_jni.so:

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

Wykresy MediaPipe mają format .pbtxt, ale aby użyć ich w aplikacji, musimy wygenerować plik .binarypb za pomocą reguły kompilacji mediapipe_binary_graph.

W regule kompilacji binarnej helloworld na Androida dodaj cel mediapipe_binary_graph właściwy dla wykresu jako zasób:

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

W regule kompilacji assets możesz też dodać inne zasoby, np. modele TensorFlowLite używane na wykresie.

Dodaj też dodatkowe manifest_values dla właściwości specyficznych dla wykresu, które będą pobierane później w tabeli 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",
},

Pamiętaj, że binaryGraphName wskazuje nazwę pliku wykresu binarnego, zgodnie z polem output_name w celu mediapipe_binary_graph. inputVideoStreamName i outputVideoStreamName to nazwy wejściowego i wyjściowego strumienia wideo określone na wykresie.

Teraz MainActivity musi wczytać platformę MediaPipe. Dodatkowo platforma korzysta z OpenCV, więc MainActvity powinna też ładować tag OpenCV. Aby wczytywać obie zależności, użyj tego kodu w obiekcie MainActivity (w klasie, ale nie w żadnej funkcji):

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

Użyj wykresu w aplikacji MainActivity

Najpierw musimy wczytać zasób, który zawiera pole .binarypb skompilowane z pliku .pbtxt wykresu. W tym celu możemy użyć narzędzia MediaPipe AndroidAssetUtil.

Przed zainicjowaniem eglManager zainicjuj menedżera zasobów w: onCreate(Bundle):

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

Teraz musimy skonfigurować obiekt FrameProcessor, który wysyła do wykresu MediaPipe klatki aparatu przygotowane przez converter, a potem uruchamia wykres, przygotowuje dane wyjściowe, a następnie aktualizuje previewDisplayView, aby je wyświetlić. Dodaj ten kod, aby zadeklarować FrameProcessor:

private FrameProcessor processor;

i zainicjuj ją w onCreate(Bundle) po zainicjowaniu eglManager:

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

Element processor musi przetworzyć przekonwertowane klatki z komponentu converter. Po zainicjowaniu converter dodaj do onResume() ten wiersz:

converter.setConsumer(processor);

processor powinien wysyłać dane wyjściowe do previewDisplayView. Aby to zrobić, dodaj do naszego niestandardowego elementu SurfaceHolder.Callback te definicje funkcji:

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

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

Po utworzeniu elementu SurfaceHolder mieliśmy Surface do VideoSurfaceOutput processor. Po zniszczeniu usuwamy go z VideoSurfaceOutput processor.

Gotowe! Teraz aplikacja powinna być skompilowana i uruchomiona na urządzeniu, a na ekranie obrazu z kamery będzie działać wykrywanie brzegów Sobel Edge. Gratulacje!

edge_detection_android_gpu_gif

Jeśli napotkasz jakieś problemy, przeczytaj pełny kod samouczka tutaj.