Istnieją 2 sposoby integracji etykietowania obrazów z modelami niestandardowymi: przez dołączenie potoku jako części aplikacji lub przez użycie potoku niepowiązanego, który zależy od Usług Google Play. Jeśli wybierzesz potok niepowiązany, aplikacja będzie mniejsza. Szczegółowe informacje znajdziesz w poniższej tabeli.
| Łączenie w pakiety | Niepowiązane | |
|---|---|---|
| Nazwa biblioteki | com.google.mlkit:image-labeling-custom | com.google.android.gms:play-services-mlkit-image-labeling-custom |
Implementacja | Potok jest statycznie połączony z aplikacją w czasie kompilacji. | Potok jest pobierany dynamicznie za pomocą Usług Google Play. |
| Rozmiar aplikacji | Wzrost rozmiaru o około 3,8 MB. | Wzrost rozmiaru o około 200 KB. |
| Czas inicjowania | Potok jest dostępny od razu. | Przed pierwszym użyciem może być konieczne poczekanie na pobranie potoku. |
| Etap cyklu życia interfejsu API | Ogólna dostępność (GA) | Beta |
Model niestandardowy można zintegrować na 2 sposoby: dołączyć go, umieszczając w folderze zasobów aplikacji, lub pobrać dynamicznie z Firebase. W poniższej tabeli porównano te 2 opcje.
| Model dołączony | Model hostowany |
|---|---|
| Model jest częścią pliku APK aplikacji, co zwiększa jego rozmiar. | Model nie jest częścią pliku APK. Jest hostowany przez przesłanie do Cloud Storage. Zalecamy korzystanie z Cloud Storage dla Firebase. |
| Model jest dostępny od razu, nawet gdy urządzenie z Androidem jest offline. | Aplikacja musi zawierać kod, który pobiera model na żądanie. |
| Nie jest wymagany projekt w Firebase. | Wymaga projektu w Firebase (jeśli używasz Cloud Storage dla Firebase). |
| Aby zaktualizować model, musisz ponownie opublikować aplikację. | Wprowadzaj aktualizacje modelu bez ponownego publikowania aplikacji. |
| Brak wbudowanych testów A/B. | Testy A/B z użyciem Zdalnej konfiguracji Firebase. |
Wypróbuj
- Przykład użycia modelu dołączonego znajdziesz w aplikacji z krótkiego wprowadzenia do interfejsu Vision API , a przykład użycia modelu hostowanego – w aplikacji z krótkiego wprowadzenia do AutoML.
Zanim zaczniesz
W pliku
build.gradle.ktsna poziomie projektu dodaj repozytorium Google Maven do sekcjibuildscriptiallprojects.Dodaj zależności bibliotek ML Kit na Androida do pliku Gradle na poziomie modułu (aplikacji), który zwykle ma nazwę
app/build.gradle.kts. W zależności od potrzeb wybierz jedną z tych zależności:Aby dołączyć potok do aplikacji:
dependencies { // ... // Use this dependency to bundle the pipeline with your app implementation("com.google.mlkit:image-labeling-custom:17.0.3") }Aby używać potoku w Usługach 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") }Jeśli zdecydujesz się używać potoku w Usługach Google Play, możesz skonfigurować aplikację tak, aby automatycznie pobierała potok na urządzenie po zainstalowaniu aplikacji ze Sklepu Play. Aby to zrobić, dodaj tę deklarację do pliku
AndroidManifest.xmlaplikacji:<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>Możesz też wyraźnie sprawdzić dostępność potoku i poprosić o pobranie za pomocą interfejsu ModuleInstallClient API w Usługach Google Play.
Jeśli nie włączysz pobierania potoku w czasie instalacji ani nie poprosisz o wyraźne pobranie, potok zostanie pobrany przy pierwszym uruchomieniu narzędzia do etykietowania. Żądania wysyłane przed zakończeniem pobierania nie przyniosą żadnych rezultatów.
Jeśli chcesz pobrać model za pomocą Cloud Storage dla Firebase, upewnij się , że dodasz Firebase do projektu aplikacji na Androida, jeśli jeszcze tego nie zrobisz. Nie jest to wymagane, gdy dołączasz model.
1. Wczytaj model
Model możesz wczytać ze źródła dołączonego lokalnie lub ze źródła hostowanego zdalnie.
Konfigurowanie lokalnego źródła modelu
Aby dołączyć model do aplikacji:
Skopiuj plik modelu (zwykle z rozszerzeniem
.tflitelub.lite) do folderuassets/aplikacji. (Może być konieczne utworzenie tego folderu. Aby to zrobić, kliknij prawym przyciskiem myszy folderapp/, a potem kliknij Nowy > Folder > Folder zasobów).Utwórz obiekt
LocalModel, podając ścieżkę do pliku modelu: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();
Konfigurowanie zdalnie hostowanego źródła modelu
Aby używać modelu hostowanego zdalnie, musisz pobrać plik modelu do pamięci lokalnej urządzenia za pomocą własnej logiki aplikacji, a następnie wczytać go jako model lokalny. Do hostowania modelu zalecamy używanie Cloud Storage dla Firebase. Szczegóły implementacji znajdziesz w przewodniku po migracji z Firebase ML do Cloud Storage.
Konfigurowanie narzędzia do etykietowania obrazów
Po skonfigurowaniu źródeł modelu utwórz na ich podstawie obiekt ImageLabeler.
Dostępne są te ustawienia:
| Opcje | |
|---|---|
confidenceThreshold
|
Minimalny wynik wiarygodności wykrytych etykiet. Jeśli nie zostanie ustawiony, będzie używany próg klasyfikatora określony w metadanych modelu. Jeśli model nie zawiera metadanych lub metadane nie określają progu klasyfikatora, będzie używany domyślny próg 0,0. |
maxResultCount
|
Maksymalna liczba etykiet do zwrócenia. Jeśli nie zostanie ustawiony, będzie używana wartość domyślna 10. |
Jeśli masz tylko model dołączony lokalnie, po prostu utwórz narzędzie do etykietowania na podstawie obiektu 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);
Jeśli masz model hostowany zdalnie, przed jego uruchomieniem musisz sprawdzić, czy został pobrany.
Chociaż musisz to potwierdzić tylko przed uruchomieniem narzędzia do etykietowania, jeśli masz zarówno model hostowany zdalnie, jak i model dołączony lokalnie, warto przeprowadzić to sprawdzenie podczas tworzenia instancji narzędzia do etykietowania obrazów: utwórz narzędzie do etykietowania na podstawie modelu zdalnego, jeśli został pobrany, a w przeciwnym razie na podstawie modelu lokalnego.
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);
Jeśli masz tylko model hostowany zdalnie, wyłącz funkcje związane z modelem – na przykład wyszarz lub ukryj część interfejsu – dopóki nie potwierdzisz, że model został pobrany.
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. Przygotuj obraz wejściowy
Następnie dla każdego obrazu, który chcesz oznaczyć etykietą, utwórzInputImage
obiekt na podstawie obrazu. Narzędzie do etykietowania obrazów działa najszybciej, gdy używasz obiektu Bitmap lub, jeśli używasz interfejsu Camera2 API, obiektu media.Image w formacie YUV_420_888. Zalecamy używanie tych formatów, gdy jest to możliwe.
Obiekt InputImage
możesz utworzyć z różnych źródeł. Każde z nich opisujemy poniżej.
Używanie obiektu media.Image
Aby utworzyć obiekt InputImage
na podstawie obiektu media.Image, np. gdy robisz zdjęcie aparatem urządzenia, przekaż obiekt media.Image i obrót obrazu do InputImage.fromMediaImage().
Jeśli używasz biblioteki
CameraX, klasy OnImageCapturedListener i ImageAnalysis.Analyzer obliczają wartość obrotu.
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 // ... } } }
Jeśli nie używasz biblioteki aparatu, która podaje stopień obrotu obrazu, możesz go obliczyć na podstawie stopnia obrotu urządzenia i orientacji czujnika aparatu w urządzeniu:
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; }
Następnie przekaż obiekt media.Image i wartość stopnia obrotu do InputImage.fromMediaImage():
Kotlin
val image = InputImage.fromMediaImage(mediaImage, rotation)
Java
InputImage image = InputImage.fromMediaImage(mediaImage, rotation);
Używanie identyfikatora URI pliku
Aby utworzyć obiekt na podstawie identyfikatora URI pliku, przekaż kontekst aplikacji i identyfikator URI pliku do InputImage.fromFilePath().InputImage Jest to przydatne, gdy używasz intencji ACTION_GET_CONTENT, aby poprosić użytkownika o wybranie obrazu z aplikacji galerii.
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(); }
Używanie obiektu ByteBuffer lub ByteArray
Aby utworzyć obiekt InputImage
na podstawie obiektu ByteBuffer lub ByteArray, najpierw oblicz stopień obrotu obrazu zgodnie z opisem w przypadku danych wejściowych media.Image.
Następnie utwórz obiekt InputImage z buforem lub tablicą oraz wysokością, szerokością, formatem kodowania kolorów i stopniem obrotu obrazu:
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 );
Używanie obiektu Bitmap
Aby utworzyć obiekt InputImage
na podstawie obiektu Bitmap, użyj tej deklaracji:
Kotlin
val image = InputImage.fromBitmap(bitmap, 0)
Java
InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);
Obraz jest reprezentowany przez obiekt Bitmap wraz ze stopniami obrotu.
3. Uruchom narzędzie do etykietowania obrazów
Aby oznaczyć obiekty na obrazie, przekaż obiekt image do metody process() narzędzia ImageLabeler.
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. Uzyskaj informacje o oznaczonych encjach
Jeśli operacja etykietowania obrazu się powiedzie, do odbiornika sukcesu zostanie przekazana listaImageLabel
obiektów. Każdy obiekt ImageLabel reprezentuje coś, co zostało oznaczone etykietą na obrazie. Możesz uzyskać tekstowy opis każdej etykiety (jeśli jest dostępny w metadanych pliku modelu LiteRT), wynik wiarygodności i indeks. Na przykład:
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(); }
Wskazówki dotyczące zwiększania skuteczności w czasie rzeczywistym
Jeśli chcesz etykietować obrazy w aplikacji działającej w czasie rzeczywistym, postępuj zgodnie z tymi wskazówkami, aby uzyskać najlepszą liczbę klatek na sekundę:
- Jeśli używasz interfejsu
Cameralubcamera2API, ograniczaj liczbę wywołań narzędzia do etykietowania obrazów. Jeśli podczas działania narzędzia do etykietowania obrazów pojawi się nowa klatka wideo, pomiń ją. Przykład znajdziesz w klasieVisionProcessorBasew przykładowej aplikacji z krótkiego wprowadzenia. - Jeśli używasz interfejsu
CameraXAPI, upewnij się, że strategia przeciwdziałania nadmiarowi danych jest ustawiona na wartość domyślnąImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Gwarantuje to, że do analizy będzie dostarczany tylko 1 obraz naraz. Jeśli podczas pracy analizatora zostanie wygenerowanych więcej obrazów, zostaną one automatycznie pominięte i nie zostaną dodane do kolejki do dostarczenia. Gdy analizowany obraz zostanie zamknięty przez wywołanie ImageProxy.close(), zostanie dostarczony następny najnowszy obraz. - Jeśli używasz danych wyjściowych narzędzia do etykietowania obrazów do nakładania grafiki na
obraz wejściowy, najpierw uzyskaj wynik z ML Kit, a potem w jednym kroku wyrenderuj obraz
i nałóż na niego grafikę. Dzięki temu renderowanie na powierzchni wyświetlacza
odbywa się tylko raz na każdą klatkę wejściową. Przykład znajdziesz w klasach
CameraSourcePreviewiGraphicOverlayw przykładowej aplikacji z krótkiego wprowadzenia. - Jeśli używasz interfejsu Camera2 API, rób zdjęcia w
ImageFormat.YUV_420_888formacie. Jeśli używasz starszego interfejsu Camera API, rób zdjęcia wImageFormat.NV21formacie.