سلام دنیا! در iOS

معرفی

این سلام دنیا! آموزش از MediaPipe Framework برای توسعه یک برنامه iOS استفاده می کند که یک نمودار MediaPipe را در iOS اجرا می کند.

آنچه را که خواهید ساخت

یک برنامه دوربین ساده برای تشخیص لبه Sobel در زمان واقعی برای پخش جریانی ویدیویی زنده در دستگاه iOS اعمال می شود.

edge_detection_ios_gpu_gif

برپایی

  1. MediaPipe Framework را روی سیستم خود نصب کنید، برای جزئیات به راهنمای نصب Framework مراجعه کنید.
  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 از طریق 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 در زیر فهرست شده است:

  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 در مکانی کپی کنید که می تواند به کد منبع 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 خود بسازید و اجرا کنید. شما باید نتایج اجرای نمودار تشخیص لبه را در فید ویدیویی زنده ببینید. تبریک میگم

edge_detection_ios_gpu_gif

لطفاً توجه داشته باشید که نمونه‌های iOS اکنون از یک برنامه قالب معمولی استفاده می‌کنند. کد موجود در این آموزش در اپلیکیشن قالب رایج استفاده می شود. برنامه helloworld وابستگی های فایل BUILD مناسب را برای نمودار تشخیص لبه دارد.