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

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

רוצה לנסות?

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

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

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

1. יוצרים מופע של Segmenter

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

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

מצב חלוקה לפלחים

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

STREAM_MODE (default)

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

SINGLE_IMAGE_MODE (default)

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

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

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

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

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

מציינים את האפשרויות של המקטע:

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 של Segmenter.
  • כדאי לצלם תמונות ברזולוציה נמוכה יותר. עם זאת, חשוב לזכור גם את הדרישות של ה-API הזה לגבי מידות התמונה.
  • כדי לעבד פריימים של סרטונים, צריך להשתמש ב-API הסינכרוני results(in:) של המקטע. קוראים ל-method הזה מהפונקציה captureOutput(_, didOutput:from:) של AVCaptureVideoDataOutputSampleBufferDelegate כדי לקבל תוצאות באופן סינכרוני מהפריים הנתון. כדי להגביל את הקריאות למקטע, צריך להשאיר את הערך של alwaysDiscardsLateVideoFrames של AVCaptureVideoDataOutput כ-true. אם פריים חדש של סרטון יהיה זמין בזמן שהמקטע פועל, הוא יוסר.
  • אם אתם משתמשים בפלט של כלי הפילוח כדי להוסיף שכבת-על של גרפיקה לתמונת הקלט, קודם צריך לקבל את התוצאה מ-ML Kit, ואז לעבד את התמונה ואת שכבת-העל בשלב אחד. כך, העיבוד של משטח התצוגה מתבצע רק פעם אחת לכל פריים קלט שעבר עיבוד. לדוגמה, אפשר לעיין במחלקות previewOverlayView ו-CameraViewController בדוגמה למדריך למתחילים של ML Kit.