Introduction
Bonjour le monde ! utilise MediaPipe pour développer une application Android exécute un graphique MediaPipe sur Android.
Objectifs de l'atelier
Application d'appareil photo simple pour la détection en temps réel des bords Sobel à une vidéo en direct diffuser en streaming sur un appareil Android.
Configuration
- Installez MediaPipe Framework sur votre système, consultez la section Installation de Framework guide pour plus d'informations.
- Installez le SDK Android Development et le NDK Android. Pour savoir comment procéder, consultez [Guide d'installation du framework].
- Activez les options pour les développeurs sur votre appareil Android.
- Configurez Bazel sur votre système pour créer et déployer l'application Android.
Graphique de détection des bords
Nous utiliserons le graphique suivant, 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"
}
Voici une visualisation du graphique:
Ce graphique comporte un seul flux d'entrée nommé input_video
pour tous les frames entrants
qui sera fournie par l'appareil photo de votre appareil.
Le premier nœud du graphique, LuminanceCalculator
, accepte un seul paquet (image
cadre) et applique un changement de luminance à l'aide d'un nuanceur OpenGL. Le résultat
la trame d'image est envoyée au flux de sortie luma_video
.
Le deuxième nœud, SobelEdgesCalculator
, applique la détection en périphérie aux
paquets dans le flux luma_video
et génère une sortie output_video
flux.
Notre application Android affiche les images de sortie du
Flux output_video
.
Configuration initiale de l'application minimale
Commençons par une application Android simple qui affiche le message "Hello World!".
à l'écran. Vous pouvez ignorer cette étape si vous savez créer des applications Android
applications à l'aide de bazel
.
Créez un répertoire dans lequel vous allez créer votre application Android. Pour
exemple, le code complet de ce didacticiel est disponible à l'adresse
mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic
Nous
désigner ce chemin par $APPLICATION_PATH
tout au long de l'atelier de programmation.
Notez que dans le chemin d'accès à l'application:
- L'application est nommée
helloworld
. - Le
$PACKAGE_PATH
de l'application estcom.google.mediapipe.apps.basic
. Il est utilisé dans les extraits de code de ce tutoriel. N'oubliez donc pas d'utiliser votre propre$PACKAGE_PATH
lorsque vous copiez/utilisez les extraits de code.
Ajoutez un fichier activity_main.xml
à $APPLICATION_PATH/res/layout
. Cela permet d'afficher
Un TextView
en plein écran de l'application avec la chaîne 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>
Ajoutez un élément MainActivity.java
simple à $APPLICATION_PATH
pour charger le contenu.
de la mise en page activity_main.xml
, comme indiqué ci-dessous:
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);
}
}
Ajoutez un fichier manifeste, AndroidManifest.xml
à $APPLICATION_PATH
, qui
lance MainActivity
au démarrage de l'application:
<?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>
Dans notre application, nous utilisons un thème Theme.AppCompat
. Nous devons donc
les références
appropriées au thème. Ajouter colors.xml
à
$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>
Ajoutez styles.xml
à $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>
Pour compiler l'application, ajoutez un fichier BUILD
à $APPLICATION_PATH
, puis
${appName}
et ${mainActivity}
dans le fichier manifeste seront remplacés par des chaînes
spécifié dans BUILD
, comme indiqué ci-dessous.
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 règle android_library
ajoute des dépendances pour MainActivity
et les fichiers de ressources
et AndroidManifest.xml
.
La règle android_binary
utilise la bibliothèque Android basic_lib
générée pour
compiler un APK binaire pour l'installer sur votre appareil Android.
Pour créer l'application, exécutez la commande suivante:
bazel build -c opt --config=android_arm64 $APPLICATION_PATH:helloworld
Installez le fichier APK généré à l'aide de adb install
. Exemple :
adb install bazel-bin/$APPLICATION_PATH/helloworld.apk
Ouvrez l'application sur votre appareil. Un écran avec le texte
Hello World!
Utilisation de l'appareil photo via CameraX
Autorisations de l'appareil photo
Pour utiliser l'appareil photo dans notre application, nous devons demander à l'utilisateur de fournir
l'accès à l'appareil photo. Pour demander l'autorisation d'accéder à l'appareil photo, ajoutez le code suivant à
AndroidManifest.xml
:
<!-- For using the camera -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
Remplacez la version minimale du SDK par 21
et la version cible du SDK 27
dans le
même fichier:
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="27" />
Cela garantit que l'utilisateur est invité à demander l'autorisation d'accéder à l'appareil photo et active d'utiliser la bibliothèque CameraX pour accéder à l'appareil photo.
Pour demander l'autorisation d'accéder à l'appareil photo, nous pouvons utiliser un utilitaire fourni par MediaPipe Framework.
à savoir PermissionHelper
. Pour l'utiliser, ajoutez une dépendance
"//mediapipe/java/com/google/mediapipe/components:android_components"
dans la
Règle mediapipe_lib
dans BUILD
.
Pour utiliser PermissionHelper
dans MainActivity
, ajoutez la ligne suivante au
Fonction onCreate
:
PermissionHelper.checkAndRequestCameraPermissions(this);
Une boîte de dialogue s'affiche alors à l'écran pour demander l'autorisation d'accéder à utiliser l'appareil photo dans cette application.
Ajoutez le code suivant pour gérer la réponse de l'utilisateur:
@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() {}
Nous allons laisser la méthode startCamera()
vide pour le moment. Réponse de l'utilisateur
à l'invite, MainActivity
reprend et onResume()
est appelé.
Le code confirme que les autorisations d'utilisation de l'appareil photo ont été accordées
avant de démarrer la caméra.
Recompilez et installez l'application. Vous devriez maintenant voir une invite demandant l'accès à l'appareil photo pour l'application.
Accès à l'appareil photo
Lorsque les autorisations d'accès à l'appareil photo sont disponibles, nous pouvons démarrer et récupérer des images caméra.
Pour afficher les images de l'appareil photo, nous allons utiliser un SurfaceView
. Chaque image
de l'appareil photo sont stockés dans un objet SurfaceTexture
. Pour les utiliser, nous
nous devons d'abord modifier
la mise en page de notre application.
Supprimez l'intégralité du bloc de code TextView
du
$APPLICATION_PATH/res/layout/activity_main.xml
et ajoutez le code suivant :
à la place:
<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>
Ce bloc de code comporte un nouveau FrameLayout
nommé preview_display_layout
et un
TextView
imbriqué à l'intérieur, nommé no_camera_access_preview
. Lorsque l'appareil photo
les autorisations d'accès ne sont pas accordées, l'application affiche
TextView
par un message de chaîne, stocké dans la variable no_camera_access
.
Ajoutez la ligne suivante au fichier $APPLICATION_PATH/res/values/strings.xml
:
<string name="no_camera_access" translatable="false">Please grant camera permissions.</string>
Lorsque l'utilisateur n'accorde pas l'autorisation d'accéder à l'appareil photo, l'écran ressemble maintenant à ceci:
Nous allons maintenant ajouter les objets SurfaceTexture
et SurfaceView
à
MainActivity
:
private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;
Dans la fonction onCreate(Bundle)
, ajoutez les deux lignes suivantes avant
demande l'autorisation d'accéder à l'appareil photo:
previewDisplayView = new SurfaceView(this);
setupPreviewDisplayView();
Ajoutez maintenant le code définissant setupPreviewDisplayView()
:
private void setupPreviewDisplayView() {
previewDisplayView.setVisibility(View.GONE);
ViewGroup viewGroup = findViewById(R.id.preview_display_layout);
viewGroup.addView(previewDisplayView);
}
Nous définissons un nouvel objet SurfaceView
et l'ajoutons au
preview_display_layout
FrameLayout
afin de pouvoir l'utiliser pour afficher
les images de l'appareil photo à l'aide d'un objet SurfaceTexture
nommé previewFrameTexture
.
Pour utiliser previewFrameTexture
afin d'obtenir des images de l'appareil photo, nous allons utiliser CameraX.
Le framework fournit un utilitaire nommé CameraXPreviewHelper
permettant d'utiliser CameraX.
Cette classe met à jour un écouteur lorsque la caméra est démarrée via
onCameraStarted(@Nullable SurfaceTexture)
Pour utiliser cet utilitaire, modifiez le fichier BUILD
pour ajouter une dépendance à
"//mediapipe/java/com/google/mediapipe/components:android_camerax_helper"
Importez maintenant CameraXPreviewHelper
et ajoutez la ligne suivante à
MainActivity
:
private CameraXPreviewHelper cameraHelper;
Nous pouvons maintenant ajouter notre implémentation à 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);
});
}
Un objet CameraXPreviewHelper
est alors créé, et un objet
sur l'objet. Lorsque cameraHelper
signale que la caméra a démarré
et qu'un surfaceTexture
est disponible pour récupérer des images,
surfaceTexture
en tant que previewFrameTexture
, et la previewDisplayView
visible afin de pouvoir commencer à voir les images de previewFrameTexture
.
Cependant, avant de démarrer l'appareil photo, nous devons choisir la caméra à utiliser
utiliser. CameraXPreviewHelper
hérite de CameraHelper
, qui fournit deux
les options FRONT
et BACK
. Nous pouvons transmettre la décision à partir du fichier BUILD
.
comme les métadonnées, de sorte qu'aucune modification du code ne soit nécessaire pour créer une autre version
avec une autre caméra.
Supposons que nous souhaitons utiliser la caméra BACK
pour effectuer une détection des bords sur une scène en direct.
que nous regardons depuis l'appareil photo, ajoutez les métadonnées dans AndroidManifest.xml
:
...
<meta-data android:name="cameraFacingFront" android:value="${cameraFacingFront}"/>
</application>
</manifest>
et spécifiez la sélection dans BUILD
dans la règle binaire Android helloworld
.
par une nouvelle entrée dans manifest_values
:
manifest_values = {
"applicationId": "com.google.mediapipe.apps.basic",
"appName": "Hello World",
"mainActivity": ".MainActivity",
"cameraFacingFront": "False",
},
Maintenant, dans MainActivity
pour récupérer les métadonnées spécifiées dans manifest_values
,
Ajoutez un objet ApplicationInfo
:
private ApplicationInfo applicationInfo;
Dans la fonction onCreate()
, ajoutez:
try {
applicationInfo =
getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
Log.e(TAG, "Cannot find application info: " + e);
}
Ajoutez maintenant la ligne suivante à la fin de la fonction startCamera()
:
CameraHelper.CameraFacing cameraFacing =
applicationInfo.metaData.getBoolean("cameraFacingFront", false)
? CameraHelper.CameraFacing.FRONT
: CameraHelper.CameraFacing.BACK;
cameraHelper.startCamera(this, cameraFacing, /*unusedSurfaceTexture=*/ null);
À ce stade, l'application devrait se compiler correctement. Toutefois, lorsque vous exécutez
l'application sur votre appareil, un écran noir s'affiche (même si l'appareil photo
ont été accordées.) En effet, même si nous sauvegardons
la variable surfaceTexture
fournie par le CameraXPreviewHelper
, la
previewSurfaceView
n'utilise pas encore sa sortie et ne l'affiche pas encore à l'écran.
Puisque nous voulons utiliser les frames dans un graphique MediaPipe, nous n'ajouterons pas de code à la sortie de la caméra directement dans ce tutoriel. Au lieu de cela, nous allons passer à la façon nous pouvons envoyer les images de l'appareil photo à un graphique MediaPipe pour les traiter du graphique à l'écran.
Configuration de ExternalTextureConverter
Un SurfaceTexture
capture des images d'un flux sous forme d'OpenGL ES
ou la texture. Pour utiliser un graphique MediaPipe, les images capturées avec l'appareil photo doivent être
stocké dans un objet de texture Open GL standard. Le framework fournit une classe,
ExternalTextureConverter
pour convertir l'image stockée dans un SurfaceTexture
à un objet de texture OpenGL standard.
Pour utiliser ExternalTextureConverter
, nous avons également besoin d'un EGLContext
, qui est
créées et gérées par un objet EglManager
. Ajouter une dépendance à BUILD
pour utiliser EglManager
, "//mediapipe/java/com/google/mediapipe/glutil"
.
Dans MainActivity
, ajoutez les déclarations suivantes:
private EglManager eglManager;
private ExternalTextureConverter converter;
Dans la fonction onCreate(Bundle)
, ajoutez une instruction pour initialiser
eglManager
avant de demander les autorisations d'accès à l'appareil photo:
eglManager = new EglManager(null);
Rappelez-vous que nous avons défini la fonction onResume()
dans MainActivity
pour confirmer
les autorisations d'accès à l'appareil photo ont été accordées et appeler startCamera()
. Avant cela
vérifiez, ajoutez la ligne suivante dans onResume()
pour initialiser converter
objet:
converter = new ExternalTextureConverter(eglManager.getContext());
Ce converter
utilise désormais le GLContext
géré par eglManager
.
Nous devons également remplacer la fonction onPause()
dans MainActivity
pour que
Si l'application est mise en pause, nous fermons converter
correctement:
@Override
protected void onPause() {
super.onPause();
converter.close();
}
Pour diriger la sortie de previewFrameTexture
vers converter
, ajoutez le
bloc de code suivant vers 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) {}
});
Dans ce bloc de code, nous ajoutons un SurfaceHolder.Callback
personnalisé à
previewDisplayView
et implémenter la fonction surfaceChanged(SurfaceHolder holder, int
format, int width, int height)
pour calculer une taille d'affichage appropriée
cadres de l'appareil photo sur l'écran de l'appareil et pour lier le previewFrameTexture
et envoyer les frames du displaySize
calculé au converter
.
Nous sommes maintenant prêts à utiliser des images d'appareil photo dans un graphique MediaPipe.
Utiliser un graphique MediaPipe dans Android
Ajouter les dépendances pertinentes
Pour utiliser un graphe MediaPipe, nous devons ajouter des dépendances au framework MediaPipe
sur Android. Nous allons d'abord ajouter une règle de compilation pour compiler un cc_binary
à l'aide du code JNI
du framework MediaPipe, puis créez une règle cc_library
pour utiliser ce binaire
dans notre application. Ajoutez le bloc de code suivant à votre fichier 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,
)
Ajoutez la dépendance ":mediapipe_jni_lib"
à la règle de compilation mediapipe_lib
dans
le fichier BUILD
.
Nous devons ensuite ajouter des dépendances spécifiques au graphe MediaPipe que nous voulons utiliser. dans l'application.
Tout d'abord, ajoutez des dépendances à tout le code de la calculatrice dans libmediapipe_jni.so
.
règle de compilation:
"//mediapipe/graphs/edge_detection:mobile_calculators",
Les graphiques MediaPipe sont des fichiers .pbtxt
, mais pour les utiliser dans l'application, nous avons besoin
pour utiliser la règle de compilation mediapipe_binary_graph
afin de générer un fichier .binarypb
.
Dans la règle de compilation binaire Android helloworld
, ajoutez mediapipe_binary_graph
.
cible spécifique au graphique en tant qu'élément:
assets = [
"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
],
assets_dir = "",
Dans la règle de compilation assets
, vous pouvez également ajouter d'autres éléments tels que TensorFlowLite.
utilisés dans votre graphique.
De plus, ajoutez des manifest_values
supplémentaires pour les établissements spécifiques
graphique, qui sera récupéré ultérieurement dans 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",
},
Notez que binaryGraphName
indique le nom de fichier du graphe binaire,
déterminé par le champ output_name
de la cible mediapipe_binary_graph
.
inputVideoStreamName
et outputVideoStreamName
sont les entrées et les sorties.
nom du flux vidéo spécifié respectivement dans le graphique.
MainActivity
doit maintenant charger le framework MediaPipe. Par ailleurs,
framework utilise OpenCV. MainActvity
doit donc également charger OpenCV
. Utilisez les
le code suivant dans MainActivity
(à l'intérieur de la classe, mais pas dans une fonction) ;
pour charger les deux dépendances:
static {
// Load all native libraries needed by the app.
System.loadLibrary("mediapipe_jni");
System.loadLibrary("opencv_java3");
}
Utiliser le graphique en MainActivity
Tout d'abord, nous devons charger l'élément qui contient le .binarypb
compilé à partir de
le fichier .pbtxt
du graphique. Pour ce faire, nous pouvons utiliser
un utilitaire MediaPipe,
AndroidAssetUtil
Initialisez le gestionnaire d'assets dans onCreate(Bundle)
avant de procéder à l'initialisation.
eglManager
:
// Initialize asset manager so that MediaPipe native libraries can access the app assets, e.g.,
// binary graphs.
AndroidAssetUtil.initializeNativeAssetManager(this);
Nous devons maintenant configurer un objet FrameProcessor
qui envoie les images de l'appareil photo.
préparé par converter
au graphe MediaPipe, exécute le graphe, prépare
puis met à jour le previewDisplayView
pour l'afficher. Ajouter
le code suivant pour déclarer FrameProcessor
:
private FrameProcessor processor;
puis initialisez-le dans onCreate(Bundle)
après avoir initialisé eglManager
:
processor =
new FrameProcessor(
this,
eglManager.getNativeContext(),
applicationInfo.metaData.getString("binaryGraphName"),
applicationInfo.metaData.getString("inputVideoStreamName"),
applicationInfo.metaData.getString("outputVideoStreamName"));
processor
doit utiliser les frames convertis à partir de converter
pour
en cours de traitement. Ajoutez la ligne suivante à onResume()
après avoir initialisé
converter
:
converter.setConsumer(processor);
processor
doit envoyer sa sortie à previewDisplayView
. Pour ce faire, ajoutez
les définitions de fonction suivantes à notre SurfaceHolder.Callback
personnalisé:
@Override
public void surfaceCreated(SurfaceHolder holder) {
processor.getVideoSurfaceOutput().setSurface(holder.getSurface());
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
processor.getVideoSurfaceOutput().setSurface(null);
}
Lorsque SurfaceHolder
a été créé, nous avions le Surface
à la
VideoSurfaceOutput
sur processor
. Lorsqu'elle est détruite, nous la supprimons
le VideoSurfaceOutput
de processor
.
Et voilà ! Vous devriez maintenant être en mesure de créer et d'exécuter application sur l'appareil et la détection des bords Sobel s'exécute sur une caméra en direct flux ! Félicitations !
Si vous avez rencontré des problèmes, veuillez consulter le code complet du tutoriel cliquez ici.