簡介
本 Hello World! 教學課程使用 MediaPipe Framework,開發在 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」,並使用適當的機構 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
目錄內。
請將這些檔案複製到名為 HelloWorld
的目錄,一個可以存取 MediaPipe Framework 原始碼的位置。例如,本教學課程將建構的應用程式原始碼位於 mediapipe/examples/ios/HelloWorld
。在本程式碼研究室中,我們會將這個路徑稱為 $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 emulators],選取您的裝置,然後將上述指令產生的 .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 架構提供另一個名為 MPPLayerRenderer
的公用程式,可在螢幕上顯示圖片。此公用程式可用於顯示 CVPixelBufferRef
物件,也就是 MPPCameraInputSource
提供給其委派代表的圖片類型。
在 ViewController.m
中,新增下列匯入行:
#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 裝置上建構並執行應用程式。接受相機權限後,您應該會看到攝影機的即時監控畫面。
現在可以在 MediaPipe 圖表中使用相機影格。
在 iOS 中使用 MediaPipe 圖表
新增相關的依附元件
我們已新增 MediaPipe 架構程式碼的依附元件,其中包含使用 iOS API 的 iOS API,以便使用 MediaPipe 圖表。要使用 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 "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
以 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 裝置上建構並執行應用程式。您應該在即時視訊動態饋給中看見執行邊緣偵測圖表的結果。恭喜!
請注意,iOS 範例現在會使用 common 範本應用程式。本教學課程中的程式碼用於 common 範本應用程式。helloworld 應用程式擁有邊緣偵測圖適用的 BUILD
檔案依附元件。