Hello World! (iOS)

简介

你好!世界教程使用 MediaPipe Framework 开发 iOS 在 iOS 上运行 MediaPipe 图的应用。

构建内容

一款简单的相机应用,可对实时视频进行实时 Sobel 边缘检测 在 iOS 设备上流式传输。

edge_detection_ios_gpu_gif

设置

  1. 在系统上安装 MediaPipe 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 项目”新建 >单一视图应用

将产品名称设置为“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.hAppDelegate.m
  2. ViewController.hViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboardLaunch.storyboard
  6. Assets.xcassets 目录中。

将这些文件复制到名为 HelloWorld 的目录中,并放在可以访问的位置 MediaPipe 框架源代码。例如, 我们在本教程中构建的应用位于 mediapipe/examples/ios/HelloWorld。我们将此路径称为 $APPLICATION_PATH

$APPLICATION_PATH 中创建 BUILD 文件,并添加以下 build 规则:

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 规则可为 AppDelegateViewController 类、main.m 和应用分镜脚本。通过 模板化应用仅依赖于 UIKit SDK。

ios_application 规则使用 HelloWorldAppLibrary Objective-C 库 生成用于构建要在 iOS 设备上安装的 iOS 应用。

如需构建应用,请在终端中使用以下命令:

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

例如,如需在 Google Cloud 中构建 HelloWorldApp 应用,请执行以下操作: mediapipe/examples/ios/helloworld,请使用以下命令:

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

然后返回 XCode,打开“Window”(窗口)>设备和模拟器,选择您的 然后将上述命令生成的 .ipa 文件添加到您的设备。 请参阅有关设置和编译 iOS Framework 应用的文档。

在设备上打开该应用。由于它是空的,因此应该显示 空白屏幕。

使用摄像头拍摄实时画面

在本教程中,我们将使用 MPPCameraInputSource 类访问和 从相机抓取画面。此类使用 AVCaptureSession API 获取 从相机取景

但在使用此类之前,请更改 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 框架提供 另一个名为 MPPLayerRenderer 的实用程序,用于在屏幕上显示图像。本次 实用程序可用于显示 CVPixelBufferRef 对象,该对象的类型是 MPPCameraInputSource 向其代理提供的图片。

ViewController.m 中,添加以下导入行:

#import "mediapipe/objc/MPPLayerRenderer.h"

为了显示屏幕的图片,我们需要添加一个名为 UIView _liveViewViewController

将以下代码行添加到 ViewController 的实现块中:

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

转到 Main.storyboard,将对象库中的 UIView 对象添加到 ViewController 类的 View。将此视图的引用输出添加到 刚刚添加到 ViewController 类的 _liveView 对象。调整 视图,使其居中并覆盖整个应用屏幕。

返回 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 设备上构建并运行应用。您应该会看到 接受相机权限后启用相机视图 Feed。

现在,我们可以在 MediaPipe 图中使用相机帧了。

在 iOS 中使用 MediaPipe 图

添加相关依赖项

我们已经添加了 MediaPipe 框架代码的依赖项,其中包含 iOS API 以使用 MediaPipe 图。要使用 MediaPipe 图,我们需要添加一个 依赖于我们打算在应用中使用的图表。添加以下代码 添加到 BUILD 文件的 data 列表中:

"//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;

为避免在处理实时视频 Feed 中的帧时出现内存争用,请将 以下行:

// 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 中收到相机的帧时, 函数,我们使用 _renderer_liveView 中显示它们。现在,我们 需要改为将这些帧发送到图表并渲染结果。修改 此函数的实现来执行以下操作:

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

使用 MPPGraphDelegate 更新 ViewController 的接口定义:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

大功告成!在 iOS 设备上构建并运行应用。您应该会看到 对实时视频 Feed 运行边缘检测图的结果。恭喜!

edge_detection_ios_gpu_gif

请注意,iOS 示例现在使用的是通用模板应用。此处的代码 通用模板应用中使用了本教程。helloworld 应用包含 适当的 BUILD 文件依赖项。