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

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

Wypróbuj

Zanim zaczniesz

  1. W pliku build.gradle na poziomie projektu dodaj repozytorium Maven firmy Google do sekcji buildscriptallprojects.
  2. Dodaj zależności dla bibliotek ML Kit na Androida do pliku Gradle na poziomie aplikacji modułu, który zwykle znajduje się w tym miejscu: app/build.gradle
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:19.0.0'
}

Możesz teraz rozpocząć rozpoznawanie tekstu w obiektach Ink.

Tworzenie obiektu Ink

Głównym sposobem tworzenia obiektu Ink jest narysowanie go na ekranie dotykowym. Na urządzeniach z Androidem możesz w tym celu użyć płótna. Obsługa zdarzeń dotykowych powinna wywoływać metodę addNewTouchEvent() pokazaną w tym fragmencie kodu, aby przechowywać punkty w pociągnięciach, które użytkownik rysuje w obiekcie Ink.

Ten ogólny wzorzec pokazujemy w tym fragmencie kodu. Bardziej szczegółowy 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ć instancję takiego rozpoznawania na podstawie 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));

Przykładowy kod powyżej zakłada, że model rozpoznawania został już pobrany, jak opisano w następnej sekcji.

Zarządzanie pobieraniem modeli

Interfejs API rozpoznawania pisma odręcznego obsługuje setki języków, ale w przypadku każdego z nich przed rozpoznaniem trzeba pobrać pewne dane. Każdy język wymaga około 20 MB miejsca na dane. Zajmuje się tym obiekt RemoteModelManager.

Pobieranie nowego modelu

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 się różnić w zależności od języka. Dokładność zależy też od stylu pisania. Funkcja rozpoznawania pisma odręcznego jest trenowana pod kątem obsługi wielu stylów pisania, ale wyniki mogą się różnić w zależności od użytkownika.

Oto kilka sposobów na zwiększenie dokładności rozpoznawania tekstu. Pamiętaj, że te techniki nie mają zastosowania do klasyfikatorów rysunków w przypadku emoji, automatycznego rysowania i kształtów.

Obszar pisania

Wiele aplikacji ma dobrze zdefiniowany obszar pisania, w którym użytkownik może wprowadzać dane. Znaczenie symbolu jest częściowo określone przez jego rozmiar w stosunku do rozmiaru obszaru pisania, w którym się znajduje. Na przykład różnica między małą i wielką literą „o” lub „c” oraz między przecinkiem a ukośnikiem.

Podanie rozpoznawaniu szerokości i wysokości obszaru pisania może zwiększyć dokładność. Jednak rozpoznawanie zakłada, że obszar pisania zawiera tylko jeden wiersz tekstu. Jeśli fizyczny obszar pisania jest wystarczająco duży, aby użytkownik mógł napisać 2 lub więcej wierszy, możesz uzyskać lepsze wyniki, przekazując wartość WritingArea o wysokości, która jest Twoim najlepszym oszacowaniem wysokości pojedynczego wiersza tekstu. Obiekt WritingArea przekazywany do rozpoznawania nie musi dokładnie odpowiadać fizycznemu obszarowi pisania na ekranie. Zmiana wysokości obszaru pisania w ten sposób sprawdza się lepiej w niektórych językach niż w innych.

Podczas określania obszaru pisania podaj jego szerokość i wysokość w tych samych jednostkach co współrzędne pociągnięcia. Argumenty współrzędnych x i y nie wymagają jednostek – interfejs API normalizuje wszystkie jednostki, więc liczy się tylko względny rozmiar i położenie pociągnięć. Możesz przekazywać współrzędne w dowolnej skali, która jest odpowiednia dla Twojego systemu.

Kontekst przed

Kontekst poprzedzający to tekst, który bezpośrednio poprzedza znaki w Ink, które próbujesz rozpoznać. Możesz pomóc rozpoznawaniu, podając mu kontekst.

Na przykład litery pisane „n” i „u” są często mylone. Jeśli użytkownik wpisał już część słowa „arg”, może kontynuować wpisywanie znaków, które można rozpoznać jako „ument” lub „nment”. Określenie kontekstu poprzedzającego „arg” rozwiązuje niejednoznaczność, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.

Kontekst przed słowem może też pomóc rozpoznawaniu w określaniu przerw między słowami, czyli spacji. Możesz wpisać spację, ale nie możesz jej narysować. Jak więc system rozpoznawania może określić, kiedy kończy się jedno słowo, a zaczyna drugie? Jeśli użytkownik napisał już „hello” i kontynuuje pisanie słowa „world”, bez kontekstu wstępnego rozpoznawanie zwróci ciąg znaków „world”. Jeśli jednak określisz kontekst wstępny „hello”, model zwróci ciąg znaków „ world” ze spacją na początku, ponieważ „hello world” ma większy sens niż „helloword”.

Podaj jak najdłuższy ciąg znaków przed kontekstem (maksymalnie 20 znaków, w tym spacji). Jeśli ciąg jest dłuższy, rozpoznawanie obejmuje tylko ostatnie 20 znaków.

Poniższy przykładowy kod pokazuje, jak zdefiniować obszar pisania i użyć obiektu RecognitionContext do określenia kontekstu wstępnego.

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ść pociągnięć

Dokładność rozpoznawania zależy od kolejności kreślenia. Rozpoznawanie pisma odręcznego oczekuje, że pociągnięcia będą wykonywane w kolejności, w jakiej ludzie naturalnie piszą, np. od lewej do prawej w przypadku języka angielskiego. Każdy przypadek, który odbiega od tego wzorca, np. napisanie zdania w języku angielskim zaczynającego się od ostatniego słowa, daje mniej dokładne wyniki.

Inny przykład to usunięcie słowa ze środka Ink i zastąpienie go innym słowem. Poprawka prawdopodobnie znajduje się w środku zdania, ale pociągnięcia związane z poprawką są na końcu sekwencji pociągnięć. W takim przypadku zalecamy wysłanie nowo napisanego słowa osobno do interfejsu API i połączenie wyniku z poprzednimi rozpoznaniami za pomocą własnej logiki.

Radzenie sobie z niejednoznacznymi kształtami

Czasami znaczenie kształtu przekazanego do rozpoznawania jest niejednoznaczne. Na przykład prostokąt o bardzo zaokrąglonych krawędziach może być postrzegany jako prostokąt lub elipsa.

W takich niejasnych przypadkach można używać wyników rozpoznawania, jeśli są dostępne. Tylko klasyfikatory kształtów podają wyniki. Jeśli model jest bardzo pewny, wynik najlepszego rezultatu będzie znacznie lepszy niż drugiego w kolejności. Jeśli istnieje niepewność, wyniki dla 2 najlepszych rezultatów będą zbliżone. Pamiętaj też, że klasyfikatory kształtów interpretują cały znak Ink jako jeden kształt. Jeśli na przykład Ink zawiera prostokąt i elipsę obok siebie, rozpoznawanie może zwrócić jeden z nich (lub coś zupełnie innego), ponieważ pojedynczy kandydat rozpoznawania nie może reprezentować dwóch kształtów.