Hello World! sur iOS

Introduction

Ce tutoriel "Hello World!" utilise le framework MediaPipe pour développer une application iOS qui exécute un graphique MediaPipe sur iOS.

Objectifs de l'atelier

Application d'appareil photo simple pour la détection de périphérie Sobel en temps réel appliquée à un flux vidéo en direct sur un appareil iOS.

edge_detection_ios_gpu_gif

Préparation

  1. Installez MediaPipe Framework sur votre système. Pour en savoir plus, consultez le guide d'installation du framework.
  2. Configurez votre appareil iOS pour le développement.
  3. Configurez Bazel sur votre système pour créer et déployer l'application iOS.

Graphique pour la détection en périphérie

Nous allons utiliser le graphe suivant, 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"
}

Voici une visualisation du graphique:

edge_detection_mobile_gpu

Ce graphe comporte un seul flux d'entrée nommé input_video pour toutes les trames entrantes qui seront fournies par l'appareil photo de votre appareil.

Le premier nœud du graphique, LuminanceCalculator, accepte un seul paquet (cadre d'image) et applique un changement de luminance à l'aide d'un nuanceur OpenGL. La trame d'image résultante est envoyée au flux de sortie luma_video.

Le deuxième nœud, SobelEdgesCalculator, applique la détection en périphérie aux paquets entrants dans le flux luma_video et renvoie les résultats dans le flux de sortie output_video.

Notre application iOS affiche les images de sortie du flux output_video.

Configuration minimale initiale de l'application

Nous commencerons par une application iOS simple et nous expliquons comment utiliser bazel pour la compiler.

Commencez par créer un projet XCode via File > New > Single View App (Fichier > Nouveau > Application Single View).

Définissez le nom du produit sur "HelloWorld" et utilisez un identifiant d'organisation approprié, tel que com.google.mediapipe. L'identifiant d'organisation avec le nom du produit constitue le bundle_id de l'application, par exemple com.google.mediapipe.HelloWorld.

Définissez la langue sur Objective-C.

Enregistrez le projet à un emplacement approprié. Appelons-la $PROJECT_TEMPLATE_LOC. Votre projet se trouve donc dans le répertoire $PROJECT_TEMPLATE_LOC/HelloWorld. Il contiendra un autre répertoire nommé HelloWorld et un fichier HelloWorld.xcodeproj.

HelloWorld.xcodeproj ne sera pas utile pour ce tutoriel, car Bazel nous permettra de créer l'application iOS. Le contenu du répertoire $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld est indiqué ci-dessous:

  1. AppDelegate.h et AppDelegate.m
  2. ViewController.h et ViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboard et Launch.storyboard
  6. Assets.xcassets.

Copiez ces fichiers dans un répertoire nommé HelloWorld, à un emplacement pouvant accéder au code source de MediaPipe Framework. Par exemple, le code source de l'application que nous allons compiler dans ce tutoriel se trouve dans mediapipe/examples/ios/HelloWorld. Nous appellerons ce chemin d'accès $APPLICATION_PATH tout au long de cet atelier de programmation.

Créez un fichier BUILD dans $APPLICATION_PATH et ajoutez les règles de compilation suivantes:

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 règle objc_library ajoute des dépendances pour les classes AppDelegate et ViewController, main.m et les storyboards d'application. L'application modélisée ne dépend que du SDK UIKit.

La règle ios_application utilise la bibliothèque Objective-C HelloWorldAppLibrary générée pour créer une application iOS à installer sur votre appareil iOS.

Pour créer l'application, exécutez la commande suivante dans un terminal:

bazel build -c opt --config=ios_arm64 <$APPLICATION_PATH>:HelloWorldApp'

Par exemple, pour compiler l'application HelloWorldApp dans mediapipe/examples/ios/helloworld, utilisez la commande suivante:

bazel build -c opt --config=ios_arm64 mediapipe/examples/ios/helloworld:HelloWorldApp

Revenez ensuite dans XCode, ouvrez Window > Devices and Simulators, sélectionnez votre appareil et ajoutez-y le fichier .ipa généré par la commande ci-dessus. Ce document explique comment configurer et compiler des applications du framework iOS.

Ouvrez l'application sur votre appareil. Comme il est vide, un écran blanc doit s'afficher.

Utiliser la caméra pour le flux de la vidéo en direct

Dans ce tutoriel, nous allons utiliser la classe MPPCameraInputSource pour accéder aux images de l'appareil photo et les récupérer. Cette classe utilise l'API AVCaptureSession pour obtenir les images de l'appareil photo.

Toutefois, avant d'utiliser cette classe, modifiez le fichier Info.plist pour permettre l'utilisation de l'appareil photo dans l'application.

Dans ViewController.m, ajoutez la ligne d'importation suivante:

#import "mediapipe/objc/MPPCameraInputSource.h"

Ajoutez le code suivant à son bloc d'implémentation pour créer un objet _cameraSource:

@implementation ViewController {
  // Handles camera access via AVCaptureSession library.
  MPPCameraInputSource* _cameraSource;
}

Ajoutez le code suivant à 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;
}

Le code initialise _cameraSource, définit le préréglage de la session de capture et l'appareil photo à utiliser.

Nous devons obtenir les frames de _cameraSource dans l'application ViewController pour les afficher. MPPCameraInputSource est une sous-classe de MPPInputSource, qui fournit un protocole à ses délégués, à savoir MPPInputSourceDelegate. Notre application ViewController peut donc être un délégué de _cameraSource.

Mettez à jour la définition de l'interface de ViewController en conséquence:

@interface ViewController () <MPPInputSourceDelegate>

Pour gérer la configuration de l'appareil photo et traiter les images entrantes, nous devons utiliser une file d'attente différente de la file d'attente principale. Ajoutez le code suivant au bloc d'implémentation de ViewController:

// Process camera frames on this queue.
dispatch_queue_t _videoQueue;

Dans viewDidLoad(), ajoutez la ligne suivante après avoir initialisé l'objet _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

Ajoutez le code suivant pour initialiser la file d'attente avant de configurer l'objet _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);

Nous allons utiliser une file d'attente série avec la priorité QOS_CLASS_USER_INTERACTIVE pour traiter les images de la caméra.

Ajoutez la ligne suivante après l'importation de l'en-tête en haut du fichier, avant l'interface/l'implémentation de ViewController:

static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue";

Avant d'implémenter une méthode à partir du protocole MPPInputSourceDelegate, nous devons d'abord configurer un moyen d'afficher les images de la caméra. Mediapipe Framework fournit un autre utilitaire appelé MPPLayerRenderer pour afficher des images à l'écran. Cet utilitaire permet d'afficher des objets CVPixelBufferRef, qui correspondent au type d'images fournies par MPPCameraInputSource à ses délégués.

Dans ViewController.m, ajoutez la ligne d'importation suivante:

#import "mediapipe/objc/MPPLayerRenderer.h"

Pour afficher les images de l'écran, nous devons ajouter un nouvel objet UIView appelé _liveView à ViewController.

Ajoutez les lignes suivantes au bloc d'implémentation de ViewController:

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

Accédez à Main.storyboard, puis ajoutez un objet UIView de la bibliothèque d'objets au View de la classe ViewController. À partir de cette vue, ajoutez une prise de référence à l'objet _liveView que vous venez d'ajouter à la classe ViewController. Redimensionnez la vue pour qu'elle soit centrée et couvre l'intégralité de l'écran de l'application.

Revenez à ViewController.m et ajoutez le code suivant à viewDidLoad() pour initialiser l'objet _renderer:

_renderer = [[MPPLayerRenderer alloc] init];
_renderer.layer.frame = _liveView.layer.bounds;
[_liveView.layer addSublayer:_renderer.layer];
_renderer.frameScaleMode = MPPFrameScaleModeFillAndCrop;

Pour obtenir des images à partir de l'appareil photo, nous allons implémenter la méthode suivante:

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

Il s'agit d'une méthode déléguée de MPPInputSource. Nous vérifions d'abord que les frames proviennent de la bonne source, à savoir _cameraSource. Nous affichons ensuite la trame reçue de l'appareil photo via _renderer dans la file d'attente principale.

Nous devons maintenant démarrer la caméra dès que la vue pour afficher les images est sur le point d'apparaître. Pour ce faire, nous allons implémenter la fonction viewWillAppear:(BOOL)animated:

-(void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
}

Avant de commencer à utiliser la caméra, nous avons besoin de l'autorisation de l'utilisateur. MPPCameraInputSource fournit une fonction requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler pour demander l'accès à l'appareil photo et effectuer des tâches lorsque l'utilisateur a répondu. Ajoutez le code suivant à viewWillAppear:animated :

[_cameraSource requestCameraAccessWithCompletionHandler:^void(BOOL granted) {
  if (granted) {
    dispatch_async(_videoQueue, ^{
      [_cameraSource start];
    });
  }
}];

Avant de compiler l'application, ajoutez les dépendances suivantes à votre fichier BUILD:

sdk_frameworks = [
    "AVFoundation",
    "CoreGraphics",
    "CoreMedia",
],
deps = [
    "//mediapipe/objc:mediapipe_framework_ios",
    "//mediapipe/objc:mediapipe_input_sources_ios",
    "//mediapipe/objc:mediapipe_layer_renderer",
],

À présent, créez et exécutez l'application sur votre appareil iOS. Après avoir accepté les autorisations d'accès à la caméra, vous devriez voir le flux en direct de la caméra.

Nous sommes maintenant prêts à utiliser des images d'appareil photo dans un graphique MediaPipe.

Utiliser un graphique MediaPipe dans iOS

Ajouter des dépendances pertinentes

Nous avons déjà ajouté les dépendances du code du framework MediaPipe, qui contient l'API iOS, pour utiliser un graphique MediaPipe. Pour utiliser un graphique MediaPipe, nous devons ajouter une dépendance au graphique que nous avons l'intention d'utiliser dans notre application. Ajoutez la ligne suivante à la liste data de votre fichier BUILD:

"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",

Ajoutez maintenant la dépendance aux calculateurs utilisés dans ce graphique, dans le champ deps du fichier BUILD:

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

Enfin, renommez le fichier ViewController.m en ViewController.mm pour assurer la compatibilité avec Objective-C++.

Utiliser le graphique dans ViewController

Dans ViewController.m, ajoutez la ligne d'importation suivante:

#import "mediapipe/objc/MPPGraph.h"

Déclarez une constante statique avec le nom du graphe, le flux d'entrée et le flux de sortie:

static NSString* const kGraphName = @"mobile_gpu";

static const char* kInputStream = "input_video";
static const char* kOutputStream = "output_video";

Ajoutez la propriété suivante à l'interface de ViewController:

// The MediaPipe graph currently in use. Initialized in viewDidLoad, started in viewWillAppear: and
// sent video frames on _videoQueue.
@property(nonatomic) MPPGraph* mediapipeGraph;

Comme expliqué dans le commentaire ci-dessus, nous allons d'abord initialiser ce graphique dans viewDidLoad. Pour ce faire, nous devons charger le graphique à partir du fichier .pbtxt à l'aide de la fonction suivante:

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

Utilisez cette fonction pour initialiser le graphique dans viewDidLoad comme suit:

self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName];

Le graphe doit renvoyer les résultats du traitement des images de l'appareil photo à ViewController. Ajoutez la ligne suivante après avoir initialisé le graphique pour définir ViewController en tant que délégué de l'objet mediapipeGraph:

self.mediapipeGraph.delegate = self;

Pour éviter les conflits de mémoire lors du traitement des images du flux vidéo en direct, ajoutez la ligne suivante:

// Set maxFramesInFlight to a small value to avoid memory contention for real-time processing.
self.mediapipeGraph.maxFramesInFlight = 2;

À présent, lancez le graphique lorsque l'utilisateur a accordé l'autorisation d'utiliser l'appareil photo dans notre application:

[_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];
    });
  }
}];

Précédemment, lorsque nous avons reçu des images de l'appareil photo dans la fonction processVideoFrame, nous les avons affichées dans _liveView à l'aide de _renderer. Nous devons maintenant envoyer ces frames au graphe et afficher les résultats. Modifiez l'implémentation de cette fonction pour effectuer les opérations suivantes:

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

Nous envoyons le imageBuffer à self.mediapipeGraph en tant que paquet de type MPPPacketTypePixelBuffer dans le flux d'entrée kInputStream, par exemple "input_video".

Le graphe s'exécutera avec ce paquet d'entrée et générera un résultat dans kOutputStream, par exemple "output_video". Nous pouvons mettre en œuvre la méthode déléguée suivante pour recevoir des paquets sur ce flux de sortie et les afficher à l'écran:

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

Mettez à jour la définition de l'interface de ViewController avec MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

C'est tout ! Créez et exécutez l'application sur votre appareil iOS. Vous devriez voir les résultats de l'exécution du graphique de détection de périphérie sur un flux vidéo en direct. Félicitations !

edge_detection_ios_gpu_gif

Veuillez noter que les exemples iOS utilisent désormais un modèle d'application common. Le code de ce tutoriel est utilisé dans le modèle d'application common. L'application helloworld dispose des dépendances de fichier BUILD appropriées pour le graphique de détection de périphérie.