Rozpoznawanie cyfrowych atramentów z ML Kit na Androida

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Dzięki cyfrowemu rozpoznawaniu tuszu w ML Kit możesz rozpoznawać tekst pisany odręcznie na cyfrowej powierzchni w setkach języków i klasyfikować szkice.

Wypróbuj

Zanim zaczniesz

  1. W pliku build.gradle na poziomie projektu umieść repozytorium Google Maven w sekcjach buildscript i allprojects.
  2. Dodaj zależności bibliotek ML Kit na Androida do pliku Gradle modułu na poziomie aplikacji, który zwykle wynosi 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

Głównym sposobem na zbudowanie obiektu Ink jest narysowanie go na ekranie dotykowym. Na urządzeniu z Androidem możesz w tym celu użyć płótna. Twoje moduły obsługi zdarzeń dotykowych powinny wywoływać metodę addNewTouchEvent() wyświetlający ten fragment kodu, aby zapisywać punkty na pociągnięciach po obiekcie Ink.

Ogólny wzorzec jest przedstawiony w poniższym fragmencie kodu. Pełniejszy 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 DigitalInkrozpoznar

Aby przeprowadzić rozpoznawanie, wyślij instancję Ink do obiektu DigitalInkRecognizer. Kod poniżej pokazuje, jak utworzyć takie narzędzie do 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, tak jak to opisano w następnej sekcji.

Zarządzanie pobranymi modelami

Choć interfejs API atramentu cyfrowego obsługuje setki języków, przed każdym rozpoznawaniem trzeba pobrać dane. Wymagane ok. 20 MB miejsca na język. Jest on obsługiwany 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 zwiększania 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. Chociaż model cyfrowych atramentów jest trenowany do obsługi różnych rodzajów pisma, wyniki mogą być różne w zależności od użytkownika.

Oto kilka sposobów na zwiększenie dokładności mechanizmu rozpoznawania tekstu. Te metody nie mają zastosowania w przypadku klasyfikatorów rysunków emotikonów, automatycznych rysunków i kształtów.

Obszar pisania

Wiele aplikacji ma dobrze sprecyzowany obszar wpisywania tekstu przez użytkownika. Znaczenie symbolu jest częściowo określane przez jego wielkość w stosunku do rozmiaru obszaru roboczego, w którym się on znajduje. Na przykład różnicę między małymi a dużymi literami „o” lub „c” oraz przecinek zamiast ukośnika.

Podanie modułu rozpoznającego szerokość i wysokość obszaru pisania może zwiększyć dokładność. Moduł rozpoznawania zakłada jednak, że obszar zapisu zawiera tylko jeden wiersz tekstu. Jeśli fizyczny obszar do pisania jest wystarczająco duży, aby użytkownik mógł napisać co najmniej 2 wiersze, możesz uzyskać lepsze wyniki, przekazując obszar do pisania z wysokością, która jest szacowaną wysokością pojedynczego wiersza tekstu. Obiekt WriteArea przekazywany do modułu rozpoznawania nie musi dokładnie odpowiadać fizycznemu obszarowi pisania na ekranie. W ten sposób zmiana wysokości obszaru pisania działa lepiej w niektórych językach niż w innych.

Podczas określania obszaru pisania podaj szerokość i wysokość w tych samych jednostkach, co współrzędne kreski. Argumenty współrzędnej x,y nie mają wymagań dotyczących jednostki – interfejs API normalizuje wszystkie jednostki, więc najważniejsza jest względna wielkość i położenie kreski. Możesz wykorzystać współrzędne w dowolnej skali.

Wstęp do kontekstu

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

Na przykład kursywę „n” i „u” często myli się ze sobą nawzajem. Jeśli użytkownik ma już wpisane częściowe słowo „argument”, może kontynuować rysowanie, które można rozpoznać jako „ument” lub „mentor”. Określenie kontekstu w poddaniu „argument” rozwiązuje wątpliwości, ponieważ słowo „argument” jest bardziej prawdopodobne niż argument „argument”.

Kontekst może też pomóc w rozpoznawaniu podziałów słów oraz spacji między słowami. Można wpisać spację, ale nie rysować znaku. Jak moduł rozpoznający może określić, kiedy kończy się jedno słowo i zaczyna się drugie? Jeśli użytkownik wpisał już „Cześć” i chce wpisać ciąg „world”, bez kontekstu, moduł rozpoznawania zwróci ciąg znaków „world”. Jeśli jednak określisz kontekst „hello”, model zwróci ciąg znaków „world” ze spacją na początku, ponieważ „hello world” ma sens więcej niż „helloword”.

Musisz wpisać najdłuższy ciąg tekstowy z kontekstu (maksymalnie 20 znaków razem ze spacjami). Jeśli ciąg jest dłuższy, moduł rozpoznawania wykorzysta tylko 20 ostatnich znaków.

Przykładowy kod poniżej pokazuje, jak zdefiniować obszar pisania i użyć obiektu RecognitionContext, aby określić kontekst wstępny.

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 zależy od kolejności pociągnięć. Moduły rozpoznawania spodziewają się, że udarem wystąpią w kolejności, w jakiej naturalnie brzmią, np. od lewej do prawej w przypadku języka angielskiego. Każdy przypadek, który różni się od danego wzorca, na przykład angielskie zdanie zaczynające się od ostatniego słowa, daje mniej dokładne wyniki.

Inny przykład to usunięcie słowa w środku elementu Ink i zastąpienie go innym słowem. Wersja prawdopodobnie znajduje się w połowie zdania, ale kreski są na końcu sekwencji kreski. W takim przypadku zalecamy wysłanie nowego słowa osobno do interfejsu API i scalenie wyniku z wcześniejszymi rozpoznaniami przy użyciu własnej logiki.

Radzenie sobie z niejednoznacznymi kształtami

Zdarza się, że znaczenie modułu rozpoznawania jest niejednoznaczne. Na przykład prostokąt z bardzo zaokrąglonymi krawędziami może być prostokątem lub wielokropkiem.

Niejasne przypadki można rozwiązać, korzystając z wyników rozpoznawania, gdy są dostępne. Wyniki zapewniają tylko klasyfikatory kształtów. Jeśli model ma dużą pewność, wyniki pierwszego wyniku będą znacznie lepsze od drugiego wyniku. W razie niepewności wyniki dotyczące 2 pierwszych wyników będą zamknięte. Pamiętaj też, że klasyfikatory kształtu interpretują całą Ink jako pojedynczy kształt. Jeśli na przykład Ink zawiera obok siebie prostokąt i kropek obok siebie, moduł rozpoznawania może zwrócić wynik (lub coś zupełnie innego), ponieważ pojedynczy kandydat do rozpoznawania nie może reprezentować dwóch kształtów.