Введение
Этот Привет, Мир! Учебное пособие использует MediaPipe Framework для разработки приложения iOS, которое запускает граф MediaPipe на iOS.
Что вы построите
Простое приложение камеры для обнаружения границ Собела в режиме реального времени, применяемое к живому видеопотоку на устройстве iOS.
Настраивать
- Установите MediaPipe Framework в вашей системе. Подробности см. в руководстве по установке Framework .
- Настройте свое устройство iOS для разработки.
- Настройте 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"
}
Визуализация графика показана ниже:
Этот граф имеет один входной поток с именем 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
указано ниже:
-
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
.
Создайте файл 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. Вы должны увидеть результаты запуска графика обнаружения границ в прямом видеопотоке. Поздравляю!
Обратите внимание, что примеры iOS теперь используют общее приложение-шаблон. Код в этом руководстве используется в общем приложении-шаблоне. Приложение helloworld имеет соответствующие зависимости файлов BUILD
для графа обнаружения границ.