Reconhecimento de tinta digital com o Kit de ML no Android

Com o reconhecimento de tinta digital do Kit de ML, é possível reconhecer texto escrito à mão em uma superfície digital em centenas de idiomas e classificar esboços.

Testar

Antes de começar

  1. No arquivo build.gradle no nível do projeto, inclua o repositório Maven do Google nas seções buildscript e allprojects.
  2. Adicione as dependências das bibliotecas do Android do Kit de ML ao arquivo Gradle no nível do app do módulo, que geralmente é app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

Agora está tudo pronto para você reconhecer texto em objetos Ink.

Criar um objeto Ink

A principal maneira de criar um objeto Ink é desenhá-lo em uma tela sensível ao toque. No Android, você pode usar um Canvas para essa finalidade. Os manipuladores de eventos de toque precisam chamar o método addNewTouchEvent() mostrado no snippet de código a seguir para armazenar os pontos nos traços que o usuário desenha no objeto Ink.

Esse padrão geral é demonstrado no snippet de código a seguir. Consulte a amostra do guia de início rápido do Kit de ML para um exemplo mais completo.

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

Acessar uma instância do DigitalInk Reconhecedor

Para realizar o reconhecimento, envie a instância Ink para um objeto DigitalInkRecognizer. O código abaixo mostra como instanciar esse reconhecedor de uma tag 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());

Processar um objeto 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));

O exemplo de código acima pressupõe que o modelo de reconhecimento já foi baixado, conforme descrito na próxima seção.

Como gerenciar downloads de modelos

Embora a API de reconhecimento de tinta digital ofereça suporte a centenas de idiomas, é necessário fazer o download de alguns dados antes de qualquer reconhecimento. São necessários cerca de 20 MB de armazenamento por idioma. Isso é processado pelo objeto RemoteModelManager.

Fazer o download de um novo modelo

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

Verificar se um modelo já foi baixado

Kotlin

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

Java

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

Excluir um modelo baixado

Remover um modelo do armazenamento do dispositivo libera espaço.

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

Dicas para melhorar a precisão do reconhecimento de texto

A precisão do reconhecimento de texto pode variar de acordo com o idioma. A precisão também depende do estilo de escrita. Embora o reconhecimento de tinta digital seja treinado para lidar com muitos tipos de estilos de escrita, os resultados podem variar de usuário para usuário.

Aqui estão algumas maneiras de melhorar a precisão de um reconhecedor de texto. Essas técnicas não se aplicam aos classificadores de desenho de emojis, desenho automático e formas.

Área de escrita

Muitos aplicativos têm uma área de gravação bem definida para entradas do usuário. O significado de um símbolo é parcialmente determinado pelo tamanho dele em relação ao tamanho da área de escrita que o contém. Por exemplo, a diferença entre uma letra minúscula ou "o" ou "c" e uma vírgula ou uma barra.

Dizer ao reconhecedor a largura e a altura da área de escrita pode melhorar a precisão. No entanto, o reconhecedor supõe que a área de escrita contém apenas uma linha de texto. Se a área de gravação física for grande o suficiente para permitir que o usuário escreva duas ou mais linhas, você poderá conseguir melhores resultados transmitindo uma área de escrita com uma altura que seja a melhor estimativa da altura de uma única linha de texto. O objeto WritingArea que você transmite ao reconhecedor não precisa corresponder exatamente à área de gravação física na tela. Alterar a altura da WritingArea dessa maneira funciona melhor em alguns idiomas do que em outros.

Ao especificar a área de escrita, especifique a largura e a altura nas mesmas unidades que as coordenadas do traço. Os argumentos de coordenadas x,y não têm requisito de unidade. A API normaliza todas as unidades, então o que importa é o tamanho relativo e a posição dos traços. Você pode passar as coordenadas em qualquer escala que faça sentido para seu sistema.

Pré-contexto

Pré-contexto é o texto que precede imediatamente os traços no Ink que você está tentando reconhecer. Você pode ajudar o reconhecedor contando sobre o pré-contexto.

Por exemplo, as letras cursivas "n" e "u" são frequentemente confundidas uma com a outra. Se o usuário já tiver inserido a palavra parcial "arg", ele poderá continuar com traços que possam ser reconhecidos como "ument" ou "nment". Especificar o "arg" de pré-contexto resolve a ambiguidade, já que a palavra "argumento" é mais provável do que "argnment".

O pré-contexto também pode ajudar o reconhecedor a identificar quebras de palavras, os espaços entre palavras. Você pode digitar um caractere de espaço, mas não pode desenhar um, então como um reconhecedor pode determinar quando uma palavra termina e a próxima começa? Se o usuário já tiver escrito "hello" e continuar com a palavra escrita "world", sem pré-contexto, o reconhecedor retornará a string "world". No entanto, se você especificar o pré-contexto "hello", o modelo retornará a string "world", com um espaço inicial, já que "hello world" faz mais sentido do que "helloword".

Forneça a string de pré-contexto mais longa possível, com até 20 caracteres, incluindo espaços. Se a string for maior, o reconhecedor usará apenas os últimos 20 caracteres.

O exemplo de código abaixo mostra como definir uma área de escrita e usar um objeto RecognitionContext para especificar o pré-contexto.

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

Ordenação dos traços

A precisão do reconhecimento depende da ordem dos traços. Os reconhecedores esperam que os traços ocorram na ordem em que as pessoas escreveriam naturalmente, por exemplo, da esquerda para a direita no inglês. Qualquer caso que se desvie desse padrão, como escrever uma frase em inglês começando com a última palavra, oferece resultados menos precisos.

Outro exemplo é quando uma palavra no meio de uma Ink é removida e substituída por outra palavra. A revisão provavelmente está no meio de uma frase, mas os traços da revisão estão no final da sequência do traço. Nesse caso, recomendamos enviar a palavra recém-escrita separadamente para a API e mesclar o resultado com os reconhecimentos anteriores usando sua própria lógica.

Como lidar com formas ambíguas

Há casos em que o significado da forma fornecida ao reconhecedor é ambíguo. Por exemplo, um retângulo com bordas muito arredondadas pode ser visto como um retângulo ou uma elipse.

Esses casos pouco claros podem ser tratados com o uso de pontuações de reconhecimento quando disponíveis. Somente os classificadores de formas fornecem pontuações. Se o modelo estiver muito confiante, a pontuação do primeiro resultado será muito melhor do que o segundo melhor. Se houver incerteza, as pontuações dos dois primeiros resultados serão próximas. Além disso, lembre-se de que os classificadores de formas interpretam a Ink inteira como uma única forma. Por exemplo, se a Ink contiver um retângulo e uma elipse lado a lado, o reconhecedor poderá retornar um ou outro (ou algo completamente diferente) como resultado, já que um único candidato de reconhecimento não pode representar duas formas.