Распознавание рукописного ввода с помощью ML Kit на Android

С помощью функции распознавания цифровых чернил ML Kit вы можете распознавать текст, написанный от руки на цифровой поверхности, на сотнях языков, а также классифицировать эскизы.

Попробовать

Подготовка

  1. В файле build.gradle на уровне проекта добавьте репозиторий Maven от Google в разделы buildscript и allprojects.
  2. Добавьте зависимости для библиотек ML Kit для Android в файл Gradle на уровне приложения (обычно это app/build.gradle):
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:19.0.0'
}

Теперь вы можете распознавать текст на Ink объектах.

Создайте объект Ink

Основной способ создать объект Ink – нарисовать его на сенсорном экране. На Android для этого можно использовать Canvas. Обработчики событий касания должны вызывать метод addNewTouchEvent(), показанный в следующем фрагменте кода, чтобы сохранять точки штрихов, которые пользователь рисует в объекте Ink.

Этот общий шаблон показан в следующем фрагменте кода. Более подробный пример можно найти в кратком руководстве по 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();

Получите экземпляр DigitalInkRecognizer

Чтобы выполнить распознавание, отправьте экземпляр Ink объекту DigitalInkRecognizer. В коде ниже показано, как создать экземпляр такого распознавателя из тега 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());

Обработка объекта 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));

В приведенном выше образце кода предполагается, что модель распознавания уже загружена, как описано в следующем разделе.

Управление скачиванием моделей

Хотя API распознавания цифровых чернил поддерживает сотни языков, для каждого языка требуется загрузить некоторые данные перед распознаванием. Для каждого языка требуется около 20 МБ свободного места. Для этого используется объект RemoteModelManager.

Скачать новую модель

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

Проверьте, скачана ли модель

Kotlin

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

Java

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

Как удалить скачанную модель

Удаление модели из хранилища устройства освобождает место.

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

Советы по улучшению распознавания текста

Точность распознавания текста может различаться в зависимости от языка. Точность также зависит от стиля письма. Хотя Digital Ink Recognition обучен распознавать разные стили письма, результаты могут отличаться от пользователя к пользователю.

Вот несколько способов повысить точность распознавания текста. Обратите внимание, что эти методы не применяются к классификаторам рисунков для эмодзи, авторисования и фигур.

Область для письма

Во многих приложениях есть специальная область для рукописного ввода. Значение символа частично определяется его размером относительно размера области письма, в которой он находится. Например, разница между строчной и заглавной буквами "о" или "с", а также между запятой и косой чертой.

Если указать ширину и высоту области письма, точность распознавания может повыситься. Однако распознаватель предполагает, что область письма содержит только одну строку текста. Если физическая область письма достаточно велика, чтобы пользователь мог написать две или более строк, вы можете получить лучшие результаты, передав WritingArea с высотой, которая является вашей наилучшей оценкой высоты одной строки текста. Объект WritingArea, который вы передаете распознавателю, не обязательно должен точно соответствовать физической области письма на экране. Изменение высоты WritingArea таким образом работает лучше на некоторых языках, чем на других.

При указании области письма укажите ее ширину и высоту в тех же единицах, что и координаты штриха. Для аргументов координат x,y не требуется указывать единицы измерения – API нормализует все единицы, поэтому важны только относительный размер и положение штрихов. Вы можете передавать координаты в любом масштабе, который подходит для вашей системы.

Предварительный контекст

Предконтекст – это текст, который непосредственно предшествует штрихам в Ink, которые вы пытаетесь распознать. Вы можете помочь распознавателю, указав предварительный контекст.

Например, прописные буквы "п" и "и" часто путают. Если пользователь уже ввел часть слова "arg", он может продолжить ввод штрихами, которые могут быть распознаны как "ument" или "nment". Указание контекста "arg" устраняет неоднозначность, поскольку слово "argument" более вероятно, чем "argnment".

Предварительный контекст также может помочь распознавателю определить пробелы между словами. Вы можете ввести пробел, но не можете его нарисовать. Как же распознаватель определяет, где заканчивается одно слово и начинается другое? Если пользователь уже написал "привет" и продолжает писать "мир", без предварительного контекста распознаватель возвращает строку "мир". Однако если вы укажете предварительный контекст "hello", модель вернет строку " world" с пробелом в начале, поскольку "hello world" имеет больше смысла, чем "helloword".

Укажите как можно более длинную строку предварительного контекста (до 20 символов, включая пробелы). Если строка длиннее, распознаватель использует только последние 20 символов.

В примере кода ниже показано, как определить область письма и использовать объект RecognitionContext, чтобы задать предварительный контекст.

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

Порядок штрихов

Точность распознавания зависит от порядка штрихов. Распознаватели ожидают, что штрихи будут появляться в том порядке, в котором люди обычно пишут, например слева направо для английского языка. Любой случай, который отклоняется от этой модели, например, написание английского предложения, начинающегося с последнего слова, дает менее точные результаты.

Другой пример – когда слово в середине Ink удаляется и заменяется другим словом. Исправление, скорее всего, находится в середине предложения, но штрихи для исправления находятся в конце последовательности штрихов. В этом случае мы рекомендуем отправлять в API только новое слово, а затем объединять результат с предыдущими распознаваниями, используя собственную логику.

Неоднозначные фигуры

В некоторых случаях значение фигуры, предоставленной распознавателю, неоднозначно. Например, прямоугольник с сильно скругленными углами может быть распознан как прямоугольник или как эллипс.

В таких случаях можно использовать оценки распознавания, если они доступны. Оценки предоставляют только классификаторы фигур. Если модель уверена в результате, оценка лучшего результата будет намного выше, чем у второго по качеству. Если есть неопределенность, оценки для двух лучших результатов будут близки. Также имейте в виду, что классификаторы фигур интерпретируют весь Ink как одну фигуру. Например, если Ink содержит прямоугольник и эллипс рядом друг с другом, распознаватель может вернуть один или другой (или что-то совершенно другое) в качестве результата, поскольку один кандидат на распознавание не может представлять две фигуры.