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

您可以使用機器學習套件辨識圖片中的實體並加上標籤。這個 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 檔案中,請務必在您的 buildscriptallprojects 區段中加入 Google 的 Maven 存放區。

  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.1'
    }
    

    在 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-beta4'
    }
    
  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.1'
      // Or use the dynamically downloaded pipeline in Google Play Services
      // implementation 'com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta4'
      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 取得結果,然後透過單一步驟轉譯圖片和疊加層。每個輸入框只會向顯示途徑轉譯一次。如需範例,請參閱快速入門導覽課程範例應用程式中的 CameraSourcePreviewGraphicOverlay 類別。
  • 如果您使用 Camera2 API,請以 ImageFormat.YUV_420_888 格式擷取圖片。如果您使用的是舊版 Camera API,請以 ImageFormat.NV21 格式擷取圖片。