אפשר להשתמש ב-ML Kit כדי לזהות אובייקטים בפריימים רצופים של וידאו ולעקוב אחריהם.
כשמעבירים תמונה לערכת ML, היא מזהה עד חמישה אובייקטים בתמונה יחד עם המיקום של כל אובייקט בתמונה. כשמזהים אובייקטים בסרטוני וידאו בסטרימינג, לכל אובייקט יש מזהה ייחודי שאפשר להשתמש בו כדי לעקוב אחרי האובייקט ממסגרת למסגרת. אפשר גם להפעיל סיווג גס של אובייקטים, שמוסיף תוויות לאובייקטים עם תיאורים רחבים של קטגוריות.
אני רוצה לנסות
- מומלץ להתנסות באפליקציה לדוגמה כדי לראות דוגמה לשימוש ב-API הזה.
- תוכלו לעיין באפליקציית התצוגה של Material Design כדי לקבל מידע על ההטמעה של ה-API הזה מקצה לקצה.
לפני שמתחילים
- בקובץ
build.gradle
ברמת הפרויקט, חשוב לכלול את מאגר Maven של Google בקטעbuildscript
וגם בקטעallprojects
. - מוסיפים את יחסי התלות של ספריות ML Kit Android לקובץ GRid ברמת האפליקציה של המודול, שהוא בדרך כלל
app/build.gradle
:dependencies { // ... implementation 'com.google.mlkit:object-detection:17.0.1' }
1. הגדרת מזהה האובייקטים
כדי לזהות אובייקטים ולעקוב אחריהם, קודם יוצרים מופע של ObjectDetector
, ואפשר לציין גם את הגדרות המזהה שרוצים לשנות מברירת המחדל.
מגדירים את מזהה האובייקטים לתרחיש לדוגמה שלכם באמצעות אובייקט
ObjectDetectorOptions
. אפשר לשנות את ההגדרות הבאות:הגדרות של מזהה אובייקטים מצב זיהוי STREAM_MODE
(ברירת מחדל) |SINGLE_IMAGE_MODE
ב-
STREAM_MODE
(ברירת המחדל), מזהה האובייקטים פועל בזמן אחזור קצר, אבל יכול להיות שהוא יפיק תוצאות חלקיות (כמו תיבות תוחמות או תוויות של קטגוריות שלא צוינו) בהפעלות הראשונות של המזהה. בנוסף, ב-STREAM_MODE
, הגלאי מקצה לאובייקטים מזהים לצורכי מעקב, ואפשר להשתמש בהם כדי לעקוב אחרי אובייקטים בין מסגרות. כדאי להשתמש במצב הזה כשרוצים לעקוב אחרי אובייקטים או כשחשוב בזמן אחזור קצר, למשל כשמעבדים שידורי וידאו בזמן אמת.ב-
SINGLE_IMAGE_MODE
, מזהה האובייקטים מחזיר את התוצאה אחרי קביעת התיבה התוחמת של האובייקט. אם מפעילים גם את הסיווג, היא מחזירה את התוצאה אחרי שהתיבה התוחמת והתווית של הקטגוריה זמינות. כתוצאה מכך, זמן האחזור של הזיהוי עלול להיות ארוך יותר. כמו כן, ב-SINGLE_IMAGE_MODE
, מזהים לצורכי מעקב לא מוקצים. כדאי להשתמש במצב הזה אם זמן האחזור אינו קריטי ואתם לא רוצים לטפל בתוצאות חלקיות.זיהוי של מספר אובייקטים ומעקב אחריהם false
(ברירת מחדל) |true
האם לזהות ולעקוב אחר עד חמישה אובייקטים או רק את האובייקט הבולט ביותר (ברירת המחדל).
סיווג אובייקטים false
(ברירת מחדל) |true
האם לסווג את האובייקטים שזוהו לקטגוריות גסות. כשההגדרה מופעלת, מזהה האובייקטים מסווג את האובייקטים לקטגוריות הבאות: מוצרי אופנה, מזון, מוצרים לבית, מקומות וצמחים.
ה-API לזיהוי אובייקטים ולמעקב מותאם לשני תרחישי השימוש העיקריים הבאים:
- זיהוי בזמן אמת ומעקב אחרי האובייקט הבולט ביותר בעינית המצלמה.
- זיהוי של מספר אובייקטים מתמונה סטטית.
כדי להגדיר את ה-API לתרחישים לדוגמה האלה:
Kotlin
// Live detection and tracking val options = ObjectDetectorOptions.Builder() .setDetectorMode(ObjectDetectorOptions.STREAM_MODE) .enableClassification() // Optional .build() // Multiple object detection in static images val options = ObjectDetectorOptions.Builder() .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE) .enableMultipleObjects() .enableClassification() // Optional .build()
Java
// Live detection and tracking ObjectDetectorOptions options = new ObjectDetectorOptions.Builder() .setDetectorMode(ObjectDetectorOptions.STREAM_MODE) .enableClassification() // Optional .build(); // Multiple object detection in static images ObjectDetectorOptions options = new ObjectDetectorOptions.Builder() .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE) .enableMultipleObjects() .enableClassification() // Optional .build();
מקבלים מופע של
ObjectDetector
:Kotlin
val objectDetector = ObjectDetection.getClient(options)
Java
ObjectDetector objectDetector = ObjectDetection.getClient(options);
2. הכנת תמונת הקלט
כדי לזהות אובייקטים ולעקוב אחריהם, צריך להעביר תמונות ל-methodprocess()
של המכונה ObjectDetector
.
מזהה האובייקטים פועל ישירות מ-Bitmap
, מ-NV21 ByteBuffer
או מ-YUV_420_888 media.Image
. מומלץ לבנות InputImage
מהמקורות האלה אם יש לכם גישה ישירה לאחד מהם. אם תיצרו InputImage
ממקורות אחרים, אנחנו נטפל בהמרה באופן פנימי ויכול להיות שהיא תהיה פחות יעילה.
מבצעים את הפעולות הבאות לכל פריים של סרטון או תמונה ברצף:
אפשר ליצור אובייקט 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. עיבוד התמונה
מעבירים את התמונה ל-methodprocess()
:
Kotlin
objectDetector.process(image) .addOnSuccessListener { detectedObjects -> // Task completed successfully // ... } .addOnFailureListener { e -> // Task failed with an exception // ... }
Java
objectDetector.process(image) .addOnSuccessListener( new OnSuccessListener<List<DetectedObject>>() { @Override public void onSuccess(List<DetectedObject> detectedObjects) { // Task completed successfully // ... } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });
4. קבלת מידע על אובייקטים שזוהו
אם הקריאה ל-process()
מצליחה, מועברת רשימה של DetectedObject
למאזינים המוצלחים.
כל DetectedObject
מכיל את המאפיינים (properties) הבאים:
תיבה קשורה | Rect שמציין את המיקום של האובייקט בתמונה. |
||||||
מזהה לצורכי מעקב | מספר שלם שמזהה את האובייקט בתמונות. הערך ריק (null) ב-SINGLE_IMAGE_מצב. | ||||||
Labels (תוויות) |
|
Kotlin
for (detectedObject in detectedObjects) { val boundingBox = detectedObject.boundingBox val trackingId = detectedObject.trackingId for (label in detectedObject.labels) { val text = label.text if (PredefinedCategory.FOOD == text) { ... } val index = label.index if (PredefinedCategory.FOOD_INDEX == index) { ... } val confidence = label.confidence } }
Java
// The list of detected objects contains one item if multiple // object detection wasn't enabled. for (DetectedObject detectedObject : detectedObjects) { Rect boundingBox = detectedObject.getBoundingBox(); Integer trackingId = detectedObject.getTrackingId(); for (Label label : detectedObject.getLabels()) { String text = label.getText(); if (PredefinedCategory.FOOD.equals(text)) { ... } int index = label.getIndex(); if (PredefinedCategory.FOOD_INDEX == index) { ... } float confidence = label.getConfidence(); } }
הבטחת חוויית משתמש מעולה
כדי ליהנות מחוויית המשתמש הטובה ביותר, מומלץ לפעול לפי ההנחיות הבאות באפליקציה:
- ההצלחה של זיהוי אובייקטים תלויה במורכבות הוויזואלית של האובייקט. כדי לזהות אובייקטים עם מספר קטן של מאפיינים חזותיים, יכול להיות שהם יצטרכו לתפוס חלק גדול יותר מהתמונה. צריך לספק למשתמשים הדרכה לגבי צילום קלט שפועל בצורה טובה עם סוגי האובייקטים שרוצים לזהות.
- כשמשתמשים בסיווג, אם רוצים לזהות אובייקטים שלא מתאימים לקטגוריות הנתמכות, צריך להטמיע טיפול מיוחד לאובייקטים לא ידועים.
בנוסף, כדאי לעיין באפליקציית התצוגה של ML Kit Material Design ובאוסף Templates for למידת מכונה לתכונות שמבוססות על למידת מכונה
Improving performance
אם רוצים להשתמש בזיהוי אובייקטים באפליקציה בזמן אמת, צריך לפעול לפי ההנחיות האלה כדי להשיג את קצב הפריימים הטוב ביותר:
כשמשתמשים במצב סטרימינג באפליקציה בזמן אמת, לא כדאי להשתמש בזיהוי אובייקטים מרובים, כי רוב המכשירים לא יוכלו לייצר קצב פריימים הולם.
אפשר להשבית את הסיווג אם לא צריך אותו.
- אם משתמשים ב-API של
Camera
אוcamera2
, צריך לווסת את הקריאות למזהה. אם פריים חדש בווידאו הופך לזמין בזמן שהגלאי פועל, משחררים את הפריים. לדוגמה, תוכלו לעיין בשיעורVisionProcessorBase
באפליקציה לדוגמה של המדריך למתחילים. - אם משתמשים ב-API של
CameraX
, צריך לוודא שאסטרטגיית לחיצה לאחור מוגדרת לערך ברירת המחדל שלהImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
. כך אפשר להבטיח שרק תמונה אחת תוצג לניתוח בכל פעם. אם יופקו עוד תמונות כשהכלי לניתוח נתונים עמוס, הן יוסרו באופן אוטומטי ולא ימתינו לתור למשלוח. לאחר סגירת התמונה שמנתחת על ידי קריאה ל-ImageProxy.close(), מתקבלת התמונה העדכנית ביותר. - אם משתמשים בפלט של הגלאי כדי ליצור שכבת-על של גרפיקה בתמונת הקלט, מקבלים קודם את התוצאה מ-ML Kit ואז מעבדים את התמונה ושכבת-העל בפעולה אחת. הרינדור של משטח המסך מתבצע פעם אחת בלבד לכל מסגרת קלט. לדוגמה, אפשר לעיין בכיתות
CameraSourcePreview
ו-GraphicOverlay
באפליקציה לדוגמה של המדריך למתחילים. - אם משתמשים ב- Camera2 API, צריך לצלם תמונות
בפורמט
ImageFormat.YUV_420_888
. אם משתמשים בגרסה הקודמת של Camera API, צריך לצלם תמונות בפורמטImageFormat.NV21
.