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.
Configurazione
- Installa MediaPipe Framework sul tuo sistema, consulta la guida all'installazione di Framework per maggiori dettagli.
- Installa l'SDK Android Development e Android NDK. Scopri come fare anche nella [Guida all'installazione di Framework].
- Attiva le opzioni sviluppatore sul tuo dispositivo Android.
- 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:
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!
.
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:
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!
Se hai riscontrato problemi, consulta il codice completo del tutorial qui.