Hello World! trên iOS

Giới thiệu

Hướng dẫn "Hello World!" này sử dụng Khung MediaPipe để phát triển một ứng dụng iOS chạy biểu đồ MediaPipe trên iOS.

Sản phẩm bạn sẽ tạo ra

Một ứng dụng máy ảnh đơn giản giúp phát hiện cạnh Sobel theo thời gian thực, được áp dụng cho luồng video trực tiếp trên thiết bị iOS.

edge_detection_ios_gpu_gif

Thiết lập

  1. Cài đặt MediaPipe Framework trên hệ thống của bạn, xem phần Hướng dẫn cài đặt Frame để biết thông tin chi tiết.
  2. Thiết lập thiết bị iOS cho mục đích phát triển.
  3. Thiết lập Bazel trên hệ thống của bạn để xây dựng và triển khai ứng dụng iOS.

Biểu đồ cho tính năng phát hiện cạnh

Chúng ta sẽ sử dụng biểu đồ sau đây, 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"
}

Hình ảnh của biểu đồ được hiển thị dưới đây:

edge_detection_mobile_gpu

Biểu đồ này có một luồng đầu vào duy nhất tên là input_video cho tất cả khung hình đến mà máy ảnh của thiết bị cung cấp.

Nút đầu tiên trong biểu đồ, LuminanceCalculator, lấy một gói duy nhất (khung hình ảnh) và áp dụng thay đổi về độ chói bằng chương trình đổ bóng OpenGL. Khung hình ảnh thu được sẽ được gửi đến luồng đầu ra luma_video.

Nút thứ hai, SobelEdgesCalculator áp dụng tính năng phát hiện cạnh cho các gói đến trong luồng luma_video và xuất kết quả là luồng đầu ra output_video.

Ứng dụng iOS của chúng ta sẽ hiển thị khung hình ảnh đầu ra của luồng output_video.

Thiết lập ứng dụng tối thiểu ban đầu

Trước tiên, chúng tôi bắt đầu bằng một ứng dụng iOS đơn giản và minh hoạ cách sử dụng bazel để tạo ứng dụng này.

Trước tiên, hãy tạo một dự án XCode thông qua File > New > Single View App (Tệp > Mới > Ứng dụng khung hiển thị đơn).

Hãy đặt tên sản phẩm thành "HelloWorld" và sử dụng giá trị nhận dạng tổ chức thích hợp, chẳng hạn như com.google.mediapipe. Giá trị nhận dạng tổ chức cùng với tên sản phẩm sẽ là bundle_id của ứng dụng, chẳng hạn như com.google.mediapipe.HelloWorld.

Đặt ngôn ngữ thành Objective-C.

Lưu dự án vào một vị trí thích hợp. Hãy gọi đây là $PROJECT_TEMPLATE_LOC. Vì vậy, dự án của bạn sẽ nằm trong thư mục $PROJECT_TEMPLATE_LOC/HelloWorld. Thư mục này sẽ chứa một thư mục khác có tên HelloWorld và một tệp HelloWorld.xcodeproj.

HelloWorld.xcodeproj sẽ không hữu ích cho hướng dẫn này, vì chúng ta sẽ sử dụng bazel để xây dựng ứng dụng iOS. Dưới đây là nội dung của thư mục $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld:

  1. AppDelegate.hAppDelegate.m
  2. ViewController.hViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboardLaunch.storyboard
  6. Thư mục Assets.xcassets.

Sao chép các tệp này vào một thư mục có tên HelloWorld ở một vị trí có thể truy cập vào mã nguồn MediaPipe Framework. Ví dụ: mã nguồn của ứng dụng mà chúng tôi sẽ tạo trong hướng dẫn này nằm trong mediapipe/examples/ios/HelloWorld. Chúng tôi sẽ gọi đường dẫn này là $APPLICATION_PATH trong suốt lớp học lập trình này.

Tạo tệp BUILD trong $APPLICATION_PATH và thêm các quy tắc bản dựng sau:

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 = [],
)

Quy tắc objc_library sẽ thêm các phần phụ thuộc cho các lớp AppDelegateViewController, main.m và bảng phân cảnh của ứng dụng. Ứng dụng theo mẫu chỉ phụ thuộc vào SDK UIKit.

Quy tắc ios_application sử dụng thư viện Objective-C HelloWorldAppLibrary được tạo để tạo ứng dụng iOS để cài đặt trên thiết bị iOS của bạn.

Để tạo ứng dụng, hãy dùng lệnh sau trong cửa sổ dòng lệnh:

bazel build -c opt --config=ios_arm64 <$APPLICATION_PATH>:HelloWorldApp'

Ví dụ: để tạo ứng dụng HelloWorldApp trong mediapipe/examples/ios/helloworld, hãy dùng lệnh sau:

bazel build -c opt --config=ios_arm64 mediapipe/examples/ios/helloworld:HelloWorldApp

Sau đó, quay lại XCode, mở Cửa sổ > Thiết bị và Trình mô phỏng, chọn thiết bị của bạn và thêm tệp .ipa do lệnh ở trên tạo vào thiết bị của bạn. Đây là tài liệu về cách thiết lập và biên dịch ứng dụng Khung iOS.

Mở ứng dụng trên thiết bị. Vì trống nên sẽ hiển thị một màn hình trắng trống.

Sử dụng máy ảnh cho trang Chế độ xem trực tiếp

Trong hướng dẫn này, chúng ta sẽ sử dụng lớp MPPCameraInputSource để truy cập và lấy khung hình qua máy ảnh. Lớp này sử dụng API AVCaptureSession để lấy khung hình từ máy ảnh.

Tuy nhiên, trước khi sử dụng lớp này, hãy thay đổi tệp Info.plist để hỗ trợ việc sử dụng máy ảnh trong ứng dụng.

Trong ViewController.m, hãy thêm dòng nhập sau:

#import "mediapipe/objc/MPPCameraInputSource.h"

Thêm đoạn mã sau vào khối triển khai để tạo đối tượng _cameraSource:

@implementation ViewController {
  // Handles camera access via AVCaptureSession library.
  MPPCameraInputSource* _cameraSource;
}

Thêm mã sau vào 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;
}

Mã này sẽ khởi chạy _cameraSource, đặt giá trị đặt trước cho phiên chụp và máy ảnh sẽ sử dụng.

Chúng ta cần đưa các khung hình từ _cameraSource vào ViewController của ứng dụng để hiển thị các khung đó. MPPCameraInputSource là một lớp con của MPPInputSource, cung cấp giao thức cho các thực thể đại diện, cụ thể là MPPInputSourceDelegate. Vì vậy, ứng dụng ViewController của chúng ta có thể là đại diện của _cameraSource.

Hãy cập nhật định nghĩa giao diện của ViewController cho phù hợp:

@interface ViewController () <MPPInputSourceDelegate>

Để xử lý việc thiết lập máy ảnh và xử lý khung hình đến, chúng ta nên sử dụng hàng đợi khác với hàng đợi chính. Thêm đoạn mã sau đây vào khối triển khai của ViewController:

// Process camera frames on this queue.
dispatch_queue_t _videoQueue;

Trong viewDidLoad(), hãy thêm dòng sau sau khi khởi tạo đối tượng _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

Và thêm mã sau để khởi chạy hàng đợi trước khi thiết lập đối tượng _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);

Chúng ta sẽ sử dụng hàng đợi nối tiếp có mức độ ưu tiên QOS_CLASS_USER_INTERACTIVE để xử lý khung hình máy ảnh.

Hãy thêm dòng sau đây sau mục nhập tiêu đề ở đầu tệp, trước khi giao diện/triển khai của ViewController:

static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue";

Trước khi triển khai bất kỳ phương thức nào từ giao thức MPPInputSourceDelegate, trước tiên, chúng ta phải thiết lập cách hiển thị khung máy ảnh. Mediapipe Framework cung cấp một tiện ích khác có tên là MPPLayerRenderer để hiển thị hình ảnh trên màn hình. Bạn có thể dùng tiện ích này để hiển thị các đối tượng CVPixelBufferRef (là loại hình ảnh do MPPCameraInputSource cung cấp cho thực thể đại diện).

Trong ViewController.m, hãy thêm dòng nhập sau:

#import "mediapipe/objc/MPPLayerRenderer.h"

Để hiển thị hình ảnh màn hình, chúng ta cần thêm một đối tượng UIView mới có tên là _liveView vào ViewController.

Thêm các dòng sau vào khối triển khai của ViewController:

// Display the camera preview frames.
IBOutlet UIView* _liveView;
// Render frames in a layer.
MPPLayerRenderer* _renderer;

Chuyển đến Main.storyboard, thêm một đối tượng UIView từ thư viện đối tượng vào View của lớp ViewController. Thêm một lối ra tham chiếu từ khung hiển thị này vào đối tượng _liveView mà bạn vừa thêm vào lớp ViewController. Đổi kích thước khung hiển thị để khung hiển thị đó nằm chính giữa và bao phủ toàn bộ màn hình ứng dụng.

Quay lại ViewController.m và thêm mã sau vào viewDidLoad() để khởi động đối tượng _renderer:

_renderer = [[MPPLayerRenderer alloc] init];
_renderer.layer.frame = _liveView.layer.bounds;
[_liveView.layer addSublayer:_renderer.layer];
_renderer.frameScaleMode = MPPFrameScaleModeFillAndCrop;

Để lấy khung hình từ máy ảnh, chúng ta sẽ triển khai phương thức sau:

// 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);
  });
}

Đây là phương thức uỷ quyền của MPPInputSource. Trước tiên, chúng ta sẽ kiểm tra để đảm bảo rằng mình đang nhận khung hình từ đúng nguồn, tức là _cameraSource. Sau đó, chúng ta sẽ hiển thị khung hình nhận được từ máy ảnh thông qua _renderer trên hàng đợi chính.

Bây giờ, chúng ta cần khởi động máy ảnh ngay khi khung hiển thị để hiển thị khung hình sắp xuất hiện. Để thực hiện việc này, chúng ta sẽ triển khai hàm viewWillAppear:(BOOL)animated:

-(void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
}

Trước khi bắt đầu chạy máy ảnh, chúng ta cần người dùng cho phép truy cập vào máy ảnh. MPPCameraInputSource cung cấp một hàm requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler để yêu cầu quyền truy cập vào máy ảnh và thực hiện một số thao tác khi người dùng phản hồi. Thêm mã sau vào viewWillAppear:animated:

[_cameraSource requestCameraAccessWithCompletionHandler:^void(BOOL granted) {
  if (granted) {
    dispatch_async(_videoQueue, ^{
      [_cameraSource start];
    });
  }
}];

Trước khi tạo ứng dụng, hãy thêm các phần phụ thuộc sau vào tệp BUILD:

sdk_frameworks = [
    "AVFoundation",
    "CoreGraphics",
    "CoreMedia",
],
deps = [
    "//mediapipe/objc:mediapipe_framework_ios",
    "//mediapipe/objc:mediapipe_input_sources_ios",
    "//mediapipe/objc:mediapipe_layer_renderer",
],

Bây giờ, hãy tạo và chạy ứng dụng trên thiết bị iOS của bạn. Bạn sẽ thấy nguồn cấp dữ liệu chế độ xem camera trực tiếp sau khi chấp nhận các quyền truy cập vào camera.

Chúng ta hiện đã sẵn sàng sử dụng khung máy ảnh trong biểu đồ MediaPipe.

Sử dụng biểu đồ MediaPipe trong iOS

Thêm các phần phụ thuộc có liên quan

Chúng tôi đã thêm các phần phụ thuộc của mã khung MediaPipe có chứa API iOS để sử dụng biểu đồ MediaPipe. Để sử dụng biểu đồ MediaPipe, chúng ta cần thêm phần phụ thuộc vào biểu đồ mà chúng ta định sử dụng trong ứng dụng của mình. Thêm dòng sau vào danh sách data trong tệp BUILD:

"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",

Bây giờ, hãy thêm phần phụ thuộc vào các công cụ tính toán dùng trong biểu đồ này tại trường deps của tệp BUILD:

"//mediapipe/graphs/edge_detection:mobile_calculators",

Cuối cùng, hãy đổi tên tệp ViewController.m thành ViewController.mm để hỗ trợ Objective-C++.

Sử dụng biểu đồ trong ViewController

Trong ViewController.m, hãy thêm dòng nhập sau:

#import "mediapipe/objc/MPPGraph.h"

Khai báo một hằng số tĩnh theo tên của biểu đồ, luồng đầu vào và luồng đầu ra:

static NSString* const kGraphName = @"mobile_gpu";

static const char* kInputStream = "input_video";
static const char* kOutputStream = "output_video";

Thêm thuộc tính sau vào giao diện của ViewController:

// The MediaPipe graph currently in use. Initialized in viewDidLoad, started in viewWillAppear: and
// sent video frames on _videoQueue.
@property(nonatomic) MPPGraph* mediapipeGraph;

Như đã giải thích trong nhận xét ở trên, trước tiên, chúng tôi sẽ khởi chạy biểu đồ này trong viewDidLoad. Để làm như vậy, chúng ta cần tải biểu đồ từ tệp .pbtxt bằng hàm sau:

+   (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;
}

Hãy dùng hàm này để khởi tạo biểu đồ trong viewDidLoad như sau:

self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName];

Biểu đồ sẽ gửi kết quả xử lý khung máy ảnh cho ViewController. Hãy thêm dòng sau sau khi khởi chạy biểu đồ để thiết lập ViewController dưới dạng thực thể đại diện của đối tượng mediapipeGraph:

self.mediapipeGraph.delegate = self;

Để tránh tranh chấp bộ nhớ trong khi xử lý khung hình từ nguồn cấp dữ liệu video trực tiếp, hãy thêm dòng sau:

// Set maxFramesInFlight to a small value to avoid memory contention for real-time processing.
self.mediapipeGraph.maxFramesInFlight = 2;

Bây giờ, hãy bắt đầu biểu đồ khi người dùng cấp quyền sử dụng máy ảnh trong ứng dụng của chúng ta:

[_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];
    });
  }
}];

Trước đó, khi nhận được khung hình từ máy ảnh trong hàm processVideoFrame, chúng ta đã hiển thị các khung hình đó trong _liveView bằng _renderer. Bây giờ, chúng ta cần gửi các khung đó đến biểu đồ và kết xuất kết quả. Sửa đổi cách triển khai hàm này để thực hiện những việc sau:

-   (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];
}

Chúng ta gửi imageBuffer đến self.mediapipeGraph dưới dạng một gói thuộc loại MPPPacketTypePixelBuffer vào luồng đầu vào kInputStream, tức là "input_video".

Biểu đồ sẽ chạy bằng gói đầu vào này và xuất kết quả trong kOutputStream, tức là "output_video". Chúng ta có thể triển khai phương thức uỷ quyền sau đây để nhận các gói trên luồng đầu ra này và hiển thị các gói đó trên màn hình:

-   (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);
    });
  }
}

Cập nhật phần định nghĩa giao diện của ViewController bằng MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

Chỉ vậy thôi! Tạo và chạy ứng dụng trên thiết bị iOS của bạn. Bạn sẽ thấy kết quả chạy biểu đồ phát hiện cạnh trên nguồn cấp dữ liệu video trực tiếp. Xin chúc mừng!

edge_detection_ios_gpu_gif

Xin lưu ý rằng các ví dụ về iOS hiện sử dụng một ứng dụng mẫu common. Mã trong hướng dẫn này được dùng trong ứng dụng mẫu common. Ứng dụng helloworld có tệp phần phụ thuộc BUILD thích hợp cho biểu đồ phát hiện cạnh.