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

Dzięki rozpoznawaniu atramentu cyfrowego w ML Kit możesz rozpoznawać tekst pisany 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 Maven firmy Google w sekcjach buildscriptallprojects.
  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'
}

Możesz już rozpoznawać tekst w obiektach Ink.

Tworzenie obiektu Ink

Głównym sposobem tworzenia obiektu Ink jest narysowanie go na ekranie dotykowym. Wł. Androida, możesz użyć Canvas dla w tym celu. Obsługa zdarzenia dotyku powinna wywoływać metodę addNewTouchEvent(), aby zapisywać punkty w ścieżkach rysowanych przez użytkownika w obiekcie Ink.

Ten ogólny schemat przedstawia poniższy fragment kodu. Zobacz Przykład krótkiego wprowadzenia do ML Kit , aby uzyskać pełniejszy przykład.

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 DigitalInkRecognizer obiekt. Poniższy kod pokazuje, jak utworzyć instancję takiego rozpoznawacza 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ż pobrane, jak opisano 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. Na każdy język potrzeba około 20 MB miejsca na dane. Zarządza nim 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 powoduje 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ż na styl pisania. Chociaż rozpoznawanie pisma cyfrowego jest trenowane pod kątem obsługi wielu stylów pisania, 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 są stosowane do klasyfikatorów rysunków dla emotikonów, automatycznego rysowania i kształtów.

Obszar pisania

Wiele aplikacji ma dobrze zdefiniowany obszar pisania, w którym użytkownik może wprowadzać tekst. Znaczenie symbolu to zależy częściowo od rozmiaru obszaru pisania, w którym się znajduje. na przykład różnicę między małą lub wielką literą „o”; lub „c” i przecinek zamiast a. ukośnik prawy.

Podanie rozpoznawalności szerokości i wysokości obszaru pisania może zwiększyć dokładność. Rozpoznawca zakłada jednak, że obszar pisania zawiera tylko jeden wiersz tekstu. Jeśli obszar pisania jest wystarczająco duży, aby użytkownik mógł napisać dwa lub więcej wierszy, przez przekazanie obszaru do pisania o wysokości, która jest najdokładniejszym oszacowaniem wysokości jeden wiersz tekstu. Obiekt WriteArea przekazywany do modułu rozpoznawania nie musi odpowiadać z użyciem fizycznego obszaru do pisania na ekranie. Zmiana wysokości WritingArea w ten sposób działa 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 obrysu. Argumenty współrzędnych x,y nie mają wymagań dotyczących jednostek – interfejs API normalizuje wszystkie jednostek, więc zwracają uwagę tylko na ich względny rozmiar i pozycję pociągnięć. Masz prawo i przekazywać współrzędne w dowolnej skali.

Kontekst wstępny

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

na przykład pisane kursywą litery „n” i „u” są często mylone. Jeśli użytkownik wpisał już część słowa „arg”, może kontynuować pisanie za pomocą ruchów, które mogą zostać rozpoznane jako „ument” lub „nment”. Określanie elementu „arg” powiązanego ze wstępnie kontekstem rozwiązuje niejednoznaczność, "argument" jest bardziej prawdopodobne niż „argnment”.

Kontekst sytuacyjny może też pomóc rozpoznawacza w identyfikowaniu podziału wyrazów i spacji między wyrazami. Możesz wpisać spację, ale nie możesz jej narysować. Jak więc rozpoznawacz może określić, kiedy kończy się jedno słowo, a zaczyna następne? Jeśli użytkownik napisał już „Cześć” i kontynuuje od słowa pisanego „world” (bez wstępnego kontekstu) moduł rozpoznawania zwraca ciąg „world” (świat). Jeśli jednak podasz kontekst wstępny „hello”, model zwróci ciąg znaków „world” z przecinem wiodącym, ponieważ „helloworld” ma więcej sensu niż „helloword”.

Musisz podać najdłuższy możliwy ciąg znaków poprzedzający kontekst, do 20 znaków, w tym spacje. 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żywać RecognitionContext obiekt 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ść uderzeń

Dokładność rozpoznawania zależy od kolejności kresek. Rozpoznawacze oczekują, że pociągnięcia będą wykonywane w kolejności, w jakiej ludzie naturalnie piszą, np. w języku angielskim od lewej do prawej. Wszelkie odstępstwa od tego wzoru, np. pisanie zdania w języku angielskim, zaczynając od ostatniego słowa, daje mniej dokładne wyniki.

Innym przykładem jest usunięcie słowa w środku ciągu Ink i zastąpienie go lub inne słowo. Poprawka znajduje się prawdopodobnie w środku zdania, ale jej ścieżki są na końcu sekwencji ścieżek. W takim przypadku zalecamy wysłanie do interfejsu API nowo napisanego słowa osobno i zlanie wyniku z poprzednimi rozpoznaniami za pomocą własnej logiki.

Rozwiązywanie problemów z niejednoznacznymi kształtami

Czasami znaczenie kształtu przekazanego do rozpoznawania jest niejednoznaczne. Dla: na przykład prostokąt z mocno zaokrąglonymi krawędziami może być prostokątny albo elipsa.

W takich niejasnych przypadkach można użyć wyników rozpoznawania, jeśli są dostępne. Tylko klasyfikatory kształtu podają wyniki. Jeśli model jest bardzo pewny siebie, wynik pierwszego wyniku będzie znacznie lepszy niż wynik drugiego najlepszego. Jeśli nie ma pewności, wyniki dwóch pierwszych pozycji będą być blisko. Pamiętaj też, że klasyfikatory kształtów interpretują cały Ink jako jeden kształt. Jeśli na przykład Ink zawiera prostokąt i elipsę obok siebie, rozpoznawacz może zwrócić jeden z nich (lub coś zupełnie innego), ponieważ pojedynczy kandydat do rozpoznania nie może reprezentować 2 kształtów.