مقدمه
این سلام دنیا! آموزش از MediaPipe Framework برای توسعه یک برنامه iOS استفاده می کند که یک نمودار MediaPipe را در iOS اجرا می کند.
آنچه را که خواهید ساخت
یک برنامه دوربین ساده برای تشخیص لبه Sobel در زمان واقعی برای پخش جریانی ویدیویی زنده در دستگاه iOS اعمال می شود.
راه اندازی
- MediaPipe Framework را روی سیستم خود نصب کنید، برای جزئیات به راهنمای نصب Framework مراجعه کنید.
- دستگاه iOS خود را برای توسعه راه اندازی کنید.
- 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"
}
تصویری از نمودار در زیر نشان داده شده است:
این نمودار دارای یک جریان ورودی به نام input_video
برای همه فریم های ورودی است که توسط دوربین دستگاه شما ارائه می شود.
اولین گره در نمودار، LuminanceCalculator
، یک بسته واحد (فریم تصویر) را می گیرد و با استفاده از سایه زن OpenGL، تغییر در روشنایی را اعمال می کند. فریم تصویر حاصل به جریان خروجی luma_video
ارسال می شود.
گره دوم، SobelEdgesCalculator
تشخیص لبه را برای بسته های دریافتی در جریان luma_video
اعمال می کند و نتایج را در جریان خروجی output_video
خروجی می دهد.
برنامه iOS ما فریم های تصویر خروجی جریان output_video
را نمایش می دهد.
حداقل راه اندازی اولیه برنامه
ابتدا با یک برنامه ساده iOS شروع می کنیم و نحوه استفاده از bazel
برای ساخت آن نشان می دهیم.
ابتدا یک پروژه XCode از طریق File > New > Single View ایجاد کنید.
نام محصول را روی "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
در زیر فهرست شده است:
-
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
در سراسر کدنویسی اشاره خواهیم کرد.
یک فایل BUILD
در $APPLICATION_PATH
ایجاد کنید و قوانین ساخت زیر را اضافه کنید:
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
و استوریبردهای برنامه اضافه میکند. برنامه قالب فقط به UIKit
SDK بستگی دارد.
قانون ios_application
از کتابخانه HelloWorldAppLibrary
Objective-C استفاده می کند که برای ساخت یک برنامه 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 برگردید، Window > Devices and Simulators را باز کنید، دستگاه خود را انتخاب کنید و فایل .ipa
تولید شده توسط دستور بالا را به دستگاه خود اضافه کنید. در اینجا سند راه اندازی و کامپایل برنامه های فریم ورک iOS است.
برنامه را روی دستگاه خود باز کنید. از آنجایی که خالی است، باید یک صفحه سفید خالی نمایش دهد.
از دوربین برای فید نمای زنده استفاده کنید
در این آموزش از کلاس 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"
برای نمایش تصاویر صفحه، باید یک شی 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
خود اضافه کنید:
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
وابستگی های مربوطه را اضافه کنید
ما قبلاً وابستگیهای کد چارچوب MediaPipe را اضافه کردهایم که حاوی API iOS برای استفاده از نمودار MediaPipe است. برای استفاده از یک نمودار MediaPipe، باید یک وابستگی به نموداری که قصد استفاده از آن را در برنامه خود داریم اضافه کنیم. خط زیر را به لیست data
فایل BUILD
خود اضافه کنید:
"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
حالا وابستگی را به ماشین حساب های استفاده شده در این نمودار در قسمت deps
در فایل BUILD
اضافه کنید:
"//mediapipe/graphs/edge_detection:mobile_calculators",
در نهایت برای پشتیبانی از Objective-C++، نام فایل ViewController.m
را به ViewController.mm
تغییر دهید.
از نمودار در 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 اکنون از یک برنامه قالب معمولی استفاده میکنند. کد موجود در این آموزش در اپلیکیشن قالب رایج استفاده می شود. برنامه helloworld وابستگی های فایل BUILD
مناسب را برای نمودار تشخیص لبه دارد.