Hello World!(iOS 版)

はじめに

この Hello World! チュートリアルでは、MediaPipe フレームワークを使用して、iOS で MediaPipe グラフを実行する iOS アプリを開発します。

作業内容

iOS デバイスでライブ動画ストリームに適用される、リアルタイムの Sobel エッジ検出用のシンプルなカメラアプリ。

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 出力ストリームに送信されます。

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 ディレクトリの内容を以下に示します。

  1. AppDelegate.hAppDelegate.m
  2. ViewController.hViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboardLaunch.storyboard
  6. このディレクトリは、Assets.xcassets ディレクトリ内に配置してはいけません。

これらのファイルを、MediaPipe Framework のソースコードにアクセスできる場所に HelloWorld というディレクトリにコピーします。たとえば、このチュートリアルで構築するアプリケーションのソースコードは mediapipe/examples/ios/HelloWorld にあります。Codelab では、このパスを $APPLICATION_PATH と呼びます。

$APPLICATION_PATHBUILD ファイルを作成し、次のビルドルールを追加します。

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/helloworldHelloWorldApp アプリケーションをビルドするには、次のコマンドを使用します。

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 にフレームを取得する必要があります。MPPCameraInputSourceMPPInputSource のサブクラスであり、委譲にプロトコルを提供する 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 に返されます。グラフを初期化した後に次の行を追加して、ViewControllermediapipeGraph オブジェクトのデリゲートとして設定します。

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];
}

imageBufferself.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 デバイスでアプリをビルドして実行します。ライブ動画フィードでエッジ検出グラフを実行した結果が表示されます。おめでとうございます!

edge_detection_ios_gpu_gif

iOS の例では、commonテンプレート アプリを使用することに注意してください。このチュートリアルのコードは、一般的なcommonテンプレート アプリで使用されます。helloworld アプリには、エッジ検出グラフに適した BUILD ファイルの依存関係があります。