Hello World! en iOS

Introducción

¡Hola, mundo! en el que se usa el framework MediaPipe para desarrollar un que ejecuta un gráfico de MediaPipe en iOS.

Qué compilarás

Una app de cámara simple para la detección de bordes de Sobel en tiempo real aplicada a un video en vivo transmitir en un dispositivo iOS.

edge_detection_ios_gpu_gif

Configuración

  1. Instala el framework de MediaPipe en tu sistema; consulta Instalación del framework guía para obtener más información.
  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 un solo flujo de entrada llamado 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 (imagen fotograma) y aplica un cambio de luminancia usando un sombreador OpenGL. El resultado marco de imagen se envía al flujo de salida luma_video.

El segundo nodo, SobelEdgesCalculator, aplica la detección de borde a las llamadas paquetes en la transmisión luma_video y los resultados se generan en output_video en tiempo real.

Nuestra aplicación para iOS mostrará los marcos de la imagen de salida de output_video. en tiempo real.

Configuración inicial mínima de la aplicación

Comenzaremos con una aplicación para iOS simple y demostraremos cómo usar bazel para crearlo.

Primero, crea un proyecto de Xcode desde Archivo > Nuevo > app de vista única.

Establece el nombre del producto como "HelloWorld" y usa una organización adecuada. identificador, 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. Asígnale el nombre $PROJECT_TEMPLATE_LOC Tu proyecto estará en el $PROJECT_TEMPLATE_LOC/HelloWorld. Este directorio contendrá otro directorio llamado HelloWorld y un archivo HelloWorld.xcodeproj.

El elemento HelloWorld.xcodeproj no será útil para este instructivo, ya que usaremos bazel para compilar la aplicación para iOS. El contenido del El directorio $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld se muestra a continuación:

  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 a la que se pueda acceder. el código fuente del framework de MediaPipe. Por ejemplo, el código fuente del que compilaremos en este instructivo se ubica en mediapipe/examples/ios/HelloWorld Nos referiremos a esta ruta como la $APPLICATION_PATH en todo el codelab.

Crea un archivo BUILD en $APPLICATION_PATH y agrega la siguiente compilación reglas:

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 AppDelegate y Clases ViewController, main.m y los guiones gráficos de la aplicación. El La app con plantilla depende solo del SDK de UIKit.

La regla ios_application usa la biblioteca de Objective-C de HelloWorldAppLibrary generar una aplicación para iOS para instalarla 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, seleccione y agrega a tu dispositivo el archivo .ipa generado por el comando anterior. Este es el documento sobre la configuración y compilación de apps del framework de iOS.

Abre la aplicación en tu dispositivo. Como está vacía, debería mostrar un pantalla en blanco en blanco.

Usa la cámara para el feed de visualización en vivo

En este instructivo, usaremos la clase MPPCameraInputSource para acceder y y tomar fotogramas de la cámara. 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 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 qué para usar la cámara.

Necesitamos obtener fotogramas de _cameraSource en nuestra aplicación ViewController para mostrarlas. MPPCameraInputSource es una subclase de MPPInputSource, que proporciona un protocolo para sus delegados, es decir, el MPPInputSourceDelegate Por lo tanto, nuestra aplicación ViewController puede ser un delegado de _cameraSource.

Actualiza la definición de interfaz de ViewController según corresponda:

@interface ViewController () <MPPInputSourceDelegate>

Para manejar la configuración de la cámara y procesar 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];

Agrega el siguiente código para inicializar la cola antes de configurar la 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 o el procesamiento de fotogramas de cámara.

Agrega la siguiente línea después de que el encabezado se importe 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, debemos primero configuraste una forma de mostrar los marcos de la cámara. El framework Mediapipe proporciona otra utilidad llamada MPPLayerRenderer para mostrar imágenes en la pantalla. Esta se puede usar para mostrar objetos CVPixelBufferRef, que es el tipo de las 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 desde esta vista a El objeto _liveView que acabas de agregar a la clase ViewController Cambia el tamaño de esté centrada y cubra toda la pantalla de la aplicación.

Regresa a ViewController.m y agrega el siguiente código a viewDidLoad() para inicializa 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 estamos obtener fotogramas de la fuente correcta, es decir, el _cameraSource Luego, mostramos el fotograma recibido de la cámara a través de _renderer en la fila principal.

Ahora, debemos iniciar la cámara tan pronto como se visualice la vista para mostrar los fotogramas a punto de aparecer. Para ello, implementaremos el 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 una tarea cuando el usuario respondió. 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 a tu BUILD archivo:

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 una transmisión en vivo feed de vista de cámara después de aceptar los permisos de la cámara.

Ya está todo listo para usar marcos de cámara en un gráfico de MediaPipe.

Cómo usar un gráfico de MediaPipe en iOS

Agrega las dependencias relevantes

Ya agregamos las dependencias del código del framework MediaPipe, que contiene la API de iOS para usar un gráfico de MediaPipe. Para usar un gráfico de MediaPipe, debemos agregar un elemento del gráfico que queremos usar en nuestra aplicación. Agrega lo siguiente a la lista data en tu 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. En el archivo BUILD:

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

Por último, cambia el nombre del archivo ViewController.m a ViewController.mm para admitirlo. Objective-C++.

Cómo usar el gráfico en 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, inicializaremos este gráfico en viewDidLoad primero. Para ello, debemos cargar el gráfico desde el archivo .pbtxt. usando 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 fotogramas de cámara al ViewController Agrega la siguiente línea después de inicializar el gráfico para establecer la ViewController como delegado del objeto mediapipeGraph:

self.mediapipeGraph.delegate = self;

Para evitar la contención de memoria mientras procesas los 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 haya otorgado permiso para usar la cámara en nuestra aplicación:

[_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 el processVideoFrame las mostramos en _liveView con _renderer. Ahora, debes enviar esos marcos al gráfico y, en su lugar, renderizar los resultados. Modificar la implementación de esta función para hacer 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 elemento imageBuffer a self.mediapipeGraph como un paquete de tipo. MPPPacketTypePixelBuffer en la transmisión de entrada kInputStream, p.ej., “input_video”.

El grafo se ejecutará con este paquete de entrada y generará un resultado kOutputStream, es decir, "output_video". Podemos implementar el siguiente delegado para recibir paquetes en esta transmisión 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 la 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 plantilla común. El código de este instructivo se usa en la app de plantilla común. La app de helloworld tiene las siguientes características: las dependencias de archivo BUILD adecuadas para el gráfico de detección de aristas