אתם יכולים להשתמש ב-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
, אפשר גם לציין את הגדרות הגלאי שרוצים לשנות מברירת המחדל.
מגדירים את ה-Object Detector לתרחיש לדוגמה באמצעות אובייקט
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. הפעלת הכלי לזיהוי אובייקטים
כדי לזהות אובייקטים ולעקוב אחריהם, מעבירים תמונות לשיטה processImage()
של המכונה FirebaseVisionObjectDetector
.
בכל פריים של סרטון או תמונה ברצף, מבצעים את הפעולות הבאות:
יוצרים אובייקט
FirebaseVisionImage
מהתמונה.-
כדי ליצור אובייקט
FirebaseVisionImage
מאובייקטmedia.Image
, למשל כשיוצרים תמונה ממצלמת המכשיר, מעבירים את האובייקטmedia.Image
ואת סיבוב התמונה אלFirebaseVisionImage.fromMediaImage()
.אם אתם משתמשים בספרייה CameraX, הערך של הזווית מסתובב בעצמו על ידי הכיתות
OnImageCapturedListener
ו-ImageAnalysis.Analyzer
, כך שצריך רק להמיר את הזווית לאחד מהקבועיםROTATION_
של ML Kit לפני שמפעילים את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
כדי לבקש מהמשתמש לבחור תמונה מאפליקציית הגלריה שלו.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, ואז לבצע עיבוד (רנדור) של התמונה ולהוסיף את שכבת-העל בשלב אחד. כך תוכלו לבצע עיבוד (render) למשטח התצוגה רק פעם אחת לכל מסגרת קלט.
-
אם אתם משתמשים ב-Camera2 API, כדאי לצלם תמונות בפורמט
ImageFormat.YUV_420_888
.אם משתמשים ב-Camera API הקודם, צריך לצלם תמונות בפורמט
ImageFormat.NV21
.