מבוא
במדריך הזה של Hello World! נעשה שימוש ב-MediaPipe Framework כדי לפתח אפליקציה ל-iOS שמפעילה תרשים MediaPipe ב-iOS.
מה תפַתחו
אפליקציית מצלמה פשוטה לזיהוי קצה Sobel בזמן אמת שמוחלת על שידור וידאו בשידור חי במכשיר iOS.
הגדרה
- לפרטים נוספים, קראו את המדריך להתקנת מסגרת כדי להתקין את MediaPipe Framework.
- צריך להגדיר את מכשיר ה-iOS לצורכי פיתוח.
- כדי לפתח את האפליקציה ל-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"
}
המחשה חזותית של התרשים מוצגת בהמשך:
התרשים הזה כולל מקור קלט יחיד בשם 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
מופיע בהמשך:
AppDelegate.h
וגםAppDelegate.m
ViewController.h
וגםViewController.m
main.m
Info.plist
Main.storyboard
וגםLaunch.storyboard
- הספרייה
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. אתם אמורים לראות את התוצאות של הפעלת תרשים זיהוי הקצה בפיד וידאו בשידור חי. איזה כיף!
שים לב שהדוגמאות ל-iOS כוללות עכשיו אפליקציית תבנית common. הקוד במדריך הזה משמש באפליקציית התבניות common. באפליקציה helloworld יש יחסי תלות של BUILD
בקובץ עבור תרשים זיהוי הקצה.