مقدمة
يستخدم برنامج Hello World! التعليمي هذا إطار عمل MediaPipe لتطوير تطبيق iOS يعرض رسمًا بيانيًا لـ MediaPipe على نظام iOS.
ما ستقوم بإنشائه
هو تطبيق كاميرا بسيط لرصد حافة Sobel في الوقت الفعلي عند تطبيقه على بث فيديو مباشر على جهاز iOS.
ضبط إعدادات الجهاز
- ثبِّت إطار عمل MediaPipe على النظام الخاص بك، ويمكنك الاطّلاع على دليل تثبيت إطار العمل لمعرفة التفاصيل.
- عليك إعداد جهاز 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 عبر الملف > جديد > تطبيق عرض واحد.
اضبط اسم المنتج على "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
إلى موقع يمكنه الوصول إلى رمز مصدر MediaPipeframe. على سبيل المثال، يمكنك العثور على رمز مصدر التطبيق الذي سننشئه في هذا البرنامج التعليمي في 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
، ومخططات القصة للتطبيقات. ولا يعتمد التطبيق
الذي يتضمّن نموذجًا إلا على حزمة تطوير البرامج (SDK) UIKit
.
تستخدم القاعدة 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، وافتح النافذة > الأجهزة والمحاكيات، واختَر جهازك وأضِف ملف .ipa
الذي تم إنشاؤه باستخدام الأمر أعلاه إلى جهازك.
في ما يلي المستند الذي يتناول إعداد وتجميع تطبيقات إطار عمل iOS.
افتح التطبيق على جهازك. نظرًا لأنه فارغ، يُفترض أن يعرض شاشة بيضاء فارغة.
استخدام الكاميرا مع خلاصة العرض المباشر
في هذا البرنامج التعليمي، سنستخدم الفئة MPPCameraInputSource
للوصول إلى الإطارات
والتقاطها من الكاميرا. تستخدم هذه الفئة واجهة برمجة التطبيقات 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
، يجب أولاً
تحديد طريقة لعرض إطارات الكاميرا. يوفر إطار عمل MediaMedia أداة أخرى تُعرف باسم 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 الذي يحتوي على واجهة برمجة تطبيقات 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
للرسم البياني لرصد الحواف.