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

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

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

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

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

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

1. יצירת מופע של פילוח

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