はじめに
この Hello World! チュートリアルでは、MediaPipe フレームワークを使用して、iOS で MediaPipe グラフを実行する iOS アプリを開発します。
作業内容
iOS デバイスでライブ動画ストリームに適用される、リアルタイムの Sobel エッジ検出用のシンプルなカメラアプリ。
セットアップ
- MediaPipe Framework をシステムにインストールします。詳しくは、フレームワークのインストール ガイドをご覧ください。
- 開発用に iOS デバイスをセットアップします。
- システムに 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"
}
グラフを図にまとめると、以下のようになります。
このグラフには、デバイスのカメラから提供されるすべての受信フレームについて、input_video
という名前の単一の入力ストリームがあります。
グラフの最初のノードである LuminanceCalculator
は、単一のパケット(画像フレーム)を受け取り、OpenGL シェーダーを使用して輝度の変更を適用します。生成された画像フレームは luma_video
出力ストリームに送信されます。
2 番目のノード SobelEdgesCalculator
は、luma_video
ストリームの受信パケットにエッジ検出を適用し、output_video
出力ストリームに結果を出力します。
iOS アプリケーションは、output_video
ストリームの出力画像フレームを表示します。
最小限のアプリケーションの初期セットアップ
まず、シンプルな iOS アプリケーションを作成し、bazel
を使用してそのアプリケーションを構築する方法を示します。
まず、[File] > [New] > [Single View App] で XCode プロジェクトを作成します。
プロダクト名を「HelloWorld」に設定し、適切な組織 ID(com.google.mediapipe
など)を使用します。組織 ID とプロダクト名がアプリケーションの bundle_id
になります(例: com.google.mediapipe.HelloWorld
)。
言語を Objective-C に設定します。
プロジェクトを適切な場所に保存します。これを $PROJECT_TEMPLATE_LOC
としましょう。そのため、プロジェクトは $PROJECT_TEMPLATE_LOC/HelloWorld
ディレクトリにあります。このディレクトリには、HelloWorld
という名前の別のディレクトリと HelloWorld.xcodeproj
ファイルが含まれます。
このチュートリアルでは、bazel を使用して iOS アプリケーションをビルドするため、HelloWorld.xcodeproj
は役に立ちません。$PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld
ディレクトリの内容を以下に示します。
AppDelegate.h
、AppDelegate.m
ViewController.h
、ViewController.m
main.m
Info.plist
Main.storyboard
、Launch.storyboard
- このディレクトリは、
Assets.xcassets
ディレクトリ内に配置してはいけません。
これらのファイルを、MediaPipe Framework のソースコードにアクセスできる場所に HelloWorld
というディレクトリにコピーします。たとえば、このチュートリアルで構築するアプリケーションのソースコードは 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] > [Devices and Simulators] を開き、デバイスを選択して、上記のコマンドで生成された .ipa
ファイルをデバイスに追加します。iOS フレームワーク アプリのセットアップとコンパイルに関するドキュメントをご覧ください。
デバイスでアプリを開きます。空のので、空白の白い画面が表示されます。
ライブビュー フィードにカメラを使用する
このチュートリアルでは、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 Framework には、画面に画像を表示するための MPPLayerRenderer
という別のユーティリティが用意されています。このユーティリティは、CVPixelBufferRef
オブジェクトの表示に使用できます。これは、MPPCameraInputSource
からデリゲートに提供される画像のタイプです。
ViewController.m
に、次のインポート行を追加します。
#import "mediapipe/objc/MPPLayerRenderer.h"
画面の画像を表示するには、_liveView
という新しい UIView
オブジェクトを 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 デバイスでアプリケーションをビルドして実行します。カメラの権限を許可すると、ライブカメラ ビュー フィードが表示されます。
これで、MediaPipe グラフでカメラフレームを使用する準備が整いました。
iOS で MediaPipe グラフを使用する
関連する依存関係を追加する
MediaPipe グラフを使用するための iOS API を含む MediaPipe フレームワーク コードの依存関係はすでに追加されています。MediaPipe グラフを使用するには、アプリケーションで使用するグラフへの依存関係を追加する必要があります。BUILD
ファイルの data
リストに次の行を追加します。
"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
次に、このグラフで使用されている計算ツールに、BUILD
ファイルの deps
フィールドに依存関係を追加します。
"//mediapipe/graphs/edge_detection:mobile_calculators",
最後に、Objective-C++ をサポートするため、ファイル ViewController.m
の名前を ViewController.mm
に変更します。
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;
ライブ動画フィードのフレームの処理中にメモリ競合を回避するには、次の行を追加します。
// 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);
});
}
}
ViewController
のインターフェース定義を MPPGraphDelegate
で更新します。
@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>
これで終わりです。iOS デバイスでアプリをビルドして実行します。ライブ動画フィードでエッジ検出グラフを実行した結果が表示されます。おめでとうございます!
iOS の例では、commonテンプレート アプリを使用することに注意してください。このチュートリアルのコードは、一般的なcommonテンプレート アプリで使用されます。helloworld アプリには、エッジ検出グラフに適した BUILD
ファイルの依存関係があります。