Ler códigos de barras com o Kit de ML no Android

É possível usar o Kit de ML para reconhecimento e decodificação de códigos de barras.

Há duas maneiras de integrar a leitura de código de barras: agrupando o modelo como parte do app ou usando um modelo desagrupado que depende do Google Play Services. Se você selecionar o modelo desagrupado, o app será menor. Veja mais detalhes na tabela abaixo.

Seleção deDesagrupadoAgrupados
ImplementaçãoO download do modelo é feito dinamicamente pelo Google Play Services.O modelo é estaticamente vinculado ao seu app no momento da compilação.
Tamanho do appAumento de cerca de 600 KB.Aumento de cerca de 3,2 MB.
Tempo de inicializaçãoPode ser necessário aguardar o download do modelo para usá-lo pela primeira vez.O modelo estará disponível imediatamente.

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. Adicionar as dependências das bibliotecas do Android do Kit de ML ao arquivo do Gradle no nível do app do seu módulo, que geralmente é app/build.gradle. Escolha uma das seguintes dependências com base nas suas necessidades:

    Para empacotar o modelo com o app, faça o seguinte:

    dependencies {
      // ...
      // Use this dependency to bundle the model with your app
      implementation 'com.google.mlkit:barcode-scanning:17.0.2'
    }
    

    Para usar o modelo no Google Play Services:

    dependencies {
      // ...
      // Use this dependency to use the dynamically downloaded model in Google Play Services
      implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0'
    }
    
  3. Se você quiser usar o modelo no Google Play Services, poderá configurar o app para fazer o download automático do modelo no dispositivo após a instalação do app na 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="barcode" >
          <!-- To use multiple models: android:value="barcode,model2,model3" -->
    </application>
    

    Também é possível verificar explicitamente a disponibilidade do modelo e solicitar o download por meio da API ModuleInstallClient do Google Play Services.

    Se você não ativar os downloads do modelo de tempo de 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.

Diretrizes de imagem de entrada

  • Para que o Kit de ML leia códigos de barras com precisão, as imagens de entrada devem conter códigos de barras representados por dados de pixel suficientes.

    Os requisitos específicos de dados de pixel dependem do tipo de código de barras e da quantidade de dados codificados nele, já que muitos códigos de barras são compatíveis com payload de tamanho variável. Em geral, a menor unidade significativa do código de barras deve ter pelo menos 2 pixels de largura e, para códigos bidimensionais, 2 pixels de altura.

    Por exemplo, os códigos de barras EAN-13 são compostos de barras e espaços de 1, 2, 3 ou 4 unidades de largura, portanto, uma imagem de código de barras EAN-13 possui barras e espaços de, no mínimo, 2, 4, 6 e 8 pixels de largura. Como um código de barras EAN-13 tem 95 unidades no total, o código de barras deve ter pelo menos 190 pixels de largura.

    Formatos mais densos, como o PDF417, precisam de dimensões em pixels maiores para que o Kit de ML possa ler de forma confiável. Por exemplo, um código PDF417 pode ter até 34 palavras"de 17 unidades em uma única linha, o que, em uma situação ideal, seria de pelo menos 1.156 pixels de largura.

  • O foco inadequado da imagem pode afetar a precisão da verificação. Se o app não estiver conseguindo resultados aceitáveis, peça para o usuário recapturar a imagem.

  • Para aplicativos típicos, recomenda-se fornecer uma imagem de resolução mais alta, como 1280 x 720 ou 1920 x 1080, o que torna os códigos de barras detectáveis a uma distância maior da câmera.

    No entanto, em aplicativos em que a latência é crítica, é possível melhorar o desempenho capturando imagens com resolução mais baixa, mas exigindo que o código de barras compacte a maior parte da imagem de entrada. Consulte também Dicas para melhorar o desempenho em tempo real.

1. Configurar o leitor de código de barras

Se souber quais formatos de código de barras espera ler, você poderá aumentar a velocidade do detector de código de barras configurando-o para detectar apenas esses formatos.

Por exemplo, para detectar apenas o código Aztec e códigos QR, crie um objeto BarcodeScannerOptions como no exemplo a seguir:

Kotlin

val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(
                Barcode.FORMAT_QR_CODE,
                Barcode.FORMAT_AZTEC)
        .build()

Java

BarcodeScannerOptions options =
        new BarcodeScannerOptions.Builder()
        .setBarcodeFormats(
                Barcode.FORMAT_QR_CODE,
                Barcode.FORMAT_AZTEC)
        .build();

Os seguintes formatos são compatíveis:

  • Código 128 (FORMAT_CODE_128)
  • Código 39 (FORMAT_CODE_39)
  • Código 93 (FORMAT_CODE_93)
  • Codabar (FORMAT_CODABAR)
  • EAN-13 (FORMAT_EAN_13)
  • EAN-8 (FORMAT_EAN_8)
  • ITF (FORMAT_ITF)
  • UPC-A (FORMAT_UPC_A)
  • UPC-E (FORMAT_UPC_E)
  • Código QR (FORMAT_QR_CODE)
  • PDF417 (FORMAT_PDF417).
  • Asteca (FORMAT_AZTEC)
  • Matriz de dados (FORMAT_DATA_MATRIX)

2. Preparar a imagem de entrada

Para reconhecer códigos de barras em uma imagem, crie um objeto InputImage com base em um Bitmap, media.Image, ByteBuffer, matriz de bytes ou um arquivo no dispositivo. Em seguida, transmita o objeto InputImage para o método process do BarcodeScanner.

É possível criar um objeto InputImage usando diferentes origens, cada uma explicada abaixo.

Como usar um media.Image

Para criar um objeto InputImage usando um objeto media.Image, como quando você captura uma imagem de uma 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, poderá 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);

Como usar um URI de arquivo

Para criar um objeto InputImage a partir de um URI de 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 aplicativo 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 usando um ByteBuffer ou ByteArray, primeiro calcule o grau de rotação da imagem conforme descrito anteriormente para a entrada media.Image. Em seguida, crie o objeto InputImage com o buffer ou a matriz, 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 usando 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. Receber uma instância do BarcodeScanner

Kotlin

val scanner = BarcodeScanning.getClient()
// Or, to specify the formats to recognize:
// val scanner = BarcodeScanning.getClient(options)

Java

BarcodeScanner scanner = BarcodeScanning.getClient();
// Or, to specify the formats to recognize:
// BarcodeScanner scanner = BarcodeScanning.getClient(options);

4. Processar a imagem

Transmita a imagem para o método process:

Kotlin

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

Java

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

5. Receber informações de códigos de barras

Se a operação de reconhecimento de código de barras for bem-sucedida, uma lista de objetos de classe Barcode será transmitida para o listener de êxito. Cada objeto Barcode representa um código de barras detectado na imagem. Para cada código de barras, é possível receber as coordenadas delimitadoras na imagem de entrada, bem como os dados brutos codificados pelo código de barras. Além disso, se o leitor de código de barras tiver conseguido determinar o tipo de dado codificado pelo código de barras, será possível gerar um objeto com dados analisados.

Exemplo:

Kotlin

for (barcode in barcodes) {
    val bounds = barcode.boundingBox
    val corners = barcode.cornerPoints

    val rawValue = barcode.rawValue

    val valueType = barcode.valueType
    // See API reference for complete list of supported types
    when (valueType) {
        Barcode.TYPE_WIFI -> {
            val ssid = barcode.wifi!!.ssid
            val password = barcode.wifi!!.password
            val type = barcode.wifi!!.encryptionType
        }
        Barcode.TYPE_URL -> {
            val title = barcode.url!!.title
            val url = barcode.url!!.url
        }
    }
}

Java

for (Barcode barcode: barcodes) {
    Rect bounds = barcode.getBoundingBox();
    Point[] corners = barcode.getCornerPoints();

    String rawValue = barcode.getRawValue();

    int valueType = barcode.getValueType();
    // See API reference for complete list of supported types
    switch (valueType) {
        case Barcode.TYPE_WIFI:
            String ssid = barcode.getWifi().getSsid();
            String password = barcode.getWifi().getPassword();
            int type = barcode.getWifi().getEncryptionType();
            break;
        case Barcode.TYPE_URL:
            String title = barcode.getUrl().getTitle();
            String url = barcode.getUrl().getUrl();
            break;
    }
}

Dicas para melhorar o desempenho em tempo real

Caso você queira ler códigos de barras em um aplicativo em tempo real, siga estas diretrizes para obter as melhores taxas de frames:

  • Não capture entradas na resolução nativa da câmera. Em alguns dispositivos, a captura da entrada na resolução nativa produz imagens extremamente grandes (mais de 10 megapixels), o que resulta em latência muito baixa sem nenhum benefício para a precisão. Em vez disso, solicite apenas o tamanho da câmera que seja necessário para a detecção de código de barras, que geralmente não é maior que 2 megapixels.

    Se a velocidade de leitura for importante, você poderá reduzir ainda mais a resolução da captura de imagem. No entanto, considere os requisitos mínimos de tamanho de código de barras descritos acima.

    Se você estiver tentando reconhecer códigos de barras de uma sequência de frames de streaming de vídeo, o reconhecedor poderá produzir resultados diferentes de frame para frame. Aguarde até receber uma série consecutiva de mesmo valor para ter certeza de que está retornando um bom resultado.

    O dígito da soma de verificação não é compatível com ITF e CODE-39.

  • Se você usa a API Camera ou camera2, o limite de chamadas para o detector é limitado. Se um novo frame de vídeo for disponibilizado enquanto o detector estiver em execução, descarte o frame. Consulte a classe VisionProcessorBase no app de amostra do guia de início rápido para ver um exemplo.
  • Se você usar a API CameraX, verifique se a estratégia de contrapressão está definida com o valor padrão ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Isso garante que apenas uma imagem seja enviada por vez para análise. Se mais imagens forem produzidas quando o analisador estiver ocupado, elas serão descartadas automaticamente e não serão colocadas na fila para entrega. Quando a imagem que está sendo analisada é fechada, chamando ImageProxy.close(), a próxima imagem mais recente é enviada.
  • 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. Ela será renderizada 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 ver 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.