在 Android 上使用自訂模型為圖片加上標籤

您可以使用 ML Kit 辨識圖片中的實體,並加上標籤。這個 API 支援多種自訂圖片分類模型。如要瞭解模型相容性需求、如何尋找預先訓練模型,以及如何訓練自己的模型,請參閱「使用 ML Kit 自訂模型」一文。

整合圖片標籤與自訂模型的方法有兩種:一種是將管道與應用程式的一部分進行繫結,或使用依附於 Google Play 服務的無組合管道。如果您選取未組合的管道,應用程式會較小。詳情請參閱下表。

組合未分類
媒體庫名稱com.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom
導入作業管道會在建構期間以靜態方式連結至應用程式。管道是透過 Google Play 服務動態下載。
應用程式大小大小增加約 3.8 MB。大約增加 200 KB。
初始化時間管道可立即使用。可能必須等待管道下載完成後才能開始使用。
API 生命週期階段正式發布版Beta 版

整合自訂模型的方式有兩種:將模型放入應用程式的資產資料夾內,或從 Firebase 動態下載模型。下表比較這兩種選項。

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

立即體驗

事前準備

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

  2. 將 ML Kit Android 程式庫的依附元件新增至模組的應用程式層級 Gradle 檔案,通常為 app/build.gradle。請依據您的需求選擇下列其中一個依附元件:

    若為管道與應用程式組合:

    dependencies {
      // ...
      // Use this dependency to bundle the pipeline with your app
      implementation 'com.google.mlkit:image-labeling-custom:17.0.2'
    }
    

    在 Google Play 服務中使用管道:

    dependencies {
      // ...
      // Use this dependency to use the dynamically downloaded pipeline in Google Play Services
      implementation 'com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta5'
    }
    
  3. 如果您選擇在 Google Play 服務中使用管道,可將應用程式設為從 Play 商店安裝應用程式後,自動將管道下載到裝置。方法是在應用程式的 AndroidManifest.xml 檔案中加入以下宣告:

    <application ...>
        ...
        <meta-data
            android:name="com.google.mlkit.vision.DEPENDENCIES"
            android:value="custom_ica" />
        <!-- To use multiple downloads: android:value="custom_ica,download2,download3" -->
    </application>
    

    您也可以透過 Google Play 服務 ModuleInstallClient API 明確檢查管道可用性並要求下載。

    如果未啟用安裝期間管道下載或要求明確下載,系統會在您首次執行標籤工具時下載管道。您在下載完成前提出的要求不會產生任何結果。

  4. 如要透過 Firebase 動態下載模型,請新增 linkFirebase 依附元件:

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

    dependencies {
      // ...
      // Image labeling feature with model downloaded from Firebase
      implementation 'com.google.mlkit:image-labeling-custom:17.0.2'
      // Or use the dynamically downloaded pipeline in Google Play Services
      // implementation 'com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta5'
      implementation 'com.google.mlkit:linkfirebase:17.0.0'
    }
    
  5. 如要下載模型,請先將 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 建立 RemoteModel 物件,並指定您在發布模型時指派的名稱:

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

許多應用程式會在其初始化程式碼中啟動下載工作,但在需要使用模型之前,您隨時可以執行此操作。

設定映像檔標籤工具

設定模型來源後,請從其中一個來源建立 ImageLabeler 物件。

可用選項如下所示:

選項
confidenceThreshold

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

maxResultCount

要傳回的標籤數量上限。如未設定,系統會使用預設值 10。

如果您只有本機組合模型,請直接從 LocalModel 物件建立標籤人員:

Kotlin

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

Java

CustomImageLabelerOptions customImageLabelerOptions =
        new CustomImageLabelerOptions.Builder(localModel)
            .setConfidenceThreshold(0.5f)
            .setMaxResultCount(5)
            .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

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

雖然只有在執行標籤人員前才須確認,但如果您同時擁有遠端託管模型和本機組合模型,則在執行個體化圖片標籤工具時,可能還是執行這項檢查:如果已下載遠端模型,請透過遠端模型建立標籤工具,否則從本機模型建立。

Kotlin

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
    val optionsBuilder =
        if (isDownloaded) {
            CustomImageLabelerOptions.Builder(remoteModel)
        } else {
            CustomImageLabelerOptions.Builder(localModel)
        }
    val options = optionsBuilder
                  .setConfidenceThreshold(0.5f)
                  .setMaxResultCount(5)
                  .build()
    val labeler = ImageLabeling.getClient(options)
}

Java

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener() {
            @Override
            public void onSuccess(Boolean isDownloaded) {
                CustomImageLabelerOptions.Builder optionsBuilder;
                if (isDownloaded) {
                    optionsBuilder = new CustomImageLabelerOptions.Builder(remoteModel);
                } else {
                    optionsBuilder = new CustomImageLabelerOptions.Builder(localModel);
                }
                CustomImageLabelerOptions options = optionsBuilder
                    .setConfidenceThreshold(0.5f)
                    .setMaxResultCount(5)
                    .build();
                ImageLabeler labeler = ImageLabeling.getClient(options);
            }
        });

如果您只有遠端託管模型,建議停用模型相關功能 (例如顯示為灰色或隱藏部分 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.
            }
        });

2. 準備輸入圖片

然後,針對要加上標籤的每張圖片,從圖片建立 InputImage 物件。使用 Bitmap 時,圖片標籤工具的執行速度最快。如果您使用 camera2 API,建議盡可能使用 YUV_420_888 media.Image

您可以從不同來源建立 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 物件以旋轉度數表示。

3. 執行映像檔標籤工具

如要為圖片中的物件加上標籤,請將 image 物件傳遞至 ImageLabelerprocess() 方法。

Kotlin

labeler.process(image)
        .addOnSuccessListener { labels ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

Java

labeler.process(image)
        .addOnSuccessListener(new OnSuccessListener<List<ImageLabel>>() {
            @Override
            public void onSuccess(List<ImageLabel> labels) {
                // Task completed successfully
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Task failed with an exception
                // ...
            }
        });

4. 取得已加上標籤實體的相關資訊

如果圖片標籤作業成功,系統會將 ImageLabel 物件清單傳遞至成功事件監聽器。每個 ImageLabel 物件都代表圖片中已加上標籤的項目。您可以取得每個標籤的文字說明 (若 TensorFlow Lite 模型檔案的中繼資料有提供)、可信度分數和索引。例如:

Kotlin

for (label in labels) {
    val text = label.text
    val confidence = label.confidence
    val index = label.index
}

Java

for (ImageLabel label : labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
    int index = label.getIndex();
}

改善即時成效的訣竅

如要在即時應用程式中為圖片加上標籤,請遵循下列準則,以便達到最佳畫面更新率:

  • 如果您使用 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 格式擷取圖片。