Hello World! no iOS

Introdução

Neste tutorial, Hello World! usa o MediaPipe Framework para desenvolver um aplicativo iOS que executa um gráfico do MediaPipe no iOS.

O que você vai criar

Um app de câmera simples para detecção de borda Sobel em tempo real, aplicado a um stream de vídeo ao vivo em um dispositivo iOS.

edge_detection_ios_gpu_gif

Configuração

  1. Instale o MediaPipe Framework no sistema. Consulte o Guia de instalação do framework para saber mais.
  2. Configure seu dispositivo iOS para desenvolvimento.
  3. Configure o Bazel no seu sistema para criar e implantar o app iOS.

Gráfico para detecção de borda

Usaremos o gráfico a seguir, 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"
}

Confira a visualização do gráfico abaixo:

edge_detection_mobile_gpu

Esse gráfico tem um único stream de entrada, chamado input_video, para todos os frames recebidos que serão fornecidos pela câmera do dispositivo.

O primeiro nó no gráfico, LuminanceCalculator, usa um único pacote (frame de imagem) e aplica uma mudança na luminosidade usando um sombreador OpenGL. O frame de imagem resultante é enviado ao stream de saída luma_video.

O segundo nó, SobelEdgesCalculator, aplica a detecção de borda aos pacotes recebidos no stream luma_video e gera resultados no stream de saída output_video.

Nosso aplicativo iOS exibirá os frames de imagem de saída do stream output_video.

Configuração mínima inicial do aplicativo

Primeiro, vamos começar com um aplicativo iOS simples e demonstrar como usar o bazel para criá-lo.

Primeiro, crie um projeto XCode em File > New > Single View App.

Defina o nome do produto como "HelloWorld" e use um identificador da organização adequado, como com.google.mediapipe. O identificador da organização com o nome do produto será o bundle_id do aplicativo, como com.google.mediapipe.HelloWorld.

Defina a linguagem como Objective-C.

Salve o projeto em um local adequado. Vamos chamá-lo de $PROJECT_TEMPLATE_LOC. Portanto, seu projeto estará no diretório $PROJECT_TEMPLATE_LOC/HelloWorld. Esse diretório conterá outro diretório chamado HelloWorld e um arquivo HelloWorld.xcodeproj.

O HelloWorld.xcodeproj não será útil para este tutorial, porque usaremos o Bazel para criar o aplicativo para iOS. O conteúdo do diretório $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld está listado abaixo:

  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. Assets.xcassets.

Copie esses arquivos para um diretório chamado HelloWorld em um local que possa acessar o código-fonte do MediaPipe Framework. Por exemplo, o código-fonte do aplicativo que criaremos neste tutorial está localizado em mediapipe/examples/ios/HelloWorld. Chamaremos esse caminho de $APPLICATION_PATH em todo o codelab.

Crie um arquivo BUILD no $APPLICATION_PATH e adicione as seguintes regras de criação:

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 = [],
)

A regra objc_library adiciona dependências para as classes AppDelegate e ViewController, main.m e os storyboards do aplicativo. O app modelo depende apenas do SDK UIKit.

A regra ios_application usa a biblioteca Objective-C HelloWorldAppLibrary gerada para criar um aplicativo iOS para instalação no dispositivo iOS.

Para criar o app, use o seguinte comando em um terminal:

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

Por exemplo, para criar o aplicativo HelloWorldApp em mediapipe/examples/ios/helloworld, use o seguinte comando:

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

Em seguida, volte ao XCode, abra Window > Devices and Simulators, selecione seu dispositivo e adicione o arquivo .ipa gerado pelo comando acima ao seu dispositivo. Confira o documento sobre configuração e compilação de apps da estrutura para iOS.

Abra o aplicativo no seu dispositivo. Como ele está vazio, ele vai mostrar uma tela em branco.

Usar a câmera para o feed de imagens ao vivo

Neste tutorial, vamos usar a classe MPPCameraInputSource para acessar e capturar frames da câmera. Essa classe usa a API AVCaptureSession para conseguir os frames da câmera.

Mas, antes de usar essa classe, mude o arquivo Info.plist para oferecer suporte ao uso da câmera no app.

Em ViewController.m, adicione a seguinte linha de importação:

#import "mediapipe/objc/MPPCameraInputSource.h"

Adicione o código abaixo ao bloco de implementação para criar um objeto _cameraSource:

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

Adicione o código a seguir 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;
}

O código inicializa _cameraSource, define a predefinição da sessão de captura e qual câmera usar.

Precisamos receber frames do _cameraSource no ViewController do nosso aplicativo para exibi-los. MPPCameraInputSource é uma subclasse de MPPInputSource, que fornece um protocolo para os delegados, ou seja, o MPPInputSourceDelegate. Portanto, nosso aplicativo ViewController pode ser um delegado de _cameraSource.

Atualize a definição da interface de ViewController conforme necessário:

@interface ViewController () <MPPInputSourceDelegate>

Para gerenciar a configuração da câmera e processar os frames recebidos, é preciso usar uma fila diferente da principal. Adicione o seguinte ao bloco de implementação do ViewController:

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

Em viewDidLoad(), adicione a seguinte linha depois de inicializar o objeto _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

E adicione o seguinte código para inicializar a fila antes de configurar o 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 uma fila serial com prioridade QOS_CLASS_USER_INTERACTIVE para processar frames da câmera.

Adicione a linha abaixo depois que o cabeçalho importar a parte de cima do arquivo, antes da interface/implementação do ViewController:

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

Antes de implementar qualquer método do protocolo MPPInputSourceDelegate, é preciso configurar uma maneira de exibir os frames da câmera. O Mediapipe Framework fornece outro utilitário, chamado MPPLayerRenderer, para exibir imagens na tela. Esse utilitário pode ser usado para exibir objetos CVPixelBufferRef, que são o tipo das imagens fornecidas por MPPCameraInputSource aos delegados.

Em ViewController.m, adicione a seguinte linha de importação:

#import "mediapipe/objc/MPPLayerRenderer.h"

Para mostrar imagens da tela, precisamos adicionar um novo objeto UIView com o nome _liveView ao ViewController.

Adicione as seguintes linhas ao bloco de implementação do ViewController:

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

Acesse Main.storyboard e adicione um objeto UIView da biblioteca de objetos ao View da classe ViewController. Adicione uma saída de referência dessa visualização ao objeto _liveView que você acabou de adicionar à classe ViewController. Redimensione a visualização para que ela fique centralizada e cubra toda a tela do aplicativo.

Volte para ViewController.m e adicione o seguinte código a viewDidLoad() para inicializar o objeto _renderer:

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

Para receber frames da câmera, vamos implementar o seguinte 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);
  });
}

Esse é um método delegado de MPPInputSource. Primeiro, verificamos se estamos recebendo frames da origem correta, ou seja, o _cameraSource. Em seguida, exibimos o frame recebido da câmera via _renderer na fila principal.

Agora, precisamos iniciar a câmera assim que a visualização para mostrar os frames estiver prestes a aparecer. Para fazer isso, vamos implementar a função viewWillAppear:(BOOL)animated:

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

Antes de começar a executar a câmera, precisamos da permissão do usuário para acessá-la. O MPPCameraInputSource fornece uma função requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler para solicitar acesso à câmera e realizar algum trabalho quando o usuário responder. Adicione o código a seguir à viewWillAppear:animated:

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

Antes de criar o aplicativo, adicione as seguintes dependências ao arquivo BUILD:

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

Agora crie e execute o aplicativo no seu dispositivo iOS. Depois de aceitar as permissões da câmera, você verá um feed de visualização da câmera ao vivo.

Agora já podemos usar os frames da câmera em um gráfico do MediaPipe.

Como usar um gráfico do MediaPipe no iOS

Adicionar dependências relevantes

Já adicionamos as dependências do código do framework do MediaPipe que contém a API do iOS para usar um gráfico do MediaPipe. Para usar um gráfico do MediaPipe, precisamos adicionar uma dependência ao gráfico que pretendemos usar no aplicativo. Adicione a seguinte linha à lista data no arquivo BUILD:

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

Agora, adicione a dependência às calculadoras usadas neste gráfico no campo deps no arquivo BUILD:

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

Por fim, renomeie o arquivo ViewController.m como ViewController.mm para oferecer suporte a Objective-C++.

Usar o gráfico em ViewController

Em ViewController.m, adicione a seguinte linha de importação:

#import "mediapipe/objc/MPPGraph.h"

Declare uma constante estática com o nome do gráfico, o stream de entrada e o fluxo de saída:

static NSString* const kGraphName = @"mobile_gpu";

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

Adicione a propriedade abaixo à interface do ViewController:

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

Conforme explicado no comentário acima, primeiro vamos inicializar esse gráfico em viewDidLoad. Para isso, precisamos carregar o gráfico do arquivo .pbtxt usando a seguinte função:

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

Use essa função para inicializar o gráfico no viewDidLoad da seguinte maneira:

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

O gráfico precisa enviar os resultados do processamento de frames da câmera para o ViewController. Adicione a seguinte linha depois de inicializar o gráfico para definir o ViewController como um delegado do objeto mediapipeGraph:

self.mediapipeGraph.delegate = self;

Para evitar a contenção de memória ao processar frames do feed de vídeo ao vivo, adicione a seguinte linha:

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

Agora, inicie o gráfico quando o usuário conceder permissão para usar a câmera no nosso 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];
    });
  }
}];

Anteriormente, quando recebemos frames da câmera na função processVideoFrame, eles eram mostrados no _liveView usando a _renderer. Agora, precisamos enviar esses frames para o gráfico e renderizar os resultados. Modifique a implementação dessa função para fazer o seguinte:

-   (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 o imageBuffer para self.mediapipeGraph como um pacote do tipo MPPPacketTypePixelBuffer no fluxo de entrada kInputStream, ou seja, "input_video".

O gráfico será executado com esse pacote de entrada e vai gerar um resultado em kOutputStream, ou seja, "output_video". Podemos implementar o seguinte método delegado para receber pacotes nesse stream de saída e exibi-los na tela:

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

Atualize a definição da interface de ViewController com MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

E isso é tudo! Crie e execute o app no seu dispositivo iOS. Você verá os resultados da execução do gráfico de detecção de borda em um feed de vídeo ao vivo. Parabéns!

edge_detection_ios_gpu_gif

Os exemplos de iOS agora usam um aplicativo de modelo common. O código neste tutorial é usado no aplicativo de modelo common. O aplicativo helloworld tem as dependências de arquivo BUILD apropriadas para o gráfico de detecção de borda.