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

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

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

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

לפני שאתה מתחיל

  1. הוסף את התלות של ספריות אנדרואיד של ML Kit לקובץ Gradle ברמת האפליקציה שלך, שהוא בדרך כלל app/build.gradle :

    לאגד דגם עם האפליקציה שלך:

    dependencies {
      // ...
      // Image labeling feature with bundled automl model
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
    }
    

    להורדה דינמית של מודל מ-Firebase, הוסף את התלות של linkFirebase :

    dependencies {
      // ...
      // Image labeling feature with automl model downloaded
      // from firebase
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
      implementation 'com.google.mlkit:linkfirebase:16.1.0'
    }
    
  2. אם ברצונך להוריד דגם , הקפד להוסיף את Firebase לפרויקט האנדרואיד שלך , אם עדיין לא עשית זאת. זה לא נדרש כאשר אתה מצרף את הדגם.

1. טען את הדגם

הגדר מקור דגם מקומי

כדי לאגד את הדגם עם האפליקציה שלך:

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

  2. כלול את הדגם שלך וקובצי המטא נתונים שלו בחבילת האפליקציה שלך:

    1. אם אין לך תיקיית נכסים בפרויקט שלך, צור אחת על ידי לחיצה ימנית על app/ תיקיה ולאחר מכן לחיצה על חדש > תיקיה > תיקיית נכסים .
    2. צור תיקיית משנה תחת תיקיית הנכסים שתכיל את קבצי הדגם.
    3. העתק את הקבצים model.tflite , dict.txt ו- manifest.json לתיקיית המשנה (כל שלושת הקבצים חייבים להיות באותה תיקיה).
  3. הוסף את הדברים הבאים לקובץ build.gradle של האפליקציה שלך כדי להבטיח ש-Gradle לא ידחוס את קובץ הדגם בעת בניית האפליקציה:

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

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

  4. צור אובייקט LocalModel , תוך ציון הנתיב לקובץ המניפסט של המודל:

    Java

    AutoMLImageLabelerLocalModel localModel =
        new AutoMLImageLabelerLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            // or .setAbsoluteFilePath(absolute file path to manifest file)
            .build();
    

    קוטלין

    val localModel = LocalModel.Builder()
        .setAssetManifestFilePath("manifest.json")
        // or .setAbsoluteManifestFilePath(absolute file path to manifest file)
        .build()
    

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

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

Java

// Specify the name you assigned in the Firebase console.
FirebaseModelSource firebaseModelSource =
    new FirebaseModelSource.Builder("your_model_name").build();
CustomRemoteModel remoteModel =
    new CustomRemoteModel.Builder(firebaseModelSource).build();

קוטלין

// Specify the name you assigned in the Firebase console.
val firebaseModelSource = FirebaseModelSource.Builder("your_model_name")
    .build()
val remoteModel = CustomRemoteModel.Builder(firebaseModelSource).build()

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

Java

DownloadConditions downloadConditions = new DownloadConditions.Builder()
        .requireWifi()
        .build();
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(@NonNull Task<Void> task) {
                // Success.
            }
        });

קוטלין

val downloadConditions = DownloadConditions.Builder()
    .requireWifi()
    .build()
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
    .addOnSuccessListener {
        // Success.
    }

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

צור תוויות תמונה מהדגם שלך

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

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

Java

CustomImageLabelerOptions customImageLabelerOptions = new CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

קוטלין

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

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

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

Java

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener<Boolean>() {
            @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.0f)  // Evaluate your model in the Cloud console
                                                       // to determine an appropriate threshold.
                        .build();

                ImageLabeler labeler = ImageLabeling.getClient(options);
            }
        });

קוטלין

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
        val optionsBuilder =
            if (isDownloaded) {
                CustomImageLabelerOptions.Builder(remoteModel)
            } else {
                CustomImageLabelerOptions.Builder(localModel)
            }
        // Evaluate your model in the Cloud console to determine an appropriate threshold.
        val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
        val labeler = ImageLabeling.getClient(options)
}

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

Java

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

קוטלין

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

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

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

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

שימוש media.Image

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

אם אתה משתמש בספריית CameraX , המחלקות OnImageCapturedListener ו- ImageAnalysis.Analyzer מחשבות עבורך את ערך הסיבוב.

Kotlin+KTX

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) {
        if (imageProxy == null || imageProxy.getImage() == null) {
            return;
        }
        Image mediaImage = imageProxy.getImage();
        InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees);
        // Pass image to an ML Kit Vision API
        // ...
    }
}

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

Kotlin+KTX

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
}

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

לאחר מכן, העבר את אובייקט media.Image ואת ערך דרגת הסיבוב ל- InputImage.fromMediaImage() :

Kotlin+KTX

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

שימוש ב-URI של קובץ

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

Kotlin+KTX

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

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* 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
);

שימוש Bitmap

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

Kotlin+KTX

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

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

3. הפעל את תווית התמונות

כדי לתייג אובייקטים בתמונה, העבירו את אובייקט image לשיטת process() של ImageLabeler .

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

קוטלין

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

4. קבל מידע על אובייקטים מסומנים

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

Java

for (ImageLabel label : labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
    int index = label.getIndex();
}

קוטלין

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

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

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

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

    אם אתה משתמש בממשק ה-API הישן יותר של Camera, צלם תמונות בפורמט ImageFormat.NV21 .