Digitale Tinte mit ML Kit unter Android erkennen

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

Ausprobieren

  • Probieren Sie die Beispiel-App aus, um ein Beispiel für die Verwendung dieser API zu sehen.

Hinweis

  1. Fügen Sie in der Datei build.gradle auf Projektebene in den Abschnitten buildscript und allprojects das Maven-Repository von Google ein.
  2. Fügen Sie der Gradle-Datei Ihres Moduls auf App-Ebene die Abhängigkeiten für die ML Kit-Android-Bibliotheken hinzu. Diese ist normalerweise app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

Sie können jetzt mit der Texterkennung in Ink-Objekten beginnen.

Ink-Objekt erstellen

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

Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. Ein ausführlicheres 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

Senden Sie die Ink-Instanz an ein DigitalInkRecognizer-Objekt, um eine Erkennung durchzuführen. Der folgende Code zeigt, wie eine solche Erkennung von einem BCP-47 instanziiert 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 Digital Ink Detection API 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. Dies wird vom RemoteModelManager-Objekt verarbeitet.

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

Durch das Entfernen eines Modells aus dem Gerätespeicher 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. Genauigkeit hängt auch vom Schreibstil ab. Die digitale Tintenerkennung wurde für viele Arten von Schreibstilen trainiert, die Ergebnisse können jedoch von Nutzer zu Nutzer variieren.

Im Folgenden finden Sie einige Möglichkeiten, wie Sie die Genauigkeit der Texterkennung verbessern können. Beachten Sie, dass diese Techniken nicht für die Klassifikatoren für Emojis, AutoDraw und Formen gelten.

Schreibbereich

Viele Anwendungen haben einen klar definierten Schreibbereich 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. Zum Beispiel der Unterschied zwischen einem Groß- oder Kleinbuchstaben „o“ oder „c“ und einem Komma und einem Schrägstrich.

Wenn Sie der Erkennung die Breite und Höhe des Schreibbereichs mitteilen, kann die Genauigkeit verbessert werden. Die Erkennung geht jedoch davon aus, dass der Schreibbereich nur eine einzige Textzeile enthält. Wenn der physische Schreibbereich groß genug ist, um dem Nutzer das Schreiben von zwei oder mehr Zeilen zu ermöglichen, erhalten 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 die Erkennung übergeben, muss nicht genau dem physischen Schreibbereich auf dem Bildschirm entsprechen. Das Ändern der Höhe von WritingArea funktioniert in einigen Sprachen besser als in anderen.

Wenn Sie den Schreibbereich festlegen, geben Sie seine Breite und Höhe mit denselben Einheiten wie die Strichkoordinaten an. Für die x- und y-Koordinatenargumente ist keine Einheit erforderlich. Die API normalisiert alle Einheiten, sodass nur die relative Größe und Position der Striche wichtig ist. Sie können Koordinaten in jedem für Ihr System sinnvollen Maßstab übergeben.

Vor dem Kontext

„Vorkontext“ ist der Text, der den Strichen in Ink, die Sie erkennen möchten, unmittelbar vorausgeht. Sie können der Erkennung helfen, indem Sie sie über den Vorkontext informieren.

Zum Beispiel werden die Schreibbuchstaben „n“ und „u“ oft verwechselt. Wenn der Nutzer den Teilwort „arg“ bereits eingegeben hat, kann er mit Strichen fortfahren, die als „ument“ oder „nment“ erkannt werden. Durch Angabe des Vorkontexts "arg" wird die Mehrdeutigkeit behoben, da das Wort "argument" wahrscheinlicher als "argnment" ist.

Vor dem Kontext kann die Erkennung auch beim Erkennen von Wortumbrüchen – den Leerzeichen zwischen Wörtern – helfen. Sie können ein Leerzeichen eingeben, aber kein Zeichen zeichnen. Wie kann dann eine Erkennung erkennen, wann ein Wort endet und das nächste beginnt? Wenn der Nutzer bereits „hello“ geschrieben hat und mit dem geschriebenen Wort „world“ (ohne Vorkontext) fortfährt, gibt die Erkennung den String „world“ zurück. Wenn Sie jedoch „hello“ vor dem Kontext angeben, gibt das Modell den String „world“ mit einem vorangestellten Leerzeichen zurück, da „hello world“ sinnvoller ist als „helloword“.

Geben Sie den längsten möglichen String vor dem Kontext an. Er kann bis zu 20 Zeichen, einschließlich Leerzeichen, enthalten. Wenn der String länger ist, verwendet die Erkennung nur die letzten 20 Zeichen.

Das folgende Codebeispiel zeigt, wie Sie einen Schreibbereich definieren und ein RecognitionContext-Objekt verwenden, um einen Vorkontext anzugeben.

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

Strichanordnung

Die Genauigkeit der Erkennung hängt von der Reihenfolge der Striche ab. Bei der Erkennung wird erwartet, dass die Schlaganschläge in der Reihenfolge auftreten, in der sie normalerweise schreiben, z. B. von links nach rechts für Englisch. Wenn Sie von diesem Muster abweichen, z. B. wenn ein Satz auf Englisch mit dem letzten Wort beginnt, erhalten Sie weniger genaue Ergebnisse.

Ein anderes Beispiel wäre, wenn ein Wort mitten in einem Ink entfernt und durch ein anderes Wort ersetzt wird. Die Überarbeitung befindet sich wahrscheinlich mitten in einem Satz, aber die Striche für die Überarbeitung befinden sich am Ende der Strichsequenz. In diesem Fall empfehlen wir, das neu geschriebene Wort separat an die API zu senden und das Ergebnis mit der vorherigen Erkennung unter Verwendung Ihrer eigenen Logik zusammenzuführen.

Umgang mit zweideutigen Formen

Es gibt Fälle, in denen die Bedeutung der Form, die dem Erkennungsmodul bereitgestellt wird, mehrdeutig ist. Beispielsweise kann ein Rechteck mit stark abgerundeten Kanten entweder als Rechteck oder als Ellipse betrachtet werden.

Bei diesen unklaren Fällen können Sie die entsprechenden Bewertungen verwenden, sofern diese verfügbar sind. Nur Formklassifikatoren liefern Bewertungen. Wenn das Modell sehr sicher ist, ist die Punktzahl des besten Ergebnisses viel besser als das zweitbeste. Bei Unsicherheit werden die Punktzahlen für die beiden besten Ergebnisse nah beieinander liegen. Beachten Sie außerdem, dass die Formklassifikatoren die gesamte Ink als einzelne Form interpretieren. Wenn Ink beispielsweise ein Rechteck und eine Ellipse nebeneinander enthält, gibt die Erkennung möglicherweise eine der beiden Formen (oder etwas völlig anderes) als Ergebnis zurück, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.