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

您可以使用 ML Kit 辨識圖片中的實體並加上標籤。 這個 API 支援多種自訂圖片分類模型。 如需模型相容性規定、預先訓練模型取得方式,以及如何訓練自有模型的相關指引,請參閱「使用 ML Kit 的自訂模型」。

您可以透過兩種方式,將圖片標籤與自訂模型整合:將管道與應用程式一併封裝,或是使用依附於 Google Play 服務的未封裝管道。如果選取未綁定的管道,應用程式就會較小。如需詳細資訊,請參閱下表。

組合未綁定
程式庫名稱com.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom

導入
Pipeline 會在建構時靜態連結至應用程式。系統會使用 Google Play 服務動態下載管道。
應用程式大小大小增加約 3.8 MB。大小增加約 200 KB。
初始化時間Pipeline 會立即啟用。首次使用前可能需要等待管線下載。
API 生命週期階段正式發布 (GA)Beta 版

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

組合模式 代管模型
模型是應用程式 APK 的一部分,因此會增加 APK 大小。 模型不會納入 APK,而是上傳至 Cloud Storage 進行代管。建議使用 Firebase 適用的 Cloud Storage
即使 Android 裝置未連上網路,也能立即使用模型 應用程式必須包含程式碼,才能視需要下載模型
不需要 Firebase 專案 需要 Firebase 專案 (如果使用 Cloud Storage for Firebase)。
您必須重新發布應用程式,才能更新模型 無須重新發布應用程式,即可推送模型更新
沒有內建的 A/B 測試 使用 Firebase 遠端設定進行 A/B 測試

立即試用

事前準備

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

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

    將管道與應用程式套件組合:

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

    在 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. 如要使用 Cloud Storage for Firebase 下載模型,請務必將 Firebase 新增至 Android 專案 (如果尚未新增)。如果您是將模型與應用程式一併發布,則不需要執行這項操作。

1. 載入模型

您可以從本機組合來源或遠端代管來源載入模型。

設定本機模型來源

如要將模型與應用程式組合,請按照下列步驟操作:

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

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

    Kotlin

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

    Java

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

設定遠端代管模型來源

如要使用遠端託管模型,您必須使用自己的應用程式邏輯,將模型檔案下載至裝置的本機儲存空間,然後載入為本機模型。建議使用 Cloud Storage for Firebase 託管模型。如需實作詳細資料,請參閱 Firebase ML 遷移至 Cloud Storage 指南

設定圖片標籤器

設定模型來源後,請從其中一個來源建立 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);

如果您有遠端代管模型,必須先檢查模型是否已下載,才能運作執行。

雖然您只需要在執行標籤器前確認這一點,但如果您同時有遠端代管模型和本機套裝模型,在例項化圖片標籤器時執行這項檢查可能很有意義:如果已下載遠端模型,請從遠端模型建立標籤器,否則請從本機模型建立標籤器。

Kotlin

val modelFile = File(context.cacheDir, "my_downloaded_model.tflite")
val model = if (modelFile.exists()) {
    // Use the downloaded model if available
    LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
} else {
    // Fall back to the bundled model
    LocalModel.Builder().setAssetFilePath("model.tflite").build()
}
val options = CustomImageLabelerOptions.Builder(model)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(options)

Java

File modelFile = new File(context.getCacheDir(), "my_downloaded_model.tflite");
LocalModel model;
if (modelFile.exists()) {
    // Use the downloaded model if available
    model = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
} else {
    // Fall back to the bundled model
    model = new LocalModel.Builder().setAssetFilePath("model.tflite").build();
}
CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(model)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build();
ImageLabeler labeler = ImageLabeling.getClient(options);

如果只有遠端代管模型,您應停用模型相關功能 (例如將部分 UI 設為灰色或隱藏),直到確認模型已下載為止。

Kotlin

val localFile = File(context.cacheDir, "my_remote_model.tflite")
if (localFile.exists()) {
    initializeLabeler(localFile)
} else {
    showLoadingUI()
    val storage = Firebase.storage
    val modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite")
    modelRef.getFile(localFile)
        .addOnSuccessListener {
            hideLoadingUI()
            initializeLabeler(localFile)
        }
        .addOnFailureListener {
            showErrorUI()
        }
}

private fun initializeLabeler(modelFile: File) {
    val localModel = LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
    val options = CustomImageLabelerOptions.Builder(localModel).build()
    val labeler = ImageLabeling.getClient(options)
    enableMLFeatures(labeler)
}

Java

File localFile = new File(context.getCacheDir(), "my_remote_model.tflite");
if (localFile.exists()) {
    initializeLabeler(localFile);
} else {
    showLoadingUI();
    FirebaseStorage storage = FirebaseStorage.getInstance();
    StorageReference modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite");
    modelRef.getFile(localFile)
        .addOnSuccessListener(new OnSuccessListener<FileDownloadTask.TaskSnapshot>() {
            @Override
            public void onSuccess(FileDownloadTask.TaskSnapshot taskSnapshot) {
                hideLoadingUI();
                initializeLabeler(localFile);
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception exception) {
                showErrorUI();
            }
        });
}

private void initializeLabeler(File modelFile) {
    LocalModel localModel = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
    CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(localModel).build();
    ImageLabeler labeler = ImageLabeling.getClient(options);
    enableMLFeatures(labeler);
}

2. 準備輸入圖片

接著,針對要標記的每張圖片,從圖片建立 InputImage 物件。使用 Bitmap 或 YUV_420_888 media.Image (如果使用 camera2 API) 時,圖片標籤器執行速度最快,建議盡可能使用這些格式。

您可以從不同來源建立 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. 執行圖片標籤器

如要為圖片中的物件加上標籤,請將 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 物件代表圖片中標示的項目。您可以取得每個標籤的文字說明 (如果 LiteRT 模型檔案的中繼資料提供這項資訊)、信賴分數和索引。例如:

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 格式擷取圖片。