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

Funkcja rozpoznawania atramentu cyfrowego w ML Kit pozwala rozpoznawać tekst odręczny na cyfrowej powierzchni w setkach języków oraz klasyfikować szkice.

Wypróbuj

Zanim zaczniesz

  1. W pliku build.gradle na poziomie projektu dodaj repozytorium Google Maven w sekcjach buildscript i allprojects.
  2. Dodaj zależności bibliotek ML Kit na Androida do pliku Gradle na poziomie aplikacji modułu, którym jest zwykle 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 utworzenia obiektu Ink jest rysowanie go na ekranie dotykowym. Na Androidzie możesz do tego użyć Canvas. Twoje moduły obsługi zdarzeń dotknięcia powinny wywoływać metodę addNewTouchEvent() wyświetlaną poniżej, by zapisać punkty w ruchach rysowanych przez użytkownika w obiekcie Ink.

Ten ogólny wzorzec przedstawiono w poniższym fragmencie kodu. Pełniejszy przykład znajdziesz w krótkim wprowadzeniu do korzystania z 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 DigitalInkAdaptiver

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 opisaliśmy w następnej sekcji.

Zarządzanie pobieraniem modeli

Chociaż interfejs API rozpoznawania atramentu cyfrowego obsługuje setki języków, każdy z nich wymaga pobrania pewnych danych przed rozpoznaniem. Każdy język wymaga około 20 MB miejsca na dane. Jest to obsługiwane 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 być różna w zależności od języka. Dokładność zależy też od stylu pisania. Chociaż cyfrowe rozpoznawanie atramentu jest trenowane tak, aby obsługiwać wiele rodzajów stylów pisania, wyniki mogą być różne dla każdego 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 dla emotikonów, automatycznego rysowania i kształtów.

Miejsce do pisania

Wiele aplikacji ma dobrze zdefiniowany obszar do pisania przez użytkownika. Znaczenie symbolu zależy częściowo od jego rozmiaru w zależności od wielkości obszaru do pisania, w którym się on znajduje. Może to być na przykład różnica między małą lub wielką literą „o” lub „c” i przecinkiem a ukośnikiem.

Określanie szerokości i wysokości obszaru pisania może zwiększyć dokładność rozpoznawania. Moduł rozpoznawania zakłada jednak, że obszar do pisania zawiera tylko jeden wiersz tekstu. Jeśli obszar do pisania jest na tyle duży, że użytkownik może napisać 2 lub więcej wierszy, lepsze wyniki uzyskasz, przesyłając obszar do pisania o wysokości, która najlepiej szacuje wysokość 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 do pisania działa lepiej w niektórych językach niż w innych.

Określasz obszar do pisania, określając jego szerokość i wysokość w tych samych jednostkach co współrzędne kreski. Argumenty współrzędnych x,y nie mają wymagań dotyczących jednostek – interfejs API normalizuje wszystkie jednostki, a jedynie istotną rzeczą jest względny rozmiar i położenie kresek. Możesz podawać 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, podając mu informacje o wstępnym kontekście.

Na przykład litery „n” i „u” są często mylone ze sobą. Jeśli użytkownik wpisał już częściowe słowo „argument”, może kontynuować kreski, które rozpoznają jako „ument” lub „nment”. Określenie wstępnego kontekstu „argument” rozwiązało niejednoznaczność, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.

Kontekst wstępny może też pomóc systemowi rozpoznawania w wykrywaniu podziałów słów, czyli spacji między słowami. Możesz wpisać spację, ale nie możesz narysować jakiegoś. Jak więc moduł rozpoznawania może określić, kiedy jedno słowo się kończy, a zaczyna następne? Jeśli użytkownik napisze już „Cześć” i przejdzie do słowa „world” (świat), bez wstępnego kontekstu moduł rozpoznawania zwróci ciąg „world” (świat). Jeśli jednak określisz prefiks „hello”, model zwróci ciąg znaków „world” ze spacją na początku, ponieważ „hello world” ma sens niż „helloword”.

Należy podać najdłuższy możliwy ciąg znaków poprzedzający kontekst (do 20 znaków łącznie ze spacjami). Jeśli ciąg jest dłuższy, moduł rozpoznawania używa tylko ostatnich 20 znaków.

Poniższy przykładowy kod pokazuje, jak zdefiniować 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ść ruchów

Dokładność rozpoznawania zależy od kolejności ruchów. Moduły rozpoznawania oczekują, że ruchy będą się odbywać w takiej kolejności, w jakiej piszą normalnie, np. od lewej do prawej w przypadku języka angielskiego. Każdy przypadek odchodzący od tego wzorca, na przykład napisanie w języku angielskim zdania zaczynającego się od ostatniego słowa, dają mniej dokładne wyniki.

Inny przykład to usunięcie słowa w środku ciągu Ink i zastąpienie go innym słowem. Rewizja znajduje się prawdopodobnie w środku zdania, ale pociągnięcia w wersji znajdują się na końcu. W takim przypadku zalecamy wysłanie nowo utworzonego słowa osobno do interfejsu API i połączenie 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ć prostokątny lub elipsa.

W takich niejasnych przypadkach można użyć wyników rozpoznawania, jeśli są dostępne. Wyniki są podawane tylko przez klasyfikatory kształtów. Jeśli model ma dużą pewność, wynik najlepszego wyniku będzie znacznie lepszy niż drugi najlepszy. W przypadku wątpliwości wyniki 2 pierwszych wyników będą zbliżone. Pamiętaj też, że klasyfikatory kształtów interpretują cały obiekt Ink jako pojedynczy kształt. Jeśli na przykład Ink zawiera obok siebie prostokąt i elipsę, moduł rozpoznawania może zwrócić jeden lub drugi (albo coś zupełnie innego) jako wynik, ponieważ pojedynczy kandydat do rozpoznawania nie może reprezentować dwóch kształtów.