פילוח תמונת סלפי באמצעות ML Kit ב-iOS

ML Kit מספק SDK מותאם לסגמנטציה של תמונות סלפי. הנכסים של פילוח הסלפי מקושרים באופן סטטי לאפליקציה בזמן ה-build. זה יגדיל את גודל האפליקציה ב-24MB. זמן האחזור של ה-API יכול לנוע בין 7 אלפיות השנייה ל-12 אלפיות שנייה, בהתאם לגודל של תמונת הקלט, כפי שנמדד ב-iPhone X.

אני רוצה לנסות

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

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

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

‫1. יוצרים מופע של כלי הפילוח

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

אפשרויות פילוח

מצב פילוח

Segmenter פועל בשני מצבים. חשוב לבחור את השיטה שמתאימה לתרחיש לדוגמה שלכם.

STREAM_MODE (default)

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

SINGLE_IMAGE_MODE (default)

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

הפעלת מסכת גודל גולמי

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

גודל המסכה הגולמית (למשל 256x256) קטן בדרך כלל מגודל הקלט של תמונת הקלט.

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

מציינים את אפשרויות הפילוח:

Swift

let options = SelfieSegmenterOptions()
options.segmenterMode = .singleImage
options.shouldEnableRawSizeMask = true

Objective-C

MLKSelfieSegmenterOptions *options = [[MLKSelfieSegmenterOptions alloc] init];
options.segmenterMode = MLKSegmenterModeSingleImage;
options.shouldEnableRawSizeMask = YES;

לבסוף, מקבלים מופע של Segmenter. מעבירים את האפשרויות שציינתם:

Swift

let segmenter = Segmenter.segmenter(options: options)

Objective-C

MLKSegmenter *segmenter = [MLKSegmenter segmenterWithOptions:options];

‫2. הכנת תמונת הקלט

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

יוצרים אובייקט 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];

3. עיבוד התמונה

מעבירים את האובייקט VisionImage לאחת משיטות עיבוד התמונות של Segmenter. אפשר להשתמש בשיטה process(image:) האסינכרונית או בשיטה results(in:) הסינכרונית.

כדי לבצע סגמנטציה של תמונת סלפי באופן סינכרוני:

Swift

var mask: [SegmentationMask]
do {
  mask = try segmenter.results(in: image)
} catch let error {
  print("Failed to perform segmentation with error: \(error.localizedDescription).")
  return
}

// Success. Get a segmentation mask here.

Objective-C

NSError *error;
MLKSegmentationMask *mask =
    [segmenter resultsInImage:image error:&error];
if (error != nil) {
  // Error.
  return;
}

// Success. Get a segmentation mask here.

כדי לבצע סגמנטציה בתמונת סלפי באופן אסינכרוני:

Swift

segmenter.process(image) { mask, error in
  guard error == nil else {
    // Error.
    return
  }
  // Success. Get a segmentation mask here.

Objective-C

[segmenter processImage:image
             completion:^(MLKSegmentationMask * _Nullable mask,
                          NSError * _Nullable error) {
               if (error != nil) {
                 // Error.
                 return;
               }
               // Success. Get a segmentation mask here.
             }];

‫4. הצגת מסיכת הפילוח

אפשר לקבל את תוצאת הפילוח כך:

Swift

let maskWidth = CVPixelBufferGetWidth(mask.buffer)
let maskHeight = CVPixelBufferGetHeight(mask.buffer)

CVPixelBufferLockBaseAddress(mask.buffer, CVPixelBufferLockFlags.readOnly)
let maskBytesPerRow = CVPixelBufferGetBytesPerRow(mask.buffer)
var maskAddress =
    CVPixelBufferGetBaseAddress(mask.buffer)!.bindMemory(
        to: Float32.self, capacity: maskBytesPerRow * maskHeight)

for _ in 0...(maskHeight - 1) {
  for col in 0...(maskWidth - 1) {
    // Gets the confidence of the pixel in the mask being in the foreground.
    let foregroundConfidence: Float32 = maskAddress[col]
  }
  maskAddress += maskBytesPerRow / MemoryLayout<Float32>.size
}

Objective-C

size_t width = CVPixelBufferGetWidth(mask.buffer);
size_t height = CVPixelBufferGetHeight(mask.buffer);

CVPixelBufferLockBaseAddress(mask.buffer, kCVPixelBufferLock_ReadOnly);
size_t maskBytesPerRow = CVPixelBufferGetBytesPerRow(mask.buffer);
float *maskAddress = (float *)CVPixelBufferGetBaseAddress(mask.buffer);

for (int row = 0; row < height; ++row) {
  for (int col = 0; col < width; ++col) {
    // Gets the confidence of the pixel in the mask being in the foreground.
    float foregroundConfidence = maskAddress[col];
  }
  maskAddress += maskBytesPerRow / sizeof(float);
}

דוגמה מלאה לאופן השימוש בתוצאות הפילוח מופיעה בדוגמה למתחילים של ML Kit.

טיפים לשיפור הביצועים

איכות התוצאות תלויה באיכות של תמונת הקלט:

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

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

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