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

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

Попробуйте это

Прежде чем вы начнете

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

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

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

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

Этот общий шаблон продемонстрирован в следующем фрагменте кода. Более полный пример см. в образце быстрого запуска ML Kit .

Котлин

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

Джава

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

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

Чтобы выполнить распознавание, отправьте экземпляр Ink в объект DigitalInkRecognizer . Код ниже показывает, как создать экземпляр такого распознавательного устройства из тега BCP-47 .

Котлин

// 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())

Джава

// 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

Котлин

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")
    }

Джава

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 .

Загрузите новую модель

Котлин

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")
    }

Джава

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

Проверьте, загружена ли уже модель

Котлин

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

Джава

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

Удаление загруженной модели

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

Котлин

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

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

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

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

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

Многие приложения имеют четко определенную область ввода данных пользователем. Значение символа частично определяется его размером относительно размера области письма, в которой он содержится. Например, разница между строчной или прописной буквой «o» или «c» и запятой и косой чертой.

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

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

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

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

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

Предварительный контекст также может помочь распознавателю идентифицировать разрывы слов, пробелы между словами. Вы можете ввести пробел, но не можете его нарисовать, так как же распознавателю определить, когда заканчивается одно слово и начинается следующее? Если пользователь уже написал «привет» и продолжает писать слово «мир», без предварительного контекста распознаватель возвращает строку «мир». Однако если вы укажете предконтекст «привет», модель вернет строку «мир» с пробелом в начале, поскольку «привет, мир» имеет больше смысла, чем «привет, слово».

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

В приведенном ниже примере кода показано, как определить область письма и использовать объект RecognitionContext для указания предварительного контекста.

Котлин

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)

Джава

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 содержит прямоугольник и эллипс рядом друг с другом, распознаватель может вернуть в результате один или другой (или что-то совершенно другое), поскольку один кандидат на распознавание не может представлять две фигуры.