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

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