Digitale Tinte mit ML Kit unter 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.

Ausprobieren

  • Probieren Sie die Beispiel-App aus, um sich ein Anwendungsbeispiel für diese API anzusehen.

Hinweis

  1. Fügen Sie in der Datei build.gradle auf Projektebene das Maven-Repository von Google in die Abschnitte buildscript und allprojects ein.
  2. Fügen Sie die Abhängigkeiten für die ML Kit-Android-Bibliotheken in die Gradle-Datei auf App-Ebene Ihres Moduls ein, die normalerweise app/build.gradle ist:
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 dafür einen Canvas verwenden. Die Touch-Event-Handler sollten die im folgenden Code-Snippet gezeigte Methode addNewTouchEvent() aufrufen, um die Punkte in den Strichen zu speichern, die der Nutzer in das Ink-Objekt zieht.

Dieses allgemeine Muster wird im folgenden Code-Snippet demonstriert. Ein ausführlicheres Beispiel finden Sie in der Kurzanleitung zu 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 DigitalInkDetectr abrufen

Senden Sie die Instanz Ink an ein DigitalInkRecognizer-Objekt, um eine 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 oben wird davon ausgegangen, dass das Erkennungsmodell bereits heruntergeladen wurde, wie im nächsten Abschnitt beschrieben.

Modelldownloads verwalten

Während die Digital Ink Detection API Hunderte von Sprachen unterstützt, müssen für jede Sprache vor der Erkennung einige Daten heruntergeladen werden. Pro Sprache werden etwa 20 MB Speicherplatz benötigt. Dies wird vom RemoteModelManager-Objekt gehandhabt.

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 digitale Tintenerkennung wurde zwar für die Verarbeitung vieler Arten von Schreibstilen trainiert, die Ergebnisse können jedoch von Nutzer zu Nutzer variieren.

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

Schreibbereich

Viele Anwendungen haben einen klar definierten Schreibbereich für Nutzereingaben. 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 die Differenz zwischen einem Klein- oder Großbuchstaben „o“ oder „c“ und einem Komma im Vergleich zu einem Schrägstrich.

Wenn Sie der Erkennung die Breite und Höhe des Schreibbereichs mitteilen, kann dies die Genauigkeit verbessern. Das Erkennungsmodul geht jedoch davon aus, dass der Schreibbereich nur eine einzige Textzeile enthält. Wenn der physische Schreibbereich groß genug ist, damit der Nutzer zwei oder mehr Zeilen schreiben kann, erhalten Sie möglicherweise bessere Ergebnisse, wenn Sie einen Schreibbereich mit einer Höhe übergeben, die Ihrer bestmöglichen Schätzung der Höhe einer einzelnen Textzeile entspricht. Das WritingArea-Objekt, das Sie an die Erkennung übergeben, muss nicht genau mit dem physischen Schreibbereich auf dem Bildschirm übereinstimmen. Das Ändern der Schreibbereichshöhe funktioniert in einigen Sprachen besser als in anderen.

Wenn Sie den Schreibbereich angeben, geben Sie seine Breite und Höhe in den gleichen Einheiten wie die Strichkoordinaten an. Für die x- und y-Koordinatenargumente gibt es keine Einheitenanforderungen. Die API normalisiert alle Einheiten. Es kommt also nur auf die relative Größe und Position der Striche an. Sie können Koordinaten in jedem für Ihr System sinnvollen Maßstab übergeben.

Vor dem Kontext

Der Kontext vor dem Kontext ist der Text, der den Strichen im Ink unmittelbar vorausgeht, den Sie erkennen möchten. Sie können dem Erkennungsmodul helfen, indem Sie es über den Vorkontext informieren.

Zum Beispiel werden die Kursbuchstaben „n“ und „u“ oft verwechselt. Wenn der Nutzer bereits den Teilwort „arg“ eingegeben hat, kann er mit Strichen fortfahren, die als „ument“ oder „nment“ erkannt werden. Die Angabe des Vorkontexts „arg“ löst die Ambiguität, da das Wort „Argument“ wahrscheinlicher als „argnment“ ist.

Vor dem Kontext kann die Erkennung auch dabei helfen, Wortumbrüche, also die Leerzeichen zwischen Wörtern, zu erkennen. Sie können ein Leerzeichen eingeben, aber keins zeichnen. Wie kann also ein Erkennungsgerät feststellen, wann ein Wort endet und das nächste beginnt? Wenn der Nutzer bereits „hello“ geschrieben hat und mit dem geschriebenen Wort „world“ fortfährt, gibt die Erkennung ohne Vorkontext den String „world“ zurück. Wenn Sie jedoch den Vorkontext „hello“ angeben, gibt das Modell den String „world“ mit einem vorangestellten Leerzeichen zurück, da „hello world“ sinnvoller ist als „helloword“.

Geben Sie den längstmöglichen Vorkontext-String an. Er darf maximal 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 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);

Strichreihenfolge

Die Genauigkeit der Erkennung hängt von der Reihenfolge der Striche ab. Die Erkennungsfunktionen erwarten, dass die Striche in der Reihenfolge auftreten, in der die Nutzer schreiben würden, z. B. von links nach rechts für Englisch. Jeder Fall, der von diesem Muster abweicht, z. B. wenn ein englischer Satz mit dem letzten Wort beginnt, liefert weniger genaue Ergebnisse.

Ein weiteres Beispiel ist, 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 mithilfe Ihrer eigenen Logik mit den vorherigen Erkennungen zusammenzuführen.

Umgang mit mehrdeutigen Formen

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

Diese unklaren Fälle können mithilfe von Erkennungswerten bearbeitet werden, sofern sie verfügbar sind. Nur Formklassifikatoren liefern Bewertungen. Wenn das Modell sehr sicher ist, ist die Punktzahl des besten Ergebnisses viel besser als die zweitbeste. Bei Unsicherheit liegen die Punktzahlen für die beiden besten Ergebnisse nahe. Außerdem interpretieren die Formklassifikatoren das gesamte Ink als eine Form. Wenn die Ink beispielsweise ein Rechteck und eine Ellipse enthält, die nebeneinander steht, kann die Erkennung eine der beiden Formen (oder etwas völlig anderes) zurückgeben, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.