Créer une application Android de classification de chiffres manuscrits avec MediaPipe Tasks

1. Introduction

Qu'est-ce que MediaPipe ?

MediaPipe Solutions vous permet d'appliquer des solutions de machine learning (ML) à vos applications. Il fournit un framework permettant de configurer des pipelines de traitement prédéfinis qui fournissent des résultats immédiats, attrayants et utiles aux utilisateurs. Vous pouvez même personnaliser ces solutions avec MediaPipe Model Maker afin de mettre à jour les modèles par défaut.

La classification d'images est l'une des nombreuses tâches de vision ML proposées par MediaPipe Solutions. MediaPipe Tasks est disponible pour Android, iOS, Python (y compris le Raspberry Pi !) et le Web.

Dans cet atelier de programmation, vous allez commencer avec une application Android qui vous permet de dessiner des chiffres à l'écran. Vous ajouterez ensuite une fonctionnalité qui classe ces chiffres en tant que valeur unique de 0 à 9.

Points abordés

  • Intégrer une tâche de classification d'images dans une application Android avec MediaPipe Tasks

Prérequis

  • Une version installée d'Android Studio (cet atelier de programmation a été écrit et testé avec Android Studio Giraffe)
  • Un appareil ou un émulateur Android pour exécuter l'application
  • Vous disposez de connaissances de base en développement Android (il ne s'agit pas de "Hello World", mais ce n'est pas trop loin !).

2. Ajouter MediaPipe Tasks à l'application Android

Télécharger l'application de démarrage Android

Cet atelier de programmation commencera avec un exemple prédéfini qui vous permettra de dessiner à l'écran. Vous trouverez cette application de démarrage dans le dépôt officiel MediaPipe Samples, sur cette page. Clonez le dépôt ou téléchargez le fichier ZIP en cliquant sur Code > Télécharger le fichier ZIP

Importer l'application dans Android Studio

  1. Ouvrez Android Studio.
  2. Sur l'écran Welcome to Android Studio (Bienvenue dans Android Studio), sélectionnez Open (Ouvrir) en haut à droite.

a0b5b070b802e4ea.png

  1. Accédez à l'emplacement où vous avez cloné ou téléchargé le dépôt, puis ouvrez le répertoire codelabs/digitclassifier/android/start.
  2. Vérifiez que tout s'est correctement ouvert en cliquant sur la flèche verte Run (Exécuter) (7e15a9c9e1620fe7.png) en haut à droite d'Android Studio.
  3. L'application devrait s'ouvrir avec un écran noir sur lequel vous pouvez dessiner, ainsi qu'un bouton Effacer permettant de réinitialiser cet écran. Bien que vous puissiez dessiner sur cet écran, cela ne fait pas grand-chose d'autre, nous allons donc commencer à résoudre ce problème maintenant.

11a0f6fe021fdc92.jpeg

Modèle

Lorsque vous exécutez l'application pour la première fois, vous remarquerez peut-être qu'un fichier nommé mnist.tflite est téléchargé et stocké dans le répertoire assets de votre application. Par souci de simplicité, nous avons déjà pris un modèle connu, MNIST, qui classe les chiffres, et l'avons ajouté à l'application en utilisant le script download_models.gradle dans le projet. Si vous décidez d'entraîner votre propre modèle personnalisé (par exemple, pour les lettres manuscrites), supprimez le fichier download_models.gradle, supprimez la référence à celui-ci dans le fichier build.gradle au niveau de l'application, puis modifiez le nom du modèle ultérieurement dans le code (en particulier dans le fichier DigitClassifierHelper.kt).

Mettre à jour le fichier build.gradle

Avant de pouvoir utiliser MediaPipe Tasks, vous devez importer la bibliothèque.

  1. Ouvrez le fichier build.gradle situé dans le module app, puis faites défiler la page jusqu'au bloc depends (dépendances).
  2. Vous devriez voir le commentaire // ÉTAPE 1 Importation de dépendances au bas de ce bloc.
  3. Remplacez cette ligne par l'implémentation suivante :
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Cliquez sur le bouton Sync Now (Synchroniser) qui s'affiche dans la bannière en haut d'Android Studio pour télécharger cette dépendance.

3. Créer un assistant de classificateur numérique de tâches MediaPipe

À l'étape suivante, vous allez apprendre à faire le gros du travail pour la classification du machine learning. Ouvrez le fichier DigitClassifierHelper.kt et lancez-vous !

  1. Recherchez le commentaire en haut de la classe qui indique // ÉTAPE 2 Créer un écouteur
  2. Remplacez cette ligne par le code suivant. Cette opération crée un écouteur qui sera utilisé pour transmettre les résultats de la classe DigitClassifierHelper à l'emplacement où ces résultats sont écoutés (dans le cas présent, il s'agira de votre classe DigitCanvasFragment, mais nous y reviendrons bientôt).
// STEP 2 Create listener

interface DigitClassifierListener {
    fun onError(error: String)
    fun onResults(
        results: ImageClassifierResult,
        inferenceTime: Long
    )
}
  1. Vous devez également accepter un DigitClassifierListener comme paramètre facultatif pour la classe:
class DigitClassifierHelper(
    val context: Context,
    val digitClassifierListener: DigitClassifierListener?
) {
  1. En revenant à la ligne // ÉTAPE 3 définir le classificateur, ajoutez la ligne suivante afin de créer un espace réservé pour ImageClassifier qui sera utilisé pour cette application:

// ÉTAPE 3 : définir le classificateur

private var digitClassifier: ImageClassifier? = null
  1. Ajoutez la fonction suivante là où vous voyez le commentaire // ÉTAPE 4 set up classifier:
// STEP 4 set up classifier
private fun setupDigitClassifier() {

    val baseOptionsBuilder = BaseOptions.builder()
        .setModelAssetPath("mnist.tflite")

    // Describe additional options
    val optionsBuilder = ImageClassifierOptions.builder()
        .setRunningMode(RunningMode.IMAGE)
        .setBaseOptions(baseOptionsBuilder.build())

    try {
        digitClassifier =
            ImageClassifier.createFromOptions(
                context,
                optionsBuilder.build()
            )
    } catch (e: IllegalStateException) {
        digitClassifierListener?.onError(
            "Image classifier failed to initialize. See error logs for " +
                    "details"
        )
        Log.e(TAG, "MediaPipe failed to load model with error: " + e.message)
    }
}

La section ci-dessus apporte son lot de nouveautés. Examinons donc plus en détail les étapes précédentes pour bien comprendre ce qui se passe.

val baseOptionsBuilder = BaseOptions.builder()
    .setModelAssetPath("mnist.tflite")

// Describe additional options
val optionsBuilder = ImageClassifierOptions.builder()
    .setRunningMode(RunningMode.IMAGE)
    .setBaseOptions(baseOptionsBuilder.build())

Ce bloc définira les paramètres utilisés par ImageClassifier. Cela inclut le modèle stocké dans votre application (mnist.tflite) sous BaseOptions et le mode RunningMode sous ImageClassifierOptions (dans ce cas, IMAGE, mais VIDEO et LIVE_STREAM sont des options supplémentaires disponibles). Les autres paramètres disponibles sont MaxResults, qui limite le modèle à renvoyer un nombre maximal de résultats, et ScoreThreshold, qui définit le niveau de confiance minimal que le modèle doit avoir dans un résultat avant de le renvoyer.

try {
    digitClassifier =
        ImageClassifier.createFromOptions(
            context,
            optionsBuilder.build()
        )
} catch (e: IllegalStateException) {
    digitClassifierListener?.onError(
        "Image classifier failed to initialize. See error logs for " +
                "details"
    )
    Log.e(TAG, "MediaPipe failed to load model with error: " + e.message)
}

Après avoir créé vos options de configuration, vous pouvez créer votre ImageClassifier en transmettant un contexte et les options. En cas de problème lors du processus d'initialisation, une erreur est renvoyée via votre DigitClassifierListener.

  1. Étant donné que nous allons initialiser ImageClassifier avant de l'utiliser, vous pouvez ajouter un bloc "init" pour appeler setupDigitClassifier().
init {
    setupDigitClassifier()
}
  1. Enfin, faites défiler la page vers le bas jusqu'au commentaire qui dit // ÉTAPE 5 créez une fonction de classification et ajoutez le code suivant. Cette fonction accepte un Bitmap (dans ce cas, le chiffre tracé) et le convertit en objet MediaPipe Image (MPImage), puis classe cette image à l'aide d'ImageClassifier et enregistre la durée de l'inférence, avant de renvoyer ces résultats via le DigitClassifierListener.
// STEP 5 create classify function
fun classify(image: Bitmap) {
    if (digitClassifier == null) {
        setupDigitClassifier()
    }

    // Convert the input Bitmap object to an MPImage object to run inference.
    // Rotating shouldn't be necessary because the text is being extracted from
    // a view that should always be correctly positioned.
    val mpImage = BitmapImageBuilder(image).build()

    // Inference time is the difference between the system time at the start and finish of the
    // process
    val startTime = SystemClock.uptimeMillis()

    // Run image classification using MediaPipe Image Classifier API
    digitClassifier?.classify(mpImage)?.also { classificationResults ->
        val inferenceTimeMs = SystemClock.uptimeMillis() - startTime
        digitClassifierListener?.onResults(classificationResults, inferenceTimeMs)
    }
}

C'est tout pour le fichier d'aide ! Dans la section suivante, vous allez compléter les dernières étapes pour commencer à classer les nombres tirés.

4. Exécuter une inférence avec des tâches MediaPipe

Vous pouvez commencer cette section en ouvrant la classe DigitCanvasFragment dans Android Studio, où tous les travaux seront effectués.

  1. Tout en bas de ce fichier, vous devriez voir le commentaire // ÉTAPE 6 Configurer l'écouteur. Vous ajouterez ici les fonctions onResults() et onError() associées à l'écouteur.
// STEP 6 Set up listener
override fun onError(error: String) {
    activity?.runOnUiThread {
        Toast.makeText(requireActivity(), error, Toast.LENGTH_SHORT).show()
        fragmentDigitCanvasBinding.tvResults.text = ""
    }
}

override fun onResults(
    results: ImageClassifierResult,
    inferenceTime: Long
) {
    activity?.runOnUiThread {
        fragmentDigitCanvasBinding.tvResults.text = results
            .classificationResult()
            .classifications().get(0)
            .categories().get(0)
            .categoryName()

        fragmentDigitCanvasBinding.tvInferenceTime.text = requireActivity()
            .getString(R.string.inference_time, inferenceTime.toString())
    }
}

onResults() est particulièrement important car il affichera les résultats reçus d'ImageClassifier. Comme ce rappel est déclenché à partir d'un thread d'arrière-plan, vous devez également exécuter les mises à jour de l'UI sur le thread UI d'Android.

  1. Comme vous allez ajouter de nouvelles fonctions à partir d'une interface à l'étape ci-dessus, vous devrez également ajouter la déclaration d'implémentation en haut de la classe.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. Vers le haut de la classe, vous devriez voir le commentaire suivant : // ÉTAPE 7a Initialiser le classificateur. C'est ici que vous placerez la déclaration pour DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. En revenant à l'étape // ÉTAPE 7b Initialiser le classificateur,vous pouvez initialiser digitClassifierHelper dans la fonction onViewCreated().
// STEP 7b Initialize classifier
// Initialize the digit classifier helper, which does all of the
// ML work. This uses the default values for the classifier.
digitClassifierHelper = DigitClassifierHelper(
    context = requireContext(), digitClassifierListener = this
)
  1. Pour les dernières étapes, recherchez le commentaire // STEP 8a*: classifier*, puis ajoutez le code suivant pour appeler une nouvelle fonction que vous ajouterez plus tard. Ce bloc de code déclenche la classification lorsque vous relevez le doigt de la zone de dessin dans l'application.
// STEP 8a: classify
classifyDrawing()
  1. Enfin, recherchez le commentaire // STEP 8b classify pour ajouter la fonction "classifyDrawing()". Un bitmap est alors extrait du canevas, puis transmis à DigitClassifierHelper afin d'effectuer une classification afin de recevoir les résultats dans la fonction d'interface onResults().
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5. Déployer et tester l'application

Après tout cela, vous devriez avoir une application fonctionnelle capable de classer les chiffres dessinés à l'écran. Vous pouvez maintenant déployer l'application sur Android Emulator ou sur un appareil Android physique pour la tester.

  1. Cliquez sur Exécuter ( 7e15a9c9e1620fe7.png) dans la barre d'outils d'Android Studio pour exécuter l'application.
  2. Dessinez un chiffre sur le bloc de dessin et voyez si l'application peut le reconnaître. Il devrait à la fois afficher le chiffre qui, selon le modèle, a été dessiné, ainsi que le temps nécessaire pour prédire ce chiffre.

7f37187f8f919638.gif

6. Félicitations !

Bravo ! Dans cet atelier de programmation, vous avez appris à ajouter une classification d'images à une application Android, et plus précisément à classer des chiffres dessinés à la main à l'aide du modèle MNIST.

Étapes suivantes

  • Maintenant que vous savez classer des chiffres, vous pouvez entraîner votre propre modèle à classer des lettres dessinées, des animaux ou un nombre infini d'autres éléments. La documentation sur l'entraînement d'un nouveau modèle de classification d'images avec MediaPipe Model Maker est disponible sur la page developers.google.com/mediapipe.
  • Découvrez les autres tâches MediaPipe disponibles pour Android, y compris la détection de points de repère faciales, la reconnaissance gestuelle et la classification audio.

Nous avons hâte de découvrir toutes vos créations !