Hello World! sur iOS

Introduction

Bonjour le monde ! utilise MediaPipe pour développer une application qui exécute un graphique MediaPipe sur iOS.

Objectifs de l'atelier

Application d'appareil photo simple pour la détection en temps réel des bords Sobel à une vidéo en direct diffuser sur un appareil iOS.

edge_detection_ios_gpu_gif

Configuration

  1. Installez MediaPipe Framework sur votre système, consultez la section Installation de Framework guide pour plus d'informations.
  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 de détection des bords

Nous utiliserons le graphique 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 graphique comporte un seul flux d'entrée nommé input_video pour tous les frames entrants qui sera fournie par l'appareil photo de votre appareil.

Le premier nœud du graphique, LuminanceCalculator, accepte un seul paquet (image cadre) et applique un changement de luminance à l'aide d'un nuanceur OpenGL. Le résultat la trame d'image 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 dans le flux luma_video et génère une sortie output_video flux.

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

Configuration initiale de l'application minimale

Nous commencerons par une application iOS simple et nous montrerons comment utiliser bazel. pour la construire.

Commencez par créer un projet XCode via Fichier > Nouveau > Single View App.

Définissez le nom du produit sur "HelloWorld" et utilisez une organisation appropriée. tel que com.google.mediapipe. Identifiant de l'organisation ainsi que le nom de produit correspond à l'bundle_id de l'application, par exemple com.google.mediapipe.HelloWorld

Définissez le langage sur Objective-C.

Enregistrez le projet à l'emplacement approprié. Appelons cela $PROJECT_TEMPLATE_LOC Votre projet sera donc Répertoire $PROJECT_TEMPLATE_LOC/HelloWorld. Ce répertoire contiendra un autre répertoire nommé HelloWorld et un fichier HelloWorld.xcodeproj.

Le HelloWorld.xcodeproj ne sera pas utile pour ce tutoriel, car nous allons utiliser bazel pour créer l'application iOS. Le contenu de la Le 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 et vers un emplacement accessible le code source du framework MediaPipe. Par exemple, le code source du que nous allons créer dans ce didacticiel se trouve mediapipe/examples/ios/HelloWorld Nous appellerons ce chemin $APPLICATION_PATH tout au long de l'atelier de programmation.

Créez un fichier BUILD dans $APPLICATION_PATH et ajoutez le build suivant règles:

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 AppDelegate et ViewController, main.m et les storyboards d'application. La l'application modélisée ne dépend que du SDK UIKit.

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

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

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

Par exemple, pour créer l'application HelloWorldApp dans mediapipe/examples/ios/helloworld, exécutez la commande suivante:

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

Revenez ensuite dans XCode, ouvrez Fenêtre > Appareils et simulateurs, sélectionnez votre puis ajoutez à votre appareil le fichier .ipa généré par la commande ci-dessus. Voici un document sur la configuration et la compilation des applications utilisant le framework iOS.

Ouvrez l'application sur votre appareil. Étant donné qu'il est vide, un écran blanc vierge.

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

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

Mais avant d'utiliser cette classe, modifiez le fichier Info.plist pour qu'il soit compatible avec 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 définit les paramètres l'appareil photo à utiliser.

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

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

@interface ViewController () <MPPInputSourceDelegate>

Pour gérer la configuration de la caméra 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 le ViewController:

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

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

[_cameraSource setDelegate:self queue:_videoQueue];

Ajoutez le code suivant pour initialiser la file d'attente avant de configurer 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 et traiter les images de l'appareil photo.

Ajoutez la ligne suivante après les importations d'en-têtes 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 du protocole MPPInputSourceDelegate, nous devons configurez tout d'abord un moyen d'afficher les images de l'appareil photo. Mediapipe Framework fournit un autre utilitaire appelé MPPLayerRenderer pour afficher des images à l'écran. Ce vous permet d'afficher les objets CVPixelBufferRef, qui est le type de les 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 objet UIView appelé _liveView sur 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, ajoutez un objet UIView de la bibliothèque d'objets à View de la classe ViewController. Ajoutez une sortie de référence à partir de cette vue pour l'objet _liveView que vous venez d'ajouter à la classe ViewController ; Redimensionnez la afin qu'elle soit centrée et recouvre tout l'écran de l'application.

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

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

Pour obtenir les images 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 nous obtenir des images de la bonne source, c'est-à-dire de _cameraSource. Ensuite, nous affichons image reçue de l'appareil photo via _renderer dans la file d'attente principale.

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

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

Avant de commencer à utiliser l'appareil photo, nous avons besoin de l'autorisation de l'utilisateur pour y accéder. 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 a répondu. Ajoutez le code suivant à viewWillAppear:animated :

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

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

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

Maintenant, créez et exécutez l'application sur votre appareil iOS. Vous devriez voir flux de vue de la caméra après avoir accepté les autorisations d'accès à l'appareil photo.

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

Utiliser un graphique MediaPipe dans iOS

Ajouter les 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 graphe MediaPipe, nous devons ajouter sur le graphique que nous avons l'intention d'utiliser dans notre application. Ajoutez les éléments suivants : à la liste data de votre fichier BUILD:

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

Ajoutez maintenant la dépendance aux calculatrices utilisées dans ce graphique dans le champ deps dans le fichier BUILD:

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

Enfin, remplacez le nom du fichier ViewController.m par ViewController.mm pour qu'il accepte Objective-C++

Utiliser le graphique en ViewController

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

#import "mediapipe/objc/MPPGraph.h"

Déclarez une constante statique avec le nom du graphique, 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 initialiser ce graphique dans viewDidLoad d'abord. 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 graphique doit renvoyer les résultats du traitement des images de l'appareil photo ViewController Après avoir initialisé le graphique, ajoutez la ligne suivante pour définir le ViewController en tant que délégué de l'objet mediapipeGraph:

self.mediapipeGraph.delegate = self;

Pour éviter les conflits dans la 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;

Démarrer le graphique une fois que l'utilisateur a autorisé l'utilisation de 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 recevions des images de l'appareil photo dans processVideoFrame nous les avons affichés dans _liveView à l'aide de _renderer. Maintenant, nous vous devez envoyer ces images au graphique et afficher les résultats à la place. Modifier 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, c'est-à-dire "input_video".

Le graphique s'exécutera avec ce paquet d'entrée et renverra un résultat dans kOutputStream, par exemple "output_video". Nous pouvons implémenter le délégué suivant : 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>

Et c'est tout ! Créez et exécutez l'application sur votre appareil iOS. Vous devriez voir résultats de l'exécution du graphique de détection des bords 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 commun. Le code ce tutoriel est utilisé dans le modèle d'application common. L'application helloworld contient les les dépendances appropriées du fichier BUILD pour le graphique de détection de périphérie.