Digitale Tinte mit ML Kit unter Android erkennen

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

Jetzt ausprobieren

  • In der Beispiel-App finden Sie ein Beispiel für die Verwendung dieser API.

Hinweis

  1. Fügen Sie in der build.gradle-Datei 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 der Gradle-Datei Ihres Moduls auf App-Ebene hinzu. Diese Datei ist normalerweise app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:19.0.0'
}

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

Ink-Objekt erstellen

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

Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. Ein vollständigeres Beispiel finden Sie im ML Kit-Beispiel für den Schnelleinstieg.

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 die Erkennung durchzuführen. Der folgende Code zeigt, wie Sie einen solchen Erkenner aus einem BCP-47-Tag instanziieren.

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 obigen Beispielcode wird davon ausgegangen, dass das Erkennungsmodell bereits heruntergeladen wurde, wie im nächsten Abschnitt beschrieben.

Modell-Downloads verwalten

Die API für die Erkennung digitaler Tinte 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 übernommen.

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 Schrifterkennung ist zwar darauf trainiert, viele verschiedene Schreibstile zu verarbeiten, die Ergebnisse können jedoch von Nutzer zu Nutzer variieren.

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

Schreibfläche

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 sich befindet. Beispiele: der Unterschied zwischen einem Klein- oder Großbuchstaben „o“ oder „c“ und einem Komma im Vergleich zu einem Schrägstrich.

Wenn Sie dem Erkennungssystem die Breite und Höhe des Schreibbereichs mitteilen, kann dies die Genauigkeit verbessern. Die Erkennung 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 Recognizer ü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.

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

Kontext vor der Frage

Der Präkontext ist der Text, der unmittelbar vor den Strichen in der Ink steht, die Sie erkennen möchten. Sie können die Erkennung unterstützen, indem Sie den Vorabkontext angeben.

Beispielsweise werden die Schreibschriftbuchstaben „n“ und „u“ oft verwechselt. Wenn der Nutzer bereits das Teilwort „arg“ eingegeben hat, kann er mit Strichen fortfahren, die als „ument“ oder „nment“ erkannt werden können. Durch die Angabe des Vorkontexts „arg“ wird die Mehrdeutigkeit aufgelöst, da das Wort „argument“ wahrscheinlicher ist als „argnment“.

Der Vorabkontext kann dem Erkennungssystem auch dabei helfen, Wortgrenzen, also die Leerzeichen zwischen Wörtern, zu identifizieren. Sie können ein Leerzeichen eingeben, aber nicht zeichnen. Wie kann ein Erkennungssystem 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 die Erkennung ohne Vorkontext den String „Welt“ zurück. Wenn Sie jedoch den Vorabkontext „hello“ angeben, gibt das Modell den String „ world“ mit einem führenden Leerzeichen zurück, da „hello world“ mehr Sinn ergibt als „helloword“.

Sie sollten einen möglichst langen String für den Vorabkontext angeben, bis zu 20 Zeichen, einschließlich Leerzeichen. Wenn der String länger ist, verwendet der Erkennungsdienst nur die letzten 20 Zeichen.

Im folgenden Codebeispiel wird gezeigt, wie Sie einen Schreibbereich definieren und ein RecognitionContext-Objekt verwenden, um den Vorabkontext 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 Erkennungsgenauigkeit hängt von der Reihenfolge der Striche ab. Die Erkennungssysteme erwarten, dass die Striche in der Reihenfolge erfolgen, in der Menschen normalerweise 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, führt 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 Überarbeitung befindet sich wahrscheinlich mitten in einem Satz, 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 mithilfe Ihrer eigenen Logik mit den vorherigen Erkennungen zusammenzuführen.

Umgang mit mehrdeutigen Formen

In manchen Fällen ist die Bedeutung der Form, die dem Erkennungstool bereitgestellt wird, mehrdeutig. Ein Rechteck mit stark abgerundeten Ecken kann beispielsweise entweder als Rechteck oder als Ellipse angesehen werden.

Diese unklaren Fälle können mithilfe von Erkennungswerten behandelt werden, sofern diese verfügbar sind. Nur Formklassifikatoren liefern Bewertungen. Wenn das Modell sehr sicher ist, ist der Wert des besten Ergebnisses viel besser als der des zweitbesten. Wenn Unsicherheit besteht, liegen die Werte für die beiden besten Ergebnisse nahe beieinander. Beachten Sie auch, dass die Formklassifikatoren die gesamte Ink als eine einzige Form interpretieren. Wenn die Ink beispielsweise ein Rechteck und eine Ellipse nebeneinander enthält, gibt die Erkennung möglicherweise das eine oder das andere (oder etwas völlig anderes) als Ergebnis zurück, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.