Introducción
En este instructivo de Hello World!, se usa el framework de MediaPipe para desarrollar una aplicación para iOS que ejecuta un grafo de MediaPipe en iOS.
Qué crearás
Una app de cámara simple para la detección de bordes de Sobel en tiempo real aplicada a una transmisión de video en vivo en un dispositivo iOS.
Configuración
- Instala MediaPipe Framework en tu sistema. Consulta la Guía de instalación del framework para obtener más detalles.
- Configura tu dispositivo iOS para el desarrollo.
- Configura Bazel en tu sistema para compilar e implementar la app para iOS.
Gráfico de detección de bordes
Usaremos el siguiente gráfico, 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"
}
A continuación, se muestra una visualización del gráfico:
Este gráfico tiene una sola transmisión de entrada llamada input_video
para todos los fotogramas entrantes que proporcionará la cámara del dispositivo.
El primer nodo del gráfico, LuminanceCalculator
, toma un solo paquete (marco de imagen) y aplica un cambio de luminancia con un sombreador OpenGL. El marco de imagen resultante se envía a la transmisión de salida luma_video
.
El segundo nodo, SobelEdgesCalculator
, aplica la detección de perímetro a los paquetes entrantes en la transmisión luma_video
y genera como resultado una transmisión de salida output_video
.
Nuestra aplicación para iOS mostrará los marcos de imagen de salida del flujo de output_video
.
Configuración inicial mínima de la aplicación
Primero, comenzamos con una aplicación para iOS simple y demostramos cómo usar bazel
para compilarla.
Primero, crea un proyecto de Xcode a través de File > New > Single View App.
Establece el nombre del producto como "HelloWorld" y usa un identificador de organización adecuado, como com.google.mediapipe
. El identificador de la organización junto con el nombre del producto será el bundle_id
de la aplicación, como com.google.mediapipe.HelloWorld
.
Configura el lenguaje en Objective-C.
Guarda el proyecto en una ubicación adecuada. La llamaremos $PROJECT_TEMPLATE_LOC
. Por lo tanto, tu proyecto estará en el directorio $PROJECT_TEMPLATE_LOC/HelloWorld
. Este contendrá otro directorio llamado HelloWorld
y un archivo HelloWorld.xcodeproj
.
El elemento HelloWorld.xcodeproj
no será útil en este instructivo, ya que usaremos
bazel para compilar la aplicación para iOS. A continuación, se muestra el contenido del directorio $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld
:
AppDelegate.h
yAppDelegate.m
ViewController.h
yViewController.m
main.m
Info.plist
Main.storyboard
yLaunch.storyboard
Assets.xcassets
.
Copia estos archivos en un directorio llamado HelloWorld
en una ubicación que pueda acceder al código fuente del framework de MediaPipe. Por ejemplo, el código fuente de la aplicación que compilaremos en este instructivo se encuentra en mediapipe/examples/ios/HelloWorld
. Nos referiremos a esta ruta de acceso como $APPLICATION_PATH
a lo largo de este codelab.
Crea un archivo BUILD
en el archivo $APPLICATION_PATH
y agrega las siguientes reglas de compilación:
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 = [],
)
La regla objc_library
agrega dependencias para las clases AppDelegate
y ViewController
, main.m
y los guiones gráficos de la aplicación. La app con plantilla solo depende del SDK de UIKit
.
La regla ios_application
usa la biblioteca HelloWorldAppLibrary
de Objective-C
generada para compilar una aplicación para iOS que se instalará en tu dispositivo iOS.
Para compilar la app, usa el siguiente comando en una terminal:
bazel build -c opt --config=ios_arm64 <$APPLICATION_PATH>:HelloWorldApp'
Por ejemplo, para compilar la aplicación HelloWorldApp
en mediapipe/examples/ios/helloworld
, usa el siguiente comando:
bazel build -c opt --config=ios_arm64 mediapipe/examples/ios/helloworld:HelloWorldApp
Luego, vuelve a Xcode, abre Window > Dispositivos y simuladores, selecciona tu dispositivo y agrega a tu dispositivo el archivo .ipa
generado por el comando anterior.
Este es el documento sobre cómo configurar y compilar aplicaciones del framework de iOS.
Abre la aplicación en tu dispositivo. Como está vacío, debería mostrar una pantalla blanca en blanco.
Cómo usar la cámara para el feed de visualización en vivo
En este instructivo, usaremos la clase MPPCameraInputSource
para acceder a fotogramas de la cámara y tomarlos. Esta clase usa la API de AVCaptureSession
para obtener los fotogramas de la cámara.
Sin embargo, antes de usar esta clase, cambia el archivo Info.plist
para admitir el uso de la cámara en la app.
En ViewController.m
, agrega la siguiente línea de importación:
#import "mediapipe/objc/MPPCameraInputSource.h"
Agrega lo siguiente a su bloque de implementación para crear un objeto _cameraSource
:
@implementation ViewController {
// Handles camera access via AVCaptureSession library.
MPPCameraInputSource* _cameraSource;
}
Agrega el siguiente código a 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;
}
El código inicializa _cameraSource
, establece el ajuste predeterminado de la sesión de captura y la cámara que se usará.
Necesitamos obtener marcos de _cameraSource
en ViewController
de nuestra aplicación para mostrarlos. MPPCameraInputSource
es una subclase de MPPInputSource
, que proporciona un protocolo para sus delegados, es decir, MPPInputSourceDelegate
. Por lo tanto, la aplicación ViewController
puede ser un delegado de _cameraSource
.
Actualiza la definición de interfaz de ViewController
según corresponda:
@interface ViewController () <MPPInputSourceDelegate>
Para controlar la configuración de la cámara y procesar los fotogramas entrantes, debemos usar una cola diferente de la cola principal. Agrega lo siguiente al bloque de implementación de ViewController
:
// Process camera frames on this queue.
dispatch_queue_t _videoQueue;
En viewDidLoad()
, agrega la siguiente línea después de inicializar el objeto _cameraSource
:
[_cameraSource setDelegate:self queue:_videoQueue];
Además, agrega el siguiente código para inicializar la cola antes de configurar el
objeto _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);
Usaremos una cola en serie con la prioridad QOS_CLASS_USER_INTERACTIVE
para procesar los marcos de cámara.
Agrega la siguiente línea después de las importaciones del encabezado en la parte superior del archivo, antes de la interfaz o implementación de ViewController
:
static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue";
Antes de implementar cualquier método del protocolo MPPInputSourceDelegate
, primero debemos configurar una forma de mostrar los marcos de la cámara. Mediapipe Framework proporciona otra utilidad llamada MPPLayerRenderer
para mostrar imágenes en la pantalla. Esta utilidad se puede usar para mostrar objetos CVPixelBufferRef
, que es el tipo de imágenes que proporciona MPPCameraInputSource
a sus delegados.
En ViewController.m
, agrega la siguiente línea de importación:
#import "mediapipe/objc/MPPLayerRenderer.h"
Para mostrar imágenes de la pantalla, debemos agregar un nuevo objeto UIView
llamado _liveView
a ViewController
.
Agrega las siguientes líneas al bloque de implementación de ViewController
:
// Display the camera preview frames.
IBOutlet UIView* _liveView;
// Render frames in a layer.
MPPLayerRenderer* _renderer;
Ve a Main.storyboard
y agrega un objeto UIView
de la biblioteca de objetos al View
de la clase ViewController
. Agrega una salida de referencia de esta vista al objeto _liveView
que acabas de agregar a la clase ViewController
. Cambia el tamaño de la vista para que esté centrada y cubra toda la pantalla de la aplicación.
Regresa a ViewController.m
y agrega el siguiente código a viewDidLoad()
para
inicializar el objeto _renderer
:
_renderer = [[MPPLayerRenderer alloc] init];
_renderer.layer.frame = _liveView.layer.bounds;
[_liveView.layer addSublayer:_renderer.layer];
_renderer.frameScaleMode = MPPFrameScaleModeFillAndCrop;
Para obtener fotogramas de la cámara, implementaremos el siguiente método:
// 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);
});
}
Este es un método delegado de MPPInputSource
. Primero, verificamos que se obtengan fotogramas de la fuente correcta, es decir, _cameraSource
. Luego, mostramos el fotograma recibido de la cámara a través de _renderer
en la cola principal.
Ahora, debemos iniciar la cámara en cuanto esté a punto de aparecer la vista que muestra los fotogramas. Para ello, implementaremos la función viewWillAppear:(BOOL)animated
:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
}
Antes de comenzar a ejecutar la cámara, necesitamos el permiso del usuario para acceder a ella.
MPPCameraInputSource
proporciona una función requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL
granted))handler
para solicitar acceso a la cámara y realizar algunas tareas cuando el usuario responde. Agrega el siguiente código a viewWillAppear:animated
:
[_cameraSource requestCameraAccessWithCompletionHandler:^void(BOOL granted) {
if (granted) {
dispatch_async(_videoQueue, ^{
[_cameraSource start];
});
}
}];
Antes de compilar la aplicación, agrega las siguientes dependencias al archivo BUILD
:
sdk_frameworks = [
"AVFoundation",
"CoreGraphics",
"CoreMedia",
],
deps = [
"//mediapipe/objc:mediapipe_framework_ios",
"//mediapipe/objc:mediapipe_input_sources_ios",
"//mediapipe/objc:mediapipe_layer_renderer",
],
Ahora, compila y ejecuta la aplicación en tu dispositivo iOS. Deberías ver un feed de vista de cámara en vivo después de aceptar los permisos de la cámara.
Ya estamos listos para usar marcos de cámara en un gráfico de MediaPipe.
Usa un gráfico de MediaPipe en iOS
Agrega dependencias relevantes
Ya agregamos las dependencias del código del framework de MediaPipe que contiene la API de iOS para usar un gráfico de MediaPipe. Para usar un gráfico de MediaPipe, debemos agregar una dependencia en el grafo que pretendemos usar en nuestra aplicación. Agrega la siguiente línea a la lista data
en el archivo BUILD
:
"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
Ahora agrega la dependencia a las calculadoras que se usan en este gráfico en el campo deps
del archivo BUILD
:
"//mediapipe/graphs/edge_detection:mobile_calculators",
Por último, cambia el nombre del archivo ViewController.m
a ViewController.mm
para admitir Objective-C++.
Usa el gráfico de ViewController
En ViewController.m
, agrega la siguiente línea de importación:
#import "mediapipe/objc/MPPGraph.h"
Declara una constante estática con el nombre del grafo, el flujo de entrada y el flujo de salida:
static NSString* const kGraphName = @"mobile_gpu";
static const char* kInputStream = "input_video";
static const char* kOutputStream = "output_video";
Agrega la siguiente propiedad a la interfaz de ViewController
:
// The MediaPipe graph currently in use. Initialized in viewDidLoad, started in viewWillAppear: and
// sent video frames on _videoQueue.
@property(nonatomic) MPPGraph* mediapipeGraph;
Como se explicó en el comentario anterior, primero inicializaremos este gráfico en viewDidLoad
. Para ello, necesitamos cargar el gráfico del archivo .pbtxt
con la siguiente función:
+ (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;
}
Usa esta función para inicializar el gráfico en viewDidLoad
de la siguiente manera:
self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName];
El gráfico debe enviar los resultados del procesamiento de los fotogramas de la cámara a ViewController
. Agrega la siguiente línea después de inicializar el gráfico para establecer ViewController
como delegado del objeto mediapipeGraph
:
self.mediapipeGraph.delegate = self;
Para evitar la contención de memoria mientras procesas fotogramas del feed de video en vivo, agrega la siguiente línea:
// Set maxFramesInFlight to a small value to avoid memory contention for real-time processing.
self.mediapipeGraph.maxFramesInFlight = 2;
Ahora, inicia el gráfico cuando el usuario otorgue el permiso para usar la cámara en nuestra app:
[_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];
});
}
}];
Anteriormente, cuando recibimos fotogramas de la cámara en la función processVideoFrame
, los mostrábamos en _liveView
con _renderer
. Ahora, necesitamos enviar esos marcos al gráfico y renderizar los resultados en su lugar. Modifica la implementación de esta función para que haga lo siguiente:
- (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];
}
Enviamos el imageBuffer
a self.mediapipeGraph
como un paquete de tipo MPPPacketTypePixelBuffer
en el flujo de entrada kInputStream
, es decir, "input_video".
El gráfico se ejecutará con este paquete de entrada y generará un resultado en kOutputStream
, es decir, "output_video". Podemos implementar el siguiente método de delegado para recibir paquetes en este flujo de salida y mostrarlos en la pantalla:
- (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);
});
}
}
Actualiza la definición de interfaz de ViewController
con MPPGraphDelegate
:
@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>
¡Y eso es todo! Compila y ejecuta la app en tu dispositivo iOS. Deberías ver los resultados de la ejecución del gráfico de detección de bordes en un feed de video en vivo. ¡Felicitaciones!
Ten en cuenta que los ejemplos de iOS ahora usan una app de plantillas common. El código de este instructivo se usa en la app de plantillas common. La app de helloworld tiene las dependencias de archivo BUILD
adecuadas para el gráfico de detección de bordes.