mediapipeとは
Googleの開発しているML推定のパイプラインを簡単に設定できるライブラリ。Windowsを初めiOSやAndroidなどマルチプラットフォームに対応している。
mediapipeにはプリセットで手や関節の位置推定や顔の位置推定のモデルがバンドルされていてexampleで確認することができます。
https://github.com/google/mediapipe/tree/master/mediapipe/models
iOSアプリにmediapipeを組み込む
iOSアプリにmediapipeで作ったパイプラインを組み込むには、一度フレームワークとしてビルドするかiOSアプリをbazelで管理する必要があります。
ここでは既存のアプリに組み込むことを想定してフレームワークとしてビルドしてみましょう。
また今回の作業は以下のブランチで行っていたものです。
説明を省いている箇所はコミットなどを覗いてみてください。(もしくはqiitaにコメントいただければ補足します)
https://github.com/noppefoxwolf/mediapipe/tree/develop/TrackerFramework
mediapipeをクローンする
git clone git@github.com:google/mediapipe.git
bazelをインストールする
mediapipeはbazel 1.1.0が必要なため、bazelをインストールします。
bazelはgoogle製のビルドツールで、mediapipeはbazelで構築されているため必要になります。
installスクリプトを下記からダウンロードして、実行します。
$ ./bazel-1.1.0-installer-darwin-x86_64.sh
トラッカーのコードを書く
Swiftから使いやすいように簡単にトラッカーを実装します。
# import <Foundation/Foundation.h>
# import <CoreVideo/CoreVideo.h>
@class Landmark;
@class HandTracker;
@protocol TrackerDelegate <NSObject>
@optional
- (void)handTracker: (HandTracker*)handTracker didOutputLandmarks: (NSArray<Landmark *> *)landmarks;
- (void)handTracker: (HandTracker*)handTracker didOutputPixelBuffer: (CVPixelBufferRef)pixelBuffer;
@end
@interface HandTracker : NSObject
- (instancetype)init;
- (void)startGraph;
- (void)processVideoFrame:(CVPixelBufferRef)imageBuffer;
@property (weak, nonatomic) id <TrackerDelegate> delegate;
@end
@interface Landmark: NSObject
@property(nonatomic, readonly) float x;
@property(nonatomic, readonly) float y;
@property(nonatomic, readonly) float z;
@end
# import "HandTracker.h"
# import "mediapipe/objc/MPPGraph.h"
# import "mediapipe/objc/MPPCameraInputSource.h"
# import "mediapipe/objc/MPPLayerRenderer.h"
# include "mediapipe/framework/formats/landmark.pb.h"
static NSString* const kGraphName = @"hand_tracking_mobile_gpu";
static const char* kInputStream = "input_video";
static const char* kOutputStream = "output_video";
static const char* kLandmarksOutputStream = "hand_landmarks";
static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue";
@interface HandTracker() <MPPGraphDelegate>
@property(nonatomic) MPPGraph* mediapipeGraph;
@end
@interface Landmark()
- (instancetype)initWithX:(float)x y:(float)y z:(float)z;
@end
@implementation HandTracker {}
# pragma mark - Cleanup methods
- (void)dealloc {
self.mediapipeGraph.delegate = nil;
[self.mediapipeGraph cancel];
// Ignore errors since we're cleaning up.
[self.mediapipeGraph closeAllInputStreamsWithError:nil];
[self.mediapipeGraph waitUntilDoneWithError:nil];
}
# pragma mark - MediaPipe graph methods
+ (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];
[newGraph addFrameOutputStream:kLandmarksOutputStream outputPacketType:MPPPacketTypeRaw];
return newGraph;
}
- (instancetype)init
{
self = [super init];
if (self) {
self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName];
self.mediapipeGraph.delegate = self;
// Set maxFramesInFlight to a small value to avoid memory contention for real-time processing.
self.mediapipeGraph.maxFramesInFlight = 2;
}
return self;
}
- (void)startGraph {
// Start running self.mediapipeGraph.
NSError* error;
if (![self.mediapipeGraph startWithError:&error]) {
NSLog(@"Failed to start graph: %@", error);
}
}
# pragma mark - MPPGraphDelegate methods
// Receives CVPixelBufferRef from the MediaPipe graph. Invoked on a MediaPipe worker thread.
- (void)mediapipeGraph:(MPPGraph*)graph
didOutputPixelBuffer:(CVPixelBufferRef)pixelBuffer
fromStream:(const std::string&)streamName {
if (streamName == kOutputStream) {
[_delegate handTracker: self didOutputPixelBuffer: pixelBuffer];
}
}
// Receives a raw packet from the MediaPipe graph. Invoked on a MediaPipe worker thread.
- (void)mediapipeGraph:(MPPGraph*)graph
didOutputPacket:(const ::mediapipe::Packet&)packet
fromStream:(const std::string&)streamName {
if (streamName == kLandmarksOutputStream) {
if (packet.IsEmpty()) { return; }
const auto& landmarks = packet.Get<::mediapipe::NormalizedLandmarkList>();
NSMutableArray<Landmark *> *result = [NSMutableArray array];
for (int i = 0; i < landmarks.landmark_size(); ++i) {
Landmark *landmark = [[Landmark alloc] initWithX:landmarks.landmark(i).x()
y:landmarks.landmark(i).y()
z:landmarks.landmark(i).z()];
[result addObject:landmark];
}
[_delegate handTracker: self didOutputLandmarks: result];
}
}
- (void)processVideoFrame:(CVPixelBufferRef)imageBuffer {
[self.mediapipeGraph sendPixelBuffer:imageBuffer
intoStream:kInputStream
packetType:MPPPacketTypePixelBuffer];
}
@end
@implementation Landmark
- (instancetype)initWithX:(float)x y:(float)y z:(float)z
{
self = [super init];
if (self) {
_x = x;
_y = y;
_z = z;
}
return self;
}
@end
BUILDファイルを書く
bazelはBUILDファイルを書くことで、ビルドオプションを設定します。
また、mediapipeはいくつかのビルドオプションが既に設定されているので、mediapipeのディレクトリ以下に次のように配置します。
load("@build_bazel_rules_apple//apple:ios.bzl", "ios_framework")
ios_framework(
name = "HandTracker",
hdrs = [
"HandTracker.h",
],
infoplists = ["Info.plist"],
bundle_id = "com.noppelab.HandTracker",
families = ["iphone", "ipad"],
minimum_os_version = "10.0",
deps = [
":HandTrackingGpuAppLibrary",
"@ios_opencv//:OpencvFramework",
],
)
# To use the 3D model instead of the default 2D model, add "--define 3D=true" to the
# bazel build command.
config_setting(
name = "use_3d_model",
define_values = {
"3D": "true",
},
)
genrule(
name = "model",
srcs = select({
"//conditions:default": ["//mediapipe/models:hand_landmark.tflite"],
":use_3d_model": ["//mediapipe/models:hand_landmark_3d.tflite"],
}),
outs = ["hand_landmark.tflite"],
cmd = "cp $< $@",
)
objc_library(
name = "HandTrackingGpuAppLibrary",
srcs = [
"HandTracker.mm",
],
hdrs = [
"HandTracker.h",
],
data = [
":model",
"//mediapipe/graphs/hand_tracking:hand_tracking_mobile_gpu_binary_graph",
"//mediapipe/models:palm_detection.tflite",
"//mediapipe/models:palm_detection_labelmap.txt",
],
sdk_frameworks = [
"AVFoundation",
"CoreGraphics",
"CoreMedia",
"UIKit"
],
deps = [
"//mediapipe/objc:mediapipe_framework_ios",
"//mediapipe/objc:mediapipe_input_sources_ios",
"//mediapipe/objc:mediapipe_layer_renderer",
] + select({
"//mediapipe:ios_i386": [],
"//mediapipe:ios_x86_64": [],
"//conditions:default": [
"//mediapipe/graphs/hand_tracking:mobile_calculators",
"//mediapipe/framework/formats:landmark_cc_proto",
],
}),
)
Info.plistを作る
フレームワークにバンドルするInfo.plistを適当に作ります。
を参考にしてください。
フレームワークをビルドする
bazelを使ってフレームワークをビルドします。
$ bazel build --config=ios_fat --define 3D=true mediapipe/develop:HandTracker
ビルドが成功するとbazel-bin/mediapipe/develop/
にzipファイルが生成されているので、それを解凍すると.frameworkファイルが出てきます。
framework search pathやheader search pathを設定すれば、iOSアプリから呼び出すことができます。
面倒な場合
とりあえず3D HandTrackerはcarthageで入れられるようにしてみたので、面倒な場合はこちらを使ってみてください。