Hello World! en iOS

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.

edge_detection_ios_gpu_gif

Configuración

  1. Instala MediaPipe Framework en tu sistema. Consulta la Guía de instalación del framework para obtener más detalles.
  2. Configura tu dispositivo iOS para el desarrollo.
  3. 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:

edge_detection_mobile_gpu

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:

  1. AppDelegate.h y AppDelegate.m
  2. ViewController.h y ViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboard y Launch.storyboard
  6. 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!

edge_detection_ios_gpu_gif

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.