您可以透過兩種方式,將圖片標籤與自訂模型整合:將管道與應用程式一併封裝,或是使用依附於 Google Play 服務的未封裝管道。如果選取未綁定的管道,應用程式就會較小。如需詳細資訊,請參閱下表。
| 組合 | 未綁定 | |
|---|---|---|
| 程式庫名稱 | com.google.mlkit:image-labeling-custom | com.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 測試 |
立即試用
- 如需瞭解如何使用隨附模型,請參閱 Vision 快速入門應用程式;如需瞭解如何使用代管模型,請參閱 AutoML 快速入門應用程式。
事前準備
在專案層級的
build.gradle.kts檔案中,請務必在buildscript和allprojects區段中加入 Google 的 Maven 存放區。將 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") }如果您選擇在 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,明確檢查管道可用性並要求下載。
如果您未啟用安裝時管道下載或要求明確下載,管道會在您首次執行標籤器時下載。如果是在下載完成前提出要求,系統不會產生任何結果。
如要使用 Cloud Storage for Firebase 下載模型,請務必將 Firebase 新增至 Android 專案 (如果尚未新增)。如果您是將模型與應用程式一併發布,則不需要執行這項操作。
1. 載入模型
您可以從本機組合來源或遠端代管來源載入模型。
設定本機模型來源
如要將模型與應用程式組合,請按照下列步驟操作:
將模型檔案 (通常以
.tflite或.lite結尾) 複製到應用程式的assets/資料夾。(您可能需要先建立資料夾,方法是按一下滑鼠右鍵點選app/資料夾,然後依序點選「New」>「Folder」>「Assets Folder」)。建立
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 程式庫,OnImageCapturedListener 和 ImageAnalysis.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(); }
使用 ByteBuffer 或 ByteArray
如要從 ByteBuffer 或 ByteArray 建立 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 物件傳遞至 ImageLabeler 的 process() 方法。
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(); }
提升即時效能的訣竅
如要在即時應用程式中標記圖片,請遵循下列指南,盡可能提高影格速率:
- 如果您使用
Camera或camera2API,請節流對圖片標籤器的呼叫。如果圖片標籤器正在執行時有新的影片影格可用,請捨棄該影格。如需範例,請參閱快速入門範例應用程式中的VisionProcessorBase類別。 - 如果您使用
CameraXAPI,請務必將回壓策略設為預設值ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST。這可確保系統一次只會傳送一張圖片進行分析。如果分析器忙碌時產生更多圖片,系統會自動捨棄這些圖片,不會排隊等待傳送。呼叫 ImageProxy.close() 關閉正在分析的圖片後,系統會傳送下一個最新圖片。 - 如果您使用圖片標籤工具的輸出內容,在輸入圖片上疊加圖像,請先從 ML Kit 取得結果,然後在單一步驟中算繪圖片並疊加圖像。這樣一來,每個輸入影格只會算繪到顯示介面一次。如需範例,請參閱快速入門範例應用程式中的
CameraSourcePreview和GraphicOverlay類別。 - 如果您使用 Camera2 API,請以
ImageFormat.YUV_420_888格式擷取圖片。如果您使用舊版 Camera API,請以ImageFormat.NV21格式擷取圖片。