简介
你好!世界教程使用 MediaPipe Framework 开发 iOS 在 iOS 上运行 MediaPipe 图的应用。
构建内容
一款简单的相机应用,可对实时视频进行实时 Sobel 边缘检测 在 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"
}
图表的直观图示如下所示:
此图针对所有传入帧包含一个名为 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
目录如下所列:
AppDelegate.h
和AppDelegate.m
ViewController.h
和ViewController.m
main.m
Info.plist
Main.storyboard
和Launch.storyboard
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
规则可为 AppDelegate
和
ViewController
类、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
_liveView
到 ViewController
。
将以下代码行添加到 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 运行边缘检测图的结果。恭喜!
请注意,iOS 示例现在使用的是通用模板应用。此处的代码
通用模板应用中使用了本教程。helloworld 应用包含
适当的 BUILD
文件依赖项。