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.

Faça um teste

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 touchscreen. Ativado no Android, é possível usar Canvas para para esse propósito. Seu manipuladores de evento de toque precisa chamar addNewTouchEvent() mostrou o seguinte snippet de código para armazenar os pontos nos traços que 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 conferir 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 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 transferidos por download, 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 exige que alguns dados sejam baixados antes de qualquer reconhecimento. Por volta de São necessários 20 MB de armazenamento por idioma. Isso é gerenciado pela 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 sobre o 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. Observe que essas técnicas não se aplicam aos classificadores de desenho para 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 determinada por seu tamanho em relação ao tamanho da área de escrita que a contém. Por exemplo, a diferença entre uma letra "o" minúscula ou uma letra maiúscula. ou "c", e uma vírgula versus barra para a direita.

Dizer 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 o ponto físico área de escrita for grande o suficiente para permitir que o usuário escreva duas ou mais linhas, talvez seja melhor resultados passando um WritingArea com uma altura que seja a melhor estimativa da altura de um uma única linha de texto. O objeto WritingArea que você passa para o reconhecedor não precisa corresponder exatamente com a área de escrita física na tela. Como alterar a altura da área de escrita dessa forma 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 o traço. coordenadas. Os argumentos de coordenadas x,y não têm requisito de unidade. A API normaliza todos o que importa é o tamanho relativo e a posição dos traços. Você é livre para e passar coordenadas em qualquer escala que faça sentido para o seu sistema.

Pré-contexto

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

Por exemplo, as letras cursivas "n" e "u" muitas vezes são confundidos uns com os outros. Se o usuário tiver já inseriram a palavra parcial "arg", eles podem continuar com traços que podem ser reconhecidos como “umentá” ou "nment". Como especificar o "arg" de pré-contexto resolve a ambiguidade, já que a palavra "argumento" é mais provável que "argnamento".

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

Você deve fornecer a maior string de pré-contexto 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 uma 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 ocorrer na ordem em que as pessoas escreveriam naturalmente; por exemplo, da esquerda para a direita, no caso do inglês. Qualquer caso que se desvie desse padrão, como escrever uma frase 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 da revisão estão no final da sequência de traços. Nesse caso, recomendamos enviar a nova palavra escrita separadamente para a API e mesclar o 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. Para 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 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 estar perto. Além disso, lembre-se de que os classificadores de formas interpretam toda a Ink como uma forma única. Por exemplo, se Ink contiver um retângulo e uma elipse ao lado de cada o reconhecedor pode retornar um ou outro (ou algo completamente diferente) como uma resultado, já que um único candidato de reconhecimento não pode representar duas formas.