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