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.
Configurazione
- Installa il framework MediaPipe sul tuo sistema, vedi Installazione di framework. Google Cloud.
- Installa l'SDK Android Development e Android NDK. Scopri come farlo anche nella [Guida all'installazione del 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 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:
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!
.
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:
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!
Se hai riscontrato problemi, consulta il codice completo del tutorial qui