Bilder mit einem benutzerdefinierten Modell auf Android-Geräten beschriften

Mit ML Kit können Sie Entitäten in einem Bild erkennen und mit Labels versehen. Diese API unterstützt eine Vielzahl von benutzerdefinierten Bildklassifizierungsmodellen. Unter Benutzerdefinierte Modelle mit ML Kit finden Sie Informationen zu den Anforderungen an die Modellkompatibilität, dazu, wo Sie vortrainierte Modelle finden, und dazu, wie Sie eigene Modelle trainieren.

Es gibt zwei Möglichkeiten, die Bildkennzeichnung in benutzerdefinierte Modelle zu integrieren: Sie können die Pipeline in Ihre App einbinden oder eine nicht gebündelte Pipeline verwenden, die von Google Play-Diensten abhängt. Wenn Sie die entkoppelte Pipeline auswählen, ist Ihre App kleiner. Weitere Informationen finden Sie in der folgenden Tabelle.

GebündeltEntbündelt
Bibliotheksnamecom.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom

Implementierung
Die Pipeline wird zur Build-Zeit statisch mit Ihrer App verknüpft.Die Pipeline wird dynamisch über die Google Play-Dienste heruntergeladen.
App-GrößeDie Größe hat sich um etwa 3,8 MB erhöht.Die Größe nimmt um etwa 200 KB zu.
InitialisierungszeitDie Pipeline ist sofort verfügbar.Möglicherweise müssen Sie warten, bis die Pipeline heruntergeladen wurde, bevor Sie sie zum ersten Mal verwenden können.
API-LebenszyklusphaseGeneral Availability (GA)Beta

Es gibt zwei Möglichkeiten, ein benutzerdefiniertes Modell einzubinden: Sie können das Modell in den Asset-Ordner Ihrer App einfügen oder es dynamisch von Firebase herunterladen. In der folgenden Tabelle werden die beiden Optionen verglichen.

Gebündeltes Modell Gehostetes Modell
Das Modell ist Teil des APKs Ihrer App, was die Größe erhöht. Das Modell ist nicht Teil Ihres APKs. Es wird gehostet, indem es in Cloud Storage hochgeladen wird. Wir empfehlen die Verwendung von Cloud Storage for Firebase.
Das Modell ist sofort verfügbar, auch wenn das Android-Gerät offline ist. Ihre App muss Code enthalten, mit dem das Modell bei Bedarf heruntergeladen wird.
Kein Firebase-Projekt erforderlich Erfordert ein Firebase-Projekt (bei Verwendung von Cloud Storage for Firebase).
Sie müssen Ihre App neu veröffentlichen, um das Modell zu aktualisieren Modellupdates per Push übertragen, ohne die App neu zu veröffentlichen
Keine integrierten A/B-Tests A/B-Tests mit Firebase Remote Config

Jetzt ausprobieren

Hinweis

  1. In die Datei build.gradle.kts auf Projektebene muss das Maven-Repository von Google in die Abschnitte buildscript und allprojects aufgenommen werden.

  2. Fügen Sie die Abhängigkeiten für die ML Kit-Android-Bibliotheken der Gradle-Datei auf App-Ebene Ihres Moduls hinzu, die in der Regel app/build.gradle.kts ist. Wählen Sie je nach Bedarf eine der folgenden Abhängigkeiten aus:

    So bündeln Sie die Pipeline mit Ihrer App:

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

    Für die Verwendung der Pipeline in den Google Play-Diensten:

    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. Wenn Sie die Pipeline in Google Play-Diensten verwenden, können Sie Ihre App so konfigurieren, dass die Pipeline automatisch auf das Gerät heruntergeladen wird, nachdem Ihre App aus dem Play Store installiert wurde. Fügen Sie dazu der Datei AndroidManifest.xml Ihrer App die folgende Deklaration hinzu:

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

    Sie können die Pipeline-Verfügbarkeit auch explizit prüfen und den Download über die ModuleInstallClient API der Google Play-Dienste anfordern.

    Wenn Sie keine Pipeline-Downloads während der Installation aktivieren oder einen expliziten Download anfordern, wird die Pipeline beim ersten Ausführen des Labelers heruntergeladen. Anfragen, die Sie vor Abschluss des Downloads stellen, führen zu keinen Ergebnissen.

  4. Wenn Sie ein Modell mit Cloud Storage for Firebase herunterladen möchten, müssen Sie Firebase Ihrem Android-Projekt hinzufügen, falls Sie dies noch nicht getan haben. Dies ist nicht erforderlich, wenn Sie das Modell bündeln.

1. Modell laden

Sie können das Modell aus einer lokal gebündelten Quelle oder einer Remotequelle laden.

Lokale Modellquelle konfigurieren

So bündeln Sie das Modell mit Ihrer App:

  1. Kopieren Sie die Modelldatei (normalerweise mit der Endung .tflite oder .lite) in den Ordner assets/ Ihrer App. Möglicherweise müssen Sie den Ordner zuerst erstellen. Klicken Sie dazu mit der rechten Maustaste auf den Ordner app/ und dann auf Neu > Ordner > Assets-Ordner.

  2. Erstellen Sie ein LocalModel-Objekt und geben Sie den Pfad zur Modelldatei an:

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

Remote gehostete Modellquelle konfigurieren

Wenn Sie das remote gehostete Modell verwenden möchten, müssen Sie die Modelldatei mit Ihrer eigenen App-Logik in den lokalen Speicher des Geräts herunterladen und dann als lokales Modell laden. Wir empfehlen, Cloud Storage for Firebase zum Hosten eines Modells zu verwenden. Implementierungsdetails finden Sie im Migrationsleitfaden für Firebase ML zu Cloud Storage.

Bildlabeler konfigurieren

Nachdem Sie die Modellquellen konfiguriert haben, erstellen Sie ein ImageLabeler-Objekt aus einer der Quellen.

Folgende Optionen sind verfügbar:

Optionen
confidenceThreshold

Mindestkonfidenzwert für erkannte Labels. Wenn nicht festgelegt, wird ein beliebiger von den Metadaten des Modells angegebener Klassifikatorschwellenwert verwendet. Wenn das Modell keine Metadaten enthält oder in den Metadaten kein Klassifikatorschwellenwert angegeben ist, wird der Standardschwellenwert 0,0 verwendet.

maxResultCount

Maximale Anzahl der zurückzugebenden Labels. Wenn nicht festgelegt, wird der Standardwert 10 verwendet.

Wenn Sie nur ein lokal gebündeltes Modell haben, erstellen Sie einfach einen Labeler aus Ihrem LocalModel-Objekt:

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

Wenn Sie ein Modell haben, das auf einem Remote-Server gehostet wird, müssen Sie prüfen, ob es heruntergeladen wurde, bevor Sie es ausführen.

Sie müssen dies zwar nur vor dem Ausführen des Labelers bestätigen, aber wenn Sie sowohl ein remote gehostetes als auch ein lokal gebündeltes Modell haben, kann es sinnvoll sein, diese Prüfung beim Instanziieren des Bildlabelers durchzuführen: Erstellen Sie einen Labeler aus dem Remote-Modell, wenn es heruntergeladen wurde, und andernfalls aus dem lokalen Modell.

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

Wenn Sie nur ein remote gehostetes Modell haben, sollten Sie modellbezogene Funktionen deaktivieren, z. B. einen Teil der Benutzeroberfläche ausgrauen oder ausblenden, bis Sie bestätigen, dass das Modell heruntergeladen wurde.

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

Erstellen Sie dann für jedes Bild, das Sie labeln möchten, ein InputImage-Objekt aus dem Bild. Die Bildkennzeichnung erfolgt am schnellsten, wenn Sie ein Bitmap verwenden. Wenn Sie die Camera2 API verwenden, wird ein YUV_420_888-media.Image empfohlen.

Sie können ein InputImage-Objekt aus verschiedenen Quellen erstellen. Die einzelnen Quellen werden unten beschrieben.

Mit einem media.Image

Wenn Sie ein InputImage-Objekt aus einem media.Image-Objekt erstellen möchten, z. B. wenn Sie ein Bild mit der Kamera eines Geräts aufnehmen, übergeben Sie das media.Image-Objekt und die Drehung des Bildes an InputImage.fromMediaImage().

Wenn Sie die CameraX-Bibliothek verwenden, berechnen die Klassen OnImageCapturedListener und ImageAnalysis.Analyzer den Rotationswert für Sie.

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

Wenn Sie keine Kamerabibliothek verwenden, die Ihnen den Drehwinkel des Bildes liefert, können Sie ihn aus dem Drehwinkel des Geräts und der Ausrichtung des Kamerasensors im Gerät berechnen:

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

Übergeben Sie dann das media.Image-Objekt und den Wert für den Drehwinkel an InputImage.fromMediaImage():

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

Datei-URI verwenden

Wenn Sie ein InputImage-Objekt aus einem Datei-URI erstellen möchten, übergeben Sie den App-Kontext und den Datei-URI an InputImage.fromFilePath(). Das ist nützlich, wenn Sie mit einem ACTION_GET_CONTENT-Intent den Nutzer auffordern, ein Bild aus seiner Galerie-App auszuwählen.

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 oder ByteArray verwenden

Wenn Sie ein InputImage-Objekt aus einem ByteBuffer oder einem ByteArray erstellen möchten, berechnen Sie zuerst den Bildrotationsgrad wie zuvor für die media.Image-Eingabe beschrieben. Erstellen Sie dann das InputImage-Objekt mit dem Puffer oder Array sowie der Höhe, Breite, Farbcodierungsformat und dem Rotationsgrad des Bildes:

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

Mit einem Bitmap

So erstellen Sie ein InputImage-Objekt aus einem Bitmap-Objekt:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

Das Bild wird durch ein Bitmap-Objekt zusammen mit den Rotationsgraden dargestellt.

3. Bildlabeler ausführen

Wenn Sie Objekte in einem Bild mit Labels versehen möchten, übergeben Sie das image-Objekt an die process()-Methode von 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. Informationen zu gelabelten Entitäten abrufen

Wenn die Bildkennzeichnung erfolgreich ist, wird eine Liste von ImageLabel-Objekten an den Erfolgs-Listener übergeben. Jedes ImageLabel-Objekt stellt etwas dar, das im Bild gekennzeichnet wurde. Sie können die Textbeschreibung (falls in den Metadaten der LiteRT-Modelldatei verfügbar), den Konfidenzwert und den Index für jedes Label abrufen. Beispiel:

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

Tipps zur Verbesserung der Echtzeitleistung

Wenn Sie Bilder in einer Echtzeitanwendung labeln möchten, sollten Sie die folgenden Richtlinien beachten, um die besten Frameraten zu erzielen:

  • Wenn Sie die Camera- oder camera2-API verwenden, drosseln Sie die Aufrufe des Bildlabelers. Wenn ein neuer Videoframes verfügbar wird, während der Bildlabeler ausgeführt wird, verwerfen Sie den Frame. Ein Beispiel finden Sie in der Klasse VisionProcessorBase in der Beispielanwendung für die Kurzanleitung.
  • Wenn Sie die CameraX API verwenden, muss die Backpressure-Strategie auf den Standardwert ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST festgelegt sein. So wird sichergestellt, dass jeweils nur ein Bild zur Analyse bereitgestellt wird. Wenn mehr Bilder erzeugt werden, während der Analyzer beschäftigt ist, werden sie automatisch verworfen und nicht für die Übermittlung in die Warteschlange gestellt. Sobald das analysierte Bild durch Aufrufen von ImageProxy.close() geschlossen wird, wird das nächste aktuelle Bild bereitgestellt.
  • Wenn Sie die Ausgabe des Bildkennzeichners verwenden, um Grafiken auf dem Eingabebild zu überlagern, rufen Sie zuerst das Ergebnis von ML Kit ab und rendern Sie dann das Bild und die Überlagerung in einem einzigen Schritt. Dadurch wird für jeden Eingabeframe nur einmal auf der Anzeigefläche gerendert. Ein Beispiel finden Sie in der Kurzanleitung in den Klassen CameraSourcePreview und GraphicOverlay.
  • Wenn Sie die Camera2 API verwenden, nehmen Sie Bilder im ImageFormat.YUV_420_888-Format auf. Wenn Sie die ältere Camera API verwenden, nehmen Sie Bilder im ImageFormat.NV21-Format auf.