Reconocimiento de tinta digital con el Kit de AA en Android

Con el reconocimiento de tinta digital del Kit de AA, puedes reconocer texto escrito a mano en una superficie digital en cientos de idiomas y clasificar bocetos.

Probar

Antes de comenzar

  1. En tu archivo build.gradle de nivel de proyecto, asegúrate de incluir el repositorio Maven de Google en las secciones buildscript y allprojects.
  2. Agrega las dependencias para las bibliotecas de Android del ML Kit al archivo Gradle, que suele ser app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

Ya estás listo para comenzar a reconocer texto en objetos Ink.

Compila un objeto Ink

La forma principal de compilar un objeto Ink es dibujarlo en una pantalla táctil. En Android, puedes usar Canvas para este fin. Los controladores de los eventos táctiles deben llamar al método addNewTouchEvent() que se muestra en el siguiente fragmento de código para almacenar los puntos en los trazos que el usuario dibuja en el objeto Ink.

Este patrón general se demuestra en el siguiente fragmento de código. Consulta la muestra de inicio rápido del Kit de AA para ver un ejemplo más 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();

Obtener una instancia de DigitalInkRecognizer

Para realizar el reconocimiento, envía la instancia de Ink a un objeto DigitalInkRecognizer. En el siguiente código, se muestra cómo crear una instancia de ese identificador desde una etiqueta 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());

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

El código de muestra anterior supone que el modelo de reconocimiento ya se descargó, como se describe en la siguiente sección.

Administra las descargas de modelos

Si bien la API de reconocimiento de tinta digital admite cientos de idiomas, cada uno requiere que algunos datos se descarguen antes de cualquier reconocimiento. Se requieren alrededor de 20 MB de almacenamiento por idioma. Esto lo controla el objeto RemoteModelManager.

Descargar un modelo nuevo

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

Verifica si ya se descargó un modelo

Kotlin

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

Java

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

Borra un modelo descargado

Si quitas un modelo del almacenamiento del dispositivo, se libera espacio.

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

Sugerencias para mejorar la precisión del reconocimiento de texto

La precisión del reconocimiento de texto puede variar en diferentes idiomas. La precisión también depende del estilo de escritura. Si bien el reconocimiento de tinta digital está entrenado para manejar muchos tipos de estilos de escritura, los resultados pueden variar de un usuario a otro.

Estas son algunas formas de mejorar la precisión de un reconocedor de texto. Ten en cuenta que estas técnicas no se aplican a los clasificadores de dibujo para emojis, dibujos automáticos y formas.

Área de escritura

Muchas aplicaciones tienen un área de escritura bien definida para la entrada del usuario. El significado de un símbolo se determina de forma parcial por su tamaño en relación con el tamaño del área de escritura que lo contiene. Por ejemplo, la diferencia entre una letra minúscula “o” o “c” y una coma frente a una barra diagonal.

Indicar al reconocedor el ancho y la altura del área de escritura puede mejorar la precisión. Sin embargo, el reconocedor supone que el área de escritura solo contiene una sola línea de texto. Si el área de escritura física es lo suficientemente grande para permitir que el usuario escriba dos o más líneas, puedes obtener mejores resultados pasando una WriteArea con una altura que sea la mejor estimación de la altura de una sola línea de texto. El objeto WriteArea que pasas al reconocedor no tiene que corresponder exactamente con el área de escritura física en la pantalla. Cambiar la altura WriteArea de esta manera funciona mejor en algunos idiomas que en otros.

Cuando especifiques el área de escritura, especifica el ancho y la altura en las mismas unidades que las coordenadas de trazo. Los argumentos de las coordenadas x,y no tienen un requisito de unidades: la API normaliza todas las unidades, por lo que lo único importante es el tamaño y la posición relativos de los trazos. Puedes pasar las coordenadas en la escala que desees para tu sistema.

Antes del contexto

El contexto previo es el texto que precede inmediatamente a los trazos del Ink que intentas reconocer. Para ayudar al reconocedor, puedes indicarle el contexto previo.

Por ejemplo, las letras cursivas "n" y "u" suelen confundirse entre sí. Si el usuario ya ingresó la palabra parcial "arg", puede continuar con trazos que se puedan reconocer como "ument" o "nment". Especificar el "arg" previo al contexto resuelve la ambigüedad, ya que la palabra "argumento" es más probable que "argnmento".

El contexto previo también puede ayudar al reconocedor a identificar saltos de palabra, los espacios entre palabras. Puedes escribir un carácter de espacio, pero no puedes dibujar uno. ¿Cómo puede un reconocedor determinar cuándo termina una palabra y comienza la siguiente? Si el usuario ya escribió "hello" y continúa con la palabra escrita "world", sin el contexto previo, el reconocedor mostrará la string "world". Sin embargo, si especificas el “precontexto “hello”, el modelo mostrará la string “world” con un espacio inicial, ya que “hello world” tiene más sentido que “helloword”.

Debes proporcionar la string previa al contexto más larga posible, con hasta 20 caracteres, incluidos los espacios. Si la string es más larga, el reconocedor solo usará los últimos 20 caracteres.

En la siguiente muestra de código, se indica cómo definir un área de escritura y usar un objeto RecognitionContext a fin de especificar el contexto previo.

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

Orden de trazos

La precisión del reconocimiento es sensible al orden de los trazos. Los reconocedores esperan que los trazos ocurran en el orden que las personas escribirían naturalmente; por ejemplo, de izquierda a derecha para el inglés. Cualquier caso que se aleja de este patrón, como escribir una oración en inglés que comienza con la última palabra, genera resultados menos precisos.

Otro ejemplo es cuando se quita una palabra en el medio de una Ink y se reemplaza por otra. Es probable que la revisión esté en medio de una oración, pero los trazos de la revisión se encuentran al final de la secuencia de trazo. En este caso, recomendamos enviar la palabra recién escrita por separado a la API y combinar el resultado con los reconocimientos anteriores mediante tu propia lógica.

Maneja formas ambiguas

Hay casos en los que el significado de la forma que se proporciona al reconocedor es ambiguo. Por ejemplo, un rectángulo con bordes muy redondeados podría verse como un rectángulo o una elipse.

Estos casos poco claros se pueden manejar mediante las puntuaciones de reconocimiento cuando están disponibles. Solo los clasificadores de formas proporcionan puntuaciones. Si el modelo está muy seguro, la puntuación del resultado principal será mucho mejor que la segunda. Si no hay certeza, las puntuaciones de los dos resultados principales serán similares. Además, ten en cuenta que los clasificadores de formas interpretan todo el Ink como una sola forma. Por ejemplo, si la Ink contiene un rectángulo y una elipses una junto a la otra, el reconocedor puede mostrar una o la otra (o una cosa completamente diferente) como resultado, ya que un solo candidato de reconocimiento no puede representar dos formas.