在 Android 上使用机器学习套件扫描条形码

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

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

您可以通过以下两种方式集成条形码扫描:将模型作为应用的一部分捆绑在一起,或使用依赖于 Google Play 服务的未捆绑模型。如果选择未捆绑模型,您的应用会变小。请参阅下表了解详情。

特征未捆绑组合
实现模型是通过 Google Play 服务动态下载的。模型在构建时静态关联到您的应用。
应用大小大小增加约 600 KB。大小增加约 3.2 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.0.2'
    }
    

    如需在 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.1.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 码和二维码,请按照以下示例构建 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)

2. 准备输入图片

如需识别图片中的条形码,请基于设备上的以下资源创建一个 InputImage 对象:Bitmapmedia.ImageByteBuffer、字节数组或文件。然后,将 InputImage 对象传递给 BarcodeScannerprocess 方法。

您可以根据不同的来源创建 InputImage 对象,下文分别介绍了各种对象。

使用 media.Image

如需基于 media.Image 对象创建 InputImage 对象(例如从设备的相机捕获图片时),请将 media.Image 对象和图片的旋转角度传递给 InputImage.fromMediaImage()

如果您使用 CameraX 库,OnImageCapturedListenerImageAnalysis.Analyzer 类会为您计算旋转角度值。

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

提高实时性能的相关提示

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

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

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

    如果您尝试识别一系列流式视频帧中的条形码,则识别器可能会在不同帧之间产生不同的结果。您应等到获得连续的相同值系列后再确认是否会返回良好的结果。

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

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