Mit MediaPipe Tasks eine Android-App für handschriftliche Ziffern erstellen

1. Einführung

Was ist MediaPipe?

Mit MediaPipe Solutions können Sie Lösungen für maschinelles Lernen (ML) in Ihren Apps einsetzen. Es bietet ein Framework zum Konfigurieren vorgefertigter Verarbeitungspipelines, die Nutzern sofort ansprechende und nützliche Ergebnisse liefern. Sie können diese Lösungen sogar mit MediaPipe Model Maker anpassen, um die Standardmodelle zu aktualisieren.

Die Bildklassifizierung ist eine von mehreren ML-Vision-Aufgaben, die MediaPipe Solutions bietet. MediaPipe Tasks ist für Android, iOS, Python (einschließlich Raspberry Pi) und das Web verfügbar.

In diesem Codelab beginnen Sie mit einer Android-App, mit der Sie numerische Ziffern auf dem Bildschirm zeichnen können. Anschließend fügen Sie eine Funktion hinzu, die diese gezeichneten Ziffern als einzelnen Wert zwischen 0 und 9 klassifiziert.

Lerninhalte

  • So binden Sie eine Bildklassifizierungsaufgabe mit MediaPipe Tasks in eine Android-App ein.

Voraussetzungen

  • Eine installierte Version von Android Studio (dieses Codelab wurde mit Android Studio Giraffe geschrieben und getestet).
  • Ein Android-Gerät oder ein Emulator zum Ausführen der App.
  • Grundkenntnisse in der Android-Entwicklung (dies ist nicht „Hallo Welt“, aber nicht weit davon entfernt).

2. MediaPipe Tasks zur Android-App hinzufügen

Android-Starter-App herunterladen

In diesem Codelab beginnen Sie mit einem vorgefertigten Beispiel, mit dem Sie auf dem Bildschirm zeichnen können. Die Start-App finden Sie im offiziellen MediaPipe-Beispiel-Repository hier. Klonen Sie das Repository oder laden Sie die ZIP-Datei herunter, indem Sie auf „Code“ > „Download ZIP“ klicken.

App in Android Studio importieren

  1. Öffnen Sie Android Studio.
  2. Wählen Sie auf dem Bildschirm Welcome to Android Studio (Willkommen bei Android Studio) rechts oben Open (Öffnen) aus.

a0b5b070b802e4ea.png

  1. Rufen Sie den Ort auf, an dem Sie das Repository geklont oder heruntergeladen haben, und öffnen Sie das Verzeichnis codelabs/digitclassifier/android/start.
  2. Prüfen Sie, ob alles richtig geöffnet wurde, indem Sie rechts oben in Android Studio auf den grünen Ausführen-Pfeil ( 7e15a9c9e1620fe7.png) klicken.
  3. Die App sollte mit einem schwarzen Bildschirm geöffnet werden, auf dem Sie zeichnen können. Außerdem sollte eine Schaltfläche Löschen angezeigt werden, mit der Sie den Bildschirm zurücksetzen können. Sie können zwar auf diesem Bildschirm zeichnen, aber sonst passiert nicht viel. Das ändern wir jetzt.

11a0f6fe021fdc92.jpeg

Modell

Wenn Sie die App zum ersten Mal ausführen, wird möglicherweise eine Datei mit dem Namen mnist.tflite heruntergeladen und im assets-Verzeichnis Ihrer App gespeichert. Zur Vereinfachung haben wir bereits ein bekanntes Modell, MNIST, das Ziffern klassifiziert, über das Skript download_models.gradle im Projekt in die App eingefügt. Wenn Sie ein eigenes benutzerdefiniertes Modell trainieren möchten, z. B. für handschriftliche Buchstaben, entfernen Sie die Datei download_models.gradle, löschen Sie den Verweis darauf in der Datei build.gradle auf App-Ebene und ändern Sie den Namen des Modells später im Code (insbesondere in der Datei DigitClassifierHelper.kt).

Datei build.gradle aktualisieren

Bevor Sie MediaPipe Tasks verwenden können, müssen Sie die Bibliothek importieren.

  1. Öffnen Sie die Datei build.gradle im Modul app und scrollen Sie dann zum Block dependencies.
  2. Unten in diesem Block sollte der Kommentar // STEP 1 Dependency Import zu sehen sein.
  3. Ersetzen Sie diese Zeile durch die folgende Implementierung:
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Klicken Sie im Banner oben in Android Studio auf die Schaltfläche Jetzt synchronisieren, um diese Abhängigkeit herunterzuladen.

3. MediaPipe Tasks-Hilfsklasse für die Ziffernklassifizierung erstellen

Im nächsten Schritt füllen Sie eine Klasse aus, die die Hauptarbeit für die Klassifizierung mit maschinellem Lernen übernimmt. Öffnen Sie DigitClassifierHelper.kt.

  1. Suchen Sie oben im Kurs nach dem Kommentar // STEP 2 Create listener.
  2. Ersetzen Sie diese Zeile durch den folgenden Code. Dadurch wird ein Listener erstellt, mit dem Ergebnisse von der Klasse „DigitClassifierHelper“ an die Stelle zurückgegeben werden, an der auf diese Ergebnisse gewartet wird. In diesem Fall ist das die Klasse DigitCanvasFragment.
// STEP 2 Create listener

interface DigitClassifierListener {
    fun onError(error: String)
    fun onResults(
        results: ImageClassifierResult,
        inferenceTime: Long
    )
}
  1. Außerdem müssen Sie einen DigitClassifierListener als optionalen Parameter für die Klasse akzeptieren:
class DigitClassifierHelper(
    val context: Context,
    val digitClassifierListener: DigitClassifierListener?
) {
  1. Fügen Sie in der Zeile mit // STEP 3 define classifier die folgende Zeile hinzu, um einen Platzhalter für den ImageClassifier zu erstellen, der für diese App verwendet wird:

// STEP 3 define classifier

private var digitClassifier: ImageClassifier? = null
  1. Fügen Sie die folgende Funktion an der Stelle ein, an der Sie den Kommentar // STEP 4 set up classifier sehen:
// 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)
    }
}

Im obigen Abschnitt passiert einiges. Sehen wir uns daher kleinere Teile an, um wirklich zu verstehen, was passiert.

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

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

In diesem Block werden die Parameter definiert, die von ImageClassifier verwendet werden. Dazu gehören das in Ihrer App gespeicherte Modell (mnist.tflite) unter BaseOptions und der RunningMode unter ImageClassifierOptions. In diesem Fall ist das IMAGE, aber VIDEO und LIVE_STREAM sind zusätzliche verfügbare Optionen. Weitere verfügbare Parameter sind „MaxResults“, mit dem die maximale Anzahl der zurückzugebenden Ergebnisse begrenzt wird, und „ScoreThreshold“, mit dem die Mindestkonfidenz festgelegt wird, die das Modell für ein Ergebnis haben muss, bevor es zurückgegeben wird.

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)
}

Nachdem Sie Ihre Konfigurationsoptionen erstellt haben, können Sie Ihren neuen ImageClassifier erstellen, indem Sie einen Kontext und die Optionen übergeben. Wenn bei der Initialisierung ein Fehler auftritt, wird über Ihren DigitClassifierListener ein Fehler zurückgegeben.

  1. Da wir den ImageClassifier initialisieren möchten, bevor er verwendet wird, können Sie einen init-Block hinzufügen, um setupDigitClassifier() aufzurufen.
init {
    setupDigitClassifier()
}
  1. Scrollen Sie schließlich nach unten zum Kommentar „// STEP 5 create classify function“ und fügen Sie den folgenden Code hinzu. Diese Funktion akzeptiert ein Bitmap, das in diesem Fall die gezeichnete Ziffer ist, konvertiert es in ein MediaPipe-Bildobjekt (MPImage) und klassifiziert dieses Bild dann mit dem ImageClassifier. Außerdem wird aufgezeichnet, wie lange die Inferenz dauert, bevor die Ergebnisse über den DigitClassifierListener zurückgegeben werden.
// 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)
    }
}

Das war's auch schon mit der Hilfsdatei. Im nächsten Abschnitt führen Sie die letzten Schritte aus, um die gezeichneten Zahlen zu klassifizieren.

4. Inferenz mit MediaPipe Tasks ausführen

Öffnen Sie dazu die Klasse DigitCanvasFragment in Android Studio.

  1. Ganz unten in dieser Datei sollte ein Kommentar mit dem Text // STEP 6 Set up listener zu sehen sein. Hier fügen Sie die Funktionen „onResults()“ und „onError()“ hinzu, die dem Listener zugeordnet sind.
// 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() ist besonders wichtig, da damit die vom ImageClassifier empfangenen Ergebnisse angezeigt werden. Da dieser Callback über einen Hintergrundthread ausgelöst wird, müssen Sie Ihre UI-Aktualisierungen auch im UI-Thread von Android ausführen.

  1. Wenn Sie im obigen Schritt neue Funktionen aus einer Schnittstelle hinzufügen, müssen Sie auch die Implementierungsdeklaration oben in der Klasse hinzufügen.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. Oben im Kurs sollte ein Kommentar mit dem Text // STEP 7a Initialize classifier (Klassifikator initialisieren) angezeigt werden. Hier platzieren Sie die Deklaration für DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. Wenn Sie zu // STEP 7b Initialize classifier (Klassifikator initialisieren) wechseln,können Sie digitClassifierHelper in der Funktion onViewCreated() initialisieren.
// 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. Suchen Sie für die letzten Schritte den Kommentar // STEP 8a*: classify* und fügen Sie den folgenden Code hinzu, um eine neue Funktion aufzurufen, die Sie gleich hinzufügen werden. Dieser Codeblock löst die Klassifizierung aus, wenn Sie den Finger vom Zeichenbereich in der App nehmen.
// STEP 8a: classify
classifyDrawing()
  1. Suchen Sie schließlich nach dem Kommentar // STEP 8b classify, um die neue Funktion „classifyDrawing()“ hinzuzufügen. Dadurch wird eine Bitmap aus dem Canvas extrahiert und an DigitClassifierHelper übergeben, um die Klassifizierung durchzuführen und die Ergebnisse in der Schnittstellenfunktion „onResults()“ zu erhalten.
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5. App bereitstellen und testen

Danach sollten Sie eine funktionierende App haben, die gezeichnete Ziffern auf dem Bildschirm klassifizieren kann. Stellen Sie die App auf einem Android-Emulator oder einem physischen Android-Gerät bereit, um sie zu testen.

  1. Klicken Sie in der Android Studio-Symbolleiste auf „Ausführen“ (7e15a9c9e1620fe7.png), um die App auszuführen.
  2. Zeichnen Sie eine beliebige Ziffer auf das Zeichenfeld und prüfen Sie, ob die App sie erkennt. Es sollte sowohl die Ziffer angezeigt werden, die das Modell für die gezeichnete Ziffer hält, als auch die Zeit, die für die Vorhersage dieser Ziffer benötigt wurde.

7f37187f8f919638.gif

6. Glückwunsch!

Geschafft! In diesem Codelab haben Sie gelernt, wie Sie einer Android-App die Bildklassifizierung hinzufügen und insbesondere handgezeichnete Ziffern mit dem MNIST-Modell klassifizieren.

Nächste Schritte

  • Nachdem Sie nun Ziffern klassifizieren können, möchten Sie vielleicht Ihr eigenes Modell trainieren, um gezeichnete Buchstaben oder Tiere oder eine unendliche Anzahl anderer Elemente zu klassifizieren. Die Dokumentation zum Trainieren eines neuen Bildklassifizierungsmodells mit MediaPipe Model Maker finden Sie auf der Seite developers.google.com/mediapipe.
  • Weitere MediaPipe-Aufgaben für Android, darunter die Erkennung von Gesichtsmerkmalen, die Gestenerkennung und die Audio-Klassifizierung.

Wir freuen uns schon auf all die coolen Dinge, die du machen wirst!