Привет, мир! на iOS

Введение

Этот Привет, Мир! Учебное пособие использует MediaPipe Framework для разработки приложения iOS, которое запускает граф MediaPipe на iOS.

Что вы построите

Простое приложение камеры для обнаружения границ Собела в режиме реального времени, применяемое к живому видеопотоку на устройстве iOS.

Edge_detection_ios_gpu_gif

Настраивать

  1. Установите MediaPipe Framework в вашей системе. Подробности см. в руководстве по установке 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 для его создания.

Сначала создайте проект XCode через Файл > Создать > Приложение Single View.

Задайте название продукта «HelloWorld» и используйте соответствующий идентификатор организации, например com.google.mediapipe . Идентификатором организации вместе с названием продукта будет bundle_id приложения, например com.google.mediapipe.HelloWorld .

Установите язык Objective-C.

Сохраните проект в подходящее место. Назовем это $PROJECT_TEMPLATE_LOC . Таким образом, ваш проект будет находиться в каталоге $PROJECT_TEMPLATE_LOC/HelloWorld . Этот каталог будет содержать еще один каталог с именем HelloWorld и файл HelloWorld.xcodeproj .

HelloWorld.xcodeproj не будет полезен для этого руководства, поскольку для создания приложения iOS мы будем использовать bazel. Содержимое каталога $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld указано ниже:

  1. AppDelegate.h и AppDelegate.m
  2. ViewController.h и ViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboard и Launch.storyboard
  6. Каталог Assets.xcassets .

Скопируйте эти файлы в каталог с именем HelloWorld в место, где есть доступ к исходному коду MediaPipe Framework. Например, исходный код приложения, которое мы создадим в этом руководстве, находится в mediapipe/examples/ios/HelloWorld . В кодовой лаборатории мы будем называть этот путь $APPLICATION_PATH .

Создайте файл BUILD в $APPLICATION_PATH и добавьте следующие правила сборки:

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, откройте «Окно» > «Устройства и симуляторы», выберите свое устройство и добавьте на свое устройство файл .ipa , созданный приведенной выше командой. Вот документ по настройке и компиляции приложений iOS Framework.

Откройте приложение на своем устройстве. Поскольку он пуст, он должен отображать пустой белый экран.

Используйте камеру для просмотра в реальном времени

В этом уроке мы будем использовать класс MPPCameraInputSource для доступа и захвата кадров с камеры. Этот класс использует API AVCaptureSession для получения кадров с камеры.

Но прежде чем использовать этот класс, измените файл 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 Framework предоставляет еще одну утилиту под названием MPPLayerRenderer для отображения изображений на экране. Эту утилиту можно использовать для отображения объектов CVPixelBufferRef , которые представляют собой тип изображений, предоставляемых MPPCameraInputSource своим делегатам.

В ViewController.m добавьте следующую строку импорта:

#import "mediapipe/objc/MPPLayerRenderer.h"

Чтобы отображать изображения экрана, нам нужно добавить в ViewController новый объект UIView с именем _liveView .

Добавьте следующие строки в блок реализации ViewController :

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

Перейдите в Main.storyboard , добавьте объект UIView из библиотеки объектов во View класса ViewController . Добавьте ссылающийся выход из этого представления к объекту _liveView , который вы только что добавили в класс ViewController . Измените размер представления так, чтобы оно было центрировано и охватывало весь экран приложения.

Вернитесь в 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.

Использование графика MediaPipe в iOS

Добавьте соответствующие зависимости

Мы уже добавили зависимости кода платформы MediaPipe, который содержит API iOS для использования графа MediaPipe. Чтобы использовать граф MediaPipe, нам нужно добавить зависимость от графа, который мы собираемся использовать в нашем приложении. Добавьте следующую строку в список data вашего файла BUILD :

"//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 , мы отображали их в _liveView с помощью _renderer . Теперь нам нужно отправить эти кадры на график и вместо этого отобразить результаты. Измените реализацию этой функции, чтобы сделать следующее:

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

Обновите определение интерфейса ViewController с помощью MPPGraphDelegate :

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

И это все! Создайте и запустите приложение на своем устройстве iOS. Вы должны увидеть результаты запуска графика обнаружения границ в прямом видеопотоке. Поздравляю!

Edge_detection_ios_gpu_gif

Обратите внимание, что примеры iOS теперь используют общее приложение-шаблон. Код в этом руководстве используется в общем приложении-шаблоне. Приложение helloworld имеет соответствующие зависимости файлов BUILD для графа обнаружения границ.