Digitale Tinte mit ML Kit auf Android erkennen

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

Testen

  • Probieren Sie die Beispiel-App aus, um sich ein Anwendungsbeispiel dieser API anzusehen.

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 die Abhängigkeiten für die ML Kit-Android-Bibliotheken in die Gradle-Datei des Moduls auf App-Ebene ein. Diese ist normalerweise app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

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

Ink-Objekt erstellen

Die Hauptmethode zum Erstellen eines Ink-Objekts ist das Zeichnen auf einem Touchscreen. Unter Android können Sie zu diesem Zweck ein Canvas verwenden. Ihre Touch-Event-Handler sollten die addNewTouchEvent()-Methode aufrufen, in der das folgende Code-Snippet angezeigt wird, um die Punkte in den Strichen zu speichern, die der Nutzer im Objekt Ink zeichnet.

Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. Ein ausführlicheres Beispiel finden Sie in der Kurzanleitung für das ML Kit.

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 DigitalInkRecognitionr abrufen

Senden Sie die Ink-Instanz an ein DigitalInkRecognizer-Objekt, um die Erkennung auszuführen. Der folgende Code zeigt, wie eine solche Erkennung über ein 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));

Der Beispielcode oben setzt voraus, dass das Erkennungsmodell bereits heruntergeladen wurde, wie im nächsten Abschnitt beschrieben.

Modelldownloads verwalten

Während die API für die digitale Tintenerkennung Hunderte von Sprachen unterstützt, müssen für jede Sprache einige Daten vor einer Erkennung heruntergeladen werden. Pro Sprache sind etwa 20 MB Speicherplatz 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. Die digitale Tintenerkennung kann zwar mit vielen verschiedenen Schreibstilen umgehen, die Ergebnisse können jedoch von Nutzer zu Nutzer variieren.

Im Folgenden finden Sie einige Möglichkeiten, die Genauigkeit einer Texterkennung zu verbessern. Diese Techniken gelten nicht für Zeichenklassifizierungen 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 oder einem Großbuchstaben „o“ oder „c“ und einem Komma im Vergleich zu einem Schrägstrich.

Sie können die Genauigkeit verbessern, indem Sie dem Erkennungsgerät die Breite und Höhe des Schreibbereichs mitteilen. 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 zu ermöglichen, zwei oder mehr Zeilen zu schreiben, können Sie bessere Ergebnisse erzielen, wenn Sie eine WriteArea mit einer Höhe übergeben, die der bestmöglichen Schätzung der Höhe einer einzelnen Textzeile entspricht. Das Writer-Objekt, das Sie an das Erkennungsmodul übergeben, muss nicht genau mit dem physischen Schreibbereich auf dem Bildschirm übereinstimmen. Das Ändern der Höhe von „WriteArea“ funktioniert in einigen Sprachen besser als in anderen.

Wenn Sie den Schreibbereich angeben, geben Sie seine Breite und Höhe in denselben Einheiten an wie die Strichkoordinaten. Für die x- und y-Koordinatenargumente ist keine Einheit erforderlich. Die API normalisiert alle Einheiten. Nur die relative Größe und Position der Striche ist wichtig. Sie können Koordinaten in einer beliebigen Größenordnung für Ihr System übergeben.

Vorkontext

Der Pre-Kontext ist der Text, der in den Ink, den Sie zu erkennen versuchen, unmittelbar vor den Strichen platziert wird. Sie können dem Erkennungsmodul helfen, indem Sie es über den Pre-Kontext informieren.

So werden beispielsweise die Buchstaben „n“ und „u“ häufig falsch verstanden. Wenn der Nutzer bereits das unvollständige Wort „arg“ eingegeben hat, kann er weitere Striche verwenden, die als „ument“ oder „nment“ erkannt werden. Durch Angabe des Pre-Kontexts "arg" wird die Mehrdeutigkeit behoben, da das Wort "Argument" wahrscheinlicher als "argnment" ist.

Der Pre-Kontext kann auch dabei helfen, die Zeilenumbrüche zu erkennen, also die Leerzeichen zwischen den Wörtern. Sie können ein Leerzeichen eingeben, aber nicht zeichnen. Wie kann dann 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“ ohne Kontext fährt, gibt das Erkennungsmodul den String „world“ zurück. Wenn Sie jedoch „hello“ als Kontext angeben, gibt das Modell den String „world“ mit einem führenden Leerzeichen zurück, da „hello world“ sinnvoller ist als „helloword“.

Sie sollten den längstmöglichen String im Pre-Kontext angeben (bis zu 20 Zeichen, einschließlich Leerzeichen). Wenn der String länger ist, verwendet das Erkennungsmodul nur die letzten 20 Zeichen.

Das Codebeispiel unten zeigt, wie ein Schreibbereich definiert und ein RecognitionContext-Objekt verwendet wird, um einen 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 ist von der Reihenfolge der Striche abhängig. Die Erkennungssysteme erwarten Striche in der Reihenfolge, in der Nutzer schreiben, z. B. von links nach rechts für Englisch. Jeder Fall, der von diesem Muster abweicht, z. B. das Schreiben eines englischen Satzes, der mit dem letzten Wort beginnt, liefert weniger genaue Ergebnisse.

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

Umgang mit mehrdeutigen Formen

Es gibt Fälle, in denen die Bedeutung der Form, die dem Erkennungsmodul zugewiesen ist, mehrdeutig ist. Ein Rechteck mit sehr abgerundeten Kanten kann beispielsweise als Rechteck oder eine Ellipse betrachtet werden.

Diese unklaren Fälle können mithilfe von Erkennungswerten gehandhabt werden, sobald sie verfügbar sind. Nur Formklassifikatoren liefern Bewertungen. Wenn das Modell sehr zuversichtlich ist, ist die Punktzahl des besten Ergebnisses viel besser als die zweitbeste. Wenn die Werte nicht sicher sind, liegen die Ergebnisse für die beiden besten Ergebnisse nahe beieinander. Denken Sie außerdem daran, dass die Formklassifikatoren die gesamte Ink als eine Form interpretieren. Wenn beispielsweise Ink ein Rechteck und eine Ellipse nebeneinander enthält, kann die Erkennung eine oder eine andere (oder etwas völlig anderes) als Ergebnis zurückgeben, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.