Digitale Tinte mit ML Kit auf Android erkennen

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

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

Hinweis

  1. Achten Sie darauf, dass Sie in Ihrer build.gradle-Datei auf Projektebene das Maven-Repository von Google in die Abschnitte buildscript und allprojects aufnehmen.
  2. Fügen Sie die Abhängigkeiten für die ML Kit-Android-Bibliotheken der Gradle-Datei auf App-Ebene hinzu, die normalerweise auf app/build.gradle gesetzt ist:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.0.0'
}

Sie können jetzt Text in Ink-Objekten erkennen.

Ink-Objekt erstellen

Zum Erstellen eines Ink-Objekts kannst du es hauptsächlich auf einem Touchscreen zeichnen. Auf Android-Geräten können Sie zu diesem Zweck einen Canvas verwenden. Ihre Touch-Ereignis-Handler sollten die Methode addNewTouchEvent() aufrufen, die das folgende Code-Snippet zeigt, um die Punkte in den Strichen zu speichern, die der Nutzer in das Objekt Ink zeichnet.

Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. Ein ausführlicheres Beispiel finden Sie im ML Kit-Kurzanleitungsbeispiel.

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 Instanz Ink an ein DigitalInkRecognizer-Objekt, um die Erkennung durchzuführen. Der folgende Code zeigt, wie eine solche Erkennung aus einem BCP-47-Tag 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 wird davon ausgegangen, dass das Erkennungsmodell bereits heruntergeladen wurde, wie im nächsten Abschnitt beschrieben.

Modelldownloads verwalten

Die API für die digitale Tintenerkennung unterstützt Hunderte von Sprachen, allerdings müssen für jede Sprache einige Daten vor der Erkennung heruntergeladen werden. Pro Sprache sind etwa 20 MB Speicher erforderlich. Dies wird vom Objekt RemoteModelManager 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

Wenn Sie ein Modell aus dem Speicher des Geräts 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. Obwohl die digitale Tintenerkennung auf die Verarbeitung vieler Arten von Schreibstilen trainiert wurde, können die Ergebnisse von Nutzer zu Nutzer variieren.

Im Folgenden finden Sie einige Möglichkeiten, die Genauigkeit einer Texterkennung zu verbessern. Diese Techniken gelten nicht für Zeichenklassifikatoren für Emojis, AutoDraw und Formen.

Schreibbereich

Viele Anwendungen haben einen klar definierten Schreibbereich für die Nutzereingabe. Die Bedeutung eines Symbols wird teilweise durch seine Größe relativ zur Größe des Schreibbereichs bestimmt, in dem es enthalten ist. Zum Beispiel der Unterschied zwischen einem Kleinbuchstaben (&) und „&“ (C) und einem Komma im Vergleich zu einem Schrägstrich.

Wenn die Breite und Höhe des Schreibbereichs angegeben werden, kann die Genauigkeit verbessert werden. Bei der Erkennung wird jedoch davon ausgegangen, dass der Schreibbereich nur eine Textzeile enthält. Wenn der physische Schreibbereich groß genug ist, um dem Nutzer das Schreiben von zwei oder mehr Zeilen zu ermöglichen, können Sie bessere Ergebnisse erzielen, wenn Sie einen WriteArea-Wert mit einer Höhe übergeben, die der bestmöglichen Schätzung der Höhe einer einzelnen Textzeile entspricht. Das Objekt „WriteArea“, das Sie an das Erkennungsmodul übergeben, muss nicht genau mit dem physischen Schreibbereich auf dem Bildschirm übereinstimmen. Das Ändern der WriterArea-Höhe auf diese Weise funktioniert in einigen Sprachen besser als in anderen.

Wenn Sie den Schreibbereich angeben, geben Sie die Breite und Höhe in denselben Einheiten wie die Strichkoordinaten an. Für die x- und y-Koordinaten-Argumente ist keine Einheitsanforderung erforderlich. Die API normalisiert alle Einheiten, daher ist nur die relative Größe und Position der Striche wichtig. Sie können Koordinaten in jeder beliebigen Skala Ihres Systems übergeben.

Vor dem Kontext

Der Pre-Kontext ist der Text, der den Strichen im Ink, die du erkennen möchtest, unmittelbar vorangeht. Sie können dem Erkennungsmodul helfen, indem Sie es über den Kontext informieren.

So werden beispielsweise die Schreibbuchstaben "&" und "u" häufig irrtümlich für sich gehalten. Wenn der Nutzer das unvollständige Wort „&arg“ bereits eingegeben hat, kann er mit Strichen fortfahren, die als „&“ oder „n“" erkannt werden. Durch Angabe des Vorkontexts "arg" werden die Unklarheiten beseitigt, da das Wort "argument" wahrscheinlicher ist als "argnment".

Der Kontext kann auch dazu beitragen, dass das Erkennungstool Wortumbrüche erkennt, also die Leerzeichen zwischen den Wörtern. Sie können ein Leerzeichen eingeben, aber nicht zeichnen. Wie kann nun eine Erkennung bestimmen, wann ein Wort endet und das nächste beginnt? Wenn der Nutzer bereits "hello" geschrieben hat und mit dem geschriebenen Wort "world" fortfährt, ohne Vorkontext, gibt die Erkennung den String "world" zurück. Wenn Sie jedoch den Kontext „"hello"“ angeben, gibt das Modell den String „world“ mit einem führenden Leerzeichen zurück, da „Hello World“ sinnvoller ist als „&hellot;helloword"“.

Geben Sie den längsten möglichen Pre-Kontext-String mit maximal 20 Zeichen an, einschließlich Leerzeichen. Ist der String länger, werden nur die letzten 20 Zeichen verwendet.

Das Codebeispiel unten zeigt, wie Sie einen Schreibbereich definieren und ein RecognitionContext-Objekt verwenden, um einen Pre-Kontext 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 Erkennungsgenauigkeit hängt von der Reihenfolge der Striche ab. Die Erkennungssysteme erwarten, dass die Striche in der Reihenfolge vorkommen, in der Nutzer sie schreiben, z. B. von links nach rechts. Jeder Fall, der von diesem Muster abweicht, z. B. das Schreiben eines englischen Satzes, der mit dem letzten Wort beginnt, führt zu weniger genauen Ergebnissen.

Ein weiteres Beispiel: Ein Wort in der Mitte eines Ink wird entfernt und durch ein anderes Wort ersetzt. Die Überarbeitung befindet sich wahrscheinlich in der Mitte eines Satzes, aber die Striche für die Überarbeitung befinden sich am Ende der Strichfolge. In diesem Fall empfehlen wir, das neu geschriebene Wort separat an die API zu senden und das Ergebnis mit den vorherigen Erkennungsvorgängen mit Ihrer eigenen Logik zusammenzuführen.

Umgang mit mehrdeutigen 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 Kanten kann beispielsweise als Rechteck oder als Ellipse angesehen werden.

Diese unklaren Fälle können mithilfe von Erkennungswerten behoben werden, sobald sie verfügbar sind. Nur Formklassifikatoren bieten Bewertungen. Wenn das Modell sehr sicher ist, ist das Top-Ergebnis viel besser als das zweitbeste Ergebnis. Bei Unsicherheit liegen die Werte der ersten beiden Ergebnisse sehr dicht. Denken Sie auch daran, dass die Formklassifikatoren die gesamte Ink als eine Form interpretieren. Wenn beispielsweise Ink ein Rechteck und zwei Ellipsen nebeneinander enthält, kann die Erkennung eine der anderen Ergebnisformen (oder etwas völlig anderes) zurückgeben, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.