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

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Za pomocą ML Kit możesz rozpoznawać jednostki na obrazie i oznaczać je etykietami. Ten interfejs API obsługuje szeroki zakres niestandardowych modeli klasyfikacji obrazów. Wskazówki na temat wymagań dotyczących zgodności, miejsc wytrenowanych modeli oraz sposobu trenowania własnych znajdziesz w artykule Modele niestandardowe z pakietem ML Kit.

Istnieją 2 sposoby zintegrowania etykiet obrazów z modelami niestandardowymi: połączenie potoku w ramach aplikacji lub skorzystanie z nieobsługiwanego potoku zależnego od Usług Google Play. Jeśli wybierzesz niepowiązany potok, aplikacja będzie mniejsza. Szczegółowe informacje znajdziesz w poniższej tabeli.

PołączonaNiegrupowane
Nazwa bibliotekicom.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom
WdrażaniePotok jest statycznie połączony z aplikacją w czasie kompilacji.Potok jest pobierany dynamicznie przez Usługi Google Play.
Rozmiar aplikacjiZwiększenie rozmiaru o około 5,5 MB.Powiększenie o około 600 KB.
Czas inicjowaniaPotok jest dostępny od razu.Przed pierwszym użyciem może być konieczne zaczekać na pobranie potoku.
Etap cyklu życia interfejsu APIOgólna dostępnośćBeta

Są 2 sposoby zintegrowania modelu niestandardowego: możesz go spakować i umieścić w folderze z zasobami aplikacji lub pobrać go dynamicznie z Firebase. Poniższa tabela zawiera porównanie tych dwóch opcji.

Model w pakiecie Model hostowany
Model jest częścią pliku APK Twojej aplikacji, co zwiększa jego rozmiar. Model nie jest częścią Twojego pliku APK. Znajduje się w niej przesyłanie informacji do systemów uczących się Firebase.
Model jest dostępny od razu, nawet gdy urządzenie z Androidem jest offline Model jest pobierany na żądanie
Nie ma potrzeby tworzenia projektu Firebase Wymaga projektu Firebase
Aby zaktualizować model, musisz ponownie opublikować aplikację Przesyłanie aktualizacji modeli bez ponownego publikowania aplikacji
Brak wbudowanych testów A/B Łatwe testy A/B dzięki Zdalnej konfiguracji Firebase

Zanim zaczniesz

  1. W pliku build.gradle na poziomie projektu umieść repozytorium Maven firmy Google i w sekcjach buildscript i allprojects.

  2. Dodaj zależności do bibliotek ML Kit na Androida do pliku Gradle na poziomie aplikacji, który zwykle wynosi app/build.gradle. W zależności od potrzeb wybierz jedną z tych zależności:

    Aby połączyć potok z aplikacją:

    dependencies {
      // ...
      // Use this dependency to bundle the pipeline with your app
      implementation 'com.google.mlkit:image-labeling-custom:17.0.1'
    }
    

    Aby użyć 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-beta4'
    }
    
  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 jej zainstalowaniu ze Sklepu Play. Aby to zrobić, dodaj do tej aplikacji AndroidManifest.xml deklarację:

    <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ż bezpośrednio sprawdzić dostępność potoku i poprosić o pobranie danych za pomocą interfejsu ModuleInstallClient API w usługach Google Play.

    Jeśli nie włączysz pobierania potoku w czasie instalacji lub nie chcesz pobierać konkretnych treści, potok zostanie pobrany przy pierwszym uruchomieniu etykiety. Prośby przesłane przed zakończeniem pobierania nie zwracają żadnych wyników.

  4. Dodaj zależność linkFirebase, jeśli chcesz dynamicznie pobierać model z Firebase:

    Aby dynamicznie pobierać model z Firebase, dodaj linkFirebase zależności:

    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. Jeśli chcesz pobrać model, dodaj go do projektu na Androida, jeśli jeszcze go nie masz. Nie jest to wymagane w pakiecie.

1. Wczytywanie modelu

Skonfiguruj źródło modelu lokalnego

Aby połączyć model z aplikacją:

  1. Skopiuj plik modelu (zwykle zakończonego na .tflite lub .lite) do folderu assets/ aplikacji. Możliwe, że trzeba będzie najpierw utworzyć folder, klikając prawym przyciskiem myszy folder app/, a następnie wybrać Nowy > Folder > Folder zasobów.)

  2. Następnie dodaj do pliku build.gradle swoją aplikację w taki sposób, aby Gradle nie kompresował pliku modelu podczas tworzenia aplikacji:

    android {
        // ...
        aaptOptions {
            noCompress "tflite"
            // or noCompress "lite"
        }
    }
    

    Plik modelu zostanie dołączony do pakietu aplikacji i będzie dostępny dla ML Kit jako nieprzetworzony zasób.

  3. Utwórz obiekt LocalModel, podając ścieżkę do pliku modelu:

    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();

Skonfiguruj źródło modelu hostowanego w Firebase

Aby użyć modelu hostowanego w trybie zdalnym, utwórz obiekt RemoteModel przez określenie wartości FirebaseModelSource, określając nazwę przypisaną do modelu podczas jego publikowania:

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();

Następnie uruchom zadanie pobierania modelu, określając warunki, według których chcesz zezwolić na pobieranie. Jeśli model nie znajduje się na urządzeniu lub jest dostępna jego nowsza wersja, zadanie asynchronicznie pobierze model z 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.
            }
        });

Wiele aplikacji rozpoczyna zadanie pobierania w kodzie inicjowania, ale możesz to zrobić w dowolnym momencie przed użyciem modelu.

Konfigurowanie usługi dodawania etykiet do obrazów

Po skonfigurowaniu źródeł modeli utwórz obiekt ImageLabeler z jednego z nich.

Oto one:

Opcje
confidenceThreshold

Minimalny współczynnik ufności wykrytych etykiet. Jeśli jej nie skonfigurujesz, będzie używany dowolny próg klasyfikatora określony przez metadane modelu. Jeśli model nie zawiera żadnych metadanych lub metadane nie określają progu klasyfikatora, zostanie użyty domyślny próg 0,0.

maxResultCount

Maksymalna liczba etykiet do zwrócenia. Jeśli zasada nie jest skonfigurowana, używana jest wartość domyślna, czyli 10.

Jeśli masz tylko model w pakiecie lokalnym, utwórz etykietę etykiety 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 uruchomieniem musisz sprawdzić, czy został pobrany. Stan zadania pobierania modelu możesz sprawdzić za pomocą metody isModelDownloaded() w menedżerze modeli.

Chociaż musisz tylko potwierdzić tę czynność przed uruchomieniem narzędzia do oznaczania etykietami, jeśli masz model hostowany zdalnie i model w pakiecie lokalnym, możesz sprawdzić to w przypadku instancji tworzącej etykiety obrazu: utwórz etykietę na podstawie modelu zdalnego, jeśli został on pobrany, a w innym modelu lokalnym.

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);
            }
        });

Jeśli masz tylko model hostowany zdalnie, wyłącz funkcje powiązane z modelem (np. szary lub ukryj część interfejsu), dopóki nie potwierdzisz, że model został pobrany. Możesz to zrobić, dołączając odbiornik do menedżera modelu 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. Przygotowanie obrazu wejściowego

Następnie dla każdego obrazu, który chcesz oznaczyć, utwórz z niego obiekt InputImage. Oznaczenie obrazu działa szybciej, gdy używasz interfejsu Bitmap lub, jeśli używasz interfejsu API aparatu2, YUV_420_888 media.Image, które są zalecane, gdy to możliwe.

Obiekt InputImage możesz utworzyć z różnych źródeł, a każde z nich opisano poniżej.

Używanie modułu media.Image

Aby utworzyć obiekt InputImage z obiektu media.Image, na przykład podczas robienia obrazu z aparatu urządzenia, przekaż obiekt media.Image, a obraz zostanie obrócony do InputImage.fromMediaImage().

Jeśli używasz biblioteki KameraX, klasy OnImageCapturedListener i ImageAnalysis.Analyzer obliczają wartość rotacji.

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 zdjęć, która zapewnia kąt obrotu obrazu, możesz go obliczyć na podstawie stopni obrotu urządzenia i orientacji czujnika aparatu w tym 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 prześlij obiekt media.Image i wartość stopni 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 InputImage z identyfikatora URI pliku, przekaż kontekst aplikacji i identyfikator pliku do InputImage.fromFilePath(). Ta opcja jest przydatna, gdy używasz intencji ACTION_GET_CONTENT zachęcającej użytkownika do wybrania zdjęcia z 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();
}

Korzystanie z narzędzia ByteBuffer lub ByteArray

Aby utworzyć obiekt InputImage z ByteBuffer lub ByteArray, najpierw oblicz stopień obrotu obrazu, jak opisano wcześniej w przypadku danych wejściowych media.Image. Następnie utwórz obiekt InputImage z buforem lub tablicą wraz z wysokością, szerokością, formatem kodowania kolorów i stopniem obrotu:

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 modułu Bitmap

Aby utworzyć obiekt InputImage z obiektu Bitmap, złóż tę deklarację:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

Obraz jest reprezentowany przez obiekt Bitmap wraz z stopniami obrotu.

3. Uruchamianie dodawania etykiet do obrazów

Aby oznaczyć obiekty obrazem, przekaż obiekt image do metody ImageLabeler i 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. Uzyskaj informacje o elementach oznaczonych etykietą

Jeśli operacja dodawania etykiet do obrazu zakończy się pomyślnie, do detektora zostanie przekazana lista obiektów ImageLabel. Każdy obiekt ImageLabel reprezentuje coś, co zostało oznaczone na obrazie. Można uzyskać opis tekstu każdej etykiety (jeśli jest dostępny w metadanych pliku modelu TensorFlow Lite), wskaźnik ufności i indeks. 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, jak poprawić skuteczność w czasie rzeczywistym

Jeśli chcesz oznaczać etykietami obrazy w aplikacji w czasie rzeczywistym, postępuj zgodnie z tymi wskazówkami, aby uzyskać najlepszą liczbę klatek:

  • Jeśli używasz interfejsu API Camera lub camera2, ograniczaj wywołania do osoby oznaczającej obrazy. Jeśli nowa ramka wideo stanie się dostępna, gdy etykieta obrazu będzie uruchomiona, upuść tę klatkę. Przykład znajdziesz w klasie VisionProcessorBase w przykładowej aplikacji.
  • Jeśli korzystasz z interfejsu API CameraX, upewnij się, że strategia ciśnienia wstecznego jest ustawiona na wartość domyślną ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Zapewnia to tylko jeden obraz naraz. Jeśli w analizatorze będzie dużo więcej obrazów, to będą one automatycznie usuwane i nie będą umieszczane w kolejce. Po zamknięciu analizujemy obraz, wywołując polecenie ImageProxy.close(). Następny obraz jest wyświetlany.
  • Jeśli używasz danych wyjściowych osoby oznaczającej obraz, aby nakładać grafikę na obraz wejściowy, najpierw uzyskaj wynik z ML Kit, a potem wyrenderuj obraz i nakładkę w jednym kroku. W przypadku każdej klatki wejściowej jest ona renderowana tylko raz na ekranie. Przykład znajdziesz w klasach CameraSourcePreview i GraphicOverlay w przykładowej aplikacji.
  • Jeśli używasz interfejsu Camera2 API, zrób zdjęcia w formacie ImageFormat.YUV_420_888. Jeśli używasz starszego interfejsu API aparatu, zrób zdjęcia w formacie ImageFormat.NV21.