Hello World! ב-iOS

מבוא

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

מה תפַתחו

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

edge_detection_ios_gpu_gif

הגדרה

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

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

נשתמש בתרשים הבא, 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 מחיל זיהוי קצה על כניסות המנות בזרם luma_video והפלט גורמות לפלט output_video .

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

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

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

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

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

מגדירים את השפה כ-Objective-C.

שומרים את הפרויקט במיקום מתאים. רוצה לקרוא לזה? $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 מספקת כלי שירות אחר שנקרא MPPLayerRenderer להצגת תמונות על המסך. הזה יכול לשמש להצגת אובייקטים מסוג CVPixelBufferRef, תמונות שסופקו על ידי MPPCameraInputSource למעניקי ההרשאות שלו.

ב-ViewController.m, מוסיפים את שורת הייבוא הבאה:

#import "mediapipe/objc/MPPLayerRenderer.h"

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

מוסיפים את השורות הבאות לקטע ההטמעה של 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 file:

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. האפליקציה helloworld כוללת את יחסי התלות המתאימים של קובצי BUILD עבור תרשים זיהוי הקצה.