在 Android 上使用 ML Kit 掃描條碼

您可以使用 ML Kit 辨識及解碼條碼。

功能未分類組合
導入作業模型會透過 Google Play 服務動態下載。模型在建構期間會以靜態方式連結至應用程式。
應用程式大小大小增加約 200 KB。大小增加約 2.4 MB。
初始化時間可能需要等待模型下載完成才能開始使用。模型可立即使用。

立即體驗

事前準備

  1. 在專案層級的 build.gradle 檔案中,請務必在 buildscriptallprojects 區段中加入 Google 的 Maven 存放區。

  2. 將 ML Kit 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 要求下載。

    如果您未啟用安裝期間模型下載功能或要求明確下載,系統會在首次執行掃描器時下載模型。在下載完成之前提出的要求不會產生任何結果。

圖片輸入規範

  • 為了讓 ML Kit 準確讀取條碼,輸入圖片必須包含由充足像素資料表示的條碼。

    特定像素資料要求取決於條碼類型和其中編碼的資料量,因為許多條碼都支援變數大小酬載。一般來說,有意義的條碼寬度單位至少應為 2 像素,而 2D 代碼的寬度至少應為 2 像素。

    舉例來說,EAN-13 條碼由寬度 1、2、3 或 4 個單位的長條和空格組成,因此 EAN-13 條碼圖片理想中的長條寬度至少為 2、4、6 和 8 像素。由於 EAN-13 條碼的寬度為 95 個單位,因此條碼寬度至少應為 190 像素。

    PDF417 這類密度格式需要更大的像素尺寸,ML Kit 才能穩定讀取這些格式的資料。舉例來說,PDF417 程式碼在單一列中最多可有 34 個 17 個單位寬的「字詞」,最好是寬度至少 1156 像素。

  • 圖片對焦不佳可能會影響掃描的準確度。如果應用程式未取得可接受的結果,請要求使用者重新擷取圖片。

  • 對於一般應用程式,建議您提供解析度較高的圖片 (例如 1280x720 或 1920x1080),讓條碼從相機遠處掃描。

    但是,若應用程式對於延遲時間特別重要,您可以透過較低解析度擷取圖片,但要求條碼佔大部分的輸入圖片,藉此提升效能。另請參閱「改善即時效能的訣竅」。

1. 設定條碼掃描器

如果您知道需要讀取哪些條碼格式,可以將條碼偵測工具設定為僅偵測這些格式,藉此改善條碼偵測器的速度。

舉例來說,如果只要偵測 Aztec 代碼和 QR code,建構 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)
  • 科達巴 (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)
  • 阿茲特克文 (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 意圖提示使用者從圖片庫應用程式選取圖片,這項功能就非常實用。

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() 來關閉要分析的映像檔後,就會傳送下一個映像檔。
  • 如果您使用偵測工具的輸出內容,在輸入圖片上疊加圖像,請先透過 ML Kit 取得結果,然後透過單一步驟算繪圖片和重疊結果。這只會針對每個輸入影格算繪至螢幕介面一次。如需範例,請參閱快速入門導覽課程範例應用程式中的 CameraSourcePreview GraphicOverlay 類別。
  • 如果您使用 Camera2 API,請擷取 ImageFormat.YUV_420_888 格式的圖片。如果使用舊版 Camera API,請拍攝 ImageFormat.NV21 格式的圖片。