مرحبًا بالجميع على iOS

مقدمة

يستخدم برنامج Hello World! التعليمي هذا إطار عمل MediaPipe لتطوير تطبيق iOS يعرض رسمًا بيانيًا لـ MediaPipe على نظام iOS.

ما ستقوم بإنشائه

هو تطبيق كاميرا بسيط لرصد حافة Sobel في الوقت الفعلي عند تطبيقه على بث فيديو مباشر على جهاز iOS.

edge_detection_ios_gpu_gif

ضبط إعدادات الجهاز

  1. ثبِّت إطار عمل MediaPipe على النظام الخاص بك، ويمكنك الاطّلاع على دليل تثبيت إطار العمل لمعرفة التفاصيل.
  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 إلى موقع يمكنه الوصول إلى رمز مصدر 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. من المفترَض أن تظهر نتائج تشغيل الرسم البياني لرصد الحواف على خلاصة فيديو مباشر تهانينا!

edge_detection_ios_gpu_gif

تجدر الإشارة إلى أن أمثلة iOS تستخدم الآن تطبيق نموذج common. ويُستخدم الرمز البرمجي في هذا البرنامج التعليمي في تطبيق النموذج common. ويحتوي تطبيق helloworld على تبعيات الملف المناسبة BUILD للرسم البياني لرصد الحواف.