Hello World! di iOS

Pengantar

Tutorial Hello World! ini menggunakan MediaPipe Framework untuk mengembangkan aplikasi iOS yang menjalankan grafik MediaPipe di iOS.

Yang akan Anda bangun

Aplikasi kamera sederhana untuk deteksi tepi Sobel secara real-time yang diterapkan ke streaming video live di perangkat iOS.

edge_detection_ios_gpu_gif

Penyiapan

  1. Instal MediaPipe Framework di sistem Anda, lihat Panduan penginstalan framework untuk mengetahui detailnya.
  2. Siapkan perangkat iOS Anda untuk pengembangan.
  3. Siapkan Bazel di sistem untuk membangun dan men-deploy aplikasi iOS.

Grafik untuk deteksi tepi

Kita akan menggunakan grafik berikut, 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"
}

Visualisasi grafik ditampilkan di bawah ini:

edge_detection_mobile_gpu

Grafik ini memiliki satu aliran input bernama input_video untuk semua frame masuk yang akan disediakan oleh kamera perangkat Anda.

Node pertama dalam grafik, LuminanceCalculator, mengambil satu paket (frame gambar) dan menerapkan perubahan luminans menggunakan shader OpenGL. Frame gambar yang dihasilkan dikirim ke aliran output luma_video.

Node kedua, SobelEdgesCalculator, menerapkan deteksi tepi ke paket yang masuk di aliran luma_video dan menghasilkan output dalam aliran output output_video.

Aplikasi iOS kita akan menampilkan frame gambar output dari aliran data output_video.

Penyiapan aplikasi minimal awal

Pertama-tama, kami akan mulai dengan aplikasi iOS sederhana dan mendemonstrasikan cara menggunakan bazel untuk membangunnya.

Pertama, buat project XCode melalui File > New > Single View App.

Tetapkan nama produk ke "HelloWorld", dan gunakan ID organisasi yang sesuai, seperti com.google.mediapipe. ID organisasi beserta nama produk adalah bundle_id untuk aplikasi, seperti com.google.mediapipe.HelloWorld.

Setel bahasa ke Objective-C.

Simpan project ke lokasi yang tepat. Sebut saja ini $PROJECT_TEMPLATE_LOC. Dengan begitu, project Anda akan berada di direktori $PROJECT_TEMPLATE_LOC/HelloWorld. Direktori ini akan berisi direktori lain bernama HelloWorld dan file HelloWorld.xcodeproj.

HelloWorld.xcodeproj tidak akan berguna untuk tutorial ini, karena kita akan menggunakan bazel untuk membangun aplikasi iOS. Isi direktori $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld tercantum di bawah ini:

  1. AppDelegate.h dan AppDelegate.m
  2. ViewController.h dan ViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboard dan Launch.storyboard
  6. Direktori Assets.xcassets.

Salin file ini ke direktori bernama HelloWorld ke lokasi yang dapat mengakses kode sumber MediaPipe Framework. Misalnya, kode sumber aplikasi yang akan kita bangun dalam tutorial ini terletak di mediapipe/examples/ios/HelloWorld. Kita akan menyebut jalur ini sebagai $APPLICATION_PATH di seluruh codelab.

Buat file BUILD di $APPLICATION_PATH dan tambahkan aturan build berikut:

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 = [],
)

Aturan objc_library menambahkan dependensi untuk class AppDelegate dan ViewController, main.m, dan storyboard aplikasi. Aplikasi yang diberi template hanya bergantung pada UIKit SDK.

Aturan ios_application menggunakan library Objective-C HelloWorldAppLibrary yang dibuat untuk mem-build aplikasi iOS untuk diinstal di perangkat iOS Anda.

Untuk mem-build aplikasi, gunakan perintah berikut di terminal:

bazel build -c opt --config=ios_arm64 <$APPLICATION_PATH>:HelloWorldApp'

Misalnya, untuk mem-build aplikasi HelloWorldApp di mediapipe/examples/ios/helloworld, gunakan perintah berikut:

bazel build -c opt --config=ios_arm64 mediapipe/examples/ios/helloworld:HelloWorldApp

Kemudian, kembali ke XCode, buka Window > Devices and Simulators, pilih perangkat Anda, dan tambahkan file .ipa yang dihasilkan oleh perintah di atas ke perangkat Anda. Berikut adalah dokumen tentang menyiapkan dan mengompilasi aplikasi Framework iOS.

Buka aplikasi di perangkat Anda. Karena kosong, kode akan menampilkan layar putih kosong.

Menggunakan kamera untuk feed tayangan live

Dalam tutorial ini, kita akan menggunakan class MPPCameraInputSource untuk mengakses dan mengambil frame dari kamera. Class ini menggunakan AVCaptureSession API untuk mendapatkan frame dari kamera.

Namun sebelum menggunakan class ini, ubah file Info.plist untuk mendukung penggunaan kamera di aplikasi.

Di ViewController.m, tambahkan baris impor berikut:

#import "mediapipe/objc/MPPCameraInputSource.h"

Tambahkan kode berikut ke blok implementasinya untuk membuat objek _cameraSource:

@implementation ViewController {
  // Handles camera access via AVCaptureSession library.
  MPPCameraInputSource* _cameraSource;
}

Tambahkan kode berikut ke 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;
}

Kode ini menginisialisasi _cameraSource, menetapkan preset sesi pengambilan gambar, dan kamera yang akan digunakan.

Kita perlu memasukkan frame dari _cameraSource ke ViewController aplikasi untuk menampilkannya. MPPCameraInputSource adalah subclass MPPInputSource, yang menyediakan protokol untuk delegasinya, yaitu MPPInputSourceDelegate. Jadi, ViewController aplikasi kita dapat menjadi delegasi _cameraSource.

Perbarui definisi antarmuka ViewController sebagaimana mestinya:

@interface ViewController () <MPPInputSourceDelegate>

Untuk menangani penyiapan kamera dan memproses frame yang masuk, kita harus menggunakan antrean yang berbeda dengan antrean utama. Tambahkan kode berikut ke blok implementasi ViewController:

// Process camera frames on this queue.
dispatch_queue_t _videoQueue;

Di viewDidLoad(), tambahkan baris berikut setelah menginisialisasi objek _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

Lalu, tambahkan kode berikut untuk menginisialisasi antrean sebelum menyiapkan objek _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);

Kita akan menggunakan antrean serial dengan prioritas QOS_CLASS_USER_INTERACTIVE untuk memproses frame kamera.

Tambahkan baris berikut setelah header diimpor di bagian atas file, sebelum antarmuka/implementasi ViewController:

static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue";

Sebelum menerapkan metode apa pun dari protokol MPPInputSourceDelegate, kita harus menyiapkan cara untuk menampilkan frame kamera terlebih dahulu. Framework Mediapipe menyediakan utilitas lain yang disebut MPPLayerRenderer untuk menampilkan gambar di layar. Utilitas ini dapat digunakan untuk menampilkan objek CVPixelBufferRef, yang merupakan jenis gambar yang disediakan oleh MPPCameraInputSource kepada delegasinya.

Di ViewController.m, tambahkan baris impor berikut:

#import "mediapipe/objc/MPPLayerRenderer.h"

Untuk menampilkan gambar layar, kita perlu menambahkan objek UIView baru yang disebut _liveView ke ViewController.

Tambahkan baris berikut ke blok implementasi ViewController:

// Display the camera preview frames.
IBOutlet UIView* _liveView;
// Render frames in a layer.
MPPLayerRenderer* _renderer;

Buka Main.storyboard, tambahkan objek UIView dari library objek ke View dari class ViewController. Tambahkan stopkontak referensi dari tampilan ini ke objek _liveView yang baru saja Anda tambahkan ke class ViewController. Ubah ukuran tampilan agar berada di tengah dan mencakup seluruh layar aplikasi.

Kembali ke ViewController.m dan tambahkan kode berikut ke viewDidLoad() untuk melakukan inisialisasi objek _renderer:

_renderer = [[MPPLayerRenderer alloc] init];
_renderer.layer.frame = _liveView.layer.bounds;
[_liveView.layer addSublayer:_renderer.layer];
_renderer.frameScaleMode = MPPFrameScaleModeFillAndCrop;

Untuk mendapatkan frame dari kamera, kita akan mengimplementasikan metode berikut:

// 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);
  });
}

Ini adalah metode delegasi MPPInputSource. Pertama-tama, kita periksa apakah kita mendapatkan frame dari sumber yang tepat, yaitu _cameraSource. Kemudian, kita menampilkan frame yang diterima dari kamera melalui _renderer pada antrean utama.

Sekarang, kita harus memulai kamera segera setelah tampilan untuk menampilkan frame akan muncul. Untuk melakukannya, kita akan mengimplementasikan fungsi viewWillAppear:(BOOL)animated:

-(void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
}

Sebelum mulai menjalankan kamera, kita memerlukan izin pengguna untuk mengaksesnya. MPPCameraInputSource menyediakan fungsi requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler untuk meminta akses kamera dan melakukan beberapa pekerjaan saat pengguna merespons. Tambahkan kode berikut ke viewWillAppear:animated:

[_cameraSource requestCameraAccessWithCompletionHandler:^void(BOOL granted) {
  if (granted) {
    dispatch_async(_videoQueue, ^{
      [_cameraSource start];
    });
  }
}];

Sebelum mem-build aplikasi, tambahkan dependensi berikut ke file BUILD Anda:

sdk_frameworks = [
    "AVFoundation",
    "CoreGraphics",
    "CoreMedia",
],
deps = [
    "//mediapipe/objc:mediapipe_framework_ios",
    "//mediapipe/objc:mediapipe_input_sources_ios",
    "//mediapipe/objc:mediapipe_layer_renderer",
],

Sekarang, bangun dan jalankan aplikasi di perangkat iOS Anda. Anda akan melihat feed penayangan kamera live setelah menyetujui izin kamera.

Sekarang kita siap menggunakan bingkai kamera dalam grafik MediaPipe.

Menggunakan grafik MediaPipe di iOS

Menambahkan dependensi yang relevan

Kami telah menambahkan dependensi kode framework MediaPipe yang berisi API iOS untuk menggunakan grafik MediaPipe. Untuk menggunakan grafik MediaPipe, kita perlu menambahkan dependensi pada grafik yang ingin digunakan dalam aplikasi. Tambahkan baris berikut ke daftar data di file BUILD Anda:

"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",

Sekarang tambahkan dependensi ke kalkulator yang digunakan dalam grafik ini di kolom deps di file BUILD:

"//mediapipe/graphs/edge_detection:mobile_calculators",

Terakhir, ganti nama file ViewController.m menjadi ViewController.mm untuk mendukung Objective-C++.

Gunakan grafik dalam ViewController

Di ViewController.m, tambahkan baris impor berikut:

#import "mediapipe/objc/MPPGraph.h"

Deklarasikan konstanta statis dengan nama grafik, stream input, dan aliran output:

static NSString* const kGraphName = @"mobile_gpu";

static const char* kInputStream = "input_video";
static const char* kOutputStream = "output_video";

Tambahkan properti berikut ke antarmuka ViewController:

// The MediaPipe graph currently in use. Initialized in viewDidLoad, started in viewWillAppear: and
// sent video frames on _videoQueue.
@property(nonatomic) MPPGraph* mediapipeGraph;

Seperti yang dijelaskan dalam komentar di atas, kita akan melakukan inisialisasi grafik ini dalam viewDidLoad terlebih dahulu. Untuk melakukannya, kita harus memuat grafik dari file .pbtxt menggunakan fungsi berikut:

+   (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;
}

Gunakan fungsi ini untuk melakukan inisialisasi grafik di viewDidLoad seperti berikut:

self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName];

Grafik akan mengirimkan hasil pemrosesan frame kamera kembali ke ViewController. Tambahkan baris berikut setelah menginisialisasi grafik untuk menetapkan ViewController sebagai delegasi objek mediapipeGraph:

self.mediapipeGraph.delegate = self;

Untuk menghindari pertentangan memori saat memproses frame dari feed video live, tambahkan baris berikut:

// Set maxFramesInFlight to a small value to avoid memory contention for real-time processing.
self.mediapipeGraph.maxFramesInFlight = 2;

Sekarang, mulai grafik saat pengguna telah memberikan izin untuk menggunakan kamera di aplikasi kita:

[_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];
    });
  }
}];

Sebelumnya, saat menerima frame dari kamera dalam fungsi processVideoFrame, kami menampilkannya di _liveView menggunakan _renderer. Sekarang, kita harus mengirim frame tersebut ke grafik dan merender hasilnya. Ubah penerapan fungsi ini untuk melakukan hal berikut:

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

Kita mengirimkan imageBuffer ke self.mediapipeGraph sebagai paket jenis MPPPacketTypePixelBuffer ke dalam aliran input kInputStream, yaitu "input_video".

Grafik akan berjalan dengan paket input ini dan menampilkan hasil dalam kOutputStream, yaitu "output_video". Kita dapat mengimplementasikan metode delegasi berikut untuk menerima paket pada stream output ini dan menampilkannya di layar:

-   (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);
    });
  }
}

Perbarui definisi antarmuka ViewController dengan MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

Dan itu saja! Buat dan jalankan aplikasi di perangkat iOS Anda. Anda akan melihat hasil menjalankan grafik deteksi tepi pada feed video live. Selamat!

edge_detection_ios_gpu_gif

Perhatikan bahwa contoh iOS kini menggunakan aplikasi template common. Kode dalam tutorial ini digunakan di aplikasi template common. Aplikasi helloworld memiliki dependensi file BUILD yang sesuai untuk grafik deteksi tepi.