זיהוי, מעקב וסיווג של אובייקטים באמצעות מודל סיווג בהתאמה אישית ב-iOS

אפשר להשתמש ב-ML Kit כדי לזהות ולעקוב אחרי אובייקטים בפריימים עוקבים של סרטון.

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

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

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

מודל בחבילה מודל מתארח
המודל הוא חלק מקובץ .ipa של האפליקציה, ולכן הוא מגדיל את הגודל שלה. המודל לא מופיע בקובץ .ipa של האפליקציה. הוא מתארח על ידי העלאה ל-Cloud Storage. מומלץ להשתמש ב-Cloud Storage for Firebase.
המודל זמין באופן מיידי, גם כשמכשיר Android אופליין האפליקציה צריכה לכלול קוד להורדת המודל לפי דרישה
אין צורך בפרויקט Firebase נדרש פרויקט Firebase (אם משתמשים ב-Cloud Storage for Firebase).
כדי לעדכן את המודל, צריך לפרסם מחדש את האפליקציה עדכון המודל בלי לפרסם מחדש את האפליקציה
אין בדיקות A/B מובנות בדיקות A/B עם הגדרת תצורה מרחוק ב-Firebase

רוצה לנסות?

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

  1. מוסיפים את ספריות ML Kit ל-Podfile:

    pod 'GoogleMLKit/ObjectDetectionCustom', '8.0.0'
    
  2. אחרי שמתקינים או מעדכנים את ה-Pods של הפרויקט, פותחים את פרויקט Xcode באמצעות .xcworkspace. ‫ML Kit נתמך ב-Xcode בגרסה 13.2.1 ומעלה.

  3. אם רוצים להוריד מודל באמצעות Cloud Storage for Firebase, צריך לוודא שהוספתם את Firebase לפרויקט iOS, אם עדיין לא עשיתם זאת. זה לא נדרש כשמצרפים את המודל לחבילה.

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

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

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

  1. מעתיקים את קובץ המודל (בדרך כלל מסתיים ב-.tflite או ב-.lite) לפרויקט Xcode, ומוודאים לבחור באפשרות Copy bundle resources כשעושים זאת. קובץ המודל ייכלל בקובץ AAB ויהיה זמין ל-ML Kit.

  2. יוצרים אובייקט LocalModel ומציינים את הנתיב לקובץ המודל:

    Swift

    let localModel = LocalModel(path: localModelFilePath)

    Objective-C

    MLKLocalModel *localModel =
        [[MLKLocalModel alloc] initWithPath:localModelFilePath];

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

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

2. הגדרת גלאי האובייקטים

אחרי שמגדירים את מקורות המודל, מגדירים את גלאי האובייקטים לתרחיש השימוש עם אובייקט CustomObjectDetectorOptions. אפשר לשנות את ההגדרות הבאות:

הגדרות של זיהוי אובייקטים
מצב זיהוי STREAM_MODE (ברירת מחדל) | SINGLE_IMAGE_MODE

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

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

זיהוי ומעקב אחרי כמה אובייקטים false (ברירת מחדל) | true

האם לזהות ולעקוב אחרי עד חמישה אובייקטים או רק אחרי האובייקט הבולט ביותר (ברירת מחדל).

סיווג אובייקטים false (ברירת מחדל) | true

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

סף מהימנות לסיווג

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

מספר התוויות המקסימלי לכל אובייקט

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

אם יש לכם רק מודל שצורף באופן מקומי, פשוט יוצרים גלאי אובייקטים מאובייקט LocalModel:

Swift

let options = CustomObjectDetectorOptions(localModel: localModel)
options.detectorMode = .singleImage
options.shouldEnableClassification = true
options.shouldEnableMultipleObjects = true
options.classificationConfidenceThreshold = NSNumber(value: 0.5)
options.maxPerObjectLabelCount = 3

Objective-C

MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableClassification = YES;
options.shouldEnableMultipleObjects = YES;
options.classificationConfidenceThreshold = @(0.5);
options.maxPerObjectLabelCount = 3;

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

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

Swift

// Path where your download logic saves the model
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let localModelURL = documentDirectory.appendingPathComponent("my_remote_model.tflite")

let model: LocalModel
if FileManager.default.fileExists(atPath: localModelURL.path) {
  // Use the downloaded model
  model = LocalModel(path: localModelURL.path)
} else {
  // Fall back to bundled model
  guard let bundledModelPath = Bundle.main.path(forResource: "model", ofType: "tflite") else { return }
  model = LocalModel(path: bundledModelPath)
}

let options = CustomObjectDetectorOptions(localModel: model)
options.detectorMode = .singleImage
options.shouldEnableClassification = true
options.shouldEnableMultipleObjects = true
options.classificationConfidenceThreshold = NSNumber(value: 0.5)
options.maxPerObjectLabelCount = 3
let objectDetector = ObjectDetector.objectDetector(options: options)

Objective-C

NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *localModelPath = [documentsDirectory stringByAppendingPathComponent:@"my_remote_model.tflite"];

MLKLocalModel *model;
if ([NSFileManager.defaultManager fileExistsAtPath:localModelPath]) {
  // Use the downloaded model
  model = [[MLKLocalModel alloc] initWithPath:localModelPath];
} else {
  // Fall back to bundled model
  NSString *bundledModelPath = [NSBundle.mainBundle pathForResource:@"model" ofType:@"tflite"];
  model = [[MLKLocalModel alloc] initWithPath:bundledModelPath];
}

MLKCustomObjectDetectorOptions *options = [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:model];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableClassification = YES;
options.shouldEnableMultipleObjects = YES;
options.classificationConfidenceThreshold = @(0.5);
options.maxPerObjectLabelCount = 3;
MLKObjectDetector *objectDetector = [MLKObjectDetector objectDetectorWithOptions:options];

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

Swift

let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let localModelURL = documentDirectory.appendingPathComponent("my_remote_model.tflite")
if FileManager.default.fileExists(atPath: localModelURL.path) {
  // Model is already cached, initialize immediately
  self.initializeDetector(with: localModelURL)
} else {
  // Model is not yet available, show loading UI and start download
  self.showLoadingUI()
  let storage = Storage.storage()
  let modelRef = storage.reference(forURL: "gs://YOUR_BUCKET/path/to/model.tflite")
  modelRef.write(toFile: localModelURL) { url, error in
    self.hideLoadingUI()
    if let error = error {
      // Handle download error
      self.showErrorUI()
    } else if let modelURL = url {
      // Download success, initialize detector
      self.initializeDetector(with: modelURL)
    }
  }
}

func initializeDetector(with modelURL: URL) {
  let localModel = LocalModel(path: modelURL.path)
  let options = CustomObjectDetectorOptions(localModel: localModel)
  options.detectorMode = .singleImage
  options.shouldEnableClassification = true
  options.shouldEnableMultipleObjects = true
  self.objectDetector = ObjectDetector.objectDetector(options: options)
  // Enable ML features in UI
  self.enableMLFeatures()
}

Objective-C

NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *localModelPath = [documentsDirectory stringByAppendingPathComponent:@"my_remote_model.tflite"];
NSURL *localModelURL = [NSURL fileURLWithPath:localModelPath];

if ([NSFileManager.defaultManager fileExistsAtPath:localModelPath]) {
  // Model is already cached, initialize immediately
  [self initializeDetectorWithURL:localModelURL];
} else {
  // Model is not yet available, show loading UI and start download
  [self showLoadingUI];

  FIRStorage *storage = [FIRStorage storage];
  FIRStorageReference *modelRef = [storage referenceForURL:@"gs://YOUR_BUCKET/path/to/model.tflite"];

  [modelRef writeToFile:localModelURL
             completion:^(NSURL * _Nullable URL, NSError * _Nullable error) {
               [self hideLoadingUI];
               if (error != nil) {
                 // Handle download error
                 [self showErrorUI];
               } else {
                 // Download success, initialize detector
                 [self initializeDetectorWithURL:URL];
               }
             }];
}

- (void)initializeDetectorWithURL:(NSURL *)modelURL {
  MLKLocalModel *localModel = [[MLKLocalModel alloc] initWithPath:modelURL.path];
  MLKCustomObjectDetectorOptions *options = [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
  options.detectorMode = MLKObjectDetectorModeSingleImage;
  options.shouldEnableClassification = YES;
  options.shouldEnableMultipleObjects = YES;
  self.objectDetector = [MLKObjectDetector objectDetectorWithOptions:options];

  // Enable ML features in UI
  [self enableMLFeatures];
}

ממשק ה-API לזיהוי אובייקטים ולמעקב מותאם לשני תרחישי השימוש העיקריים הבאים:

  • זיהוי ומעקב בזמן אמת של האובייקט הבולט ביותר בעינית המצלמה.
  • זיהוי של כמה אובייקטים בתמונה סטטית.

כדי להגדיר את ה-API לתרחישי השימוש האלה:

Swift

// Live detection and tracking
let options = CustomObjectDetectorOptions(localModel: localModel)
options.shouldEnableClassification = true
options.maxPerObjectLabelCount = 3

// Multiple object detection in static images
let options = CustomObjectDetectorOptions(localModel: localModel)
options.detectorMode = .singleImage
options.shouldEnableMultipleObjects = true
options.shouldEnableClassification = true
options.maxPerObjectLabelCount = 3

Objective-C

// Live detection and tracking
MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.shouldEnableClassification = YES;
options.maxPerObjectLabelCount = 3;

// Multiple object detection in static images
MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableMultipleObjects = YES;
options.shouldEnableClassification = YES;
options.maxPerObjectLabelCount = 3;

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

יוצרים אובייקט VisionImage באמצעות UIImage או CMSampleBuffer.

אם אתם משתמשים ב-UIImage, פועלים לפי השלבים הבאים:

  • יוצרים אובייקט VisionImage עם UIImage. חשוב לציין את .orientation הנכון.

    Swift

    let image = VisionImage(image: UIImage)
    visionImage.orientation = image.imageOrientation

    Objective-C

    MLKVisionImage *visionImage = [[MLKVisionImage alloc] initWithImage:image];
    visionImage.orientation = image.imageOrientation;

אם אתם משתמשים ב-CMSampleBuffer, פועלים לפי השלבים הבאים:

  • מציינים את הכיוון של נתוני התמונה שמופיעים בתג CMSampleBuffer.

    כדי לקבל את כיוון התמונה:

    Swift

    func imageOrientation(
      deviceOrientation: UIDeviceOrientation,
      cameraPosition: AVCaptureDevice.Position
    ) -> UIImage.Orientation {
      switch deviceOrientation {
      case .portrait:
        return cameraPosition == .front ? .leftMirrored : .right
      case .landscapeLeft:
        return cameraPosition == .front ? .downMirrored : .up
      case .portraitUpsideDown:
        return cameraPosition == .front ? .rightMirrored : .left
      case .landscapeRight:
        return cameraPosition == .front ? .upMirrored : .down
      case .faceDown, .faceUp, .unknown:
        return .up
      }
    }
          

    Objective-C

    - (UIImageOrientation)
      imageOrientationFromDeviceOrientation:(UIDeviceOrientation)deviceOrientation
                             cameraPosition:(AVCaptureDevicePosition)cameraPosition {
      switch (deviceOrientation) {
        case UIDeviceOrientationPortrait:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationLeftMirrored
                                                                : UIImageOrientationRight;
    
        case UIDeviceOrientationLandscapeLeft:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationDownMirrored
                                                                : UIImageOrientationUp;
        case UIDeviceOrientationPortraitUpsideDown:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationRightMirrored
                                                                : UIImageOrientationLeft;
        case UIDeviceOrientationLandscapeRight:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationUpMirrored
                                                                : UIImageOrientationDown;
        case UIDeviceOrientationUnknown:
        case UIDeviceOrientationFaceUp:
        case UIDeviceOrientationFaceDown:
          return UIImageOrientationUp;
      }
    }
          
  • יוצרים אובייקט VisionImage באמצעות האובייקט CMSampleBuffer והכיוון:

    Swift

    let image = VisionImage(buffer: sampleBuffer)
    image.orientation = imageOrientation(
      deviceOrientation: UIDevice.current.orientation,
      cameraPosition: cameraPosition)

    Objective-C

     MLKVisionImage *image = [[MLKVisionImage alloc] initWithBuffer:sampleBuffer];
     image.orientation =
       [self imageOrientationFromDeviceOrientation:UIDevice.currentDevice.orientation
                                    cameraPosition:cameraPosition];

4. יצירה והרצה של גלאי האובייקטים

  1. יוצרים גלאי אובייקטים חדש:

    Swift

    let objectDetector = ObjectDetector.objectDetector(options: options)

    Objective-C

    MLKObjectDetector *objectDetector = [MLKObjectDetector objectDetectorWithOptions:options];
  2. לאחר מכן, משתמשים בכלי לגילוי:

    באופן אסינכרוני:

    Swift

    objectDetector.process(image) { objects, error in
        guard error == nil, let objects = objects, !objects.isEmpty else {
            // Handle the error.
            return
        }
        // Show results.
    }

    Objective-C

    [objectDetector
        processImage:image
          completion:^(NSArray *_Nullable objects,
                       NSError *_Nullable error) {
            if (objects.count == 0) {
                // Handle the error.
                return;
            }
            // Show results.
         }];

    באופן סינכרוני:

    Swift

    var objects: [Object]
    do {
        objects = try objectDetector.results(in: image)
    } catch let error {
        // Handle the error.
        return
    }
    // Show results.

    Objective-C

    NSError *error;
    NSArray *objects =
        [objectDetector resultsInImage:image error:&error];
    // Show results or handle the error.

5. קבלת מידע על אובייקטים עם תוויות

אם הקריאה למעבד התמונות מצליחה, הוא מעביר רשימה של Objects ל-completion handler או מחזיר את הרשימה, בהתאם לשיטה האסינכרונית או הסינכרונית שהפעלתם.

כל Object מכיל את המאפיינים הבאים:

frame CGRect שמציין את מיקום האובייקט בתמונה.
trackingID מספר שלם שמזהה את האובייקט בתמונות, או nil במצב SINGLE_IMAGE_MODE.
labels
label.text תיאור הטקסט של התווית. הערך הזה מוחזר רק אם המטא-נתונים של מודל LiteRT מכילים תיאורי תוויות.
label.index האינדקס של התווית מבין כל התוויות שהמסווג תומך בהן.
label.confidence ערך מהימנות של סיווג האובייקט.

Swift

// objects contains one item if multiple object detection wasn't enabled.
for object in objects {
  let frame = object.frame
  let trackingID = object.trackingID
  let description = object.labels.enumerated().map { (index, label) in
    "Label \(index): \(label.text), \(label.confidence), \(label.index)"
  }.joined(separator: "\n")
}

Objective-C

// The list of detected objects contains one item if multiple object detection
// wasn't enabled.
for (MLKObject *object in objects) {
  CGRect frame = object.frame;
  NSNumber *trackingID = object.trackingID;
  for (MLKObjectLabel *label in object.labels) {
    NSString *labelString =
        [NSString stringWithFormat:@"%@, %f, %lu",
                                   label.text,
                                   label.confidence,
                                   (unsigned long)label.index];
  }
}

איך מבטיחים חוויית משתמש מעולה

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

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

כדאי גם לעיין באפליקציית התצוגה של ML Kit Material Design ובאוסף הדפוסים של Material Design לתכונות מבוססות למידת מכונה.

Improving performance

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

  • כשמשתמשים במצב סטרימינג באפליקציה בזמן אמת, לא מומלץ להשתמש בכמה זיהוי אובייקטים, כי רוב המכשירים לא יוכלו להפיק קצב פריימים מספיק.

  • כדי לעבד פריימים של סרטונים, משתמשים ב-API הסינכרוני של הגלאי results(in:). קוראים לפונקציה הזו מהפונקציה AVCaptureVideoDataOutputSampleBufferDelegate's captureOutput(_, didOutput:from:) כדי לקבל תוצאות באופן סינכרוני מפריים נתון של סרטון. שומרים על AVCaptureVideoDataOutput alwaysDiscardsLateVideoFrames בתור true כדי להגביל את השיחות לגלאי. אם פריים חדש של סרטון יהיה זמין בזמן שהגלאי פועל, הוא ייפסל.
  • אם משתמשים בפלט של הגלאי כדי להוסיף גרפיקה לתמונת הקלט, צריך קודם לקבל את התוצאה מ-ML Kit, ואז לעבד את התמונה ולהוסיף את הגרפיקה בשלב אחד. כך, הרינדור מתבצע רק פעם אחת לכל פריים קלט שעבר עיבוד. דוגמה מופיעה ב-updatePreviewOverlayViewWithLastFrame במדריך לתחילת העבודה עם ML Kit.