简介
本 Hello World! 教程使用 MediaPipe 框架来开发一个在 iOS 上运行 MediaPipe 图表的 iOS 应用。
您将构建的模型
一款简单的相机应用,用于对 iOS 设备上的实时视频串流应用的实时 Sobel 边缘检测。
初始设置
边缘检测图表
我们将使用以下图表 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"
}
图表的可视化效果如下所示:
此图包含一个名为 input_video
的输入流,该输入流对应于设备摄像头将提供的所有传入帧。
图中的第一个节点 LuminanceCalculator
接受单个数据包(图像帧),并使用 OpenGL 着色器应用亮度变化。生成的图片帧会发送到 luma_video
输出流。
第二个节点 SobelEdgesCalculator
会对 luma_video
流中的传入数据包应用边缘检测,并输出 output_video
输出流。
我们的 iOS 应用将显示 output_video
流的输出图片帧。
初始最低应用设置
我们首先从一个简单的 iOS 应用开始,并演示如何使用 bazel
来构建该应用。
首先,通过“File”>“New”>“Single View App”创建一个 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
目录的内容:
AppDelegate.h
和AppDelegate.m
ViewController.h
和ViewController.m
main.m
Info.plist
Main.storyboard
和Launch.storyboard
Assets.xcassets
目录中。
将这些文件复制到名为 HelloWorld
的目录中,并放在可以访问 MediaPipe 框架源代码的位置。例如,我们在本教程中构建的应用的源代码位于 mediapipe/examples/ios/HelloWorld
中。在整个 Codelab 中,我们将此路径称为 $APPLICATION_PATH
。
在 $APPLICATION_PATH
中创建 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
规则会为 AppDelegate
和 ViewController
类、main.m
和应用故事板添加依赖项。模板化应用仅依赖于 UIKit
SDK。
ios_application
规则使用生成的 HelloWorldAppLibrary
Objective-C 库来构建 iOS 应用,以便在 iOS 设备上安装。
如需构建应用,请在终端中运行以下命令:
bazel build -c opt --config=ios_arm64 <$APPLICATION_PATH>:HelloWorldApp'
例如,如需在 mediapipe/examples/ios/helloworld
中构建 HelloWorldApp
应用,请使用以下命令:
bazel build -c opt --config=ios_arm64 mediapipe/examples/ios/helloworld:HelloWorldApp
然后,返回 XCode,依次打开“Window”>“Device and Simulators”,选择您的设备,并将上述命令生成的 .ipa
文件添加到您的设备。
本文档提供了有关设置和编译 iOS 框架应用的文档。
在设备上打开该应用。由于该区域是空的,因此应该显示一个空白的白色屏幕。
使用摄像头查看实时画面
在本教程中,我们将使用 MPPCameraInputSource
类从相机访问和抓取帧。此类使用 AVCaptureSession
API 从相机获取帧。
但在使用此类之前,请更改 Info.plist
文件以支持在应用中使用相机。
在 ViewController.m
中,添加以下 import 行:
#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 行:
#import "mediapipe/objc/MPPLayerRenderer.h"
为了显示屏幕图像,我们需要向 ViewController
添加一个名为 _liveView
的新 UIView
对象。
将以下代码行添加到 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 框架代码的依赖项,其中包含用于使用 MediaPipe 图的 iOS API。如需使用 MediaPipe 图,我们需要为要在应用中使用的图添加依赖项。将以下代码行添加到 BUILD
文件的 data
列表中:
"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
现在,在 BUILD
文件的 deps
字段中,向此图表中使用的计算器添加依赖项:
"//mediapipe/graphs/edge_detection:mobile_calculators",
最后,将文件 ViewController.m
重命名为 ViewController.mm
以支持 Objective-C++。
使用ViewController
中的图表
在 ViewController.m
中,添加以下 import 行:
#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
作为 MPPPacketTypePixelBuffer
类型的数据包发送到 self.mediapipeGraph
,并发送到输入流 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 运行边缘检测图的结果。恭喜!
请注意,iOS 示例现在使用的是common模板应用。本教程中的代码用于common模板应用。helloworld 应用具有边缘检测图的相应 BUILD
文件依赖项。