אפשר להשתמש ב-ML Kit כדי לזהות ולעקוב אחרי אובייקטים בפריים של סרטון.
כשמעבירים תמונות ל-ML Kit, ML Kit מחזיר לכל תמונה רשימה של עד חמישה אובייקטים שזוהו והמיקום שלהם בתמונה. כשמזהים אובייקטים בסטרימינג של סרטונים, לכל אובייקט יש מזהה שאפשר להשתמש בו כדי לעקוב אחרי האובייקט בתמונות. אפשר גם להפעיל סיווג גס של אובייקטים, שמוסיף לאובייקטים תוויות עם תיאורים של קטגוריות רחבות.
לפני שמתחילים
- אם עדיין לא עשיתם זאת, אתם צריכים להוסיף את Firebase לפרויקט Android.
- מוסיפים את התלויות של ספריות 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-object-detection-model:19.0.6' }
1. הגדרת גלאי האובייקטים
כדי להתחיל לזהות ולעקוב אחרי אובייקטים, קודם יוצרים מופע של FirebaseVisionObjectDetector
, ואפשר גם לציין הגדרות של גלאי שרוצים לשנות מברירת המחדל.
מגדירים את הכלי לזיהוי אובייקטים לתרחיש לדוגמה באמצעות אובייקט
FirebaseVisionObjectDetectorOptions
. אפשר לשנות את ההגדרות הבאות:הגדרות של זיהוי אובייקטים מצב זיהוי STREAM_MODE
(ברירת מחדל) |SINGLE_IMAGE_MODE
ב-
STREAM_MODE
(ברירת מחדל), גלאי האובייקטים פועל עם זמן אחזור נמוך, אבל יכול להיות שיוצרו תוצאות לא מלאות (כמו תיבות תוחמות או תוויות קטגוריה לא מוגדרות) בכמה ההפעלות הראשונות של הגלאי. בנוסף, ב-STREAM_MODE
, הגלאי מקצה מזהי מעקב לאובייקטים, שאפשר להשתמש בהם כדי לעקוב אחרי אובייקטים בפריים. משתמשים במצב הזה כשרוצים לעקוב אחרי אובייקטים, או כשחשוב להשתמש בחביון נמוך, למשל כשמעבדים זרמי וידאו בזמן אמת.ב-
SINGLE_IMAGE_MODE
, גלאי האובייקטים מחכה עד שתיבת התוחמת של אובייקט שזוהה ותווית הקטגוריה שלו (אם הפעלתם סיווג) יהיו זמינות לפני שהוא מחזיר תוצאה. כתוצאה מכך, יכול להיות שזמן האחזור של הזיהוי יהיה ארוך יותר. בנוסף, במודעות לרכישת אפליקציותSINGLE_IMAGE_MODE
לא מוקצים מזהים לצורכי מעקב. כדאי להשתמש במצב הזה אם זמן האחזור לא קריטי ואתם לא רוצים להתמודד עם תוצאות חלקיות.זיהוי ומעקב אחרי כמה אובייקטים false
(ברירת מחדל) |true
האם לזהות ולעקוב אחרי עד חמישה אובייקטים או רק אחרי האובייקט הבולט ביותר (ברירת מחדל).
סיווג אובייקטים false
(ברירת מחדל) |true
האם לסווג את האובייקטים שזוהו לקטגוריות גסות. כשהתכונה מופעלת, גלאי האובייקטים מסווג אובייקטים לקטגוריות הבאות: מוצרי אופנה, אוכל, מוצרים לבית, מקומות, צמחים ולא ידוע.
ממשק ה-API לזיהוי ולמעקב אחרי אובייקטים מותאם לשני תרחישי השימוש העיקריים האלה:
- זיהוי ומעקב בזמן אמת של האובייקט הבולט ביותר בעינית המצלמה
- זיהוי של כמה אובייקטים מתמונה סטטית
כדי להגדיר את ה-API לתרחישי השימוש האלה:
Java
// Live detection and tracking FirebaseVisionObjectDetectorOptions options = new FirebaseVisionObjectDetectorOptions.Builder() .setDetectorMode(FirebaseVisionObjectDetectorOptions.STREAM_MODE) .enableClassification() // Optional .build(); // Multiple object detection in static images FirebaseVisionObjectDetectorOptions options = new FirebaseVisionObjectDetectorOptions.Builder() .setDetectorMode(FirebaseVisionObjectDetectorOptions.SINGLE_IMAGE_MODE) .enableMultipleObjects() .enableClassification() // Optional .build();
Kotlin
// Live detection and tracking val options = FirebaseVisionObjectDetectorOptions.Builder() .setDetectorMode(FirebaseVisionObjectDetectorOptions.STREAM_MODE) .enableClassification() // Optional .build() // Multiple object detection in static images val options = FirebaseVisionObjectDetectorOptions.Builder() .setDetectorMode(FirebaseVisionObjectDetectorOptions.SINGLE_IMAGE_MODE) .enableMultipleObjects() .enableClassification() // Optional .build()
קבלת מופע של
FirebaseVisionObjectDetector
:Java
FirebaseVisionObjectDetector objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector(); // Or, to change the default settings: FirebaseVisionObjectDetector objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector(options);
Kotlin
val objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector() // Or, to change the default settings: val objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector(options)
2. הפעלת הכלי לזיהוי אובייקטים
כדי לזהות ולעקוב אחרי אובייקטים, מעבירים תמונות לשיטת FirebaseVisionObjectDetector
instanceprocessImage()
.
לכל פריים של סרטון או תמונה ברצף, מבצעים את הפעולות הבאות:
יוצרים אובייקט
FirebaseVisionImage
מהתמונה.-
כדי ליצור אובייקט
FirebaseVisionImage
מאובייקטmedia.Image
, למשל כשמצלמים תמונה ממצלמת המכשיר, מעבירים את אובייקטmedia.Image
ואת הסיבוב של התמונה אלFirebaseVisionImage.fromMediaImage()
.אם משתמשים בספריית CameraX, המחלקות
OnImageCapturedListener
ו-ImageAnalysis.Analyzer
מחשבות את ערך הסיבוב בשבילכם, כך שאתם רק צריכים להמיר את הסיבוב לאחד מהקבועים של ML KitROTATION_
לפני שקוראים ל-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
צריכה להיות זקופה, בלי שיהיה צורך בסיבוב נוסף.
-
מעבירים את התמונה לשיטה
processImage()
:Java
objectDetector.processImage(image) .addOnSuccessListener( new OnSuccessListener<List<FirebaseVisionObject>>() { @Override public void onSuccess(List<FirebaseVisionObject> detectedObjects) { // Task completed successfully // ... } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });
Kotlin
objectDetector.processImage(image) .addOnSuccessListener { detectedObjects -> // Task completed successfully // ... } .addOnFailureListener { e -> // Task failed with an exception // ... }
אם הקריאה ל-
processImage()
מצליחה, רשימה שלFirebaseVisionObject
s מועברת למאזין ההצלחה.כל
FirebaseVisionObject
מכיל את המאפיינים הבאים:תיבה תוחמת (bounding box) Rect
שמציין את המיקום של האובייקט בתמונה.מזהה לצורכי מעקב מספר שלם שמזהה את האובייקט בתמונות. Null ב-SINGLE_IMAGE_MODE. קטגוריה הקטגוריה הגסה של האובייקט. אם לא מופעלת סיווג של גלאי האובייקטים, הערך הזה תמיד יהיה FirebaseVisionObject.CATEGORY_UNKNOWN
.ודאות ערך הביטחון של סיווג האובייקט. אם זיהוי האובייקטים לא מופעל, או אם האובייקט מסווג כלא ידוע, הערך הוא null
.Java
// The list of detected objects contains one item if multiple object detection wasn't enabled. for (FirebaseVisionObject obj : detectedObjects) { Integer id = obj.getTrackingId(); Rect bounds = obj.getBoundingBox(); // If classification was enabled: int category = obj.getClassificationCategory(); Float confidence = obj.getClassificationConfidence(); }
Kotlin
// The list of detected objects contains one item if multiple object detection wasn't enabled. for (obj in detectedObjects) { val id = obj.trackingId // A number that identifies the object across images val bounds = obj.boundingBox // The object's position in the image // If classification was enabled: val category = obj.classificationCategory val confidence = obj.classificationConfidence }
שיפור השימושיות והביצועים
כדי לספק את חוויית המשתמש הכי טובה, צריך לפעול לפי ההנחיות הבאות באפליקציה:
- זיהוי אובייקט מוצלח תלוי במורכבות החזותית של האובייקט. יכול להיות שצריך שהעצמים עם מספר קטן של מאפיינים חזותיים יתפסו חלק גדול יותר בתמונה כדי שהמערכת תזהה אותם. חשוב לספק למשתמשים הנחיות לגבי צילום תמונות של אובייקטים שמתאימות לזיהוי.
- כשמשתמשים בסיווג, אם רוצים לזהות אובייקטים שלא נכנסים בצורה ברורה לקטגוריות הנתמכות, צריך להטמיע טיפול מיוחד באובייקטים לא ידועים.
כדאי גם לעיין ב[אפליקציית הדוגמאות של ML Kit Material Design][showcase-link]{: .external } ובאוסף הדפוסים של Material Design לתכונות שמבוססות על למידת מכונה.
כשמשתמשים במצב סטרימינג באפליקציה בזמן אמת, כדאי לפעול לפי ההנחיות הבאות כדי להשיג את קצב הפריימים הטוב ביותר:
אל תשתמשו בזיהוי של כמה אובייקטים במצב סטרימינג, כי רוב המכשירים לא יוכלו ליצור קצב פריימים מספיק.
אם לא צריך את הסיווג, משביתים אותו.
- הגבלת מספר השיחות למזהה. אם פריים חדש של סרטון הופך לזמין בזמן שהגלאי פועל, צריך להשליך את הפריים.
- אם אתם משתמשים בפלט של הגלאי כדי להוסיף גרפיקה לתמונת הקלט, קודם צריך לקבל את התוצאה מ-ML Kit, ואז לעבד את התמונה ולהוסיף את הגרפיקה בשלב אחד. כך, הרינדור מתבצע רק פעם אחת לכל מסגרת קלט.
-
אם אתם משתמשים ב-Camera2 API, צלמו תמונות בפורמט
ImageFormat.YUV_420_888
.אם משתמשים בגרסה ישנה יותר של Camera API, צריך לצלם תמונות בפורמט
ImageFormat.NV21
.