使用机器学习套件扫描条形码 (Android)

您可以使用机器学习套件识别和解码条形码。

特征不分类显示捆绑
实现模型通过 Google Play 服务动态下载。模型在构建时静态关联到您的应用。
应用大小大小增加约 200 KB。大小增加约 2.4 MB。
初始化时间可能需要等到模型下载完毕后才能首次使用。模型立即可用。

试试看

准备工作

  1. 请务必在您的项目级 build.gradle 文件中的 buildscriptallprojects 部分添加 Google 的 Maven 制品库。

  2. 将 Android 版机器学习套件库的依赖项添加到模块的应用级 Gradle 文件(通常为 app/build.gradle)。根据您的需求选择以下依赖项之一:

    如需将模型与应用捆绑,请执行以下操作

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

    对于在 Google Play 服务中使用模型的情况

    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.3.0'
    }
    
  3. 如果您选择在 Google Play 服务中使用模型,则可以将应用配置为在用户从 Play 商店安装后自动将模型下载到设备上。为此,请将以下声明添加到应用的 AndroidManifest.xml 文件中:

    <application ...>
          ...
          <meta-data
              android:name="com.google.mlkit.vision.DEPENDENCIES"
              android:value="barcode" >
          <!-- To use multiple models: android:value="barcode,model2,model3" -->
    </application>
    

    您还可以明确检查模型可用性并请求通过 Google Play 服务 ModuleInstallClient API 进行下载。

    如果您未启用在安装时下载模型或请求明确下载,模型会在您首次运行扫描程序时下载。您在下载完成之前发出的请求不会产生任何结果。

输入图片准则

  • 为了使机器学习套件准确读取条形码,输入图片必须包含由足够像素数据表示的条形码。

    具体的像素数据要求取决于条形码的类型和编码数据量,因为许多条形码都支持大小可变的载荷。通常,条形码的最小有效单位应至少为 2 像素宽,对于二维代码,应至少为 2 像素高。

    例如,EAN-13 条形码由宽度为 1、2、3 或 4 个单位的条形和空格组成,因此理想情况下,EAN-13 条形码图片应具有宽度至少为 2、4、6 和 8 像素的条形和空格。由于 EAN-13 条形码的总宽度为 95 个单位,因此条形码的宽度应至少为 190 像素。

    PDF417 等更密集的格式需要更大的像素尺寸,以便机器学习套件可靠地读取它们。例如,一个 PDF417 代码在一行中最多可以包含 34 个 17 单元宽的“单词”,理想情况下宽度至少为 1156 像素。

  • 图片聚焦不良会影响扫描准确性。如果您的应用未获得可接受的结果,请让用户重新拍摄图片。

  • 对于典型应用,建议提供分辨率更高的图片(例如 1280x720 或 1920x1080),这样,在离相机较远的距离上就能扫描条形码。

    但是,在延迟时间至关重要的应用中,您可以通过以较低分辨率捕获图片来提高性能,但要求条形码必须构成输入图片的主要部分。另请参阅提高实时性能的相关提示

1. 配置条形码扫描器

如果您知道要读取哪些条形码格式,则可以将条形码检测器配置为仅检测这些格式,从而提高条形码检测器的速度。

例如,如需仅检测 Aztec 码和 QR 码,请按照以下示例构建 BarcodeScannerOptions 对象:

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

支持以下格式:

  • 代码 128 (FORMAT_CODE_128)
  • 代码 39 (FORMAT_CODE_39)
  • 代码 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)
  • 二维码 (FORMAT_QR_CODE)
  • PDF417(FORMAT_PDF417
  • 阿兹特克语 (FORMAT_AZTEC)
  • 数据矩阵 (FORMAT_DATA_MATRIX)

从捆绑模型 17.1.0 和未捆绑模型 18.2.0 开始,您还可以调用 enableAllPotentialBarcodes() 来返回所有可能的条形码,即使它们无法解码也是如此。这可用于促进进一步的检测,例如,通过放大摄像头以获取更清晰的图像来匹配返回的边界框中的任何条形码。

Kotlin

val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(...)
        .enableAllPotentialBarcodes() // Optional
        .build()

Java

BarcodeScannerOptions options =
        new BarcodeScannerOptions.Builder()
        .setBarcodeFormats(...)
        .enableAllPotentialBarcodes() // Optional
        .build();

Further on, starting from bundled library 17.2.0 and unbundled library 18.3.0, a new feature called auto-zoom has been introduced to further enhance the barcode scanning experience. With this feature enabled, the app is notified when all barcodes within the view are too distant for decoding. As a result, the app can effortlessly adjust the camera's zoom ratio to the recommended setting provided by the library, ensuring optimal focus and readability. This feature will significantly enhance the accuracy and success rate of barcode scanning, making it easier for apps to capture information precisely.

To enable auto-zooming and customize the experience, you can utilize the setZoomSuggestionOptions() method along with your own ZoomCallback handler and desired maximum zoom ratio, as demonstrated in the code below.

Kotlin

val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(...)
        .setZoomSuggestionOptions(
            new ZoomSuggestionOptions.Builder(zoomCallback)
                .setMaxSupportedZoomRatio(maxSupportedZoomRatio)
                .build()) // Optional
        .build()

Java

BarcodeScannerOptions options =
        new BarcodeScannerOptions.Builder()
        .setBarcodeFormats(...)
        .setZoomSuggestionOptions(
            new ZoomSuggestionOptions.Builder(zoomCallback)
                .setMaxSupportedZoomRatio(maxSupportedZoomRatio)
                .build()) // Optional
        .build();

zoomCallback is required to be provided to handle whenever the library suggests a zoom should be performed and this callback will always be called on the main thread.

The following code snippet shows an example of defining a simple callback.

Kotlin

fun setZoom(ZoomRatio: Float): Boolean {
    if (camera.isClosed()) return false
    camera.getCameraControl().setZoomRatio(zoomRatio)
    return true
}

Java

boolean setZoom(float zoomRatio) {
    if (camera.isClosed()) {
        return false;
    }
    camera.getCameraControl().setZoomRatio(zoomRatio);
    return true;
}

maxSupportedZoomRatio is related to the camera hardware, and different camera libraries have different ways to fetch it (see the javadoc of the setter method). In case this is not provided, an unbounded zoom ratio might be produced by the library which might not be supported. Refer to the setMaxSupportedZoomRatio() method introduction to see how to get the max supported zoom ratio with different Camera libraries.

When auto-zooming is enabled and no barcodes are successfully decoded within the view, BarcodeScanner triggers your zoomCallback with the requested zoomRatio. If the callback correctly adjusts the camera to this zoomRatio, it is highly probable that the most centered potential barcode will be decoded and returned.

A barcode may remain undecodable even after a successful zoom-in. In such cases, BarcodeScanner may either invoke the callback for another round of zoom-in until the maxSupportedZoomRatio is reached, or provide an empty list (or a list containing potential barcodes that were not decoded, if enableAllPotentialBarcodes() was called) to the OnSuccessListener (which will be defined in step 4. Process the image).

2. Prepare the input image

To recognize barcodes in an image, create an InputImage object from either a Bitmap, media.Image, ByteBuffer, byte array, or a file on the device. Then, pass the InputImage object to the BarcodeScanner's process method.

You can create an InputImage object from different sources, each is explained below.

Using a media.Image

To create an InputImage object from a media.Image object, such as when you capture an image from a device's camera, pass the media.Image object and the image's rotation to InputImage.fromMediaImage().

If you use the CameraX library, the OnImageCapturedListener and ImageAnalysis.Analyzer classes calculate the rotation value for you.

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
          // ...
        }
    }
}

如果您不使用可提供图片旋转角度的相机库,则可以根据设备的旋转角度和设备中相机传感器的朝向来计算旋转角度:

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

然后,将 media.Image 对象和旋转角度值传递给 InputImage.fromMediaImage()

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

使用文件 URI

如需基于文件 URI 创建 InputImage 对象,请将应用上下文和文件 URI 传递给 InputImage.fromFilePath()。当您使用 ACTION_GET_CONTENT intent 提示用户从图库应用中选择图片时,这非常有用。

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

使用 ByteBufferByteArray

如需基于 ByteBufferByteArray 创建 InputImage 对象,请先按之前针对 media.Image 输入的说明计算图片旋转角度。然后,使用缓冲区或数组以及图片的高度、宽度、颜色编码格式和旋转角度创建 InputImage 对象:

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

使用 Bitmap

如需基于 Bitmap 对象创建 InputImage 对象,请进行以下声明:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

图片由 Bitmap 对象和旋转角度表示。

3. 获取 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. 处理图片

将图片传递给 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. 从条形码中获取信息

如果条形码识别操作成功,系统会向成功监听器传递一组 Barcode 对象。每个 Barcode 对象代表一个在图片中检测到的条形码。对于每个条形码,您可以获取它在输入图片中的边界坐标以及由条形码编码的原始数据。此外,如果条形码扫描器能够确定条形码编码的数据类型,您还可以获取一个包含已解析数据的对象。

例如:

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

提高实时性能的相关提示

如果要在实时应用中扫描条形码,请遵循以下准则以实现最佳帧速率:

  • 请勿以相机的原始分辨率捕获输入内容。在某些设备上,以原生分辨率捕获输入会生成超大(超过 1,000 万像素)的图像,导致延迟时间非常短,并且对准确性没有好处。而是应仅从相机中请求检测条形码所需的尺寸,通常不超过 200 万像素。

    如果扫描速度很重要,您可以进一步降低图片拍摄分辨率。不过,请牢记上述最小条形码大小要求。

    如果您尝试识别一系列流式视频帧中的条形码,则识别器可能会因帧而产生不同的结果。您应该等到获得包含同一值的连续序列后,才能确信自己返回良好结果。

    ITF 和 CODE-39 不支持校验和数字。

  • 如果您使用 Cameracamera2 API,请限制对检测器的调用。如果在检测器运行时有新的视频帧可用,请丢弃该帧。如需查看示例,请参阅快速入门示例应用中的 VisionProcessorBase 类。
  • 如果您使用 CameraX API,请确保背压策略设置为默认值 ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST。这可保证一次仅传送一张图片进行分析。如果在分析器处于忙碌状态时生成了更多图像,这些图像将被自动丢弃,而不会排队等待传送。通过调用 ImageProxy.close() 关闭正在分析的图片后,将传送下一张最新图片。
  • 如果使用检测器的输出在输入图片上叠加图形,请先从机器学习套件获取结果,然后在一个步骤中渲染该图片并进行叠加。对于每个输入帧,该操作仅会渲染到显示 Surface 一次。如需查看示例,请参阅快速入门示例应用中的 CameraSourcePreview GraphicOverlay 类。
  • 如果您使用 Camera2 API,请以 ImageFormat.YUV_420_888 格式捕获图片。如果您使用旧版 Camera API,请以 ImageFormat.NV21 格式捕获图片。