在 Android 上使用自訂分類模型來偵測、追蹤及分類物件

您可以使用 ML Kit 偵測並追蹤連續影片畫面中的物件。

將圖片傳送至 ML Kit 時,除了圖片中每個物件的位置外,圖片也會偵測出最多五個物件。在偵測影片串流中的物件時,每個物件都有專屬的 ID,可用來追蹤影格之間的物件。

您可以使用自訂圖片分類模型,將偵測到的物件分類。請參閱使用 ML Kit 自訂模型的相關指南,瞭解模型相容性需求、如何尋找預先訓練模型,以及如何訓練模型。

整合自訂模式的方法有兩種。您可以將該模型封裝至應用程式的資產資料夾內,也可以從 Firebase 動態下載模型。下表比較了這兩個選項。

套裝模型 代管模型
模型是應用程式 APK 的一部分,因此會增加其大小。 此模型不屬於您的 APK。託管於 Firebase 機器學習
即使 Android 裝置處於離線狀態,也可立即使用模型 隨選下載模型
不需要 Firebase 專案 必須有 Firebase 專案
您必須重新發布應用程式,才能更新模型 不必重新發布應用程式即可推送模型更新
沒有內建 A/B 測試 使用 Firebase 遠端設定輕鬆進行 A/B 版本測試

立即體驗

事前準備

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

  2. 將 ML Kit Android 程式庫的依附元件新增至模組的應用程式層級 Gradle 檔案 (通常為 app/build.gradle):

    如何將模型與應用程式組合在一起:

    dependencies {
      // ...
      // Object detection & tracking feature with custom bundled model
      implementation 'com.google.mlkit:object-detection-custom:17.0.0'
    }
    

    如要從 Firebase 動態下載模型,請新增 linkFirebase 依附元件:

    dependencies {
      // ...
      // Object detection & tracking feature with model downloaded
      // from firebase
      implementation 'com.google.mlkit:object-detection-custom:17.0.0'
      implementation 'com.google.mlkit:linkfirebase:17.0.0'
    }
    
  3. 如要下載模型,請先將 Firebase 新增至您的 Android 專案 (若您還沒這麼做的話)。

1. 載入模型

設定本機模型來源

如何將模型與應用程式整合:

  1. 將模型檔案 (通常以 .tflite.lite 結尾) 複製到應用程式的 assets/ 資料夾中。(您可能需要先在 app/ 資料夾上按一下滑鼠右鍵,然後依序按一下「New」(新增) >「Folder」(資料夾) >「Assets Folder」(素材資源資料夾)),才能建立資料夾。

  2. 接著,將下列程式碼加進應用程式的 build.gradle 檔案,以確保 Gradle 在建構應用程式時不會壓縮模型檔案:

    android {
        // ...
        aaptOptions {
            noCompress "tflite"
            // or noCompress "lite"
        }
    }
    

    模型檔案會納入應用程式套件中,並以原始資產的形式提供給 ML Kit。

  3. 建立 LocalModel 物件,指定模型檔案的路徑:

    Kotlin

    val localModel = LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute file path to model file)
            // or .setUri(URI to model file)
            .build()

    Java

    LocalModel localModel =
        new LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute file path to model file)
            // or .setUri(URI to model file)
            .build();

設定 Firebase 代管的模型來源

如要使用遠端託管模型,請透過 FirebaseModelSource 建立 CustomRemoteModel 物件,並指定您在發布模型時為其指派的名稱:

Kotlin

// Specify the name you assigned in the Firebase console.
val remoteModel =
    CustomRemoteModel
        .Builder(FirebaseModelSource.Builder("your_model_name").build())
        .build()

Java

// Specify the name you assigned in the Firebase console.
CustomRemoteModel remoteModel =
    new CustomRemoteModel
        .Builder(new FirebaseModelSource.Builder("your_model_name").build())
        .build();

然後啟動模型下載工作,指定要允許下載的條件。如果裝置上沒有模型,或有新版模型可用,則工作會以非同步方式從 Firebase 下載模型:

Kotlin

val downloadConditions = DownloadConditions.Builder()
    .requireWifi()
    .build()
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
    .addOnSuccessListener {
        // Success.
    }

Java

DownloadConditions downloadConditions = new DownloadConditions.Builder()
        .requireWifi()
        .build();
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
        .addOnSuccessListener(new OnSuccessListener() {
            @Override
            public void onSuccess(@NonNull Task task) {
                // Success.
            }
        });

許多應用程式均會在其初始化程式碼中啟動下載工作,但您也可以在使用模型前隨時進行這項操作。

2. 設定物件偵測工具

設定模型來源後,請使用 CustomObjectDetectorOptions 物件為您的用途設定物件偵測工具。您可以變更下列設定:

物件偵測工具設定
偵測模式 STREAM_MODE (預設) | SINGLE_IMAGE_MODE

STREAM_MODE (預設) 中,物件偵測工具的延遲時間較短,但在偵測工具的前幾次叫用中,可能會產生不完整的結果 (例如未指定的定界框或類別標籤)。此外,在 STREAM_MODE 中,偵測工具會將追蹤 ID 指派給物件,讓您用來追蹤跨影格的物件。當您想要追蹤物件,或有低延遲的處理情況 (例如,即時處理影片串流) 時,請使用這個模式。

SINGLE_IMAGE_MODE 中,物件偵測工具在決定物件的邊界方塊後傳回結果。如果您同時啟用分類功能,在定界框和類別標籤都可供使用時,就會傳回結果。因此,偵測延遲時間可能較長。此外,SINGLE_IMAGE_MODE 不會指派追蹤 ID。如果延遲時間不重要,且您不想處理部分結果,請使用這個模式。

偵測及追蹤多個物件 false (預設) | true

偵測並追蹤最多 5 個物件,或僅追蹤最顯眼的物件 (預設)。

分類物件 false (預設) | true

是否使用提供的自訂分類模型來分類已偵測的物件。如要使用自訂分類模型,請將其設為 true

分類可信度門檻

偵測到的標籤最低可信度分數。如未設定,系統會使用模型中繼資料指定的任何分類器門檻。如果模型未包含任何中繼資料,或中繼資料未指定分類器的閾值,系統會使用預設門檻值 0.0。

每個物件的標籤數量上限

偵測工具將傳回的物件標籤數量上限。如未設定,系統會使用預設值 10。

物件偵測與追蹤 API 已針對以下兩個核心用途進行最佳化處理:

  • 即時偵測及追蹤相機觀景窗中最顯眼的物件。
  • 從靜態圖片偵測多個物件。

如要針對這些用途設定 API,請使用本機組合模型:

Kotlin

// Live detection and tracking
val customObjectDetectorOptions =
        CustomObjectDetectorOptions.Builder(localModel)
        .setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
        .enableClassification()
        .setClassificationConfidenceThreshold(0.5f)
        .setMaxPerObjectLabelCount(3)
        .build()

// Multiple object detection in static images
val customObjectDetectorOptions =
        CustomObjectDetectorOptions.Builder(localModel)
        .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
        .enableMultipleObjects()
        .enableClassification()
        .setClassificationConfidenceThreshold(0.5f)
        .setMaxPerObjectLabelCount(3)
        .build()

val objectDetector =
        ObjectDetection.getClient(customObjectDetectorOptions)

Java

// Live detection and tracking
CustomObjectDetectorOptions customObjectDetectorOptions =
        new CustomObjectDetectorOptions.Builder(localModel)
                .setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();

// Multiple object detection in static images
CustomObjectDetectorOptions customObjectDetectorOptions =
        new CustomObjectDetectorOptions.Builder(localModel)
                .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
                .enableMultipleObjects()
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();

ObjectDetector objectDetector =
    ObjectDetection.getClient(customObjectDetectorOptions);

如果您有遠端代管的模型,在執行模型之前,必須檢查是否已下載該模型。您可以使用模型管理員的 isModelDownloaded() 方法,檢查模型下載任務的狀態。

雖然您只需在執行偵測工具之前確認,但是如果您同時擁有遠端託管模型和本機組合模型,則在對映像檔偵測工具執行個體化時,可能必須執行這項檢查:如果遠端模型已下載偵測工具,系統會下載該模型;如果已經下載,則從本機模型建立模型。

Kotlin

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
    val optionsBuilder =
        if (isDownloaded) {
            CustomObjectDetectorOptions.Builder(remoteModel)
        } else {
            CustomObjectDetectorOptions.Builder(localModel)
        }
    val customObjectDetectorOptions = optionsBuilder
            .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableClassification()
            .setClassificationConfidenceThreshold(0.5f)
            .setMaxPerObjectLabelCount(3)
            .build()
    val objectDetector =
        ObjectDetection.getClient(customObjectDetectorOptions)
}

Java

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener(new OnSuccessListener() {
        @Override
        public void onSuccess(Boolean isDownloaded) {
            CustomObjectDetectorOptions.Builder optionsBuilder;
            if (isDownloaded) {
                optionsBuilder = new CustomObjectDetectorOptions.Builder(remoteModel);
            } else {
                optionsBuilder = new CustomObjectDetectorOptions.Builder(localModel);
            }
            CustomObjectDetectorOptions customObjectDetectorOptions = optionsBuilder
                .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();
            ObjectDetector objectDetector =
                ObjectDetection.getClient(customObjectDetectorOptions);
        }
});

如果您只有遠端託管的模型,在停用模型下載之前,請先停用模型相關功能 (例如顯示為灰色或隱藏 UI 的部分)。方法是將事件監聽器附加到模型管理員的 download() 方法:

Kotlin

RemoteModelManager.getInstance().download(remoteModel, conditions)
    .addOnSuccessListener {
        // Download complete. Depending on your app, you could enable the ML
        // feature, or switch from the local model to the remote model, etc.
    }

Java

RemoteModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener() {
            @Override
            public void onSuccess(Void v) {
              // Download complete. Depending on your app, you could enable
              // the ML feature, or switch from the local model to the remote
              // model, etc.
            }
        });

3. 準備輸入圖片

從映像檔建立 InputImage 物件。物件偵測工具可以直接從 Bitmap、NV21 ByteBuffer 或 YUV_420_888 media.Image 執行。如果您可直接存取其中一個來源,建議從這些來源建構 InputImage。如果您是從其他來源建構 InputImage,將會由內部為您處理轉換作業,且效益可能會較低。

您可以從不同來源建立 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 意圖提示使用者從圖片庫應用程式中選取圖片時,這項功能就非常實用。

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 物件表示,並以旋轉度數表示。

4. 執行物件偵測工具

Kotlin

objectDetector
    .process(image)
    .addOnFailureListener(e -> {...})
    .addOnSuccessListener(results -> {
        for (detectedObject in results) {
          // ...
        }
    });

Java

objectDetector
    .process(image)
    .addOnFailureListener(e -> {...})
    .addOnSuccessListener(results -> {
        for (DetectedObject detectedObject : results) {
          // ...
        }
    });

5. 取得有標籤物件的相關資訊

如果對 process() 的呼叫成功,系統會將 DetectedObject 的清單傳送至成功的事件監聽器。

每個 DetectedObject 都包含下列屬性:

定界框 Rect 表示物件在圖片中的位置。
追蹤 ID 一個整數可用來識別圖片中的物件。SINGLE_IMAGE_MODE 可為空值。
標籤
標籤說明 標籤的文字說明。僅當 TensorFlow Lite 模型的中繼資料包含標籤說明時才會傳回。
標籤索引 該標籤的索引,用於分類器支援的所有標籤。
標籤可信度 物件分類的可信度值。

Kotlin

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (detectedObject in results) {
    val boundingBox = detectedObject.boundingBox
    val trackingId = detectedObject.trackingId
    for (label in detectedObject.labels) {
      val text = label.text
      val index = label.index
      val confidence = label.confidence
    }
}

Java

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (DetectedObject detectedObject : results) {
  Rect boundingBox = detectedObject.getBoundingBox();
  Integer trackingId = detectedObject.getTrackingId();
  for (Label label : detectedObject.getLabels()) {
    String text = label.getText();
    int index = label.getIndex();
    float confidence = label.getConfidence();
  }
}

提供良好的使用者體驗

為提供最佳使用者體驗,請在應用程式中遵守下列規範:

  • 物件偵測是否成功,取決於物件的視覺複雜程度。系統偵測物件時,如果只有少量的視覺功能,可能就必須佔滿圖片中較大的部分。建議您提供指示,讓系統針對要偵測的物件類型擷取輸入資訊。
  • 使用分類時,如果您想偵測不會完全排除在支援類別中的物件,請針對未知物件實作特殊處理方式。

您也可以參閱 ML Kit Material Design 展示應用程式和 Material Design 採用機器學習技術的模式圖案。

提升效能

如果您想在即時應用程式中使用物件偵測,請遵守下列指南,以達到最佳影格速率:

  • 在即時應用程式中使用串流模式時,請勿使用多個物件偵測,因為大部分裝置無法產生適當的畫面速率。

  • 如果您使用的是 Cameracamera2 API,請呼叫偵測工具。如果有新的影片畫面在偵測工具執行時可供使用,請捨棄該影格。如需範例,請參閱快速入門導覽課程範例應用程式中的 VisionProcessorBase 類別。
  • 如果您使用 CameraX API,請確認背壓策略已設為預設值 ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST。這麼做可確保系統每次只會傳送一張圖片進行分析。如果在分析器處於忙碌狀態時產生更多圖片,系統會自動捨棄這些圖片,不會排入佇列。透過呼叫 ImageProxy.close() 將所分析的圖片關閉之後,即可提供下一張最新的圖片。
  • 如果您使用偵測工具的輸出內容,為輸入圖片上的圖像重疊,請先透過 ML Kit 取得結果,然後透過單一步驟算繪圖像和疊加層。每個輸入框只會向顯示途徑轉譯一次。如需範例,請參閱快速入門導覽課程範例應用程式中的 CameraSourcePreviewGraphicOverlay 類別。
  • 如果您使用 Camera2 API,請以 ImageFormat.YUV_420_888 格式擷取圖片。如果您使用的是舊版 Camera API,請以 ImageFormat.NV21 格式擷取圖片。