Rozpoznawanie tuszów cyfrowych za pomocą ML Kit na Androidzie

Dzięki cyfrowemu rozpoznawaniu pisma odręcznego w ML Kit możesz rozpoznawać tekst odręczny na cyfrowej powierzchni w setkach języków, a także klasyfikować szkice.

Wypróbuj

Zanim zaczniesz

  1. Upewnij się, że w sekcji buildscript i allprojects w pliku build.gradle na poziomie projektu znajduje się repozytorium Google Maven.
  2. Dodaj zależności dla bibliotek ML Kit na Androida do pliku Gradle na poziomie modułu. Zwykle ma on postać app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

Teraz możesz zacząć rozpoznawać tekst w obiektach (Ink).

Tworzenie obiektu Ink

Podstawowym sposobem utworzenia obiektu Ink jest rysowanie na ekranie dotykowym. Na urządzeniu z Androidem możesz do tego celu użyć Canvas. Moduły obsługi zdarzeń kliknięcia powinny wywoływać metodę addNewTouchEvent() widoczną poniżej, aby zapisywać punkty pociągów narysowanych przez użytkownika w obiekcie Ink.

Ten ogólny wzorzec omawiamy we fragmencie kodu poniżej. Dokładniejszy przykład znajdziesz w krótkim wprowadzeniu do 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();

Pobieranie instancji DigitalInkRecognizer

Aby przeprowadzić rozpoznawanie, wyślij instancję Ink do obiektu DigitalInkRecognizer. Poniższy kod pokazuje, jak utworzyć wystąpienie takiego modułu rozpoznawania z tagu BCP-47.

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

Przetwarzanie obiektu Ink

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

W przykładowym kodzie powyżej założono, że model rozpoznawania został już pobrany, co opisano w następnej sekcji.

Zarządzanie pobranymi modelami

Chociaż interfejs API do cyfrowego rozpoznawania tuszu obsługuje setki języków, z każdego z nich trzeba pobrać dane, zanim zostanie ono rozpoznane. Dla każdego języka wymagane jest około 20 MB miejsca na dane. Jest ona obsługiwana przez obiekt RemoteModelManager.

Pobierz nowy model

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

Sprawdzanie, czy model został już pobrany

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.isModelDownloaded(model)

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.isModelDownloaded(model);

Usuwanie pobranego modelu

Usunięcie modelu z pamięci urządzenia spowoduje zwolnienie miejsca.

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

Wskazówki dotyczące poprawy dokładności rozpoznawania tekstu

Dokładność rozpoznawania tekstu może być różna w zależności od języka. Dokładność zależy też od stylu pisania. Chociaż technologia ta jest trenowana pod kątem obsługi różnego rodzaju stylów pisania, wyniki mogą być różne w zależności od użytkownika.

Oto kilka sposobów na zwiększenie dokładności modułu rozpoznawania tekstu. Pamiętaj, że te techniki nie dotyczą klasyfikatorów emotikonów, automatycznego rysowania i kształtów.

Obszar do pisania

Wiele aplikacji ma dobrze zdefiniowany obszar do wprowadzania danych przez użytkownika. Znaczenie symbolu jest częściowo określane przez jego rozmiar w odniesieniu do powierzchni, w której się on znajduje. Na przykład różnica między małą lub wielką literą „o” lub „c” oraz przecinkiem i ukośnikiem.

Informowanie modułu rozpoznawania o szerokości i wysokości obszaru pisania może zwiększyć dokładność. Moduł rozpoznawania zakłada jednak, że obszar pisania zawiera tylko jeden wiersz tekstu. Jeśli fizyczny obszar pisania jest wystarczająco duży, aby użytkownik mógł napisać co najmniej 2 wiersze, można uzyskać lepsze wyniki, przekazując obszar Pisanie o wysokości, która najlepiej określa wysokość pojedynczego wiersza tekstu. Obiekt WriteArea, który przekazujesz do modułu rozpoznawania, nie musi dokładnie odpowiadać fizycznemu obszarowi do pisania na ekranie. Ta zmiana wysokości obszaru pisania działa lepiej w niektórych językach niż w innych.

Określając obszar do pisania, określ jego szerokość i wysokość w tych samych jednostkach co współrzędne kreski. Argumenty współrzędnych x,y nie mają wymagań jednostki – interfejs API normalizuje wszystkie jednostki, więc liczą się tylko względy rozmiar i położenie kresek. Możesz wprowadzać współrzędne w dowolnej skali.

Przed kontekstem

Wstępny kontekst to tekst, który bezpośrednio poprzedza kreski w elemencie Ink, które próbujesz rozpoznać. Możesz pomóc modułowi rozpoznawania, informując go o kontekście wstępnym.

Na przykład litery „n” i „u” są często mylone ze sobą. Jeśli użytkownik wpisał już część słowa „argument”, może kontynuować rysowanie, które zostanie rozpoznane jako „ument” lub „nment”. Określenie argumentu „argment” pozwala rozwiązać ten problem, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.

Wstępny kontekst może również pomóc modułowi rozpoznawania w identyfikowaniu podziałów słów, czyli spacji między nimi. Można wpisywać spację, ale nie można jej narysować. Jak zatem moduł rozpoznawania może określić, kiedy kończy się jedno słowo, a zaczyna następne? Jeśli użytkownik napisał już „cześć”, a ciągnie dalej z tekstem „world”, moduł rozpoznawania zwróci ciąg „world” (bez kontekstu). Jeśli jednak podasz w kontekście ciąg „hello”, model zwróci ciąg „world” z początkową spacją, ponieważ słowo „helloworld” ma większą sens niż „helloword”.

Musisz podać jak najdłuższy ciąg znaków dostępny przed kontekstem (maksymalnie 20 znaków łącznie ze spacjami). Jeśli jest dłuższy, moduł rozpoznawania użyje tylko ostatnich 20 znaków.

Przykładowy kod poniżej pokazuje, jak określić obszar do pisania i użyć obiektu RecognitionContext do określenia wstępnego kontekstu.

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

Kolejność kresek

Dokładność rozpoznawania jest zależna od kolejności kresek. Moduły rozpoznawania oczekują, że kreski będą występować w takiej kolejności, w jakiej ludzie piszą w sposób naturalny, np. od lewej do prawej w przypadku języka angielskiego. Strony, które odstają od tego wzorca, na przykład pisanie angielskie zdania rozpoczynającego się od ostatniego słowa, dają mniej dokładne wyniki.

Innym przykładem jest usunięcie słowa w elemencie Ink i zastąpienie go innym słowem. Wersja prawdopodobnie znajduje się w środku zdania, ale zmiany kreski znajdują się na końcu sekwencji kresek. W takim przypadku zalecamy wysłanie nowo napisanego słowa oddzielnie do interfejsu API i scalenie wyniku z wcześniejszymi rozpoznaniami za pomocą własnej logiki.

Radzenie sobie z niejednoznacznymi kształtami

W niektórych przypadkach znaczenie kształtu przekazanego modułowi rozpoznawania jest niejednoznaczne. Na przykład prostokąt z bardzo zaokrąglonymi krawędziami może być prezentowany jako prostokąt lub elipsa.

W przypadku takich niejasnych przypadków można zastosować oceny rozpoznawania, jeśli są dostępne. Wyniki podają tylko klasyfikatory kształtów. Jeśli model jest bardzo pewny, wynik z najwyższego wyniku będzie dużo wyższy niż drugi najlepszy. W przypadku niepewności wyniki dwóch pierwszych wyników będą zbliżone. Pamiętaj też, że klasyfikatory kształtów interpretują cały Ink jako pojedynczy kształt. Jeśli na przykład element Ink zawiera obok siebie prostokątny i elipsę, moduł rozpoznawania może w wyniku wyświetlić jeden albo drugi element (lub coś zupełnie innego), ponieważ pojedynczy kandydat do rozpoznawania nie może reprezentować 2 kształtów.