Digitalize códigos de barras com kit de ML no Android

Você pode usar o ML Kit para reconhecer e decodificar códigos de barras.

Antes de você começar

  1. Adicione o Firebase ao seu projeto Android , caso ainda não o tenha feito.
  2. Adicione as dependências das bibliotecas Android do ML Kit 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 imagem de entrada

  • Para que o ML Kit 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 a maioria dos códigos de barras suporta uma carga útil 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 com 1, 2, 3 ou 4 unidades de largura, portanto, uma imagem de código de barras EAN-13 idealmente possui barras e espaços com pelo menos 2, 4, 6 e 8 pixels de largura. Como um código de barras EAN-13 tem 95 unidades de largura no total, o código de barras deve ter pelo menos 190 pixels de largura.

    Formatos mais densos, como PDF417, precisam de dimensões de pixels maiores para que o ML Kit possa lê-los com segurança. Por exemplo, um código PDF417 pode ter até 34 "palavras" de 17 unidades de largura em uma única linha, o que idealmente teria pelo menos 1156 pixels de largura.

  • O foco inadequado da imagem pode prejudicar a precisão da digitalização. Se você não estiver obtendo resultados aceitáveis, tente pedir ao usuário para recapturar a imagem.

  • Para aplicações típicas, recomenda-se fornecer uma imagem de resolução mais alta (como 1280x720 ou 1920x1080), o que torna os códigos de barras detectáveis ​​a uma distância maior da câmera.

    No entanto, em aplicações onde a latência é crítica, você pode melhorar o desempenho capturando imagens em uma resolução mais baixa, mas exigindo que o código de barras represente a maior parte da imagem de entrada. Consulte também Dicas para melhorar o desempenho em tempo real .

1. Configure o detector de código de barras

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

Por exemplo, para detectar apenas códigos astecas e códigos QR, crie um objeto FirebaseVisionBarcodeDetectorOptions como no exemplo a seguir:

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

Os seguintes formatos são suportados:

  • 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. Execute o detector de código de barras

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

  1. Crie um objeto FirebaseVisionImage a partir da sua imagem.

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

      Se você usar a biblioteca CameraX , as classes OnImageCapturedListener e ImageAnalysis.Analyzer calcularão o valor de rotação para você, então você só precisa converter a rotação em uma das constantes ROTATION_ do kit de ML antes de chamar FirebaseVisionImage.fromMediaImage() :

      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
             
      // ...
         
      }
      }
      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âmeras que fornece a rotação da imagem, você pode calculá-la a partir da rotação do dispositivo e da orientação do sensor da câmera no dispositivo:

      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;
      }
      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, passe o objeto media.Image e o valor de rotação para FirebaseVisionImage.fromMediaImage() :

      FirebaseVisionImage image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
      val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
    • Para criar um objeto FirebaseVisionImage a partir de um URI de arquivo, passe o contexto do aplicativo e o URI do arquivo para FirebaseVisionImage.fromFilePath() . Isso é útil quando você usa uma intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem do aplicativo de galeria.
      FirebaseVisionImage image;
      try {
          image
      = FirebaseVisionImage.fromFilePath(context, uri);
      } catch (IOException e) {
          e
      .printStackTrace();
      }
      val image: FirebaseVisionImage
      try {
          image
      = FirebaseVisionImage.fromFilePath(context, uri)
      } catch (e: IOException) {
          e
      .printStackTrace()
      }
    • Para criar um objeto FirebaseVisionImage a partir de um ByteBuffer ou de 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:

      FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
             
      .setWidth(480)   // 480x360 is typically sufficient for
             
      .setHeight(360)  // image recognition
             
      .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
             
      .setRotation(rotation)
             
      .build();
      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 array e o objeto de metadados para criar um objeto FirebaseVisionImage :

      FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
      // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);
      val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
      // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
    • Para criar um objeto FirebaseVisionImage a partir de um objeto Bitmap :
      FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);
      val image = FirebaseVisionImage.fromBitmap(bitmap)
      A imagem representada pelo objeto Bitmap deve estar na vertical, sem necessidade de rotação adicional.

  2. Obtenha uma instância do FirebaseVisionBarcodeDetector :

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

    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
                   
    // ...
               
    }
                   
    });
    val result = detector.detectInImage(image)
           
    .addOnSuccessListener { barcodes ->
               
    // Task completed successfully
               
    // ...
           
    }
           
    .addOnFailureListener {
               
    // Task failed with an exception
               
    // ...
           
    }

3. Obtenha 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 ao listener de sucesso. Cada objeto FirebaseVisionBarcode representa um código de barras que foi detectado na imagem. Para cada código de barras, você pode obter suas coordenadas delimitadoras na imagem de entrada, bem como os dados brutos codificados pelo código de barras. Além disso, se o detector de código de barras foi capaz de determinar o tipo de dados codificados pelo código de barras, você poderá obter um objeto contendo dados analisados.

Por exemplo:

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;
   
}
}
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 você deseja ler códigos de barras em um aplicativo em tempo real, siga estas diretrizes para obter as melhores taxas de quadros:

  • Não capture a entrada na resolução nativa da câmera. Em alguns dispositivos, a captura de 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 da câmera apenas o tamanho necessário para a detecção de código de barras: geralmente não mais que 2 megapixels.

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

  • Limite as chamadas para o detector. Se um novo quadro de vídeo ficar disponível enquanto o detector estiver em execução, elimine o quadro.
  • Se você estiver usando a saída do detector para sobrepor gráficos na imagem de entrada, primeiro obtenha o resultado do Kit de ML e, em seguida, renderize a imagem e a sobreposição em uma única etapa. Ao fazer isso, você renderiza na superfície de exibição apenas uma vez para cada quadro 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 .