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

مقدمة

مرحبًا بكم برنامج تعليمي حول استخدام MediaPipeframe لتطوير نظام 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 مفيدة لهذا البرنامج التعليمي، لأننا سنستخدم لإنشاء تطبيق 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 مكتبة 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.

افتح التطبيق على جهازك. نظرًا لأنها فارغة، يجب أن تعرض شاشة بيضاء فارغة.

استخدام الكاميرا لخلاصة "العرض المباشر"

في هذا البرنامج التعليمي، سنستخدم الفئة 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، ينبغي لنا عليكم أولاً إعداد طريقة لعرض إطارات الكاميرا. يوفّر إطار عمل Mediapi أداة أخرى اسمها 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 لدعمه. الهدف-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 تستخدم الآن تطبيق النماذج الشائعة. الكود في يُستخدم هذا البرنامج التعليمي في تطبيق النموذج الشائع. يحتوي تطبيق helloworld على تبعيات ملف BUILD المناسبة للرسم البياني لكشف الحواف.