Hello World! no iOS

Introdução

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

O que você criará

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

edge_detection_ios_gpu_gif

Configuração

  1. Instale o MediaPipe Framework no seu sistema. Consulte Instalação do framework para mais detalhes.
  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

Vamos usar o seguinte gráfico, 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 abaixo uma visualização do gráfico:

edge_detection_mobile_gpu

Este gráfico tem um único fluxo de entrada chamado input_video para todos os frames recebidos que será fornecido pela câmera do seu dispositivo.

O primeiro nó do gráfico, LuminanceCalculator, recebe um único pacote (imagem frame) e aplica uma alteração na luminância usando um sombreador do OpenGL. O resultado frame de imagem é enviado para o stream de saída luma_video.

O segundo nó, SobelEdgesCalculator, aplica a detecção de borda Pacotes no fluxo luma_video e gera uma saída output_video riacho.

Nosso aplicativo iOS vai mostrar os frames de imagem de saída do output_video. riacho.

Configuração inicial mínima do aplicativo

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

Primeiro, crie um projeto XCode em File > Novo > App de visualização única.

Definir o nome do produto como "HelloWorld" e usar uma organização apropriada identificador, 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 o idioma como Objective-C.

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

O HelloWorld.xcodeproj não será útil para este tutorial, porque usaremos Bazel para criar o aplicativo iOS. O conteúdo do $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 em um diretório chamado HelloWorld para um local que possa acessar o código-fonte do MediaPipe Framework. Por exemplo, o código-fonte da que vamos criar neste tutorial está localizado em mediapipe/examples/ios/HelloWorld: Chamaremos este caminho de $APPLICATION_PATH no codelab.

Crie um arquivo BUILD no $APPLICATION_PATH e adicione o seguinte build regras:

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 AppDelegate e As classes ViewController, main.m e os storyboards do aplicativo. A app baseado em modelo depende apenas do SDK do UIKit.

A regra ios_application usa a biblioteca HelloWorldAppLibrary do Objective-C gerado para criar um aplicativo iOS para instalação em seu 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

Depois, volte ao XCode, abra Window > Dispositivos e simuladores, selecione seu dispositivo e adicione ao seu dispositivo o arquivo .ipa gerado pelo comando acima. Este é o documento sobre como configurar e compilar apps de framework do iOS.

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

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

Neste tutorial, usaremos a classe MPPCameraInputSource para acessar e captura de frames da câmera. Essa classe usa a API AVCaptureSession para receber os frames da câmera.

No entanto, antes de usar essa classe, mude o arquivo Info.plist para oferecer suporte à câmera uso no app.

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

#import "mediapipe/objc/MPPCameraInputSource.h"

Adicione o seguinte 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 o _cameraSource, define a predefinição da sessão de captura e qual usar.

Precisamos transmitir os frames do _cameraSource para o aplicativo. ViewController para que elas sejam mostradas. MPPCameraInputSource é uma subclasse de MPPInputSource, que fornece um protocolo para os delegados, ou seja, 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 lidar com a configuração da câmera e processar os frames recebidos, devemos usar uma fila diferente da fila principal. Adicione o seguinte ao bloco de implementação de o ViewController:

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

No viewDidLoad(), adicione a linha abaixo depois de inicializar o Objeto _cameraSource:

[_cameraSource setDelegate:self queue:_videoQueue];

E adicione o código a seguir 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 processando frames da câmera.

Adicione a linha a seguir após a importação do cabeçalho no topo do arquivo, antes de a interface/implementação do ViewController:

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

Antes de implementar qualquer método do protocolo MPPInputSourceDelegate, precisamos configure uma maneira de exibir os frames da câmera. O framework Mediapipe fornece outro utilitário chamado MPPLayerRenderer para mostrar imagens na tela. Isso utilitário pode ser usado para mostrar objetos CVPixelBufferRef, que é o tipo de as 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 chamado _liveView para ViewController.

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

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

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

Volte para ViewController.m e adicione o seguinte código ao arquivo viewDidLoad() para inicialize 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 receber frames da origem certa, ou seja, a _cameraSource. Em seguida, exibimos o frame recebido da câmera por _renderer na fila principal.

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

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

Antes de executarmos a câmera, precisamos da permissão do usuário para acessá-la. MPPCameraInputSource fornece uma função requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler para solicitar acesso à câmera e realizar algumas tarefas quando o usuário tiver respondeu. 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 arquivo:

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. Você verá um feed de visualização da câmera depois de aceitar as permissões da câmera.

Agora estamos prontos para usar 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 MediaPipe. Para usar um gráfico do MediaPipe, precisamos adicionar um dependência do gráfico que pretendemos usar no aplicativo. Adicione o seguinte: linha à lista data no seu arquivo BUILD:

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

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

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

Por fim, renomeie o arquivo ViewController.m para ViewController.mm. Objective-C++:

Usar o gráfico no 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 fluxo de entrada e o stream de saída:

static NSString* const kGraphName = @"mobile_gpu";

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

Adicione a seguinte propriedade à 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;

Como explicado no comentário acima, vamos inicializar o gráfico viewDidLoad primeiro. Para fazer 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 em viewDidLoad da seguinte maneira:

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

O gráfico deve enviar os resultados do processamento de frames da câmera de volta ao ViewController: Adicione a linha a seguir depois de inicializar o gráfico para definir ViewController como delegado do objeto mediapipeGraph:

self.mediapipeGraph.delegate = self;

Para evitar contenção de memória ao processar quadros 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 recebíamos frames da câmera no processVideoFrame as exibimos no _liveView usando a _renderer. Agora, nós precisa enviar esses frames ao gráfico e renderizar os resultados. Modificar 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 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 gerará um resultado kOutputStream, ou seja, "output_video". Podemos implementar o seguinte 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 de interface de ViewController com MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

E isso é tudo! Crie e execute o app no seu dispositivo iOS. Aparecerá 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 comum. O código no este tutorial é usado no app de modelo comum. O aplicativo helloworld tem a dependências do arquivo BUILD apropriadas para o gráfico de detecção de borda.