在 Android 上使用以 AutoML 訓練的模型為圖片加上標籤

使用 AutoML Vision Edge 訓練專屬模型後,即可在應用程式中使用該模型標記圖片。

事前準備

  1. 如果您尚未將 Firebase 新增至 Android 專案,請先新增。
  2. 將 ML Kit Android 程式庫的依附元件新增至模組 (應用程式層級) Gradle 檔案 (通常為 app/build.gradle):
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      implementation 'com.google.firebase:firebase-ml-vision-automl:18.0.5'
    }

1. 載入模型

機器學習套件會在裝置上執行 AutoML 生成的模型。不過,您可以設定 ML Kit,從 Firebase 遠端載入模型、從本機儲存空間載入模型,或同時從這兩處載入模型。

將模型代管在 Firebase 上,即可更新模型,不必發布新版應用程式,還能使用 Remote ConfigA/B Testing,動態為不同使用者群組提供不同模型。

如果您選擇只透過 Firebase 代管模型,而不將模型與應用程式一併封裝,即可縮減應用程式的初始下載大小。但請注意,如果模型未與應用程式一併封裝,應用程式必須先下載模型,才能使用任何與模型相關的功能。

將模型與應用程式一併封裝,可確保應用程式的 ML 功能在 Firebase 代管模型無法使用時,仍能正常運作。

設定 Firebase 託管的模型來源

如要使用遠端代管模型,請建立 FirebaseAutoMLRemoteModel 物件,並指定您發布模型時指派的名稱:

Java

// Specify the name you assigned in the Firebase console.
FirebaseAutoMLRemoteModel remoteModel =
    new FirebaseAutoMLRemoteModel.Builder("your_remote_model").build();

Kotlin

// Specify the name you assigned in the Firebase console.
val remoteModel = FirebaseAutoMLRemoteModel.Builder("your_remote_model").build()

然後啟動模型下載工作,並指定允許下載的條件。如果裝置上沒有模型,或是模型有新版本,工作會從 Firebase 異步下載模型:

Java

FirebaseModelDownloadConditions conditions = new FirebaseModelDownloadConditions.Builder()
        .requireWifi()
        .build();
FirebaseModelManager.getInstance().download(remoteModel, conditions)
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                // Success.
            }
        });

Kotlin

val conditions = FirebaseModelDownloadConditions.Builder()
    .requireWifi()
    .build()
FirebaseModelManager.getInstance().download(remoteModel, conditions)
    .addOnCompleteListener {
        // Success.
    }

許多應用程式會在初始化程式碼中啟動下載工作,但您可以在需要使用模型之前的任何時間點執行這項操作。

設定本機模型來源

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

  1. 從您從 Firebase 控制台下載的 ZIP 封存檔中,解壓縮模型及其相關中繼資料。建議您使用下載的檔案,不要修改檔案 (包括檔案名稱)。
  2. 在應用程式套件中加入模型及其相關中繼資料檔案:

    1. 如果專案中沒有資產資料夾,請在 app/ 資料夾上按一下滑鼠右鍵,然後依序點選「New」>「Folder」>「Assets Folder」,建立資產資料夾。
    2. 在資產資料夾下建立子資料夾,用來存放模型檔案。
    3. model.tflitedict.txtmanifest.json 檔案複製到子資料夾 (這三個檔案必須位於同一個資料夾中)。
  3. 在應用程式的 build.gradle 檔案中新增以下內容,確保 Gradle 在建構應用程式時不會壓縮模型檔案:
    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    
    模型檔案會納入應用程式套件,並以原始資產的形式提供給 ML Kit。
  4. 建立 FirebaseAutoMLLocalModel 物件,指定模型資訊清單檔案的路徑:

    Java

    FirebaseAutoMLLocalModel localModel = new FirebaseAutoMLLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            .build();
    

    Kotlin

    val localModel = FirebaseAutoMLLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            .build()
    

從模型建立圖片標籤器

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

如果只有在本機封裝的模型,只要從 FirebaseAutoMLLocalModel 物件建立標籤器,並設定所需的可信度分數門檻即可 (請參閱「評估模型」):

Java

FirebaseVisionImageLabeler labeler;
try {
    FirebaseVisionOnDeviceAutoMLImageLabelerOptions options =
            new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
                    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Firebase console
                                                   // to determine an appropriate value.
                    .build();
    labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options);
} catch (FirebaseMLException e) {
    // ...
}

Kotlin

val options = FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0)  // Evaluate your model in the Firebase console
                                // to determine an appropriate value.
    .build()
val labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options)

如果您有遠端代管模型,必須先確認模型已下載,才能執行。您可以使用模型管理工具的 isModelDownloaded() 方法,檢查模型下載工作的狀態。

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

Java

FirebaseModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener<Boolean>() {
            @Override
            public void onSuccess(Boolean isDownloaded) {
                FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder optionsBuilder;
                if (isDownloaded) {
                    optionsBuilder = new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(remoteModel);
                } else {
                    optionsBuilder = new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel);
                }
                FirebaseVisionOnDeviceAutoMLImageLabelerOptions options = optionsBuilder
                        .setConfidenceThreshold(0.0f)  // Evaluate your model in the Firebase console
                                                       // to determine an appropriate threshold.
                        .build();

                FirebaseVisionImageLabeler labeler;
                try {
                    labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options);
                } catch (FirebaseMLException e) {
                    // Error.
                }
            }
        });

Kotlin

FirebaseModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded -> 
    val optionsBuilder =
        if (isDownloaded) {
            FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(remoteModel)
        } else {
            FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
        }
    // Evaluate your model in the Firebase console to determine an appropriate threshold.
    val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
    val labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options)
}

如果只有遠端代管模型,您應停用模型相關功能 (例如將部分 UI 設為灰色或隱藏),直到確認模型已下載為止。如要這麼做,請將監聽器附加至模型管理員的 download() 方法:

Java

FirebaseModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @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.
            }
        });

Kotlin

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

2. 準備輸入圖片

接著,針對要標記的每張圖片,使用本節所述的其中一個選項建立 FirebaseVisionImage 物件,並將其傳遞至 FirebaseVisionImageLabeler 的執行個體 (下一節會說明)。

您可以從 media.Image 物件、裝置上的檔案、位元組陣列或 Bitmap 物件建立 FirebaseVisionImage 物件:

  • 如要從 media.Image 物件建立 FirebaseVisionImage 物件 (例如從裝置的相機擷取圖片時),請將 media.Image 物件和圖片的旋轉角度傳遞至 FirebaseVisionImage.fromMediaImage()

    如果您使用 CameraX 程式庫,OnImageCapturedListenerImageAnalysis.Analyzer 類別會為您計算旋轉值,因此您只需在呼叫 FirebaseVisionImage.fromMediaImage() 前,將旋轉值轉換為 ML Kit 的 ROTATION_ 常數之一:

    Java

    private class YourAnalyzer implements ImageAnalysis.Analyzer {
    
        private int degreesToFirebaseRotation(int degrees) {
            switch (degrees) {
                case 0:
                    return FirebaseVisionImageMetadata.ROTATION_0;
                case 90:
                    return FirebaseVisionImageMetadata.ROTATION_90;
                case 180:
                    return FirebaseVisionImageMetadata.ROTATION_180;
                case 270:
                    return FirebaseVisionImageMetadata.ROTATION_270;
                default:
                    throw new IllegalArgumentException(
                            "Rotation must be 0, 90, 180, or 270.");
            }
        }
    
        @Override
        public void analyze(ImageProxy imageProxy, int degrees) {
            if (imageProxy == null || imageProxy.getImage() == null) {
                return;
            }
            Image mediaImage = imageProxy.getImage();
            int rotation = degreesToFirebaseRotation(degrees);
            FirebaseVisionImage image =
                    FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
            // Pass image to an ML Kit Vision API
            // ...
        }
    }

    Kotlin

    private class YourImageAnalyzer : ImageAnalysis.Analyzer {
        private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
            0 -> FirebaseVisionImageMetadata.ROTATION_0
            90 -> FirebaseVisionImageMetadata.ROTATION_90
            180 -> FirebaseVisionImageMetadata.ROTATION_180
            270 -> FirebaseVisionImageMetadata.ROTATION_270
            else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
        }
    
        override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
            val mediaImage = imageProxy?.image
            val imageRotation = degreesToFirebaseRotation(degrees)
            if (mediaImage != null) {
                val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                // Pass image to an ML Kit Vision API
                // ...
            }
        }
    }

    如果您使用的相機程式庫未提供圖片的旋轉角度,可以根據裝置的旋轉角度和裝置中相機感應器的方向計算:

    Java

    private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
    static {
        ORIENTATIONS.append(Surface.ROTATION_0, 90);
        ORIENTATIONS.append(Surface.ROTATION_90, 0);
        ORIENTATIONS.append(Surface.ROTATION_180, 270);
        ORIENTATIONS.append(Surface.ROTATION_270, 180);
    }
    
    /**
     * 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, Context context)
            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);
    
        // On most devices, the sensor orientation is 90 degrees, but for some
        // devices it is 270 degrees. For devices with a sensor orientation of
        // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
        CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
        int sensorOrientation = cameraManager
                .getCameraCharacteristics(cameraId)
                .get(CameraCharacteristics.SENSOR_ORIENTATION);
        rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;
    
        // Return the corresponding FirebaseVisionImageMetadata rotation value.
        int result;
        switch (rotationCompensation) {
            case 0:
                result = FirebaseVisionImageMetadata.ROTATION_0;
                break;
            case 90:
                result = FirebaseVisionImageMetadata.ROTATION_90;
                break;
            case 180:
                result = FirebaseVisionImageMetadata.ROTATION_180;
                break;
            case 270:
                result = FirebaseVisionImageMetadata.ROTATION_270;
                break;
            default:
                result = FirebaseVisionImageMetadata.ROTATION_0;
                Log.e(TAG, "Bad rotation value: " + rotationCompensation);
        }
        return result;
    }

    Kotlin

    private val ORIENTATIONS = SparseIntArray()
    
    init {
        ORIENTATIONS.append(Surface.ROTATION_0, 90)
        ORIENTATIONS.append(Surface.ROTATION_90, 0)
        ORIENTATIONS.append(Surface.ROTATION_180, 270)
        ORIENTATIONS.append(Surface.ROTATION_270, 180)
    }
    /**
     * 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, context: Context): 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)
    
        // On most devices, the sensor orientation is 90 degrees, but for some
        // devices it is 270 degrees. For devices with a sensor orientation of
        // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
        val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
        val sensorOrientation = cameraManager
                .getCameraCharacteristics(cameraId)
                .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
        rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
    
        // Return the corresponding FirebaseVisionImageMetadata rotation value.
        val result: Int
        when (rotationCompensation) {
            0 -> result = FirebaseVisionImageMetadata.ROTATION_0
            90 -> result = FirebaseVisionImageMetadata.ROTATION_90
            180 -> result = FirebaseVisionImageMetadata.ROTATION_180
            270 -> result = FirebaseVisionImageMetadata.ROTATION_270
            else -> {
                result = FirebaseVisionImageMetadata.ROTATION_0
                Log.e(TAG, "Bad rotation value: $rotationCompensation")
            }
        }
        return result
    }

    接著,將 media.Image 物件和旋轉值傳遞至 FirebaseVisionImage.fromMediaImage()

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation);

    Kotlin

    val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
  • 如要從檔案 URI 建立 FirebaseVisionImage 物件,請將應用程式內容和檔案 URI 傳遞至 FirebaseVisionImage.fromFilePath()。當您使用 ACTION_GET_CONTENT 意圖提示使用者從相簿應用程式選取圖片時,這項功能就非常實用。

    Java

    FirebaseVisionImage image;
    try {
        image = FirebaseVisionImage.fromFilePath(context, uri);
    } catch (IOException e) {
        e.printStackTrace();
    }

    Kotlin

    val image: FirebaseVisionImage
    try {
        image = FirebaseVisionImage.fromFilePath(context, uri)
    } catch (e: IOException) {
        e.printStackTrace()
    }
  • 如要從 ByteBuffer 或位元組陣列建立 FirebaseVisionImage 物件,請先計算圖片旋轉角度,如上文所述,以做為 media.Image 輸入內容。

    接著,建立 FirebaseVisionImageMetadata 物件,其中包含圖片的高度、寬度、色彩編碼格式和旋轉角度:

    Java

    FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
            .setWidth(480)   // 480x360 is typically sufficient for
            .setHeight(360)  // image recognition
            .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
            .setRotation(rotation)
            .build();

    Kotlin

    val metadata = FirebaseVisionImageMetadata.Builder()
            .setWidth(480) // 480x360 is typically sufficient for
            .setHeight(360) // image recognition
            .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
            .setRotation(rotation)
            .build()

    使用緩衝區或陣列和中繼資料物件,建立 FirebaseVisionImage 物件:

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
    // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

    Kotlin

    val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
    // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
  • 如要從 Bitmap 物件建立 FirebaseVisionImage 物件,請執行下列操作:

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

    Kotlin

    val image = FirebaseVisionImage.fromBitmap(bitmap)
    Bitmap 物件代表的圖片必須直立,不需額外旋轉。

3. 執行圖片標籤器

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

Java

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

Kotlin

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

如果圖片標籤作業成功,系統會將 FirebaseVisionImageLabel 物件陣列傳遞至成功事件監聽器。您可以從每個物件取得圖片中辨識到的特徵相關資訊。

例如:

Java

for (FirebaseVisionImageLabel label: labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
}

Kotlin

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

提升即時成效的訣竅

  • 節流對偵測器的呼叫。如果偵測器執行期間有新的影片影格可用,請捨棄該影格。
  • 如果使用偵測器的輸出內容,在輸入圖片上疊加圖像,請先從 ML Kit 取得結果,然後在單一步驟中算繪圖片並疊加圖像。這樣做的話,每個輸入影格只會轉譯到顯示表面一次。
  • 如果您使用 Camera2 API,請以 ImageFormat.YUV_420_888 格式擷取圖片。

    如果使用舊版 Camera API,請以 ImageFormat.NV21 格式擷取圖片。