iOS 上的「Hello World」!

簡介

哈囉,世界!教學課程使用 MediaPipe Framework 開發 iOS 以便在 iOS 上執行 MediaPipe 圖表。

建構目標

簡易的相機應用程式,可對即時影像進行即時 Sobel 邊緣偵測 透過 iOS 裝置串流播放

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 輸出串流。

第二個節點 SobelEdgesCalculator 會將邊緣偵測功能套用至連入 位於 luma_video 資料流並輸出的封包會產生 output_video 的輸出內容 串流。

我們的 iOS 應用程式會顯示 output_video 的輸出影像影格 串流。

最低限度的應用程式設定

首先,我們先從簡單的 iOS 應用程式開始,然後示範如何使用 bazel 建構應用程式

首先,透過「File」(檔案) > 建立 XCode 專案新增 >單一檢視畫面應用程式。

將產品名稱設為「HelloWorld」,並使用適當的機構 識別碼,例如 com.google.mediapipe。機構 ID 以及產品名稱做為應用程式的 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 目錄如下:

  1. AppDelegate.hAppDelegate.m
  2. ViewController.hViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboardLaunch.storyboard
  6. 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'

例如,如要在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 通訊協定實作任何方法之前,我們必須先 第一,設定要顯示相機鏡頭的畫面媒體管道架構提供 另一個名為 MPPLayerRenderer 的公用程式,用來在螢幕上顯示圖片。這個 公用程式可用來顯示 CVPixelBufferRef 物件,也就是 由 MPPCameraInputSource 提供給委派代表的圖片。

ViewController.m 中新增以下匯入行:

#import "mediapipe/objc/MPPLayerRenderer.h"

為了顯示螢幕影像,我們必須新增一個 UIView 物件,稱為 _liveViewViewController

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 架構程式碼的依附元件,其中包含 才能使用 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;

為避免在處理即時視訊動態饋給的影格時造成記憶體爭用情形,請加入 加入下列程式碼:

// 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 裝置上建立並執行應用程式。您應該會看到 對即時影像內容執行邊緣偵測圖表的結果恭喜!

edge_detection_ios_gpu_gif

請注意,iOS 範例現已採用 common 範本應用程式。程式碼中的 本教學課程用於 common 範本應用程式。helloworld 應用程式具備 適當的 BUILD 檔案依附元件。