Reconhecer texto em imagens com o Kit de ML no Android

É possível usar o Kit de ML para reconhecer texto em imagens ou vídeos, como o texto de uma placa de rua. As principais características desse recurso são:

Engenharia de Desagrupado Agrupadas
Nome da biblioteca com.google.android.gms:play-services-mlkit-text-recognition

com.google.android.gms:play-services-mlkit-text-recognition-chinese

com.google.android.gms:play-services-mlkit-text-recognition-devanagari

com.google.android.gms:play-services-mlkit-text-recognition-japanese

com.google.android.gms:play-services-mlkit-text-recognition-korean

com.google.mlkit:text-recognition

com.google.mlkit:text-recognition-chinese

com.google.mlkit:text-recognition-devanagari

com.google.mlkit:text-recognition-japanese

com.google.mlkit:text-recognition-korean

Implementação O download do modelo é feito dinamicamente pelo Google Play Services. O modelo é vinculado estaticamente ao app no tempo de build.
Tamanho do app Aumento de cerca de 260 KB por arquitetura de script. Aumento de cerca de 4 MB por script e arquitetura.
Tempo de inicialização Talvez seja necessário aguardar o download do modelo para usar o modelo pela primeira vez. O modelo está disponível imediatamente.
Desempenho Tempo real na maioria dos dispositivos com bibliotecas de script latino, mas mais lento para outros. Tempo real na maioria dos dispositivos com bibliotecas de script latino, mas mais lento para outros.

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 do módulo no nível do app, que geralmente é app/build.gradle:

    Para agrupar o modelo e o app:

    dependencies {
      // To recognize Latin script
      implementation 'com.google.mlkit:text-recognition:16.0.0'
    
      // To recognize Chinese script
      implementation 'com.google.mlkit:text-recognition-chinese:16.0.0'
    
      // To recognize Devanagari script
      implementation 'com.google.mlkit:text-recognition-devanagari:16.0.0'
    
      // To recognize Japanese script
      implementation 'com.google.mlkit:text-recognition-japanese:16.0.0'
    
      // To recognize Korean script
      implementation 'com.google.mlkit:text-recognition-korean:16.0.0'
    }
    

    Para usar o modelo no Google Play Services:

    dependencies {
      // To recognize Latin script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition:19.0.0'
    
      // To recognize Chinese script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition-chinese:16.0.0'
    
      // To recognize Devanagari script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition-devanagari:16.0.0'
    
      // To recognize Japanese script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition-japanese:16.0.0'
    
      // To recognize Korean script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition-korean:16.0.0'
    }
    
  3. Se você optar por usar o modelo no Google Play Services, poderá configurar seu app para fazer o download automático do modelo no dispositivo após a instalação do app da Play Store. Para fazer isso, adicione a seguinte declaração ao arquivo AndroidManifest.xml do app:

    <application ...>
          ...
          <meta-data
              android:name="com.google.mlkit.vision.DEPENDENCIES"
              android:value="ocr" >
          <!-- To use multiple models: android:value="ocr,ocr_chinese,ocr_devanagari,ocr_japanese,ocr_korean,..." -->
    </application>
    

    Também é possível verificar explicitamente a disponibilidade do modelo e solicitar o download usando a API ModuleInstallClient do Google Play Services. Se você não ativar os downloads do modelo no momento da instalação ou solicitar o download explícito, o modelo será transferido na primeira vez que você executar o scanner. As solicitações feitas antes da conclusão do download não produzem resultados.

1. Criar uma instância de TextRecognizer

Crie uma instância de TextRecognizer, transmitindo as opções relacionadas à biblioteca em que você declarou uma dependência acima:

Kotlin

// When using Latin script library
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)

// When using Chinese script library
val recognizer = TextRecognition.getClient(ChineseTextRecognizerOptions.Builder().build())

// When using Devanagari script library
val recognizer = TextRecognition.getClient(DevanagariTextRecognizerOptions.Builder().build())

// When using Japanese script library
val recognizer = TextRecognition.getClient(JapaneseTextRecognizerOptions.Builder().build())

// When using Korean script library
val recognizer = TextRecognition.getClient(KoreanTextRecognizerOptions.Builder().build())

Java

// When using Latin script library
TextRecognizer recognizer =
  TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);

// When using Chinese script library
TextRecognizer recognizer =
  TextRecognition.getClient(new ChineseTextRecognizerOptions.Builder().build());

// When using Devanagari script library
TextRecognizer recognizer =
  TextRecognition.getClient(new DevanagariTextRecognizerOptions.Builder().build());

// When using Japanese script library
TextRecognizer recognizer =
  TextRecognition.getClient(new JapaneseTextRecognizerOptions.Builder().build());

// When using Korean script library
TextRecognizer recognizer =
  TextRecognition.getClient(new KoreanTextRecognizerOptions.Builder().build());

2. Preparar a imagem de entrada

Para reconhecer texto em uma imagem, crie um objeto InputImage usando um Bitmap, media.Image, ByteBuffer, uma matriz de bytes ou um arquivo no dispositivo. Em seguida, transmita o objeto InputImage para o método processImage do TextRecognizer.

Você pode criar um objeto InputImage de diferentes origens, cada uma explicada abaixo.

Como usar um media.Image

Para criar um objeto InputImage com base em um objeto media.Image, como quando você captura uma imagem da câmera de um dispositivo, transmita o objeto media.Image e a rotação da imagem para InputImage.fromMediaImage().

Se você usar a biblioteca CameraX, as classes OnImageCapturedListener e ImageAnalysis.Analyzer vão calcular o valor de rotação automaticamente.

Kotlin

private class YourImageAnalyzer : ImageAnalysis.Analyzer {

    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
}

Java

private class YourAnalyzer implements ImageAnalysis.Analyzer {

    @Override
    public void analyze(ImageProxy imageProxy) {
        Image mediaImage = imageProxy.getImage();
        if (mediaImage != null) {
          InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
          // Pass image to an ML Kit Vision API
          // ...
        }
    }
}

Se você não usar uma biblioteca de câmera que ofereça o grau de rotação da imagem, será possível calculá-lo usando o grau de rotação do dispositivo e a orientação do sensor da câmera:

Kotlin

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 0)
    ORIENTATIONS.append(Surface.ROTATION_90, 90)
    ORIENTATIONS.append(Surface.ROTATION_180, 180)
    ORIENTATIONS.append(Surface.ROTATION_270, 270)
}

/**
 * Get the angle by which an image must be rotated given the device's current
 * orientation.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Throws(CameraAccessException::class)
private fun getRotationCompensation(cameraId: String, activity: Activity, isFrontFacing: Boolean): Int {
    // Get the device's current rotation relative to its "native" orientation.
    // Then, from the ORIENTATIONS table, look up the angle the image must be
    // rotated to compensate for the device's rotation.
    val deviceRotation = activity.windowManager.defaultDisplay.rotation
    var rotationCompensation = ORIENTATIONS.get(deviceRotation)

    // Get the device's sensor orientation.
    val cameraManager = activity.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360
    }
    return rotationCompensation
}

Java

private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 0);
    ORIENTATIONS.append(Surface.ROTATION_90, 90);
    ORIENTATIONS.append(Surface.ROTATION_180, 180);
    ORIENTATIONS.append(Surface.ROTATION_270, 270);
}

/**
 * Get the angle by which an image must be rotated given the device's current
 * orientation.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private int getRotationCompensation(String cameraId, Activity activity, boolean isFrontFacing)
        throws CameraAccessException {
    // Get the device's current rotation relative to its "native" orientation.
    // Then, from the ORIENTATIONS table, look up the angle the image must be
    // rotated to compensate for the device's rotation.
    int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    int rotationCompensation = ORIENTATIONS.get(deviceRotation);

    // Get the device's sensor orientation.
    CameraManager cameraManager = (CameraManager) activity.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
    }
    return rotationCompensation;
}

Em seguida, transmita o objeto media.Image e o valor do grau de rotação para InputImage.fromMediaImage():

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

InputImage image = InputImage.fromMediaImage(mediaImage, rotation);

Usar um URI de arquivo

Para criar um objeto InputImage usando o URI de um arquivo, transmita o contexto do app e o URI do arquivo para InputImage.fromFilePath(). Isso é útil ao usar uma intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem do app de galeria dele.

Kotlin

val image: InputImage
try {
    image = InputImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e.printStackTrace()
}

Java

InputImage image;
try {
    image = InputImage.fromFilePath(context, uri);
} catch (IOException e) {
    e.printStackTrace();
}

Como usar ByteBuffer ou ByteArray

Para criar um objeto InputImage com base em ByteBuffer ou ByteArray, primeiro calcule o grau de rotação da imagem conforme descrito anteriormente para a entrada de media.Image. Em seguida, crie o objeto InputImage com o buffer ou a matriz, junto com a altura, a largura, o formato de codificação de cores e o grau de rotação da imagem:

Kotlin

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)
// Or:
val image = InputImage.fromByteArray(
        byteArray,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)

Java

InputImage image = InputImage.fromByteBuffer(byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);
// Or:
InputImage image = InputImage.fromByteArray(
        byteArray,
        /* image width */480,
        /* image height */360,
        rotation,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);

Como usar um Bitmap

Para criar um objeto InputImage com base em um objeto Bitmap, faça a seguinte declaração:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

A imagem é representada por um objeto Bitmap com os graus de rotação.

3. Processar a imagem

Transmita a imagem para o método process:

Kotlin

val result = recognizer.process(image)
        .addOnSuccessListener { visionText ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

Java

Task<Text> result =
        recognizer.process(image)
                .addOnSuccessListener(new OnSuccessListener<Text>() {
                    @Override
                    public void onSuccess(Text visionText) {
                        // Task completed successfully
                        // ...
                    }
                })
                .addOnFailureListener(
                        new OnFailureListener() {
                            @Override
                            public void onFailure(@NonNull Exception e) {
                                // Task failed with an exception
                                // ...
                            }
                        });

4. Extrair texto de blocos de texto reconhecido

Se a operação de reconhecimento de texto for bem-sucedida, um objeto Text será transmitido para o listener de êxito. Um objeto Text contém o texto completo reconhecido na imagem e zero ou mais objetos TextBlock.

Cada TextBlock representa um bloco de texto retangular, que contém zero ou mais objetos Line. Cada objeto Line representa uma linha de texto, que contém zero ou mais objetos Element. Cada objeto Element representa uma palavra ou uma entidade semelhante a palavra, que contém zero ou mais objetos Symbol. Cada objeto Symbol representa um caractere, um dígito ou uma entidade semelhante a uma palavra.

Para cada objeto TextBlock, Line, Element e Symbol, é possível receber o texto reconhecido na região, as coordenadas delimitadoras da região e muitos outros atributos, como informações de rotação, pontuação de confiança etc.

Exemplo:

Kotlin

val resultText = result.text
for (block in result.textBlocks) {
    val blockText = block.text
    val blockCornerPoints = block.cornerPoints
    val blockFrame = block.boundingBox
    for (line in block.lines) {
        val lineText = line.text
        val lineCornerPoints = line.cornerPoints
        val lineFrame = line.boundingBox
        for (element in line.elements) {
            val elementText = element.text
            val elementCornerPoints = element.cornerPoints
            val elementFrame = element.boundingBox
        }
    }
}

Java

String resultText = result.getText();
for (Text.TextBlock block : result.getTextBlocks()) {
    String blockText = block.getText();
    Point[] blockCornerPoints = block.getCornerPoints();
    Rect blockFrame = block.getBoundingBox();
    for (Text.Line line : block.getLines()) {
        String lineText = line.getText();
        Point[] lineCornerPoints = line.getCornerPoints();
        Rect lineFrame = line.getBoundingBox();
        for (Text.Element element : line.getElements()) {
            String elementText = element.getText();
            Point[] elementCornerPoints = element.getCornerPoints();
            Rect elementFrame = element.getBoundingBox();
            for (Text.Symbol symbol : element.getSymbols()) {
                String symbolText = symbol.getText();
                Point[] symbolCornerPoints = symbol.getCornerPoints();
                Rect symbolFrame = symbol.getBoundingBox();
            }
        }
    }
}

Diretrizes de imagens de entrada

  • Para que o Kit de ML reconheça o texto com precisão, as imagens de entrada precisam conter texto representado por dados de pixel suficientes. O ideal é que cada caractere tenha pelo menos 16 x 16 pixels. Geralmente, não há benefício de precisão em caracteres maiores que 24 x 24 pixels.

    Por exemplo, uma imagem de 640 x 480 pode funcionar bem para digitalizar um cartão de visita que ocupe toda a largura da imagem. Para digitalizar um documento impresso em papel de tamanho carta, pode ser necessária uma imagem de 720 x 1280 pixels.

  • Uma imagem com foco inadequado pode afetar a precisão do reconhecimento de texto. Se você não está conseguindo resultados aceitáveis, peça para o usuário recapturar a imagem.

  • Se você estiver reconhecendo texto em um aplicativo em tempo real, considere as dimensões gerais das imagens de entrada. Imagens menores podem ser processadas mais rapidamente. Para reduzir a latência, verifique se o texto ocupe o máximo possível da imagem e capture imagens em resoluções mais baixas, tendo em mente os requisitos de precisão mencionados acima. Para mais informações, consulte Dicas para melhorar o desempenho.

Dicas para melhorar o desempenho

  • Se você usa a API Camera ou camera2, limite as chamadas para o detector. Se um novo frame de vídeo ficar disponível enquanto o detector estiver em execução, descarte esse frame. Consulte a classe VisionProcessorBase no app de amostra do guia de início rápido para conferir um exemplo.
  • Se você usar a API CameraX, verifique se a estratégia de pressão de retorno está definida com o valor padrão ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Isso garante que apenas uma imagem será enviada para análise por vez. Se mais imagens forem produzidas quando o analisador estiver ocupado, elas serão descartadas automaticamente e não serão enfileiradas para entrega. Quando a imagem analisada é fechada chamando ImageProxy.close(), a próxima imagem mais recente é entregue.
  • Se você usar a saída do detector para sobrepor elementos gráficos na imagem de entrada, primeiro acesse o resultado do Kit de ML e, em seguida, renderize a imagem e a sobreposição em uma única etapa. Isso é renderizado na superfície de exibição apenas uma vez para cada frame de entrada. Consulte as classes CameraSourcePreview e GraphicOverlay no app de amostra do guia de início rápido para conferir um exemplo.
  • Se você usar a API Camera2, capture imagens no formato ImageFormat.YUV_420_888. Se você usar a API Camera mais antiga, capture imagens no formato ImageFormat.NV21.
  • Capture imagens em uma resolução mais baixa. No entanto, lembre-se também dos requisitos de dimensão de imagem dessa API.