הוספת תוויות למודלים עם אימון אוטומטי של Android ב-Android

אחרי שמאמנים מודל משלכם באמצעות AutoML Vision Edge, אפשר להשתמש בו באפליקציה כדי לתייג תמונות.

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

  1. אם עדיין לא עשיתם זאת, אתם צריכים להוסיף את Firebase לפרויקט Android.
  2. מוסיפים את התלויות של ספריות ML Kit Android לקובץ Gradle של המודול (ברמת האפליקציה) (בדרך כלל app/build.gradle):
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      implementation 'com.google.firebase:firebase-ml-vision-automl:18.0.5'
    }

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

‫ML Kit מריץ את המודלים שנוצרו על ידי AutoML במכשיר. עם זאת, אתם יכולים להגדיר את ML Kit לטעינת המודל מרחוק מ-Firebase, מהאחסון המקומי או משניהם.

אירוח המודל ב-Firebase מאפשר לכם לעדכן את המודל בלי להשיק גרסה חדשה של האפליקציה, ואתם יכולים להשתמש ב-Remote Config וב-A/B Testing כדי להציג באופן דינמי מודלים שונים לקבוצות שונות של משתמשים.

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

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

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

כדי להשתמש במודל שמתארח מרחוק, יוצרים אובייקט FirebaseAutoMLRemoteModel ומציינים את השם שהקציתם למודל כשפרסמתם אותו:

Java

// Specify the name you assigned in the Firebase console.
FirebaseAutoMLRemoteModel remoteModel =
    new FirebaseAutoMLRemoteModel.Builder("your_remote_model").build();

Kotlin

// Specify the name you assigned in the Firebase console.
val remoteModel = FirebaseAutoMLRemoteModel.Builder("your_remote_model").build()

לאחר מכן, מתחילים את משימת ההורדה של המודל ומציינים את התנאים שבהם רוצים לאפשר הורדה. אם המודל לא נמצא במכשיר, או אם יש גרסה חדשה יותר של המודל, המשימה תוריד את המודל מ-Firebase באופן אסינכרוני:

Java

FirebaseModelDownloadConditions conditions = new FirebaseModelDownloadConditions.Builder()
        .requireWifi()
        .build();
FirebaseModelManager.getInstance().download(remoteModel, conditions)
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                // Success.
            }
        });

Kotlin

val conditions = FirebaseModelDownloadConditions.Builder()
    .requireWifi()
    .build()
FirebaseModelManager.getInstance().download(remoteModel, conditions)
    .addOnCompleteListener {
        // Success.
    }

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

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

כדי לארוז את המודל עם האפליקציה:

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

    1. אם אין לכם תיקיית נכסים בפרויקט, אתם יכולים ליצור אותה על ידי לחיצה ימנית על התיקייה app/ ואז לחיצה על New > Folder > Assets Folder (חדש > תיקייה > תיקיית נכסים).
    2. יוצרים תיקיית משנה בתיקיית הנכסים שתכיל את קובצי המודל.
    3. מעתיקים את הקבצים model.tflite,‏ dict.txt ו-manifest.json לתיקיית המשנה (כל שלושת הקבצים צריכים להיות באותה תיקייה).
  3. מוסיפים את השורות הבאות לקובץ build.gradle של האפליקציה כדי לוודא ש-Gradle לא יכווץ את קובץ המודל בזמן בניית האפליקציה:
    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    
    קובץ המודל ייכלל בחבילת האפליקציה ויהיה זמין ל-ML Kit כנכס גולמי.
  4. יוצרים אובייקט FirebaseAutoMLLocalModel ומציינים את הנתיב לקובץ המניפסט של המודל:

    Java

    FirebaseAutoMLLocalModel localModel = new FirebaseAutoMLLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            .build();
    

    Kotlin

    val localModel = FirebaseAutoMLLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            .build()
    

יצירת כלי לתוויות תמונות מהמודל

אחרי שמגדירים את מקורות המודל, יוצרים אובייקט FirebaseVisionImageLabeler מאחד מהם.

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

Java

FirebaseVisionImageLabeler labeler;
try {
    FirebaseVisionOnDeviceAutoMLImageLabelerOptions options =
            new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
                    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Firebase console
                                                   // to determine an appropriate value.
                    .build();
    labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options);
} catch (FirebaseMLException e) {
    // ...
}

Kotlin

val options = FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0)  // Evaluate your model in the Firebase console
                                // to determine an appropriate value.
    .build()
val labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options)

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

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

Java

FirebaseModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener<Boolean>() {
            @Override
            public void onSuccess(Boolean isDownloaded) {
                FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder optionsBuilder;
                if (isDownloaded) {
                    optionsBuilder = new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(remoteModel);
                } else {
                    optionsBuilder = new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel);
                }
                FirebaseVisionOnDeviceAutoMLImageLabelerOptions options = optionsBuilder
                        .setConfidenceThreshold(0.0f)  // Evaluate your model in the Firebase console
                                                       // to determine an appropriate threshold.
                        .build();

                FirebaseVisionImageLabeler labeler;
                try {
                    labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options);
                } catch (FirebaseMLException e) {
                    // Error.
                }
            }
        });

Kotlin

FirebaseModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded -> 
    val optionsBuilder =
        if (isDownloaded) {
            FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(remoteModel)
        } else {
            FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
        }
    // Evaluate your model in the Firebase console to determine an appropriate threshold.
    val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
    val labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options)
}

אם יש לכם רק מודל שמתארח מרחוק, אתם צריכים להשבית את הפונקציונליות שקשורה למודל – למשל, להאפיר או להסתיר חלק מממשק המשתמש – עד שתאשרו שהמודל הורד. כדי לעשות את זה, צריך לצרף מאזין לשיטה download() של מנהל המודלים:

Java

FirebaseModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @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.
            }
        });

Kotlin

FirebaseModelManager.getInstance().download(remoteModel, conditions)
    .addOnCompleteListener {
        // Download complete. Depending on your app, you could enable the ML
        // feature, or switch from the local model to the remote model, etc.
    }

2. הכנת תמונת הקלט

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

אפשר ליצור אובייקט FirebaseVisionImage מאובייקט media.Image, מקובץ במכשיר, ממערך בייטים או מאובייקט Bitmap:

  • כדי ליצור אובייקט FirebaseVisionImage מאובייקט media.Image, למשל כשמצלמים תמונה ממצלמת המכשיר, מעבירים את אובייקט media.Image ואת הסיבוב של התמונה אל FirebaseVisionImage.fromMediaImage().

    אם משתמשים בספריית CameraX, המחלקות OnImageCapturedListener ו-ImageAnalysis.Analyzer מחשבות את ערך הסיבוב בשבילכם, כך שאתם רק צריכים להמיר את הסיבוב לאחד מהקבועים של ML Kit‏ ROTATION_ לפני שקוראים ל-FirebaseVisionImage.fromMediaImage():

    Java

    private class YourAnalyzer implements ImageAnalysis.Analyzer {
    
        private int degreesToFirebaseRotation(int degrees) {
            switch (degrees) {
                case 0:
                    return FirebaseVisionImageMetadata.ROTATION_0;
                case 90:
                    return FirebaseVisionImageMetadata.ROTATION_90;
                case 180:
                    return FirebaseVisionImageMetadata.ROTATION_180;
                case 270:
                    return FirebaseVisionImageMetadata.ROTATION_270;
                default:
                    throw new IllegalArgumentException(
                            "Rotation must be 0, 90, 180, or 270.");
            }
        }
    
        @Override
        public void analyze(ImageProxy imageProxy, int degrees) {
            if (imageProxy == null || imageProxy.getImage() == null) {
                return;
            }
            Image mediaImage = imageProxy.getImage();
            int rotation = degreesToFirebaseRotation(degrees);
            FirebaseVisionImage image =
                    FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
            // Pass image to an ML Kit Vision API
            // ...
        }
    }

    Kotlin

    private class YourImageAnalyzer : ImageAnalysis.Analyzer {
        private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
            0 -> FirebaseVisionImageMetadata.ROTATION_0
            90 -> FirebaseVisionImageMetadata.ROTATION_90
            180 -> FirebaseVisionImageMetadata.ROTATION_180
            270 -> FirebaseVisionImageMetadata.ROTATION_270
            else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
        }
    
        override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
            val mediaImage = imageProxy?.image
            val imageRotation = degreesToFirebaseRotation(degrees)
            if (mediaImage != null) {
                val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                // Pass image to an ML Kit Vision API
                // ...
            }
        }
    }

    אם לא משתמשים בספריית מצלמה שמספקת את סיבוב התמונה, אפשר לחשב את הסיבוב מסיבוב המכשיר ומכיוון חיישן המצלמה במכשיר:

    Java

    private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
    static {
        ORIENTATIONS.append(Surface.ROTATION_0, 90);
        ORIENTATIONS.append(Surface.ROTATION_90, 0);
        ORIENTATIONS.append(Surface.ROTATION_180, 270);
        ORIENTATIONS.append(Surface.ROTATION_270, 180);
    }
    
    /**
     * 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, Context context)
            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);
    
        // On most devices, the sensor orientation is 90 degrees, but for some
        // devices it is 270 degrees. For devices with a sensor orientation of
        // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
        CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
        int sensorOrientation = cameraManager
                .getCameraCharacteristics(cameraId)
                .get(CameraCharacteristics.SENSOR_ORIENTATION);
        rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;
    
        // Return the corresponding FirebaseVisionImageMetadata rotation value.
        int result;
        switch (rotationCompensation) {
            case 0:
                result = FirebaseVisionImageMetadata.ROTATION_0;
                break;
            case 90:
                result = FirebaseVisionImageMetadata.ROTATION_90;
                break;
            case 180:
                result = FirebaseVisionImageMetadata.ROTATION_180;
                break;
            case 270:
                result = FirebaseVisionImageMetadata.ROTATION_270;
                break;
            default:
                result = FirebaseVisionImageMetadata.ROTATION_0;
                Log.e(TAG, "Bad rotation value: " + rotationCompensation);
        }
        return result;
    }

    Kotlin

    private val ORIENTATIONS = SparseIntArray()
    
    init {
        ORIENTATIONS.append(Surface.ROTATION_0, 90)
        ORIENTATIONS.append(Surface.ROTATION_90, 0)
        ORIENTATIONS.append(Surface.ROTATION_180, 270)
        ORIENTATIONS.append(Surface.ROTATION_270, 180)
    }
    /**
     * 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, context: Context): 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)
    
        // On most devices, the sensor orientation is 90 degrees, but for some
        // devices it is 270 degrees. For devices with a sensor orientation of
        // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
        val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
        val sensorOrientation = cameraManager
                .getCameraCharacteristics(cameraId)
                .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
        rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
    
        // Return the corresponding FirebaseVisionImageMetadata rotation value.
        val result: Int
        when (rotationCompensation) {
            0 -> result = FirebaseVisionImageMetadata.ROTATION_0
            90 -> result = FirebaseVisionImageMetadata.ROTATION_90
            180 -> result = FirebaseVisionImageMetadata.ROTATION_180
            270 -> result = FirebaseVisionImageMetadata.ROTATION_270
            else -> {
                result = FirebaseVisionImageMetadata.ROTATION_0
                Log.e(TAG, "Bad rotation value: $rotationCompensation")
            }
        }
        return result
    }

    לאחר מכן מעבירים את האובייקט media.Image ואת ערך הסיבוב אל FirebaseVisionImage.fromMediaImage():

    Java

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

    Kotlin

    val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
  • כדי ליצור אובייקט FirebaseVisionImage מ-URI של קובץ, מעבירים את הקשר של האפליקציה ואת ה-URI של הקובץ אל FirebaseVisionImage.fromFilePath(). זה שימושי כשמשתמשים בACTION_GET_CONTENT intent כדי להנחות את המשתמש לבחור תמונה מאפליקציית הגלריה שלו.

    Java

    FirebaseVisionImage image;
    try {
        image = FirebaseVisionImage.fromFilePath(context, uri);
    } catch (IOException e) {
        e.printStackTrace();
    }

    Kotlin

    val image: FirebaseVisionImage
    try {
        image = FirebaseVisionImage.fromFilePath(context, uri)
    } catch (e: IOException) {
        e.printStackTrace()
    }
  • כדי ליצור אובייקט FirebaseVisionImage מ-ByteBuffer או ממערך בייטים, קודם צריך לחשב את סיבוב התמונה כמו שמתואר למעלה לגבי קלט media.Image.

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

    Java

    FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
            .setWidth(480)   // 480x360 is typically sufficient for
            .setHeight(360)  // image recognition
            .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
            .setRotation(rotation)
            .build();

    Kotlin

    val metadata = FirebaseVisionImageMetadata.Builder()
            .setWidth(480) // 480x360 is typically sufficient for
            .setHeight(360) // image recognition
            .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
            .setRotation(rotation)
            .build()

    משתמשים במאגר או במערך ובאובייקט המטא-נתונים כדי ליצור אובייקט FirebaseVisionImage:

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
    // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

    Kotlin

    val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
    // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
  • כדי ליצור אובייקט FirebaseVisionImage מאובייקט Bitmap:

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

    Kotlin

    val image = FirebaseVisionImage.fromBitmap(bitmap)
    התמונה שמיוצגת על ידי אובייקט Bitmap צריכה להיות זקופה, בלי שיהיה צורך בסיבוב נוסף.

3. הפעלת הכלי להוספת תוויות לתמונות

כדי להוסיף תוויות לאובייקטים בתמונה, מעבירים את האובייקט FirebaseVisionImage לשיטה processImage() של FirebaseVisionImageLabeler.

Java

labeler.processImage(image)
        .addOnSuccessListener(new OnSuccessListener<List<FirebaseVisionImageLabel>>() {
            @Override
            public void onSuccess(List<FirebaseVisionImageLabel> labels) {
                // Task completed successfully
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Task failed with an exception
                // ...
            }
        });

Kotlin

labeler.processImage(image)
        .addOnSuccessListener { labels ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

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

לדוגמה:

Java

for (FirebaseVisionImageLabel label: labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
}

Kotlin

for (label in labels) {
    val text = label.text
    val confidence = label.confidence
}

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

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

    אם משתמשים בגרסה ישנה יותר של Camera API, צריך לצלם תמונות בפורמט ImageFormat.NV21.