Digitale Tinte mit ML Kit unter Android erkennen

Mit der digitalen Tintenerkennung von ML Kit können Sie handgeschriebenen Text auf einer digitalen Oberfläche in Hunderten von Sprachen erkennen und Skizzen klassifizieren.

Jetzt ausprobieren

Hinweis

  1. In die Datei build.gradle auf Projektebene muss das Maven-Repository von Google in die Abschnitte buildscript und allprojects aufgenommen werden.
  2. Fügen Sie der Gradle-Datei Ihres Moduls auf App-Ebene (in der Regel app/build.gradle) die Abhängigkeiten für die ML Kit-Android-Bibliotheken hinzu:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

Sie können jetzt mit dem Erkennen von Text in Ink-Objekten beginnen.

Ink-Objekt erstellen

Die Hauptmethode zum Erstellen eines Ink-Objekts besteht darin, es auf einem Touchscreen zu zeichnen. Unter Android können Sie dazu einen Canvas verwenden. Die Handler für Touch-Ereignisse sollten die Methode addNewTouchEvent() aufrufen, die im folgenden Code-Snippet dargestellt ist, um die Punkte in den Strichen zu speichern, die der Nutzer in das Ink-Objekt zeichnet.

Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. Ein vollständigeres Beispiel finden Sie im ML Kit-Schnellstartbeispiel.

Kotlin

var inkBuilder = Ink.builder()
lateinit var strokeBuilder: Ink.Stroke.Builder

// Call this each time there is a new event.
fun addNewTouchEvent(event: MotionEvent) {
  val action = event.actionMasked
  val x = event.x
  val y = event.y
  var t = System.currentTimeMillis()

  // If your setup does not provide timing information, you can omit the
  // third paramater (t) in the calls to Ink.Point.create
  when (action) {
    MotionEvent.ACTION_DOWN -> {
      strokeBuilder = Ink.Stroke.builder()
      strokeBuilder.addPoint(Ink.Point.create(x, y, t))
    }
    MotionEvent.ACTION_MOVE -> strokeBuilder!!.addPoint(Ink.Point.create(x, y, t))
    MotionEvent.ACTION_UP -> {
      strokeBuilder.addPoint(Ink.Point.create(x, y, t))
      inkBuilder.addStroke(strokeBuilder.build())
    }
    else -> {
      // Action not relevant for ink construction
    }
  }
}

...

// This is what to send to the recognizer.
val ink = inkBuilder.build()

Java

Ink.Builder inkBuilder = Ink.builder();
Ink.Stroke.Builder strokeBuilder;

// Call this each time there is a new event.
public void addNewTouchEvent(MotionEvent event) {
  float x = event.getX();
  float y = event.getY();
  long t = System.currentTimeMillis();

  // If your setup does not provide timing information, you can omit the
  // third paramater (t) in the calls to Ink.Point.create
  int action = event.getActionMasked();
  switch (action) {
    case MotionEvent.ACTION_DOWN:
      strokeBuilder = Ink.Stroke.builder();
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      break;
    case MotionEvent.ACTION_MOVE:
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      break;
    case MotionEvent.ACTION_UP:
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      inkBuilder.addStroke(strokeBuilder.build());
      strokeBuilder = null;
      break;
  }
}

...

// This is what to send to the recognizer.
Ink ink = inkBuilder.build();

Instanz von DigitalInkRecognizer abrufen

Um die Erkennung durchzuführen, senden Sie die Ink-Instanz an ein DigitalInkRecognizer-Objekt. Im folgenden Code wird gezeigt, wie ein solcher Erkenner aus einem BCP-47-Tag erstellt wird.

Kotlin

// Specify the recognition model for a language
var modelIdentifier: DigitalInkRecognitionModelIdentifier
try {
  modelIdentifier = DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US")
} catch (e: MlKitException) {
  // language tag failed to parse, handle error.
}
if (modelIdentifier == null) {
  // no model was found, handle error.
}
var model: DigitalInkRecognitionModel =
    DigitalInkRecognitionModel.builder(modelIdentifier).build()


// Get a recognizer for the language
var recognizer: DigitalInkRecognizer =
    DigitalInkRecognition.getClient(
        DigitalInkRecognizerOptions.builder(model).build())

Java

// Specify the recognition model for a language
DigitalInkRecognitionModelIdentifier modelIdentifier;
try {
  modelIdentifier =
    DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US");
} catch (MlKitException e) {
  // language tag failed to parse, handle error.
}
if (modelIdentifier == null) {
  // no model was found, handle error.
}

DigitalInkRecognitionModel model =
    DigitalInkRecognitionModel.builder(modelIdentifier).build();

// Get a recognizer for the language
DigitalInkRecognizer recognizer =
    DigitalInkRecognition.getClient(
        DigitalInkRecognizerOptions.builder(model).build());

Ink-Objekt verarbeiten

Kotlin

recognizer.recognize(ink)
    .addOnSuccessListener { result: RecognitionResult ->
      // `result` contains the recognizer's answers as a RecognitionResult.
      // Logs the text from the top candidate.
      Log.i(TAG, result.candidates[0].text)
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error during recognition: $e")
    }

Java

recognizer.recognize(ink)
    .addOnSuccessListener(
        // `result` contains the recognizer's answers as a RecognitionResult.
        // Logs the text from the top candidate.
        result -> Log.i(TAG, result.getCandidates().get(0).getText()))
    .addOnFailureListener(
        e -> Log.e(TAG, "Error during recognition: " + e));

Im Beispielcode oben wird davon ausgegangen, dass das Erkennungsmodell bereits heruntergeladen wurde, wie im nächsten Abschnitt beschrieben.

Modelldownloads verwalten

Die API zur Erkennung von digitaler Handschrift unterstützt zwar Hunderte von Sprachen, für jede Sprache müssen jedoch vor der Erkennung einige Daten heruntergeladen werden. Pro Sprache sind etwa 20 MB Speicherplatz erforderlich. Das wird vom RemoteModelManager-Objekt verwaltet.

Neues Modell herunterladen

Kotlin

import com.google.mlkit.common.model.DownloadConditions
import com.google.mlkit.common.model.RemoteModelManager

var model: DigitalInkRecognitionModel =  ...
val remoteModelManager = RemoteModelManager.getInstance()

remoteModelManager.download(model, DownloadConditions.Builder().build())
    .addOnSuccessListener {
      Log.i(TAG, "Model downloaded")
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error while downloading a model: $e")
    }

Java

import com.google.mlkit.common.model.DownloadConditions;
import com.google.mlkit.common.model.RemoteModelManager;

DigitalInkRecognitionModel model = ...;
RemoteModelManager remoteModelManager = RemoteModelManager.getInstance();

remoteModelManager
    .download(model, new DownloadConditions.Builder().build())
    .addOnSuccessListener(aVoid -> Log.i(TAG, "Model downloaded"))
    .addOnFailureListener(
        e -> Log.e(TAG, "Error while downloading a model: " + e));

Prüfen, ob ein Modell bereits heruntergeladen wurde

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.isModelDownloaded(model)

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.isModelDownloaded(model);

Heruntergeladenes Modell löschen

Wenn Sie ein Modell aus dem Gerätespeicher entfernen, wird Speicherplatz freigegeben.

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.deleteDownloadedModel(model)
    .addOnSuccessListener {
      Log.i(TAG, "Model successfully deleted")
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error while deleting a model: $e")
    }

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.deleteDownloadedModel(model)
                  .addOnSuccessListener(
                      aVoid -> Log.i(TAG, "Model successfully deleted"))
                  .addOnFailureListener(
                      e -> Log.e(TAG, "Error while deleting a model: " + e));

Tipps zur Verbesserung der Genauigkeit der Texterkennung

Die Genauigkeit der Texterkennung kann je nach Sprache variieren. Die Genauigkeit hängt auch vom Schreibstil ab. Die Erkennung von digitaler Tinte ist zwar für viele Arten von Schreibstilen trainiert, die Ergebnisse können jedoch von Nutzer zu Nutzer variieren.

Hier sind einige Möglichkeiten, die Genauigkeit eines Texterkennungstools zu verbessern. Diese Techniken gelten nicht für die Zeichenklassifikatoren für Emojis, AutoDraw und Formen.

Schreibfläche

Viele Anwendungen haben einen klar definierten Eingabebereich für die Nutzereingabe. Die Bedeutung eines Symbols wird teilweise durch seine Größe im Verhältnis zur Größe des Schreibbereichs bestimmt, in dem es enthalten ist. Beispielsweise der Unterschied zwischen einem Kleinbuchstaben oder Großbuchstaben „o“ oder „c“ und einem Komma oder einem Schrägstrich.

Wenn Sie der Erkennung die Breite und Höhe des Schreibbereichs mitteilen, kann die Genauigkeit verbessert werden. Der Erkenner geht jedoch davon aus, dass der Schreibbereich nur eine einzige Textzeile enthält. Wenn der physische Schreibbereich groß genug ist, dass der Nutzer zwei oder mehr Zeilen schreiben kann, erzielen Sie möglicherweise bessere Ergebnisse, wenn Sie einen Schreibbereich mit einer Höhe übergeben, die Ihrer besten Schätzung der Höhe einer einzelnen Textzeile entspricht. Das WritingArea-Objekt, das Sie an den Erkenner übergeben, muss nicht genau mit dem physischen Schreibbereich auf dem Bildschirm übereinstimmen. Die Höhe des Schreibbereichs auf diese Weise zu ändern, funktioniert in einigen Sprachen besser als in anderen.

Geben Sie Breite und Höhe des Schreibbereichs in denselben Einheiten wie die Koordinaten der Striche an. Für die Argumente der X‑ und Y‑Koordinaten sind keine Einheiten erforderlich. Die API normalisiert alle Einheiten. Daher sind nur die relative Größe und Position der Striche wichtig. Sie können Koordinaten in einem beliebigen Maßstab für Ihr System eingeben.

Vorheriger Kontext

Der Vorkontext ist der Text, der unmittelbar vor den Strichen in der Ink steht, die Sie erkennen möchten. Sie können dem Erkennungstool helfen, indem Sie ihm den vorherigen Kontext mitteilen.

So werden beispielsweise die kursiven Buchstaben „n“ und „u“ häufig miteinander verwechselt. Wenn der Nutzer bereits das Teilwort „arg“ eingegeben hat, kann er mit Strichen fortfahren, die als „ment“ oder „nment“ erkannt werden. Durch die Angabe des vorherigen Kontexts „arg“ wird die Mehrdeutigkeit beseitigt, da das Wort „Argument“ wahrscheinlicher ist als „Argnment“.

Der Vorkontext kann dem Erkennungsmodul auch dabei helfen, Worttrennungen und Leerzeichen zwischen Wörtern zu erkennen. Sie können ein Leerzeichen eingeben, aber nicht zeichnen. Wie kann ein Recognizer also feststellen, wann ein Wort endet und das nächste beginnt? Wenn der Nutzer bereits „Hallo“ geschrieben hat und mit dem geschriebenen Wort „Welt“ fortfährt, gibt der Recognizer ohne vorherigen Kontext den String „Welt“ zurück. Wenn Sie jedoch den vorherigen Kontext „hallo“ angeben, gibt das Modell den String „welt“ mit einem vorangestellten Leerzeichen zurück, da „hallowelt“ mehr Sinn macht als „hallowort“.

Sie sollten den längsten möglichen String vor dem Kontext angeben, bis zu 20 Zeichen einschließlich Leerzeichen. Ist der String länger, werden nur die letzten 20 Zeichen verwendet.

Im folgenden Codebeispiel wird gezeigt, wie Sie einen Schreibbereich definieren und mit einem RecognitionContext-Objekt einen vorherigen Kontext angeben.

Kotlin

var preContext : String = ...;
var width : Float = ...;
var height : Float = ...;
val recognitionContext : RecognitionContext =
    RecognitionContext.builder()
        .setPreContext(preContext)
        .setWritingArea(WritingArea(width, height))
        .build()

recognizer.recognize(ink, recognitionContext)

Java

String preContext = ...;
float width = ...;
float height = ...;
RecognitionContext recognitionContext =
    RecognitionContext.builder()
                      .setPreContext(preContext)
                      .setWritingArea(new WritingArea(width, height))
                      .build();

recognizer.recognize(ink, recognitionContext);

Stroke-Reihenfolge

Die Erkennungsgenauigkeit ist von der Reihenfolge der Striche abhängig. Die Erkennungsprogramme erwarten, dass die Striche in der Reihenfolge ausgeführt werden, in der Menschen normalerweise schreiben, z. B. von links nach rechts für Englisch. Abweichungen von diesem Muster, z. B. wenn ein englischer Satz mit dem letzten Wort beginnt, führen zu weniger genauen Ergebnissen.

Ein weiteres Beispiel ist, wenn ein Wort in der Mitte eines Ink entfernt und durch ein anderes Wort ersetzt wird. Die Korrektur befindet sich wahrscheinlich in der Mitte eines Satzes, die Striche für die Korrektur sind aber am Ende der Strichsequenz. In diesem Fall empfehlen wir, das neu geschriebene Wort separat an die API zu senden und das Ergebnis mithilfe Ihrer eigenen Logik mit den vorherigen Erkennungen zusammenzuführen.

Mehrdeutige Formen

Es gibt Fälle, in denen die Bedeutung der Form, die dem Erkennungstool zur Verfügung gestellt wird, nicht eindeutig ist. Ein Rechteck mit sehr abgerundeten Ecken kann beispielsweise als Rechteck oder als Ellipse erkannt werden.

Diese unklaren Fälle können mithilfe von Erkennungswerten bearbeitet werden, sofern diese verfügbar sind. Nur Klassifikatoren für die Form liefern Bewertungen. Wenn das Modell sehr sicher ist, ist der Wert des Top-Ergebnisses viel besser als der des zweitbesten. Bei Unsicherheit sind die Bewertungen der beiden besten Ergebnisse nah beieinander. Denken Sie auch daran, dass die Formklassifikatoren die gesamte Ink als eine einzelne Form interpretieren. Wenn der Ink beispielsweise ein Rechteck und eine Ellipse nebeneinander enthält, gibt der Recognizer möglicherweise einen oder den anderen (oder etwas ganz anderes) als Ergebnis zurück, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.