Hello World! su iOS

Introduzione

Ciao mondo! il tutorial utilizza il framework MediaPipe per sviluppare una che esegue un grafico MediaPipe su iOS.

Cosa creerai

Una semplice app della fotocamera per il rilevamento dei bordi Sobel in tempo reale applicato a un video in diretta trasmettere in streaming su un dispositivo iOS.

edge_detection_ios_gpu_gif

Configurazione

  1. Installa il framework MediaPipe sul tuo sistema, vedi Installazione di framework. Google Cloud.
  2. Configura il tuo dispositivo iOS per lo sviluppo.
  3. Configura Bazel sul tuo sistema per creare e implementare l'app per iOS.

Grafico per il rilevamento dei bordi

Utilizzeremo il seguente grafico, 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 flusso di input denominato input_video per tutti i frame in entrata fornito dalla fotocamera del dispositivo.

Il primo nodo del grafico, LuminanceCalculator, accetta un singolo pacchetto (immagine frame) e applica una variazione di luminanza utilizzando uno shaker OpenGL. Il risultato frame immagine viene inviato allo stream di output luma_video.

Il secondo nodo, SobelEdgesCalculator, applica il rilevamento perimetrale alla rete di pacchetti nel flusso luma_video e restituisce l'output output_video flusso di dati.

La nostra applicazione iOS mostrerà i frame immagine di output dell'output_video flusso di dati.

Configurazione iniziale minima dell'applicazione

Iniziamo con una semplice applicazione per iOS e dimostriamo come utilizzare bazel per realizzarla.

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

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

Imposta il linguaggio su Objective-C.

Salva il progetto nella posizione appropriata. Lo chiamiamo $PROJECT_TEMPLATE_LOC. Quindi il tuo progetto sarà Directory $PROJECT_TEMPLATE_LOC/HelloWorld. Questa directory contiene un'altra directory denominata HelloWorld e un file HelloWorld.xcodeproj.

HelloWorld.xcodeproj non sarà utile per questo tutorial, perché lo utilizzeremo per creare l'applicazione iOS. I contenuti La directory $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld è elencata 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.
di Gemini Advanced.

Copia questi file in una directory denominata HelloWorld in una posizione che può accedere con il codice sorgente del framework MediaPipe. Ad esempio, il codice sorgente che creerai in questo tutorial si trova mediapipe/examples/ios/HelloWorld. Ci riferiremo a questo percorso come $APPLICATION_PATH in tutto il codelab.

Crea un file BUILD in $APPLICATION_PATH e aggiungi la seguente build regole:

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 AppDelegate e classi ViewController, main.m e gli storyboard dell'applicazione. La app basata su modelli dipende solo dall'SDK UIKit.

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

Per creare l'app, usa questo comando 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, usa questo comando:

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

Quindi, torna a XCode, apri Window > Dispositivi e simulatori, seleziona dispositivo e aggiungi al tuo dispositivo il file .ipa generato dal comando precedente. Questo è il documento sulla configurazione e la compilazione delle app Framework per iOS.

Apri l'applicazione sul dispositivo. Poiché è vuoto, dovrebbe essere visualizzato un schermata bianca vuota.

Utilizzare la videocamera per il feed della visione in diretta

In questo tutorial, utilizzeremo la classe MPPCameraInputSource per accedere e acquisire fotogrammi dalla fotocamera. Questo corso utilizza l'API AVCaptureSession per recuperare i fotogrammi della fotocamera.

Ma prima di utilizzare questo corso, modifica il file Info.plist in modo che supporti la fotocamera all'utilizzo nell'app.

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

#import "mediapipe/objc/MPPCameraInputSource.h"

Aggiungi quanto segue al relativo 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 sessione di acquisizione preimpostata e la videocamera.

Dobbiamo trasferire i frame dal _cameraSource alla nostra applicazione ViewController per visualizzarli. MPPCameraInputSource è una sottoclasse di MPPInputSource, che fornisce un protocollo per i suoi delegati, ovvero MPPInputSourceDelegate. Quindi la nostra applicazione ViewController può essere un delegato di _cameraSource.

Aggiorna la definizione dell'interfaccia di ViewController di conseguenza:

@interface ViewController () <MPPInputSourceDelegate>

Per gestire la configurazione della videocamera e l'elaborazione dei fotogrammi in arrivo, è necessario usare una coda diverso da quello della 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 il valore Oggetto _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

Aggiungi il seguente codice per inizializzare la coda prima di configurare il 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 fotocamere.

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

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

Prima di implementare qualsiasi metodo del protocollo MPPInputSourceDelegate, dobbiamo per prima cosa imposta un modo per visualizzare i fotogrammi della fotocamera. Il framework Mediapipe fornisce un'altra utility chiamata MPPLayerRenderer per visualizzare immagini sullo schermo. Questo può essere utilizzata per visualizzare CVPixelBufferRef oggetti, che è il tipo le immagini fornite da MPPCameraInputSource ai suoi delegati.

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

#import "mediapipe/objc/MPPLayerRenderer.h"

Per visualizzare 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 di oggetti al View del corso ViewController. Aggiungi uno sbocco di riferimento da questa visualizzazione a l'oggetto _liveView che hai appena aggiunto alla classe ViewController. Ridimensiona la in modo che sia centrato e copra l'intera schermata dell'applicazione.

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

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

Per ottenere 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 ricevere frame dall'origine corretta, ovvero _cameraSource. Quindi mostriamo il fotogramma ricevuto dalla fotocamera tramite _renderer nella coda principale.

Ora dobbiamo avviare la videocamera non appena la visualizzazione per visualizzare i fotogrammi è che sta per essere visualizzato. Per farlo, implementeremo il Funzione viewWillAppear:(BOOL)animated:

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

Per poter iniziare a eseguire la fotocamera, è necessaria l'autorizzazione dell'utente per l'accesso. MPPCameraInputSource fornisce una funzione requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler per richiedere l'accesso alla fotocamera e svolgere alcune operazioni quando l'utente ha hanno 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 a BUILD file:

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 una live feed visualizzazione videocamera dopo aver accettato le autorizzazioni di accesso alla videocamera.

Ora siamo pronti per utilizzare i fotogrammi 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 usare un grafico MediaPipe. Per utilizzare un grafico MediaPipe, dobbiamo aggiungere dal grafico che intendiamo usare nella nostra applicazione. Aggiungi quanto segue 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 renderlo 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, il flusso di input e 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 precedente, inizializziamo questo grafico viewDidLoad prima. 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 come segue:

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

Il grafico dovrebbe inviare i risultati dell'elaborazione dei fotogrammi della fotocamera al ViewController. Aggiungi la seguente riga dopo l'inizializzazione del grafico per impostare la ViewController come delegato dell'oggetto mediapipeGraph:

self.mediapipeGraph.delegate = self;

Per evitare la contesa della memoria durante l'elaborazione dei frame dal feed video in diretta, aggiungi nella riga seguente:

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

Ora avvia il grafico una volta che 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];
    });
  }
}];

Finora, quando ricevevamo fotogrammi dalla fotocamera nell'processVideoFrame , le abbiamo visualizzate in _liveView utilizzando la funzione _renderer. Ora inviare i frame al grafico e visualizzare i risultati. Modifica l'implementazione di questa funzione per fare quanto segue:

-   (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 all'indirizzo self.mediapipeGraph come pacchetto di tipo MPPPacketTypePixelBuffer nel flusso di input kInputStream, ad esempio "video_input".

Il grafico verrà eseguito con questo pacchetto di input e restituirà un risultato kOutputStream, ad esempio "output_video". Possiamo implementare il seguente delegato per ricevere i pacchetti nel 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>

E questo è tutto. Crea ed esegui l'app sul tuo dispositivo iOS. Dovresti vedere l'etichetta risultati dell'esecuzione del grafico del rilevamento dei bordi su un feed video in diretta. Complimenti!

edge_detection_ios_gpu_gif

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