תיוג תמונות באמצעות מודל בהתאמה אישית ב-Android

אפשר להשתמש בערכת ה-ML Kit כדי לזהות ישויות בתמונה ולהוסיף להן תוויות. ה-API הזה תומך במגוון רחב של מודלים של סיווג תמונות בהתאמה אישית. בקטע מודלים בהתאמה אישית עם למידת מכונה תוכלו למצוא הנחיות ליצירת תאימות למודלים, איפה אפשר למצוא מודלים שאומנו מראש ואיך להדריך את המודלים שלכם.

יש שתי דרכים לשלב תוויות לתמונות עם דגמים מותאמים אישית: באמצעות צינור עיבוד נתונים כחלק מהאפליקציה, או שימוש בצינור שלא מקובצים בחבילה, בהתאם לשירותי Google Play. אם תבחרו בצינור עיבוד הנתונים שאינו מקובצות בחבילות, האפליקציה שלכם תהיה קטנה יותר. הפרטים מופיעים בטבלה שבהמשך.

בחבילהלא מקובצות
שם הספרייהcom.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom
יישוםצינור עיבוד הנתונים מקושר באופן סטטי לאפליקציה בזמן הבנייה.צינור עיבוד הנתונים יורד באופן דינמי דרך Google Play Services.
גודל האפליקציהגודל של כ-5.5MB.גודל של כ-600KB.
זמן האתחולצינור עיבוד הנתונים זמין באופן מיידי.יכול להיות שתצטרכו להמתין עד להורדת צינור עיבוד הנתונים לפני השימוש הראשון.
מחזור חיים של מחזור APIזמינות כללית (GA)בטא

יש שתי דרכים לשלב מודל מותאם אישית: כדי לאמן את המודל, על ידי מיקוםו בתוך תיקיית הנכסים של האפליקציה או על ידי הורדתו מ-Firebase באופן דינמי. הטבלה הבאה משווה בין שתי האפשרויות האלה.

דגם בחבילה מודל מתארח
הדגם הוא חלק מה-APK של האפליקציה, שמגדיל את הגודל שלה. המודל אינו חלק מה-APK. כדי לארח את הקובץ, מעלים את הקובץ למידת מכונה ב-Firebase.
הדגם זמין באופן מיידי, גם כשמכשיר Android במצב אופליין הורדת המודל מתבצעת על פי דרישה
אין צורך בפרויקט Firebase נדרש פרויקט Firebase
עליך לפרסם את האפליקציה מחדש כדי לעדכן את המודל דחיפה של עדכוני מודל בלי לפרסם מחדש את האפליקציה
ללא בדיקת A/B מובנית בדיקת A/B קלה באמצעות הגדרת תצורה מרחוק ב-Firebase

לפני שמתחילים

  1. בקובץ build.gradle ברמת הפרויקט, חשוב לכלול את המאגר Maven ב-Google בקטעים buildscript ו-allprojects.

  2. מוסיפים את יחסי התלות של הספריות ל-Android Kit ב-ML Kit, ברמת הקובץ של האפליקציה, שהיא בדרך כלל app/build.gradle. בחרו באחד מהקשרים הבאים בהתאם לצרכים שלכם:

    לקיבוץ צינור עיבוד הנתונים באפליקציה:

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

    כדי להשתמש בצינור עיבוד הנתונים של שירותי 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. אם בחרתם להשתמש בצינור עיבוד הנתונים של שירותי 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>
    

    בנוסף, תוכלו לבדוק במפורש את הזמינות של צינור עיבוד הנתונים ולבקש הורדה דרך ModuleInstallClient API של Google Play Services.

    אם לא תפעילו צינורות עיבוד נתונים בזמן ההתקנה או תבקשו הורדה מפורשת, ההורדה תתבצע בפעם הראשונה שתפעילו את הלייבל. בקשות שהוגשו לפני סיום ההורדה לא מניבות תוצאות.

  4. מוסיפים את התלות linkFirebase אם רוצים להוריד מודל באופן דינמי מ-Firebase:

    כדי להוריד מודל באופן דינמי מ-Firebase, צריך להוסיף את תלות linkFirebase:

    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. אם אתם רוצים להוריד מודל, הקפידו להוסיף את Firebase לפרויקט Android. אם עדיין לא עשיתם זאת, לא תצטרכו לעשות זאת בעת החבילה של המודל.

1. טעינת המודל

הגדרת מקור של מודל מקומי

כדי לקבץ את המודל עם האפליקציה שלך:

  1. מעתיקים את קובץ הדגם (שבדרך כלל מסתיים ב-.tflite או ב-.lite) לתיקיית האפליקציה ב-assets/. (ייתכן שקודם תצטרכו ליצור את התיקייה באמצעות לחיצה ימנית על התיקייה app/ ואז לחיצה על Google &gt חדש; תיקייה &Gt; תיקיית נכסים).

  2. לאחר מכן, צריך להוסיף את הקובץ build.gradle לאפליקציה& כדי לוודא שהוא לא יידחס את קובץ ה-Graed בקובץ המודל בזמן בניית האפליקציה:

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

    קובץ המודל ייכלל בחבילה של האפליקציה וזמין ל-ML Kit כנכס גולמי.

  3. יוצרים אובייקט LocalModel ומציינים את הנתיב לקובץ המודל:

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

הגדרת מקור מודלים שמתארח ב-Firebase

כדי להשתמש במודל שמתארח מרחוק, יוצרים אובייקט RemoteModel עד FirebaseModelSource, ומציינת את השם שהקציתם למודל:

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

לאחר מכן, מתחילים את המשימה של הורדת המודל, ומציינים את התנאים שבהם רוצים לאפשר הורדה. אם המודל לא נמצא במכשיר, או אם קיימת גרסה חדשה יותר של המודל, המשימה תוריד באופן אסינכרוני את המודל מ-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.
            }
        });

אפליקציות רבות מתחילות את משימת ההורדה בקוד האתחול שלהן, אבל אפשר לעשות זאת בכל שלב לפני שתצטרכו להשתמש במודל.

הגדרת התוויות של התמונה

אחרי שמגדירים את מקורות המודל, יוצרים אובייקט 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);

אם יש לכם מודל שמתארח מרחוק, תצטרכו לבדוק שהוא הורד לפני הפעלתו. ניתן לבדוק את הסטטוס של משימת ההורדה של המודל בשיטה isModelDownloaded() של מנהל המודל.

למרות שעליך לאשר זאת רק לפני הפעלת הלייבל, אם יש לך גם מודל שמתארח מרחוק וגם מודל בחבילה מקומית, ייתכן שיהיה צורך לבצע את הבדיקה הזו גם כשיוצרים את תווית התווית: אם הורדת את הלייבל מהמודל המרוחק, אם הוא הורד ומהמודל המקומי, אחרת.

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

אם יש לכם רק מודל שמתארח מרחוק, עליכם להשבית פונקציונליות הקשורה לדגם – למשל, באפור או הסתרה של חלק מממשק המשתמש – עד שתאשרו את הורדת המודל. ניתן לעשות זאת על ידי צירוף מאזינים לשיטה 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. הכנת תמונת הקלט

לאחר מכן, עבור כל תמונה שרוצים להוסיף לה תווית, יוצרים אובייקט InputImage מתמונה. תווית התמונה פועלת מהר יותר כשמשתמשים ב-Bitmap, או אם משתמשים ב-API2 של המצלמה, YUV_420_888 media.Image, המומלצים כאשר זה אפשרי.

ניתן ליצור אובייקט InputImage ממקורות שונים, כפי שמוסבר בהמשך.

באמצעות media.Image

כדי ליצור אובייקט InputImage מאובייקט media.Image, למשל כשמצלמים תמונה ממצלמה של מכשיר, צריך להעביר את האובייקט 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 של קובץ

כדי ליצור אובייקט InputImage מ-URI של קובץ, צריך להעביר את ההקשר של האפליקציה ואת ה-URI של הקובץ אל InputImage.fromFilePath(). האפשרות הזו שימושית כשמשתמשים בכוונת ACTION_GET_CONTENT כדי לבקש מהמשתמש לבחור תמונה מאפליקציית הגלריה.

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

כדי ליצור אובייקט InputImage מ-ByteBuffer או מ-ByteArray, תחילה עליך לחשב את מידת סיבוב התמונה כמתואר למעלה עבור קלט 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

כדי ליצור אובייקט InputImage מאובייקט Bitmap, צריך ליצור את ההצהרה הבאה:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

התמונה מיוצגת על ידי אובייקט Bitmap יחד עם מעלות סיבוב.

3. הפעלה של מתייג התמונות

כדי לסמן אובייקטים בתמונה, מעבירים את האובייקט image אל השיטה ImageLabeler#39;sprocess().

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 מייצג משהו שתויג בתמונה. ניתן לקבל תיאור טקסט של כל תווית'(אם זמין במטא-נתונים של קובץ המודל של TensorFlow Lite), ציון הסמך והאינדקס. למשל:

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

טיפים לשיפור הביצועים בזמן אמת

אם רוצים להוסיף תוויות לתמונות באפליקציה בזמן אמת, יש לפעול לפי ההנחיות הבאות כדי להשיג את קצב הפריימים הטוב ביותר:

  • אם משתמשים ב-API Camera או camera2, מווסתים את השיחות לתווית התמונה. אם פריים חדש של וידאו הופך לזמין כשתווית התווית פועלת, משחררים את הפריים. לדוגמה, אפשר לעיין בקורס VisionProcessorBase באפליקציה למתחילים.
  • אם בחרת להשתמש ב-API CameraX, יש לוודא ששיטת ההחזרה מוגדרת לערך ברירת המחדל ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. כך ניתן להבטיח שתמונה אחת בלבד תישלח לניתוח בכל פעם. אם יופקו תמונות נוספות כשהמנתח יהיה עסוק, הן יושמטו אוטומטית ולא יתווספו לתור. אחרי שהתמונה תנתח, על ידי קריאה ל-Imageproxy.close() , התמונה הבאה שתישלח.
  • אם משתמשים בפלט של תווית התמונה כדי ליצור שכבת-על של גרפיקה על תמונת הקלט, תחילה יש להשיג את התוצאה מ-ML Kit, ולאחר מכן לעבד את התמונה ואת שכבת-העל בפעולה אחת. כך מתבצע עיבוד למשטח התצוגה פעם אחת בלבד עבור כל מסגרת קלט. כדי לראות דוגמה, אפשר לעיין בכיתות CameraSourcePreview ו- GraphicOverlay באפליקציה למתחילים.
  • אם משתמשים ב-Camera2 API, צריך לצלם את התמונות בפורמט ImageFormat.YUV_420_888. אם משתמשים בגרסה הישנה יותר של ממשק ה-API של המצלמה, יש לצלם תמונות בפורמט ImageFormat.NV21.