Hello World! trên iOS

Giới thiệu

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

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

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

edge_detection_ios_gpu_gif

Thiết lập

  1. Cài đặt Khung MediaPipe trên hệ thống của bạn, xem phần Cài đặt khung để biết thông tin chi tiết.
  2. Thiết lập thiết bị iOS của bạn để 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 đồ 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"
}

Biểu đồ trực quan đượ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ả các khung hình sắp tới do máy ảnh trên thiết bị của bạn cung cấp.

Nút đầu tiên trong biểu đồ, LuminanceCalculator, nhận một gói duy nhất (hình ảnh ) và áp dụng thay đổi về độ chói bằng chương trình đổ bóng OpenGL. Kết quả khung hình ảnh này 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 lệnh đến các gói trong luồng luma_video và xuất ra kết quả đầu ra output_video luồng.

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

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

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

Trước tiên, hãy tạo một dự án XCode qua File > Mới > Ứng dụng Chế độ xem đơn.

Đặt tên sản phẩm thành "HelloWorld" và sử dụng một tổ chức phù hợp mã nhận dạng, chẳng hạn như com.google.mediapipe. Mã nhận dạng tổ chức cùng với tên sản phẩm sẽ là bundle_id cho ứng dụng, chẳng hạn như com.google.mediapipe.HelloWorld

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

Lưu dự án vào một vị trí thích hợp. Đặt tên 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 để tạo ứng dụng iOS. Nội dung của Thư mục $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld được liệt kê dưới đây:

  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 thư mục có tên HelloWorld vào một vị trí có thể truy cập mã nguồn Khung MediaPipe. Ví dụ: mã nguồn của 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 một tệp BUILD trong $APPLICATION_PATH và thêm bản dựng sau đây quy tắc:

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 thêm các phần phụ thuộc cho AppDelegateViewController, main.m và bảng phân cảnh của ứng dụng. Chiến lược phát hành đĩa đơn ứ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 Object-C HelloWorldAppLibrary được tạo để tạo ứng dụng iOS nhằm cài đặt trên thiết bị iOS của bạn.

Để tạo ứng dụng, hãy sử dụng lệnh sau trong một 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ở Window > Thiết bị và Trình mô phỏng, hãy chọn thiết bị rồi thêm tệp .ipa được tạo bằng lệnh ở trên vào thiết bị của bạn. Đây là tài liệu về 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 khung nhìn sẽ hiện màn hình trắng trống.

Sử dụng camera cho nguồn cấp dữ liệu 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 từ máy ảnh. Lớp này sử dụng API AVCaptureSession để tải các 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ợ 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 một đố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 khởi chạy _cameraSource, đặt giá trị đặt trước cho phiên chụp và camera để sử dụng.

Chúng ta cần đưa các khung hình từ _cameraSource vào ứng dụng của mình ViewController để hiển thị chúng. MPPCameraInputSource là lớp con của MPPInputSource, cung cấp một giao thức cho các đại biểu, cụ thể là MPPInputSourceDelegate. Để ứng dụng ViewController của chúng ta có thể là một đại biểu trong tổng số _cameraSource.

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ý các khung hình nhận được, 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 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 chạy phương thức Đố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ẽ dùng hàng đợi nối tiếp có mức độ ưu tiên QOS_CLASS_USER_INTERACTIVE cho đang xử lý khung hình camera.

Thêm dòng sau ở đầu tệp vào sau tiêu đề nhập giao diện/cách 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, chúng ta phải đầu tiên, thiết lập cách hiển thị khung máy ảnh. Khung Mediapipe 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. Chiến dịch này phần mềm tiện ích có thể dùng để hiển thị đối tượng CVPixelBufferRef, đây là loại hình ảnh do MPPCameraInputSource cung cấp cho người được uỷ quyền.

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

#import "mediapipe/objc/MPPLayerRenderer.h"

Để hiện 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 đến 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 đối tượng UIView từ thư viện đối tượng vào View của lớp ViewController. Thêm đầu ra tham chiếu từ thành phần 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 của sao cho nó được căn 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 tạo đố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 tôi kiểm tra xem mình lấy khung hình từ nguồn phù hợp, tức là _cameraSource. Sau đó, chúng tôi hiển thị khung hình nhận được từ máy ảnh 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 chế độ xem để hiển thị các khung hình sắp xuất hiện. Để làm được điều này, chúng tôi sẽ triển khai Hàm viewWillAppear:(BOOL)animated:

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

Trước khi bắt đầu chạy camera, chúng ta cần được người dùng cho phép truy cập vào camera. 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 có đã 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 BUILD tệp:

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 một URL trực tiếp nguồn cấp dữ liệu chế độ xem camera sau khi chấp nhận 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 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 trên biểu đồ mà chúng tôi dự định sử dụng trong ứng dụng của mình. Thêm đoạn mã sau dòng vào danh sách data trong tệp BUILD của bạn:

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

Bây giờ, hãy thêm phần phụ thuộc vào các máy tính dùng trong biểu đồ này trong trường deps trong 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 để được hỗ trợ Mục tiêu 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 hằng số tĩnh bằng 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ư được giải thích trong nhận xét ở trên, chúng tôi sẽ khởi tạo đồ thị này theo viewDidLoad đầu tiên. Để thực hiện việc này, chúng ta cần tải biểu đồ từ tệp .pbtxt bằng cách sử dụ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;
}

Sử 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 trở lại ViewController. Thêm dòng sau đây sau khi khởi tạo biểu đồ để đặt giá trị ViewController làm đối tượng uỷ quyền của đối tượng mediapipeGraph:

self.mediapipeGraph.delegate = self;

Để tránh tình trạng tranh chấp bộ nhớ trong khi xử lý các 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 tôi:

[_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 chúng tôi nhận được khung hình từ máy ảnh trong processVideoFrame chúng ta đã hiển thị chúng trong _liveView bằng _renderer. Bây giờ, chúng tôi cần gửi các khung đó đến biểu đồ và hiển thị kết quả thay thế. Sửa đổi phương thức triển khai hàm này để làm 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 tin thuộc loại này MPPPacketTypePixelBuffer vào luồng đầu vào kInputStream, tức là "input_video".

Biểu đồ sẽ chạy với 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 thực thể đại diện sau để nhận các gói trên luồng đầu ra này và hiển thị chúng 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 định nghĩa giao diện của ViewController bằng MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

Chỉ vậy thôi! Tạo bản dựng 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 ứng dụng mẫu phổ biến. Mã trong hướng dẫn này được dùng trong ứng dụng mẫu phổ biến. Ứng dụng helloworld có phần phụ thuộc tệp BUILD thích hợp cho biểu đồ phát hiện cạnh.