Hello World! di iOS

Pengantar

Halo Dunia! menggunakan Framework MediaPipe untuk mengembangkan aplikasi aplikasi yang menjalankan grafik MediaPipe di iOS.

Yang akan Anda build

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

edge_detection_ios_gpu_gif

Penyiapan

  1. Instal Framework MediaPipe di sistem Anda, lihat Penginstalan framework untuk mengetahui detailnya.
  2. Siapkan perangkat iOS Anda untuk pengembangan.
  3. Siapkan Bazel di sistem Anda 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 yang masuk yang akan disediakan oleh kamera perangkat Anda.

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

Node kedua, SobelEdgesCalculator, menerapkan deteksi edge ke peristiwa masuk paket di aliran data luma_video dan menghasilkan output berupa output output_video feed.

Aplikasi iOS kita akan menampilkan frame gambar output dari output_video feed.

Penyiapan aplikasi minimal awal

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

Pertama, buat proyek XCode melalui File > Baru > Aplikasi Tampilan Tunggal.

Tetapkan nama produk menjadi "HelloWorld", dan gunakan organisasi yang sesuai ID, seperti com.google.mediapipe. ID organisasi bersama dengan nama produknya adalah bundle_id untuk aplikasi, seperti com.google.mediapipe.HelloWorld.

Tetapkan bahasa ke Objective-C.

Simpan project ke lokasi yang tepat. Sebut saja ini $PROJECT_TEMPLATE_LOC. Jadi proyek 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 dari 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 build berikut aturan:

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 AppDelegate dan Class ViewController, main.m, dan storyboard aplikasi. Tujuan aplikasi dengan template hanya bergantung pada UIKit SDK.

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

Untuk membangun aplikasi, gunakan perintah berikut di terminal:

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

Misalnya, untuk membangun 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 > Perangkat dan Simulator, pilih perangkat, 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, tampilan harus menampilkan layar putih yang kosong.

Menggunakan kamera untuk feed tayangan live

Dalam tutorial ini, kita akan menggunakan class MPPCameraInputSource untuk mengakses dan mengambil bingkai dari kamera. Class ini menggunakan AVCaptureSession API untuk mendapatkan {i>frame<i} dari kamera.

Namun, sebelum menggunakan class ini, ubah file Info.plist untuk mendukung kamera tingkat penggunaan 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 melakukan inisialisasi _cameraSource, menetapkan preset sesi pengambilan gambar, dan kamera untuk digunakan.

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

Perbarui definisi antarmuka ViewController sesuai dengan:

@interface ViewController () <MPPInputSourceDelegate>

Untuk menangani penyiapan kamera dan memproses frame yang masuk, kita harus menggunakan antrean berbeda dari 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 melakukan inisialisasi Objek _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

Dan 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 bingkai 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 mengimplementasikan metode apa pun dari protokol MPPInputSourceDelegate, kita harus pertama-tama mengatur cara untuk menampilkan {i>frame<i} kamera. Mediapipe Framework menyediakan utilitas lain bernama MPPLayerRenderer untuk menampilkan gambar di layar. Ini utilitas dapat digunakan untuk menampilkan objek CVPixelBufferRef, yang merupakan jenis gambar yang diberikan 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 sehingga berada di tengah dan menutupi seluruh layar aplikasi.

Kembali ke ViewController.m dan tambahkan kode berikut ke viewDidLoad() untuk lakukan 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 memeriksa 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 perlu memulai kamera segera setelah tampilan untuk menampilkan {i>frame<i} yang akan ditampilkan. 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 tugas saat pengguna memiliki 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 BUILD Anda file:

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 tampilan kamera setelah menyetujui izin kamera.

Sekarang kita siap menggunakan frame kamera dalam grafik MediaPipe.

Menggunakan grafik MediaPipe di iOS

Menambahkan dependensi yang relevan

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

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

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

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

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

Menggunakan grafik dalam ViewController

Di ViewController.m, tambahkan baris impor berikut:

#import "mediapipe/objc/MPPGraph.h"

Deklarasikan konstanta statis dengan nama grafik, aliran input, dan stream 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 viewDidLoad terlebih dahulu. Untuk melakukannya, kita perlu 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 sebagai berikut:

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

Grafik ini harus mengirimkan hasil pemrosesan {i>frame<i} kamera kembali ke ViewController. Tambahkan baris berikut setelah menginisialisasi grafik untuk mengatur 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 setelah pengguna 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 kita menerima frame dari kamera di processVideoFrame , kita menampilkannya di _liveView menggunakan _renderer. Sekarang, kita telah perlu mengirim {i>frame<i} tersebut ke grafik dan merender hasilnya sebagai gantinya. {i>Modify<i} implementasi 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];
}

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

Grafik akan dijalankan dengan paket input ini dan {i>output<i} sebagai hasil kOutputStream, yaitu "output_video". Kita dapat mengimplementasikan delegasi berikut untuk menerima paket pada aliran 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);
    });
  }
}

Update definisi antarmuka ViewController dengan MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

Itu saja! Bangun dan jalankan aplikasi di perangkat iOS Anda. Anda akan melihat hasil dari menjalankan grafik deteksi tepi pada feed video live. Selamat!

edge_detection_ios_gpu_gif

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