Hello World! su Android

Introduzione

Ciao mondo! utilizza il framework MediaPipe per sviluppare un'applicazione Android che esegue un grafico MediaPipe su Android.

Cosa creerai

Una semplice app della fotocamera per il rilevamento dei bordi Sobel in tempo reale applicato a un video in diretta trasmettere in streaming su un dispositivo Android.

edge_detection_android_gpu_gif

Configurazione

  1. Installa il framework MediaPipe sul tuo sistema, vedi Installazione di framework. Google Cloud.
  2. Installa l'SDK Android Development e Android NDK. Scopri come farlo anche nella [Guida all'installazione del 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 seguente grafico, 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 flusso di input denominato input_video per tutti i frame in entrata fornito dalla fotocamera del dispositivo.

Il primo nodo del grafico, LuminanceCalculator, accetta un singolo pacchetto (immagine frame) e applica una variazione di luminanza utilizzando uno shaker OpenGL. Il risultato frame immagine viene inviato allo stream di output luma_video.

Il secondo nodo, SobelEdgesCalculator, applica il rilevamento perimetrale alla rete di pacchetti nel flusso luma_video e restituisce l'output output_video flusso di dati.

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

Configurazione iniziale minima dell'applicazione

Iniziamo con una semplice applicazione per Android che mostra "Hello World!". sullo schermo. Puoi saltare questo passaggio se hai familiarità con lo sviluppo di Android che utilizzano bazel.

Crea una nuova directory in cui creerai la tua applicazione Android. Per esempio, il codice completo di questo tutorial è disponibile all'indirizzo mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic. Lo faremo fai riferimento a questo percorso come $APPLICATION_PATH in tutto il codelab.

Tieni presente che nel percorso dell'applicazione:

  • L'applicazione è denominata helloworld.
  • Il $PACKAGE_PATH dell'applicazione è com.google.mediapipe.apps.basic. Viene utilizzato negli snippet di codice di questo tutorial. Ricordati di usare il tuo $PACKAGE_PATH quando copi o utilizzi gli snippet di codice.

Aggiungi un file activity_main.xml a $APPLICATION_PATH/res/layout. Vengono visualizzati A 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 semplice MainActivity.java a $APPLICATION_PATH per caricare 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>

Nell'applicazione utilizziamo un tema Theme.AppCompat, quindi dobbiamo riferimenti ai temi appropriati. 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 e ${appName} e ${mainActivity} nel manifest saranno sostituiti da stringhe specificato 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 usa la libreria Android basic_lib generata per creare un APK binario per l'installazione sul tuo dispositivo Android.

Per creare l'app, usa questo comando:

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 di accesso alla fotocamera

Per utilizzare la fotocamera nella nostra applicazione, dobbiamo chiedere 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" />

Modifica la versione minima dell'SDK in 21 e la versione target dell'SDK in 27 nelle stesso file:

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

Ciò assicura che all'utente venga chiesto di richiedere l'autorizzazione di accesso alla fotocamera e attiva utilizzare la libreria CameraX per l'accesso alla fotocamera.

Per richiedere le autorizzazioni di accesso alla fotocamera, possiamo usare un'utilità fornita da MediaPipe Framework ossia PermissionHelper. Per utilizzarla, aggiungi una dipendenza "//mediapipe/java/com/google/mediapipe/components:android_components" in 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 usa la fotocamera in questa applicazione.

Aggiungi il codice seguente 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 lasceremo vuoto il metodo startCamera(). Quando l'utente risponde al prompt, MainActivity riprenderà e onResume() verrà chiamato. Il codice confermerà che sono state concesse le autorizzazioni per l'utilizzo della fotocamera, e poi avvierà la fotocamera.

Ricrea e installa l'applicazione. A questo punto dovresti vedere una richiesta l'accesso alla fotocamera per l'applicazione.

Accesso alla fotocamera

Con le autorizzazioni di accesso alla fotocamera disponibili, possiamo avviare e recuperare fotogrammi dal fotocamera.

Per visualizzare i fotogrammi dalla fotocamera utilizzeremo un'icona SurfaceView. Ogni frame verranno archiviati in un oggetto SurfaceTexture. Per utilizzarle, devi prima modificare il layout della nostra applicazione.

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

<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 una nuova FrameLayout denominata preview_display_layout e un TextView nidificato al suo interno, denominato no_camera_access_preview. Quando la fotocamera permessi di accesso non sono concessi, la nostra applicazione mostrerà TextView con un messaggio di stringa, memorizzato nella variabile no_camera_access. Aggiungi la seguente riga al 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 alla fotocamera, sullo schermo appare il seguente aspetto: questo:

missing_camera_permission_android

Ora aggiungeremo gli oggetti SurfaceTexture e SurfaceView MainActivity:

private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;

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

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

E 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 al preview_display_layout oggetto FrameLayout in modo da poterlo utilizzare per visualizzare l'inquadratura della fotocamera utilizzando un oggetto SurfaceTexture denominato previewFrameTexture.

Per usare previewFrameTexture per ottenere cornici della fotocamera, 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 in modo da 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 a cui viene aggiunto un nome un listener sull'oggetto. Quando cameraHelper segnala che la videocamera è stata avviata e surfaceTexture per acquisire i frame, lo salviamo surfaceTexture come previewFrameTexture e imposta previewDisplayView visibile per poter iniziare a vedere i frame di previewFrameTexture.

Prima di avviare la videocamera, però, dobbiamo decidere quale per gli utilizzi odierni. CameraXPreviewHelper eredita da CameraHelper che fornisce due opzioni, FRONT e BACK. Possiamo trasmettere la decisione dal file BUILD come i metadati in modo che non sia richiesta alcuna modifica al codice per creare un'altra versione utilizzando un'altra videocamera.

Supponendo di voler utilizzare BACK videocamera per eseguire il rilevamento perimetrale 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 correttamente. Tuttavia, quando esegui sull'applicazione sul tuo dispositivo, vedrai una schermata nera (anche se la fotocamera autorizzazioni concesse). Questo perché anche se salviamo Variabile surfaceTexture fornita da CameraXPreviewHelper, la variabile previewSurfaceView non usa ancora il suo output e lo visualizza sullo schermo.

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

Configurazione di ExternalTextureConverter

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

Per utilizzare ExternalTextureConverter, abbiamo bisogno anche di un EGLContext, che è vengono create e gestite da un oggetto EglManager. Aggiungi una dipendenza a 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 Oggetto eglManager prima di richiedere le autorizzazioni di accesso alla fotocamera:

eglManager = new EglManager(null);

Ricorda che abbiamo definito la funzione onResume() in MainActivity per confermare Sono state concesse le autorizzazioni di accesso alla fotocamera. Chiama startCamera(). Prima verifica, aggiungi la seguente riga in onResume() per inizializzare converter :

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

Questo converter ora utilizza il GLContext gestito da eglManager.

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

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

Per indirizzare l'output di previewFrameTexture a converter, aggiungi il token seguente blocco di codice per 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 una SurfaceHolder.Callback personalizzata previewDisplayView e implementa la funzione surfaceChanged(SurfaceHolder holder, int format, int width, int height) per calcolare una dimensione di visualizzazione appropriata dei fotogrammi della fotocamera sullo schermo del dispositivo e per collegare previewFrameTexture e inviare i frame del valore displaySize calcolato a converter.

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

Utilizzo di un grafico MediaPipe in Android

Aggiungi dipendenze pertinenti

Per usare un grafico MediaPipe, dobbiamo aggiungere dipendenze al framework MediaPipe su Android. Aggiungeremo prima una regola di build per creare un cc_binary utilizzando il codice JNI del framework MediaPipe e poi crea una regola cc_library per utilizzare questo file binario nella nostra applicazione. Aggiungi il seguente blocco di codice al 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 in il file BUILD.

Poi dobbiamo aggiungere dipendenze specifiche per il grafico MediaPipe da utilizzare. nell'applicazione.

Innanzitutto, aggiungi dipendenze a tutto il codice del calcolatore in libmediapipe_jni.so regola di build:

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

I grafici MediaPipe sono file .pbtxt, ma per utilizzarli nell'applicazione, abbiamo bisogno per utilizzare la regola di build mediapipe_binary_graph per generare un file .binarypb.

Nella regola della build binaria Android helloworld, aggiungi mediapipe_binary_graph target specifico per il grafico sotto forma di risorsa:

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

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

Inoltre, aggiungi altri manifest_values per le proprietà specifiche di grafico, da recuperare successivamente 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 grafo binario, determinato dal campo output_name nel target mediapipe_binary_graph. inputVideoStreamName e outputVideoStreamName sono l'input e l'output il nome dello stream video specificato rispettivamente nel grafico.

Ora MainActivity deve caricare il framework MediaPipe. Inoltre, utilizza OpenCV, quindi anche MainActvity dovrebbe caricare OpenCV. Utilizza la 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

Per prima cosa dobbiamo caricare l'asset che contiene il .binarypb compilato da il file .pbtxt del grafico. Per farlo, possiamo usare un'utilità MediaPipe, AndroidAssetUtil

Inizializza il gestore asset in onCreate(Bundle) prima dell'inizializzazione 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 invii i fotogrammi della videocamera preparato da converter al grafico MediaPipe ed esegue il grafico, si prepara l'output e quindi aggiorna previewDisplayView per visualizzarlo. Aggiungi il seguente codice per dichiarare FrameProcessor:

private FrameProcessor processor;

e inizializzalo in onCreate(Bundle) dopo aver inizializzato 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 e l'elaborazione dei dati. Aggiungi la seguente riga a onResume() dopo l'inizializzazione dell'evento converter:

converter.setConsumer(processor);

processor dovrebbe inviare il proprio output a previewDisplayView. Per farlo, aggiungi le seguenti definizioni di 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 è stato creato SurfaceHolder, avevamo Surface allo VideoSurfaceOutput di processor. Quando viene distrutta, la rimuoviamo il VideoSurfaceOutput di processor.

È tutto. Ora dovresti essere in grado di creare ed eseguire sul dispositivo e vedrai il rilevamento dei bordi di Sobel in esecuzione su una videocamera in diretta feed! Complimenti!

edge_detection_android_gpu_gif

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