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, bem como 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 você está pronto para começar a reconhecer texto em objetos Ink.

Criar um objeto Ink

A principal maneira de criar um objeto Ink é desenhá-lo em uma tela touchscreen. No Android, é possível 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 de DigitalInkReconhecer

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

Para o exemplo de código acima, supomos que o download do modelo de reconhecimento já foi feito, 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, cada idioma requer o download de alguns dados antes do reconhecimento. Por idioma, são necessários cerca de 20 MB de armazenamento. 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 o download de um modelo já foi feito

Kotlin

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

Java

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

Excluir um modelo salvo

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 entre diferentes idiomas. A precisão também depende do estilo de escrita. Embora o reconhecimento digital de tinta seja treinado para lidar com muitos tipos de estilos de escrita, os resultados podem variar de usuário para usuário.

Estas são algumas maneiras de melhorar a precisão de um reconhecedor de texto. Essas técnicas não se aplicam aos classificadores de desenho para emojis, autodraw e formas.

Área de escrita

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

Informar ao reconhecedor a largura e a altura da área de escrita pode melhorar a precisão. No entanto, o reconhecedor presume que a área de escrita contém apenas uma linha de texto. Se a área física de gravação for grande o suficiente para permitir que o usuário escreva duas ou mais linhas, você poderá ter resultados melhores transmitindo uma WriteArea com uma altura que é a melhor estimativa da altura de uma única linha de texto. O objeto WriteArea transmitido para o reconhecedor não precisa corresponder exatamente à área de gravação física na tela. Alterar a altura de 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 e 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. É possível transmitir coordenadas em qualquer escala que faça sentido para o sistema.

Pré-contexto

O pré-contexto é o texto que precede imediatamente os traços no Ink que você está tentando reconhecer. Informe o pré-contexto para ajudar o reconhecedor.

Por exemplo, as letras cursivas "n" e "u" muitas vezes são confundidas umas com as outras. Se o usuário já tiver inserido a palavra parcial "arg", ele poderá continuar com traços que podem ser reconhecidos como "ument" ou "nment". Especificar o pré-contexto "arg" 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 as 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 vai 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 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);

Ordem dos traços

A precisão do reconhecimento é sensível à ordem dos traços. Os reconhecedores esperam que os traços aconteçam na ordem em que as pessoas escreveriam naturalmente; por exemplo, da esquerda para a direita no inglês. Qualquer caso que se difere desse padrão, como escrever uma frase em inglês começando com a última palavra, fornece 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 dela estão no final da sequência de traços. 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 usando as pontuações de reconhecimento, quando elas estão disponíveis. Somente classificadores de formas fornecem pontuações. Se o modelo for muito confiante, a pontuação do resultado principal será muito melhor que a do segundo melhor. Se houver incerteza, as pontuações dos dois principais resultados serão próximas. Além disso, lembre-se de que os classificadores de formas interpretam toda a Ink como uma única forma. Por exemplo, se o Ink contiver um retângulo e uma elipse ao lado um do outro, o reconhecedor poderá retornar um ou outro (ou algo completamente diferente) como resultado, já que um único candidato a reconhecimento não pode representar duas formas.