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

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

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

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

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

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

רוצה לנסות?

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

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

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

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

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

    כדי להשתמש בצינור עיבוד הנתונים ב-Google Play Services:

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

    אפשר גם לבדוק באופן מפורש את הזמינות של צינור עיבוד הנתונים ולבקש הורדה דרך API של ModuleInstallClient ב-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.3'
      // Or use the dynamically downloaded pipeline in Google Play Services
      // implementation 'com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta5'
      implementation 'com.google.mlkit:linkfirebase:17.0.0'
    }
    
  5. אם אתם רוצים להוריד מודל, צריך לוודא מוסיפים את Firebase לפרויקט Android, אם עדיין לא עשיתם זאת. אין צורך לעשות זאת כשהמודל הוא חלק מחבילה.

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

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

כדי לצרף את המודל לאפליקציה:

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

  2. אחר כך צריך להוסיף את הפרטים הבאים לקובץ build.gradle של האפליקציה כדי לוודא Gradle לא דוחסת את קובץ המודל כשמפתחים את האפליקציה:

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

אם יש לך רק מודל שמתארח מרחוק, עליך להשבית את התכונה שקשורה למודלים פונקציונליות - לדוגמה, הצגה באפור או הסתרה של חלק מממשק המשתמש - עד מוודאים שבוצעה הורדה של המודל. אפשר לעשות זאת על ידי צירוף listen ל-method 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 או, אם משתמשים ב- Camera2 API, ב-media.Image YUV_420_888, מומלץ כשהדבר אפשרי.

אפשר ליצור 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() זה שימושי כאשר משתמשים ב-Intent 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. הפעלה של מתייג התמונה

כדי להוסיף תווית לאובייקטים בתמונה, צריך להעביר את האובייקט imageImageLabeler אמצעי תשלום אחד (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. קבלת מידע על ישויות שסומנו בתווית

אם הפעולה של הוספת תוויות לתמונה תתבצע בהצלחה, רשימה של 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();
}

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

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

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