Hello World! su iOS

Introduzione

Questo tutorial di Hello World! utilizza MediaPipe Framework per sviluppare un'applicazione per iOS che esegue un grafico MediaPipe su iOS.

Cosa creerai

Una semplice app della videocamera per il rilevamento Edge Sobel in tempo reale applicata a uno stream video in diretta su un dispositivo iOS.

edge_detection_ios_gpu_gif

Configurazione

  1. Installa MediaPipe Framework sul tuo sistema, consulta la guida all'installazione di Framework per maggiori dettagli.
  2. Configura il tuo dispositivo iOS per lo sviluppo.
  3. Configura Bazel sul tuo sistema per creare ed eseguire il deployment dell'app per iOS.

Grafico per il rilevamento dei bordi

Utilizzeremo il grafico seguente, 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"
}

Di seguito è riportata una visualizzazione del grafico:

edge_detection_mobile_gpu

Questo grafico ha un singolo stream di input denominato input_video per tutti i frame in arrivo che verranno forniti dalla videocamera del dispositivo.

Il primo nodo nel grafico, LuminanceCalculator, accetta un singolo pacchetto (frame immagine) e applica una variazione di luminanza utilizzando uno Shader OpenGL. Il frame immagine risultante viene inviato al flusso di output luma_video.

Il secondo nodo, SobelEdgesCalculator, applica il rilevamento perimetrale ai pacchetti in entrata nel flusso luma_video e restituisce come output il flusso di output output_video.

La nostra applicazione per iOS mostrerà i frame immagine di output del flusso output_video.

Configurazione minima dell'applicazione iniziale

Iniziamo con una semplice applicazione iOS e dimostriamo come usare bazel per crearla.

Innanzitutto, crea un progetto XCode tramite File > Nuovo > App con visualizzazione singola.

Imposta il nome del prodotto su "HelloWorld" e utilizza un identificatore dell'organizzazione appropriato, come com.google.mediapipe. L'identificatore organizzazione insieme al nome del prodotto sarà il bundle_id per l'applicazione, ad esempio com.google.mediapipe.HelloWorld.

Imposta la lingua su Objective-C.

Salva il progetto nella posizione appropriata. Chiamiamo questo elemento $PROJECT_TEMPLATE_LOC. Il tuo progetto sarà quindi nella directory $PROJECT_TEMPLATE_LOC/HelloWorld. Questa directory conterrà un'altra directory denominata HelloWorld e un file HelloWorld.xcodeproj.

HelloWorld.xcodeproj non sarà utile per questo tutorial perché useremo bazel per creare l'applicazione per iOS. I contenuti della directory $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld sono elencati di seguito:

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

Copia questi file in una directory denominata HelloWorld in una posizione che possa accedere al codice sorgente del framework MediaPipe. Ad esempio, il codice sorgente dell'applicazione che creeremo in questo tutorial si trova in mediapipe/examples/ios/HelloWorld. In tutto il codelab, chiameremo questo percorso come $APPLICATION_PATH.

Crea un file BUILD in $APPLICATION_PATH e aggiungi le seguenti regole di build:

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 regola objc_library aggiunge dipendenze per le classi AppDelegate e ViewController, main.m e gli storyboard dell'applicazione. L'app basata su modelli dipende solo dall'SDK UIKit.

La regola ios_application utilizza la libreria HelloWorldAppLibrary Objective-C generata per creare un'applicazione iOS da installare sul dispositivo iOS.

Per creare l'app, utilizza il comando seguente in un terminale:

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

Ad esempio, per creare l'applicazione HelloWorldApp in mediapipe/examples/ios/helloworld, utilizza il comando seguente:

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

Dopodiché torna a XCode, apri Finestra > Dispositivi e simulatori, seleziona il tuo dispositivo e aggiungi al dispositivo il file .ipa generato dal comando precedente. Qui puoi trovare il documento su come configurare e compilare le app Framework per iOS.

Apri l'applicazione sul dispositivo. Poiché è vuota, dovrebbe mostrare una schermata bianca vuota.

Utilizzare la videocamera per il feed della visione in diretta

In questo tutorial useremo la classe MPPCameraInputSource per accedere ai frame e acquisirli dalla fotocamera. Questa classe utilizza l'API AVCaptureSession per ottenere i frame dalla fotocamera.

Tuttavia, prima di utilizzare questo corso, modifica il file Info.plist per supportare l'utilizzo della videocamera nell'app.

In ViewController.m, aggiungi la seguente riga di importazione:

#import "mediapipe/objc/MPPCameraInputSource.h"

Aggiungi quanto segue al blocco di implementazione per creare un oggetto _cameraSource:

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

Aggiungi il codice seguente 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;
}

Il codice inizializza _cameraSource, imposta la preimpostazione della sessione di acquisizione e la videocamera da utilizzare.

Dobbiamo recuperare i frame da _cameraSource nella nostra applicazione ViewController per visualizzarli. MPPCameraInputSource è una sottoclasse di MPPInputSource, che fornisce un protocollo ai propri delegati, ovvero MPPInputSourceDelegate. Di conseguenza, la nostra applicazione ViewController può essere un delegato di _cameraSource.

Aggiorna di conseguenza la definizione dell'interfaccia di ViewController:

@interface ViewController () <MPPInputSourceDelegate>

Per gestire la configurazione della videocamera ed elaborare i frame in arrivo, dovremmo usare una coda diversa dalla coda principale. Aggiungi quanto segue al blocco di implementazione di ViewController:

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

In viewDidLoad(), aggiungi la seguente riga dopo aver inizializzato l'oggetto _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

Aggiungi il codice seguente per inizializzare la coda prima di configurare l'oggetto _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);

Utilizzeremo una coda seriale con priorità QOS_CLASS_USER_INTERACTIVE per l'elaborazione dei fotogrammi delle videocamere.

Aggiungi la seguente riga dopo l'importazione dell'intestazione nella parte superiore del file, prima dell'interfaccia/implementazione di ViewController:

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

Prima di implementare qualsiasi metodo dal protocollo MPPInputSourceDelegate, dobbiamo configurare un modo per visualizzare i fotogrammi delle videocamere. Mediapipe Framework fornisce un'altra utilità chiamata MPPLayerRenderer per visualizzare le immagini sullo schermo. Questa utilità può essere utilizzata per visualizzare gli oggetti CVPixelBufferRef, ovvero il tipo di immagini fornite da MPPCameraInputSource ai propri delegati.

In ViewController.m, aggiungi la seguente riga di importazione:

#import "mediapipe/objc/MPPLayerRenderer.h"

Per visualizzare le immagini dello schermo, dobbiamo aggiungere un nuovo oggetto UIView chiamato _liveView a ViewController.

Aggiungi le seguenti righe al blocco di implementazione di ViewController:

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

Vai a Main.storyboard, aggiungi un oggetto UIView dalla libreria degli oggetti a View della classe ViewController. Aggiungi una presa di riferimento da questa vista all'oggetto _liveView che hai appena aggiunto alla classe ViewController. Ridimensiona la vista in modo che sia centrata e copra l'intero schermo dell'applicazione.

Torna a ViewController.m e aggiungi il seguente codice a viewDidLoad() per inizializzare l'oggetto _renderer:

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

Per acquisire i fotogrammi dalla fotocamera, implementeremo il seguente metodo:

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

Questo è un metodo delegato di MPPInputSource. Innanzitutto verifichiamo che stiamo ricevendo i frame dall'origine corretta, ovvero _cameraSource. Dopodiché mostriamo il frame ricevuto dalla fotocamera tramite _renderer nella coda principale.

Ora dobbiamo accendere la videocamera non appena sta per apparire la visualizzazione per mostrare i fotogrammi. Per farlo, implementeremo la funzione viewWillAppear:(BOOL)animated:

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

Prima di iniziare a far funzionare la fotocamera, abbiamo bisogno dell'autorizzazione dell'utente per accedervi. MPPCameraInputSource fornisce la funzione requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler per richiedere l'accesso alla videocamera e svolgere operazioni quando l'utente ha risposto. Aggiungi il seguente codice a viewWillAppear:animated:

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

Prima di creare l'applicazione, aggiungi le seguenti dipendenze al tuo file BUILD:

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

Ora crea ed esegui l'applicazione sul tuo dispositivo iOS. Dovresti vedere un feed di visualizzazione in diretta della videocamera dopo aver accettato le autorizzazioni di accesso alla videocamera.

Ora siamo pronti per utilizzare i frame della fotocamera in un grafico MediaPipe.

Utilizzo di un grafico MediaPipe in iOS

Aggiungi dipendenze pertinenti

Abbiamo già aggiunto le dipendenze del codice del framework MediaPipe, che contiene l'API iOS, per utilizzare un grafico MediaPipe. Per utilizzare un grafico MediaPipe, dobbiamo aggiungere una dipendenza sul grafico che intendiamo utilizzare nella nostra applicazione. Aggiungi la seguente riga all'elenco data nel tuo file BUILD:

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

Ora aggiungi la dipendenza alle calcolatrici utilizzate in questo grafico nel campo deps nel file BUILD:

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

Infine, rinomina il file ViewController.m in ViewController.mm per supportare Objective-C++.

Usa il grafico in ViewController

In ViewController.m, aggiungi la seguente riga di importazione:

#import "mediapipe/objc/MPPGraph.h"

Dichiara una costante statica con il nome del grafico, del flusso di input e del flusso di output:

static NSString* const kGraphName = @"mobile_gpu";

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

Aggiungi la seguente proprietà all'interfaccia di ViewController:

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

Come spiegato nel commento sopra, inizializzamo prima questo grafico in viewDidLoad. Per farlo, dobbiamo caricare il grafico dal file .pbtxt utilizzando la seguente funzione:

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

Utilizza questa funzione per inizializzare il grafico in viewDidLoad nel seguente modo:

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

Il grafico dovrebbe inviare i risultati dell'elaborazione dei fotogrammi della videocamera all'elemento ViewController. Aggiungi la seguente riga dopo aver inizializzato il grafico per impostare ViewController come delegato dell'oggetto mediapipeGraph:

self.mediapipeGraph.delegate = self;

Per evitare il conflitto di memoria durante l'elaborazione dei frame del feed video in diretta, aggiungi la seguente riga:

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

Ora avvia il grafico quando l'utente ha concesso l'autorizzazione a utilizzare la fotocamera nella nostra 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];
    });
  }
}];

In precedenza, quando ricevevamo frame dalla fotocamera nella funzione processVideoFrame, li visualizzavamo nell'elemento _liveView utilizzando _renderer. Ora dobbiamo inviare i frame al grafico e visualizzare i risultati. Modifica l'implementazione di questa funzione nel seguente modo:

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

Inviamo imageBuffer a self.mediapipeGraph come pacchetto di tipo MPPPacketTypePixelBuffer nel flusso di input kInputStream, ad esempio "input_video".

Il grafico verrà eseguito con questo pacchetto di input e restituirà un risultato in kOutputStream, ad esempio "output_video". Possiamo implementare il seguente metodo delegato per ricevere i pacchetti in questo flusso di output e visualizzarli sullo schermo:

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

Aggiorna la definizione dell'interfaccia di ViewController con MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

Questo è tutto. Crea ed esegui l'app sul tuo dispositivo iOS. Dovresti vedere i risultati dell'esecuzione del grafico di rilevamento dei bordi su un feed video in diretta. Congratulazioni!

edge_detection_ios_gpu_gif

Tieni presente che gli esempi per iOS ora utilizzano un modello di app common. Il codice di questo tutorial viene utilizzato nell'app modello common. L'app helloworld ha le dipendenze dei file BUILD appropriate per il grafico di rilevamento perimetrale.