Hello World! ב-iOS

מבוא

במדריך הזה של Hello World! נעשה שימוש ב-MediaPipe Framework כדי לפתח אפליקציה ל-iOS שמפעילה תרשים MediaPipe ב-iOS.

מה תפַתחו

אפליקציית מצלמה פשוטה לזיהוי קצה Sobel בזמן אמת שמוחלת על שידור וידאו בשידור חי במכשיר iOS.

edge_detection_ios_gpu_gif

הגדרה

  1. לפרטים נוספים, קראו את המדריך להתקנת מסגרת כדי להתקין את MediaPipe Framework.
  2. צריך להגדיר את מכשיר ה-iOS לצורכי פיתוח.
  3. כדי לפתח את האפליקציה ל-iOS ולפרוס אותה, צריך להגדיר את Bazel במערכת.

תרשים לזיהוי הקצה

נשתמש בתרשים הבא, edge_detection_mobile_gpu.pbtxt:

# MediaPipe graph that performs GPU Sobel edge detection on a live video stream.
# Used in the examples
# mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic:helloworld
# and mediapipe/examples/ios/helloworld.

# Images coming into and out of the graph.
input_stream: "input_video"
output_stream: "output_video"

# Converts RGB images into luminance images, still stored in RGB format.
node: {
  calculator: "LuminanceCalculator"
  input_stream: "input_video"
  output_stream: "luma_video"
}

# Applies the Sobel filter to luminance images stored in RGB format.
node: {
  calculator: "SobelEdgesCalculator"
  input_stream: "luma_video"
  output_stream: "output_video"
}

המחשה חזותית של התרשים מוצגת בהמשך:

edge_detection_mobile_gpu

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

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

הצומת השני, SobelEdgesCalculator, מחיל זיהוי קצה על חבילות נכנסות ב-stream של luma_video, ופלט הפלט גורם לזרם הפלט output_video.

האפליקציה שלנו ל-iOS תציג את הפריימים של תמונות הפלט של הסטרימינג output_video.

הגדרה ראשונית של אפליקציה מינימלית

קודם כל, אנחנו מתחילים באפליקציה פשוטה ל-iOS ומדגימים איך להשתמש ב-bazel כדי לפתח אותה.

קודם כל, יוצרים פרויקט XCode דרך קובץ > חדש > אפליקציית תצוגה יחידה.

מגדירים את שם המוצר ל-"HelloWorld" ומשתמשים במזהה ארגון מתאים, כמו com.google.mediapipe. מזהה הארגון לצד שם המוצר יהיה bundle_id של האפליקציה, למשל com.google.mediapipe.HelloWorld.

מגדירים את השפה ליעד ג'.

שומרים את הפרויקט במיקום מתאים. נקרא לזה $PROJECT_TEMPLATE_LOC. לכן הפרויקט יהיה בספרייה $PROJECT_TEMPLATE_LOC/HelloWorld. הספרייה הזו תכיל ספרייה אחרת בשם HelloWorld וקובץ HelloWorld.xcodeproj.

הקוד HelloWorld.xcodeproj לא יועיל לכם במדריך הזה, כי נשתמש ב-bazel כדי לפתח את האפליקציה ל-iOS. התוכן של הספרייה $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld מופיע בהמשך:

  1. AppDelegate.h וגם AppDelegate.m
  2. ViewController.h וגם ViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboard וגם Launch.storyboard
  6. הספרייה Assets.xcassets.

מעתיקים את הקבצים האלה לספרייה בשם HelloWorld, למיקום שאפשר לגשת אליו לקוד המקור של MediaPipe Framework. לדוגמה, קוד המקור של האפליקציה שנבנה במדריך הזה נמצא ב-mediapipe/examples/ios/HelloWorld. אנחנו נתייחס לנתיב הזה בתור $APPLICATION_PATH במהלך ה-Codelab.

יוצרים קובץ BUILD ב-$APPLICATION_PATH ומוסיפים את כללי ה-build הבאים:

MIN_IOS_VERSION = "11.0"

load(
    "@build_bazel_rules_apple//apple:ios.bzl",
    "ios_application",
)

ios_application(
    name = "HelloWorldApp",
    bundle_id = "com.google.mediapipe.HelloWorld",
    families = [
        "iphone",
        "ipad",
    ],
    infoplists = ["Info.plist"],
    minimum_os_version = MIN_IOS_VERSION,
    provisioning_profile = "//mediapipe/examples/ios:developer_provisioning_profile",
    deps = [":HelloWorldAppLibrary"],
)

objc_library(
    name = "HelloWorldAppLibrary",
    srcs = [
        "AppDelegate.m",
        "ViewController.m",
        "main.m",
    ],
    hdrs = [
        "AppDelegate.h",
        "ViewController.h",
    ],
    data = [
        "Base.lproj/LaunchScreen.storyboard",
        "Base.lproj/Main.storyboard",
    ],
    sdk_frameworks = [
        "UIKit",
    ],
    deps = [],
)

הכלל objc_library מוסיף יחסי תלות למחלקות AppDelegate ו-ViewController, ל-main.m ולסטוריבורד של האפליקציה. האפליקציה בתבנית תלויה רק ב-SDK UIKit.

הכלל ios_application משתמש בספריית Objective-C HelloWorldAppLibrary שנוצרה כדי ליצור אפליקציה ל-iOS להתקנה במכשיר iOS.

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

bazel build -c opt --config=ios_arm64 <$APPLICATION_PATH>:HelloWorldApp'

לדוגמה, כדי ליצור את האפליקציה HelloWorldApp ב-mediapipe/examples/ios/helloworld, משתמשים בפקודה הבאה:

bazel build -c opt --config=ios_arm64 mediapipe/examples/ios/helloworld:HelloWorldApp

אחר כך חוזרים ל-XCode, פותחים את 'חלון' > 'מכשירים וסימולטורים', בוחרים את המכשיר ומוסיפים למכשיר את הקובץ .ipa שנוצר באמצעות הפקודה שלמעלה. זהו המסמך על הגדרה והידור של אפליקציות של iOS Framework.

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

שימוש במצלמה של פיד הצפייה בשידור חי

במדריך הזה נשתמש בכיתה MPPCameraInputSource כדי לגשת לפריימים מהמצלמה ולצלם אותם. במחלקה הזו נעשה שימוש ב-API AVCaptureSession כדי לאחזר את הפריימים מהמצלמה.

אבל לפני שמשתמשים בכיתה הזו, צריך לשנות את הקובץ Info.plist כך שיתמוך בשימוש במצלמה באפליקציה.

ב-ViewController.m, הוסף את שורת הייבוא הבאה:

#import "mediapipe/objc/MPPCameraInputSource.h"

כדי ליצור אובייקט _cameraSource, מוסיפים את הפקודה הבאה לבלוק ההטמעה שלו:

@implementation ViewController {
  // Handles camera access via AVCaptureSession library.
  MPPCameraInputSource* _cameraSource;
}

מוסיפים את הקוד הבא ל-viewDidLoad():

-(void)viewDidLoad {
  [super viewDidLoad];

  _cameraSource = [[MPPCameraInputSource alloc] init];
  _cameraSource.sessionPreset = AVCaptureSessionPresetHigh;
  _cameraSource.cameraPosition = AVCaptureDevicePositionBack;
  // The frame's native format is rotated with respect to the portrait orientation.
  _cameraSource.orientation = AVCaptureVideoOrientationPortrait;
}

הקוד מפעיל את _cameraSource, מגדיר את ההגדרה הקבועה מראש של סשן הצילום ואת המצלמה שבה רוצים להשתמש.

אנחנו צריכים להעביר פריימים מ-_cameraSource לאפליקציה ViewController כדי להציג אותם. MPPCameraInputSource היא מחלקה משנית של MPPInputSource, שמספקת פרוטוקול למקבלי הגישה שלו – MPPInputSourceDelegate. לכן, האפליקציה ViewController יכולה לשמש כנציגים של _cameraSource.

מעדכנים את הגדרת הממשק של ViewController בהתאם:

@interface ViewController () <MPPInputSourceDelegate>

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

// Process camera frames on this queue.
dispatch_queue_t _videoQueue;

ב-viewDidLoad(), מוסיפים את השורה הבאה אחרי אתחול האובייקט _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

לפני שמגדירים את האובייקט _cameraSource, מוסיפים את הקוד הבא כדי לאתחל את התור:

dispatch_queue_attr_t qosAttribute = dispatch_queue_attr_make_with_qos_class(
      DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, /*relative_priority=*/0);
_videoQueue = dispatch_queue_create(kVideoQueueLabel, qosAttribute);

לעיבוד פריימים של המצלמה נשתמש בתור טורי עם העדיפות QOS_CLASS_USER_INTERACTIVE.

בחלק העליון של הקובץ, מוסיפים את השורה הבאה אחרי הייבוא של הכותרת, לפני הממשק או ההטמעה של ViewController:

static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue";

לפני שמיישמים שיטה כלשהי מפרוטוקול MPPInputSourceDelegate, קודם אנחנו צריכים להגדיר דרך להצגת הפריימים של המצלמה. Mediapipe Framework מספק כלי עזר נוסף שנקרא MPPLayerRenderer, שמאפשר להציג תמונות במסך. אפשר להשתמש בכלי הזה כדי להציג אובייקטים מסוג CVPixelBufferRef, שהם סוג התמונות שסופקו על ידי MPPCameraInputSource למשתמשים שקיבלו הרשאה.

ב-ViewController.m, הוסף את שורת הייבוא הבאה:

#import "mediapipe/objc/MPPLayerRenderer.h"

כדי להציג תמונות של המסך, אנחנו צריכים להוסיף ל-ViewController אובייקט UIView חדש בשם _liveView.

מוסיפים את השורות הבאות לבלוק ההטמעה של ViewController:

// Display the camera preview frames.
IBOutlet UIView* _liveView;
// Render frames in a layer.
MPPLayerRenderer* _renderer;

עבור אל Main.storyboard, הוסף אובייקט UIView מספריית האובייקטים ל-View של המחלקה ViewController. צריך להוסיף שקע הפניה מהתצוגה הזו לאובייקט _liveView שהוספתם עכשיו למחלקה ViewController. משנים את הגודל של התצוגה כך שהיא תהיה מרכזית ותכסה את כל מסך האפליקציה.

חוזרים אל ViewController.m ומוסיפים את הקוד הבא ל-viewDidLoad() כדי לאתחל את האובייקט _renderer:

_renderer = [[MPPLayerRenderer alloc] init];
_renderer.layer.frame = _liveView.layer.bounds;
[_liveView.layer addSublayer:_renderer.layer];
_renderer.frameScaleMode = MPPFrameScaleModeFillAndCrop;

כדי לקבל פריימים מהמצלמה, נשתמש בשיטה הבאה:

// Must be invoked on _videoQueue.
-   (void)processVideoFrame:(CVPixelBufferRef)imageBuffer
                timestamp:(CMTime)timestamp
               fromSource:(MPPInputSource*)source {
  if (source != _cameraSource) {
    NSLog(@"Unknown source: %@", source);
    return;
  }
  // Display the captured image on the screen.
  CFRetain(imageBuffer);
  dispatch_async(dispatch_get_main_queue(), ^{
    [_renderer renderPixelBuffer:imageBuffer];
    CFRelease(imageBuffer);
  });
}

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

עכשיו אנחנו צריכים להפעיל את המצלמה ברגע שתופיע התצוגה להצגת הפריימים. כדי לעשות זאת, נטמיע את הפונקציה viewWillAppear:(BOOL)animated:

-(void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
}

לפני שנתחיל להפעיל את המצלמה, אנחנו זקוקים להרשאת הגישה של המשתמש אליה. MPPCameraInputSource מספקת פונקציה requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler לבקשת גישה למצלמה ולביצוע פעולה מסוימת כשהמשתמש מגיב. מוסיפים את הקוד הבא ל-viewWillAppear:animated:

[_cameraSource requestCameraAccessWithCompletionHandler:^void(BOOL granted) {
  if (granted) {
    dispatch_async(_videoQueue, ^{
      [_cameraSource start];
    });
  }
}];

לפני פיתוח האפליקציה, צריך להוסיף את יחסי התלות הבאים לקובץ BUILD:

sdk_frameworks = [
    "AVFoundation",
    "CoreGraphics",
    "CoreMedia",
],
deps = [
    "//mediapipe/objc:mediapipe_framework_ios",
    "//mediapipe/objc:mediapipe_input_sources_ios",
    "//mediapipe/objc:mediapipe_layer_renderer",
],

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

אנחנו מוכנים עכשיו להשתמש בפריימים של מצלמה בתרשים MediaPipe.

שימוש בתרשים MediaPipe ב-iOS

מוסיפים יחסי תלות רלוונטיים

כבר הוספנו את יחסי התלות של קוד ה-framework של MediaPipe, שמכיל את ה-API ל-iOS כדי להשתמש בתרשים MediaPipe. כדי להשתמש בתרשים MediaPipe, אנחנו צריכים להוסיף תלות בתרשים שבו אנחנו מתכוונים להשתמש באפליקציה. מוסיפים את השורה הבאה לרשימה data בקובץ BUILD:

"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",

עכשיו מוסיפים את התלות למחשבונים שבהם נעשה שימוש בתרשים הזה בשדה deps בקובץ BUILD:

"//mediapipe/graphs/edge_detection:mobile_calculators",

לבסוף, משנים את שם הקובץ ViewController.m ל-ViewController.mm כדי לתמוך ב-Objective-C++.

שימוש בתרשים בViewController

ב-ViewController.m, הוסף את שורת הייבוא הבאה:

#import "mediapipe/objc/MPPGraph.h"

מצהירים על קבוע סטטי עם שם הגרף, זרם הקלט וזרם הפלט:

static NSString* const kGraphName = @"mobile_gpu";

static const char* kInputStream = "input_video";
static const char* kOutputStream = "output_video";

הוסף את המאפיין הבא לממשק של ViewController:

// The MediaPipe graph currently in use. Initialized in viewDidLoad, started in viewWillAppear: and
// sent video frames on _videoQueue.
@property(nonatomic) MPPGraph* mediapipeGraph;

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

+   (MPPGraph*)loadGraphFromResource:(NSString*)resource {
  // Load the graph config resource.
  NSError* configLoadError = nil;
  NSBundle* bundle = [NSBundle bundleForClass:[self class]];
  if (!resource || resource.length == 0) {
    return nil;
  }
  NSURL* graphURL = [bundle URLForResource:resource withExtension:@"binarypb"];
  NSData* data = [NSData dataWithContentsOfURL:graphURL options:0 error:&configLoadError];
  if (!data) {
    NSLog(@"Failed to load MediaPipe graph config: %@", configLoadError);
    return nil;
  }

  // Parse the graph config resource into mediapipe::CalculatorGraphConfig proto object.
  mediapipe::CalculatorGraphConfig config;
  config.ParseFromArray(data.bytes, data.length);

  // Create MediaPipe graph with mediapipe::CalculatorGraphConfig proto object.
  MPPGraph* newGraph = [[MPPGraph alloc] initWithGraphConfig:config];
  [newGraph addFrameOutputStream:kOutputStream outputPacketType:MPPPacketTypePixelBuffer];
  return newGraph;
}

השתמשו בפונקציה זו כדי לאתחל את התרשים ב-viewDidLoad באופן הבא:

self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName];

הגרף אמור לשלוח את התוצאות של עיבוד הפריימים בחזרה אל ViewController. אחרי אתחול התרשים, מוסיפים את השורה ViewController לאובייקט mediapipeGraph:

self.mediapipeGraph.delegate = self;

כדי למנוע תחרות על הזיכרון במהלך עיבוד פריימים מפיד הווידאו בשידור חי, כדאי להוסיף את השורה הבאה:

// Set maxFramesInFlight to a small value to avoid memory contention for real-time processing.
self.mediapipeGraph.maxFramesInFlight = 2;

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

[_cameraSource requestCameraAccessWithCompletionHandler:^void(BOOL granted) {
  if (granted) {
    // Start running self.mediapipeGraph.
    NSError* error;
    if (![self.mediapipeGraph startWithError:&error]) {
      NSLog(@"Failed to start graph: %@", error);
    }
    else if (![self.mediapipeGraph waitUntilIdleWithError:&error]) {
      NSLog(@"Failed to complete graph initial run: %@", error);
    }

    dispatch_async(_videoQueue, ^{
      [_cameraSource start];
    });
  }
}];

מוקדם יותר, כשקיבלנו פריימים מהמצלמה בפונקציה processVideoFrame, הצגנו אותם ב_liveView באמצעות _renderer. עכשיו צריך לשלוח את הפריימים האלה לתרשים ולעבד את התוצאות במקום זאת. שנה את היישום של הפונקציה הזו כדי לבצע את הפעולות הבאות:

-   (void)processVideoFrame:(CVPixelBufferRef)imageBuffer
                timestamp:(CMTime)timestamp
               fromSource:(MPPInputSource*)source {
  if (source != _cameraSource) {
    NSLog(@"Unknown source: %@", source);
    return;
  }
  [self.mediapipeGraph sendPixelBuffer:imageBuffer
                            intoStream:kInputStream
                            packetType:MPPPacketTypePixelBuffer];
}

אנחנו שולחים את imageBuffer אל self.mediapipeGraph כחבילה מסוג MPPPacketTypePixelBuffer אל זרם הקלט kInputStream, כלומר "input_video".

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

-   (void)mediapipeGraph:(MPPGraph*)graph
   didOutputPixelBuffer:(CVPixelBufferRef)pixelBuffer
             fromStream:(const std::string&)streamName {
  if (streamName == kOutputStream) {
    // Display the captured image on the screen.
    CVPixelBufferRetain(pixelBuffer);
    dispatch_async(dispatch_get_main_queue(), ^{
      [_renderer renderPixelBuffer:pixelBuffer];
      CVPixelBufferRelease(pixelBuffer);
    });
  }
}

עדכון הגדרת הממשק של ViewController באמצעות MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

וזה הכול! יצירה והפעלה של האפליקציה במכשיר iOS. אתם אמורים לראות את התוצאות של הפעלת תרשים זיהוי הקצה בפיד וידאו בשידור חי. איזה כיף!

edge_detection_ios_gpu_gif

שים לב שהדוגמאות ל-iOS כוללות עכשיו אפליקציית תבנית common. הקוד במדריך הזה משמש באפליקציית התבניות common. באפליקציה helloworld יש יחסי תלות של BUILD בקובץ עבור תרשים זיהוי הקצה.