Oznaczanie obrazów etykietami za pomocą modelu niestandardowego na Androidzie

Za pomocą ML Kit możesz rozpoznawać encje na obrazie i dodawać do nich etykiety. Ten interfejs API obsługuje szeroką gamę niestandardowych modeli klasyfikacji obrazów. Więcej informacji o wymaganiach dotyczących zgodności modeli, o tym, gdzie znaleźć wstępnie wytrenowane modele, i o tym, jak wytrenować własne modele, znajdziesz w artykule Modele niestandardowe w ML Kit.

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 pakietyNiepowiązane
Nazwa bibliotekicom.google.mlkit:image-labeling-customcom.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 aplikacjiWzrost rozmiaru o około 3,8 MB.Wzrost rozmiaru o około 200 KB.
Czas inicjowaniaPotok jest dostępny od razu.Przed pierwszym użyciem może być konieczne poczekanie na pobranie potoku.
Etap cyklu życia interfejsu APIOgó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

  1. W pliku build.gradle.kts na poziomie projektu dodaj repozytorium Google Maven do sekcji buildscript i allprojects.

  2. 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")
    }
    
  3. 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.xml aplikacji:

    <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.

  4. 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:

  1. Skopiuj plik modelu (zwykle z rozszerzeniem .tflite lub .lite) do folderu assets/ aplikacji. (Może być konieczne utworzenie tego folderu. Aby to zrobić, kliknij prawym przyciskiem myszy folder app/, a potem kliknij Nowy > Folder > Folder zasobów).

  2. 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órz InputImage 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 lista ImageLabel 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 Camera lub camera2 API, 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 klasie VisionProcessorBase w przykładowej aplikacji z krótkiego wprowadzenia.
  • Jeśli używasz interfejsu CameraX API, 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 CameraSourcePreview i GraphicOverlay w przykładowej aplikacji z krótkiego wprowadzenia.
  • Jeśli używasz interfejsu Camera2 API, rób zdjęcia w ImageFormat.YUV_420_888 formacie. Jeśli używasz starszego interfejsu Camera API, rób zdjęcia w ImageFormat.NV21 formacie.