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.
Configuração
- Instale o MediaPipe Framework no sistema. Consulte o Guia de instalação do framework para saber mais.
- Configure seu dispositivo iOS para desenvolvimento.
- 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:
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:
AppDelegate.h
eAppDelegate.m
ViewController.h
eViewController.m
main.m
Info.plist
Main.storyboard
eLaunch.storyboard
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!
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.