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

É possível usar o Kit de ML para reconhecer e decodificar códigos de barras.

Antes de começar

  1. Adicione o Firebase ao seu projeto para Android, caso ainda não tenha feito isso.
  2. Adicione as dependências das bibliotecas do Android do Kit de ML ao arquivo Gradle do módulo (nível do aplicativo) (geralmente app/build.gradle):
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      implementation 'com.google.firebase:firebase-ml-vision-barcode-model:16.0.1'
    }
    

Diretrizes de imagens 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 nele codificados (já que a maioria dos códigos de barras é compatível com payload de comprimento 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 apresenta 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, que, em uma situação ideal, teria pelo menos 1.156 pixels de largura.

  • Uma imagem com foco inadequado pode prejudicar a precisão. Se os resultados não forem 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), que detecta códigos de barras 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 uma resolução menor, desde que o código de barras componha a maior parte da imagem de entrada. Consulte também Dicas para melhorar o desempenho em tempo real.

1. Configurar o detector de código de barras

Se você sabe quais formatos de código de barras espera ler, aumente 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 FirebaseVisionBarcodeDetectorOptions como no exemplo a seguir:

Java

FirebaseVisionBarcodeDetectorOptions options =
        new FirebaseVisionBarcodeDetectorOptions.Builder()
        .setBarcodeFormats(
                FirebaseVisionBarcode.FORMAT_QR_CODE,
                FirebaseVisionBarcode.FORMAT_AZTEC)
        .build();

Kotlin+KTX

val options = FirebaseVisionBarcodeDetectorOptions.Builder()
        .setBarcodeFormats(
                FirebaseVisionBarcode.FORMAT_QR_CODE,
                FirebaseVisionBarcode.FORMAT_AZTEC)
        .build()

Os seguintes formatos são compatíveis:

  • Code 128 (FORMAT_CODE_128)
  • Code 39 (FORMAT_CODE_39)
  • Code 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)
  • QR Code (FORMAT_QR_CODE)
  • PDF417 (FORMAT_PDF417)
  • Aztec (FORMAT_AZTEC)
  • Data Matrix (FORMAT_DATA_MATRIX)

2. Executar o detector de código de barras

Para reconhecer códigos de barras em uma imagem, crie um objeto FirebaseVisionImage usando Bitmap, media.Image, ByteBuffer, matriz de bytes ou um arquivo no dispositivo. Em seguida, transmita o objeto FirebaseVisionImage para o método detectInImage do FirebaseVisionBarcodeDetector.

  1. Crie um objeto FirebaseVisionImage usando sua imagem.

    • Para criar um objeto FirebaseVisionImage usando um objeto media.Image, como ao capturar uma imagem da câmera de um dispositivo, transmita o objeto media.Image e a rotação da imagem para FirebaseVisionImage.fromMediaImage().

      Se você usa a biblioteca CameraX, as classes OnImageCapturedListener e ImageAnalysis.Analyzer calculam o valor de rotação para você. Basta converter a rotação em uma das constantes ROTATION_ do Kit de ML antes de chamar FirebaseVisionImage.fromMediaImage():

      Java

      private class YourAnalyzer implements ImageAnalysis.Analyzer {
      
          private int degreesToFirebaseRotation(int degrees) {
              switch (degrees) {
                  case 0:
                      return FirebaseVisionImageMetadata.ROTATION_0;
                  case 90:
                      return FirebaseVisionImageMetadata.ROTATION_90;
                  case 180:
                      return FirebaseVisionImageMetadata.ROTATION_180;
                  case 270:
                      return FirebaseVisionImageMetadata.ROTATION_270;
                  default:
                      throw new IllegalArgumentException(
                              "Rotation must be 0, 90, 180, or 270.");
              }
          }
      
          @Override
          public void analyze(ImageProxy imageProxy, int degrees) {
              if (imageProxy == null || imageProxy.getImage() == null) {
                  return;
              }
              Image mediaImage = imageProxy.getImage();
              int rotation = degreesToFirebaseRotation(degrees);
              FirebaseVisionImage image =
                      FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
              // Pass image to an ML Kit Vision API
              // ...
          }
      }
      

      Kotlin+KTX

      private class YourImageAnalyzer : ImageAnalysis.Analyzer {
          private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
              0 -> FirebaseVisionImageMetadata.ROTATION_0
              90 -> FirebaseVisionImageMetadata.ROTATION_90
              180 -> FirebaseVisionImageMetadata.ROTATION_180
              270 -> FirebaseVisionImageMetadata.ROTATION_270
              else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
          }
      
          override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
              val mediaImage = imageProxy?.image
              val imageRotation = degreesToFirebaseRotation(degrees)
              if (mediaImage != null) {
                  val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                  // Pass image to an ML Kit Vision API
                  // ...
              }
          }
      }
      

      Se você não usa uma biblioteca de câmera que ofereça a rotação da imagem, faça o cálculo usando a rotação do dispositivo e a orientação do sensor da câmera:

      Java

      private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
      static {
          ORIENTATIONS.append(Surface.ROTATION_0, 90);
          ORIENTATIONS.append(Surface.ROTATION_90, 0);
          ORIENTATIONS.append(Surface.ROTATION_180, 270);
          ORIENTATIONS.append(Surface.ROTATION_270, 180);
      }
      
      /**
       * 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, Context context)
              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);
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
          int sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION);
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          int result;
          switch (rotationCompensation) {
              case 0:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  break;
              case 90:
                  result = FirebaseVisionImageMetadata.ROTATION_90;
                  break;
              case 180:
                  result = FirebaseVisionImageMetadata.ROTATION_180;
                  break;
              case 270:
                  result = FirebaseVisionImageMetadata.ROTATION_270;
                  break;
              default:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  Log.e(TAG, "Bad rotation value: " + rotationCompensation);
          }
          return result;
      }

      Kotlin+KTX

      private val ORIENTATIONS = SparseIntArray()
      
      init {
          ORIENTATIONS.append(Surface.ROTATION_0, 90)
          ORIENTATIONS.append(Surface.ROTATION_90, 0)
          ORIENTATIONS.append(Surface.ROTATION_180, 270)
          ORIENTATIONS.append(Surface.ROTATION_270, 180)
      }
      /**
       * 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, context: Context): 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)
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
          val sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          val result: Int
          when (rotationCompensation) {
              0 -> result = FirebaseVisionImageMetadata.ROTATION_0
              90 -> result = FirebaseVisionImageMetadata.ROTATION_90
              180 -> result = FirebaseVisionImageMetadata.ROTATION_180
              270 -> result = FirebaseVisionImageMetadata.ROTATION_270
              else -> {
                  result = FirebaseVisionImageMetadata.ROTATION_0
                  Log.e(TAG, "Bad rotation value: $rotationCompensation")
              }
          }
          return result
      }

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

      Java

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

      Kotlin+KTX

      val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
    • Para criar um objeto FirebaseVisionImage com base no URI de um arquivo, transmita o contexto do aplicativo e o URI do arquivo para FirebaseVisionImage.fromFilePath(). Isso é útil ao usar uma intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem no app de galeria dele.

      Java

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

      Kotlin+KTX

      val image: FirebaseVisionImage
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri)
      } catch (e: IOException) {
          e.printStackTrace()
      }
    • Para criar um objeto FirebaseVisionImage com base em um ByteBuffer ou uma matriz de bytes, primeiro calcule a rotação da imagem conforme descrito acima para a entrada media.Image.

      Em seguida, crie um objeto FirebaseVisionImageMetadata que contenha a altura, a largura, o formato de codificação de cores e a rotação da imagem:

      Java

      FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
              .setWidth(480)   // 480x360 is typically sufficient for
              .setHeight(360)  // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build();

      Kotlin+KTX

      val metadata = FirebaseVisionImageMetadata.Builder()
              .setWidth(480) // 480x360 is typically sufficient for
              .setHeight(360) // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build()

      Use o buffer ou a matriz e o objeto de metadados para criar um objeto FirebaseVisionImage:

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
      // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
      // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
    • Para criar um objeto FirebaseVisionImage com base em um objeto Bitmap:

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromBitmap(bitmap)
      A imagem representada pelo objeto Bitmap precisa estar na posição vertical, sem a necessidade de ser girada novamente.

  2. Receba uma instância de FirebaseVisionBarcodeDetector:

    Java

    FirebaseVisionBarcodeDetector detector = FirebaseVision.getInstance()
            .getVisionBarcodeDetector();
    // Or, to specify the formats to recognize:
    // FirebaseVisionBarcodeDetector detector = FirebaseVision.getInstance()
    //        .getVisionBarcodeDetector(options);

    Kotlin+KTX

    val detector = FirebaseVision.getInstance()
            .visionBarcodeDetector
    // Or, to specify the formats to recognize:
    // val detector = FirebaseVision.getInstance()
    //        .getVisionBarcodeDetector(options)
  3. Por fim, transmita a imagem para o método detectInImage:

    Java

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

    Kotlin+KTX

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

3. 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 FirebaseVisionBarcode será transmitida para o listener de êxito. Cada objeto FirebaseVisionBarcode 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 no código de barras. Além disso, se o detector de código de barras tiver sido capaz de determinar o tipo de dados codificados pelo código de barras, será possível receber um objeto contendo dados analisados.

Exemplo:

Java

for (FirebaseVisionBarcode 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 FirebaseVisionBarcode.TYPE_WIFI:
            String ssid = barcode.getWifi().getSsid();
            String password = barcode.getWifi().getPassword();
            int type = barcode.getWifi().getEncryptionType();
            break;
        case FirebaseVisionBarcode.TYPE_URL:
            String title = barcode.getUrl().getTitle();
            String url = barcode.getUrl().getUrl();
            break;
    }
}

Kotlin+KTX

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) {
        FirebaseVisionBarcode.TYPE_WIFI -> {
            val ssid = barcode.wifi!!.ssid
            val password = barcode.wifi!!.password
            val type = barcode.wifi!!.encryptionType
        }
        FirebaseVisionBarcode.TYPE_URL -> {
            val title = barcode.url!!.title
            val url = barcode.url!!.url
        }
    }
}

Dicas para melhorar o desempenho em tempo real

Se preferir ler códigos de barras em um aplicativo em tempo real, siga estas diretrizes para conseguir as melhores taxas de frames:

  • Não capture a entrada 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 necessário para a detecção de códigos de barras: normalmente, não mais que 2 megapixels.

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

  • Limite as chamadas ao detector. Se um novo frame de vídeo ficar disponível durante a execução do detector, descarte esse frame.
  • Se você estiver usando a saída do detector para sobrepor elementos gráficos na imagem de entrada, primeiro acesse o resultado do Kit de ML. Em seguida, renderize a imagem e faça a sobreposição de uma só vez. Ao fazer isso, você renderiza a superfície de exibição apenas uma vez para cada frame de entrada.
  • 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.