Hello World! su Android

Introduzione

Questo tutorial di Hello World! utilizza MediaPipe Framework per sviluppare un'applicazione per Android che esegue un grafico MediaPipe su Android.

Cosa creerai

Una semplice app della videocamera per il rilevamento dei bordi Sobel in tempo reale applicata a uno stream video in diretta su un dispositivo Android.

edge_detection_android_gpu_gif

Configurazione

  1. Installa MediaPipe Framework sul tuo sistema, consulta la guida all'installazione di Framework per maggiori dettagli.
  2. Installa l'SDK Android Development e Android NDK. Scopri come fare anche nella [Guida all'installazione di Framework].
  3. Attiva le opzioni sviluppatore sul tuo dispositivo Android.
  4. Configura Bazel sul tuo sistema per creare e implementare l'app per Android.

Grafico per il rilevamento dei bordi

Utilizzeremo il grafico seguente, 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"
}

Di seguito è riportata una visualizzazione del grafico:

edge_detection_mobile_gpu

Questo grafico ha un singolo stream di input denominato input_video per tutti i frame in arrivo che verranno forniti dalla videocamera del dispositivo.

Il primo nodo nel grafico, LuminanceCalculator, accetta un singolo pacchetto (frame immagine) e applica una variazione di luminanza utilizzando uno Shader OpenGL. Il frame immagine risultante viene inviato al flusso di output luma_video.

Il secondo nodo, SobelEdgesCalculator, applica il rilevamento perimetrale ai pacchetti in entrata nel flusso luma_video e restituisce come output il flusso di output output_video.

La nostra applicazione Android mostrerà i frame immagine di output dello stream output_video.

Configurazione minima dell'applicazione iniziale

Iniziamo con una semplice applicazione Android che visualizzi "Hello World!" sullo schermo. Puoi saltare questo passaggio se hai dimestichezza con la creazione di app per Android utilizzando bazel.

Crea una nuova directory in cui creerai la tua applicazione Android. Ad esempio, il codice completo di questo tutorial è disponibile all'indirizzo mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic. In tutto il codelab, questo percorso verrà chiamato $APPLICATION_PATH.

Tieni presente che nel percorso dell'applicazione:

  • Il nome dell'applicazione è helloworld.
  • Il $PACKAGE_PATH dell'applicazione è com.google.mediapipe.apps.basic. Viene utilizzato negli snippet di codice in questo tutorial, pertanto ricordati di usare il tuo $PACKAGE_PATH quando copi e utilizzi gli snippet di codice.

Aggiungi un file activity_main.xml a $APPLICATION_PATH/res/layout. Viene visualizzato un simbolo TextView a schermo intero dell'applicazione con la stringa 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>

Aggiungi un elemento MainActivity.java semplice a $APPLICATION_PATH che carichi i contenuti del layout activity_main.xml come mostrato di seguito:

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

Aggiungi un file manifest, AndroidManifest.xml a $APPLICATION_PATH, che avvia MainActivity all'avvio dell'applicazione:

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

Nella nostra applicazione usiamo un tema Theme.AppCompat, perciò abbiamo bisogno di riferimenti appropriati ai temi. Aggiungi colors.xml a $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>

Aggiungi styles.xml a $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>

Per creare l'applicazione, aggiungi un file BUILD a $APPLICATION_PATH; ${appName} e ${mainActivity} nel file manifest verranno sostituiti da stringhe specificate in BUILD, come mostrato di seguito.

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

La regola android_library aggiunge dipendenze per MainActivity, file di risorse e AndroidManifest.xml.

La regola android_binary utilizza la libreria Android basic_lib generata per creare un APK binario da installare sul tuo dispositivo Android.

Per creare l'app, utilizza il comando seguente:

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

Installa il file APK generato utilizzando adb install. Ad esempio:

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

Apri l'applicazione sul dispositivo. Dovrebbe essere visualizzata una schermata con il testo Hello World!.

bazel_hello_world_android

Utilizzo della fotocamera tramite CameraX

Autorizzazioni fotocamera

Per utilizzare la fotocamera nella nostra applicazione, dobbiamo richiedere all'utente di fornire l'accesso alla fotocamera. Per richiedere le autorizzazioni di accesso alla fotocamera, aggiungi quanto segue a AndroidManifest.xml:

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

Cambia la versione minima dell'SDK in 21 e la versione target dell'SDK in 27 nello stesso file:

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

Questo assicura che all'utente venga chiesto di richiedere l'autorizzazione della fotocamera e ci permette di usare la libreria CameraX per l'accesso alla fotocamera.

Per richiedere le autorizzazioni della fotocamera, possiamo utilizzare un'utilità fornita dai componenti del framework MediaPipe, ovvero PermissionHelper. Per utilizzarla, aggiungi una dipendenza "//mediapipe/java/com/google/mediapipe/components:android_components" nella regola mediapipe_lib in BUILD.

Per utilizzare PermissionHelper in MainActivity, aggiungi la seguente riga alla funzione onCreate:

PermissionHelper.checkAndRequestCameraPermissions(this);

L'utente visualizza una finestra di dialogo sullo schermo per richiedere le autorizzazioni per utilizzare la fotocamera in questa applicazione.

Aggiungi il seguente codice per gestire la risposta dell'utente:

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

Per il momento lasciamo vuoto il metodo startCamera(). Quando l'utente risponderà alla richiesta, MainActivity riprenderà e onResume() verrà chiamato. Il codice confermerà che le autorizzazioni per l'utilizzo della videocamera sono state concesse e avvia la fotocamera.

Ricrea e installa l'applicazione. A questo punto dovresti vedere un messaggio che richiede l'accesso alla fotocamera per l'applicazione.

Accesso alla fotocamera

Con le autorizzazioni della fotocamera disponibili, possiamo avviare e recuperare i fotogrammi dalla fotocamera.

Per visualizzare le cornici della fotocamera, useremo una SurfaceView. Ogni fotogramma della videocamera viene archiviato in un oggetto SurfaceTexture. Per utilizzarli, dobbiamo prima modificare il layout della nostra applicazione.

Rimuovi l'intero blocco di codice TextView da $APPLICATION_PATH/res/layout/activity_main.xml e aggiungi invece il seguente codice:

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

Questo blocco di codice ha un nuovo FrameLayout denominato preview_display_layout e un elemento TextView nidificato al suo interno, denominato no_camera_access_preview. Quando le autorizzazioni di accesso alla fotocamera non vengono concesse, la nostra applicazione mostrerà TextView con un messaggio stringa, memorizzato nella variabile no_camera_access. Aggiungi la seguente riga nel file $APPLICATION_PATH/res/values/strings.xml:

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

Se l'utente non concede l'autorizzazione di accesso alla fotocamera, la schermata avrà il seguente aspetto:

missing_camera_permission_android

Ora aggiungeremo gli oggetti SurfaceTexture e SurfaceView a MainActivity:

private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;

Nella funzione onCreate(Bundle), aggiungi le due righe seguenti prima di richiedere le autorizzazioni di accesso alla fotocamera:

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

Ora aggiungi il codice che definisce setupPreviewDisplayView():

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

Definiamo un nuovo oggetto SurfaceView e lo aggiungiamo all'oggetto preview_display_layout FrameLayout in modo da poterlo utilizzare per visualizzare i fotogrammi della fotocamera utilizzando un oggetto SurfaceTexture denominato previewFrameTexture.

Se vuoi usare previewFrameTexture per ottenere le cornici delle fotocamere, useremo CameraX. Framework fornisce un'utilità denominata CameraXPreviewHelper per utilizzare CameraX. Questo corso aggiorna un listener quando la videocamera viene avviata tramite onCameraStarted(@Nullable SurfaceTexture).

Per utilizzare questa utilità, modifica il file BUILD per aggiungere una dipendenza a "//mediapipe/java/com/google/mediapipe/components:android_camerax_helper".

Ora importa CameraXPreviewHelper e aggiungi la seguente riga a MainActivity:

private CameraXPreviewHelper cameraHelper;

Ora possiamo aggiungere la nostra implementazione a 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);
    });
}

Viene creato un nuovo oggetto CameraXPreviewHelper e viene aggiunto un listener anonimo all'oggetto. Quando cameraHelper segnala che la videocamera si è avviata e che è disponibile un surfaceTexture per recuperare i fotogrammi, salviamo il valore surfaceTexture come previewFrameTexture e rendiamo visibile previewDisplayView per poter iniziare a vedere i frame da previewFrameTexture.

Tuttavia, prima di avviare la fotocamera, dobbiamo decidere quale video utilizzare. CameraXPreviewHelper eredita da CameraHelper, che offre due opzioni, FRONT e BACK. Possiamo trasmettere la decisione dal file BUILD come metadati in modo che non sia necessario apportare modifiche al codice per creare un'altra versione dell'app utilizzando una fotocamera diversa.

Supponendo di voler utilizzare la videocamera BACK per eseguire il rilevamento dei bordi su una scena dal vivo che visualizziamo dalla videocamera, aggiungi i metadati in AndroidManifest.xml:

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

e specifica la selezione in BUILD nella regola binaria Android helloworld con una nuova voce in manifest_values:

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

Ora, in MainActivity per recuperare i metadati specificati in manifest_values, aggiungi un oggetto ApplicationInfo:

private ApplicationInfo applicationInfo;

Nella funzione onCreate(), aggiungi:

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

Ora aggiungi la seguente riga alla fine della funzione startCamera():

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

A questo punto, l'applicazione dovrebbe essere creata. Tuttavia, quando esegui l'applicazione sul tuo dispositivo, viene visualizzata una schermata nera (anche se sono state concesse le autorizzazioni della fotocamera). Questo perché, anche se salviamo la variabile surfaceTexture fornita da CameraXPreviewHelper, previewSurfaceView non utilizza ancora l'output e non la mostra ancora sullo schermo.

Poiché vogliamo utilizzare i frame in un grafico MediaPipe, non aggiungeremo codice per visualizzare l'output della fotocamera direttamente in questo tutorial. Passiamo invece a come inviare i frame della videocamera per l'elaborazione a un grafico MediaPipe e visualizzare l'output del grafico sullo schermo.

Configurazione di ExternalTextureConverter

Un elemento SurfaceTexture acquisisce i frame immagine da uno stream come texture OpenGL ES. Per utilizzare un grafico MediaPipe, i fotogrammi acquisiti dalla fotocamera devono essere archiviati in un normale oggetto texture Open GL. Il framework fornisce una classe ExternalTextureConverter per convertire l'immagine archiviata in un oggetto SurfaceTexture in un normale oggetto texture OpenGL.

Per utilizzare ExternalTextureConverter, abbiamo anche bisogno di un EGLContext, che viene creato e gestito da un oggetto EglManager. Aggiungi una dipendenza al file BUILD per utilizzare EglManager, "//mediapipe/java/com/google/mediapipe/glutil".

In MainActivity, aggiungi le seguenti dichiarazioni:

private EglManager eglManager;
private ExternalTextureConverter converter;

Nella funzione onCreate(Bundle), aggiungi un'istruzione per inizializzare l'oggetto eglManager prima di richiedere le autorizzazioni di accesso alla fotocamera:

eglManager = new EglManager(null);

Abbiamo definito la funzione onResume() in MainActivity per confermare che sono state concesse le autorizzazioni di accesso alla fotocamera e chiamare startCamera(). Prima di questo controllo, aggiungi la seguente riga in onResume() per inizializzare l'oggetto converter:

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

Questo converter ora utilizza lo strumento GLContext gestito da eglManager.

Dobbiamo anche eseguire l'override della funzione onPause() in MainActivity per in modo che, se l'applicazione entra in stato di pausa, chiudiamo converter correttamente:

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

Per reindirizzare l'output di previewFrameTexture a converter, aggiungi il seguente blocco di codice a 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) {}
     });

In questo blocco di codice, aggiungiamo un elemento SurfaceHolder.Callback personalizzato a previewDisplayView e implementiamo la funzione surfaceChanged(SurfaceHolder holder, int format, int width, int height) per calcolare le dimensioni di visualizzazione appropriate dei fotogrammi della videocamera sullo schermo del dispositivo, nonché per collegare l'oggetto previewFrameTexture e inviare i frame del valore displaySize calcolato a converter.

Ora siamo pronti per utilizzare i frame della fotocamera in un grafico MediaPipe.

Utilizzo di un grafico MediaPipe in Android

Aggiungi dipendenze pertinenti

Per utilizzare un grafico MediaPipe, dobbiamo aggiungere dipendenze al framework MediaPipe su Android. Prima aggiungeremo una regola di build per creare un cc_binary utilizzando il codice JNI del framework MediaPipe, quindi creeremo una regola cc_library per utilizzare questo programma binario nella nostra applicazione. Aggiungi il seguente blocco di codice al tuo file 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,
)

Aggiungi la dipendenza ":mediapipe_jni_lib" alla regola di build mediapipe_lib nel file BUILD.

Ora dobbiamo aggiungere dipendenze specifiche per il grafico MediaPipe che vogliamo utilizzare nell'applicazione.

Innanzitutto, aggiungi le dipendenze a tutto il codice della calcolatrice nella regola di compilazione libmediapipe_jni.so:

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

I grafici MediaPipe sono file .pbtxt, ma per utilizzarli nell'applicazione dobbiamo usare la regola di compilazione mediapipe_binary_graph per generare un file .binarypb.

Nella regola di build binaria di Android helloworld, aggiungi il target mediapipe_binary_graph specifico del grafico come asset:

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

Nella regola di build assets puoi anche aggiungere altri asset, come i modelli TensorFlowLite utilizzati nel grafico.

Inoltre, aggiungi altri valori manifest_values per le proprietà specifiche del grafico, da recuperare in un secondo momento in 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",
},

Tieni presente che binaryGraphName indica il nome del file del grafico binario, determinato dal campo output_name nella destinazione mediapipe_binary_graph. inputVideoStreamName e outputVideoStreamName sono i nomi dello stream video di input e di output specificati rispettivamente nel grafico.

Ora l'elemento MainActivity deve caricare il framework MediaPipe. Inoltre, il framework utilizza OpenCV, quindi MainActvity dovrebbe caricare anche OpenCV. Usa il seguente codice in MainActivity (all'interno della classe, ma non all'interno di alcuna funzione) per caricare entrambe le dipendenze:

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

Usa il grafico in MainActivity

Innanzitutto, dobbiamo caricare l'asset contenente il valore .binarypb compilato dal file .pbtxt del grafico. A questo scopo, possiamo utilizzare un'utilità MediaPipe, AndroidAssetUtil.

Inizializza asset manager in onCreate(Bundle) prima di inizializzare eglManager:

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

Ora dobbiamo configurare un oggetto FrameProcessor che invia i frame della videocamera preparati dall'converter al grafico MediaPipe ed esegue il grafico, prepara l'output e aggiorna previewDisplayView per visualizzare l'output. Aggiungi il seguente codice per dichiarare FrameProcessor:

private FrameProcessor processor;

e inizializzalo in onCreate(Bundle) dopo l'inizializzazione di eglManager:

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

processor deve consumare i frame convertiti da converter per l'elaborazione. Aggiungi la seguente riga a onResume() dopo aver inizializzato converter:

converter.setConsumer(processor);

processor deve inviare il proprio output a previewDisplayView. A questo scopo, aggiungi le seguenti definizioni della funzione alla nostra SurfaceHolder.Callback personalizzata:

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

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

Quando viene creato SurfaceHolder, avevamo il Surface al VideoSurfaceOutput del processor. Quando viene eliminata, la rimuoviamo dall'elemento VideoSurfaceOutput del processor.

e il gioco è fatto. Ora dovresti essere in grado di creare ed eseguire correttamente l'applicazione sul dispositivo e vedere il rilevamento dei bordi di Sobel in esecuzione su un feed della videocamera in diretta. Congratulazioni!

edge_detection_android_gpu_gif

Se hai riscontrato problemi, consulta il codice completo del tutorial qui.