Introdução
Neste tutorial, Hello World! usa o MediaPipe Framework para desenvolver um aplicativo Android que executa um gráfico do MediaPipe no Android.
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 Android.
Configuração
- Instale o MediaPipe Framework no sistema. Consulte o Guia de instalação do framework para saber mais.
- Instale o SDK de desenvolvimento do Android e o Android NDK. Consulte como fazer isso também no [Guia de instalação do framework].
- Ative as opções do desenvolvedor no seu dispositivo Android.
- Configure o Bazel no seu sistema para criar e implantar o app Android.
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 in
# mediapipe/examples/android/src/java/com/mediapipe/apps/basic and
# mediapipe/examples/ios/edgedetectiongpu.
# 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 Android exibirá os frames de imagem de saída do
fluxo output_video
.
Configuração mínima inicial do aplicativo
Primeiro, começamos com um aplicativo Android simples que exibe "Hello World!"
na tela. Você pode pular esta etapa se estiver familiarizado com a criação de aplicativos
Android usando bazel
.
Crie um novo diretório onde você vai criar o app Android. Por
exemplo, o código completo deste tutorial pode ser encontrado em
mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic
. Chamaremos
esse caminho como $APPLICATION_PATH
em todo o codelab.
Observe que, no caminho para o aplicativo:
- O nome do aplicativo é
helloworld
. - O
$PACKAGE_PATH
do aplicativo écom.google.mediapipe.apps.basic
. Isso é usado nos snippets de código neste tutorial. Por isso, lembre-se de usar seu próprio$PACKAGE_PATH
ao copiar/usar os snippets de código.
Adicione um arquivo activity_main.xml
a $APPLICATION_PATH/res/layout
. Um TextView
será exibido em tela cheia do aplicativo com a string Hello
World!
:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
Adicione um MainActivity.java
simples ao $APPLICATION_PATH
que carrega o conteúdo
do layout activity_main.xml
, conforme mostrado abaixo:
package com.google.mediapipe.apps.basic;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
/** Bare-bones main activity. */
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Adicione um arquivo de manifesto, AndroidManifest.xml
a $APPLICATION_PATH
, que
inicia MainActivity
na inicialização do aplicativo:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.mediapipe.apps.basic">
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="19" />
<application
android:allowBackup="true"
android:label="${appName}"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="${mainActivity}"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Estamos usando um tema Theme.AppCompat
no nosso app, então precisamos
de referências de tema apropriadas. Adicione colors.xml
a
$APPLICATION_PATH/res/values/
:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
</resources>
Adicione styles.xml
a $APPLICATION_PATH/res/values/
:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Para criar o aplicativo, adicione um arquivo BUILD
a $APPLICATION_PATH
.
${appName}
e ${mainActivity}
no manifesto serão substituídos por strings
especificadas em BUILD
, conforme mostrado abaixo.
android_library(
name = "basic_lib",
srcs = glob(["*.java"]),
manifest = "AndroidManifest.xml",
resource_files = glob(["res/**"]),
deps = [
"//third_party:android_constraint_layout",
"//third_party:androidx_appcompat",
],
)
android_binary(
name = "helloworld",
manifest = "AndroidManifest.xml",
manifest_values = {
"applicationId": "com.google.mediapipe.apps.basic",
"appName": "Hello World",
"mainActivity": ".MainActivity",
},
multidex = "native",
deps = [
":basic_lib",
],
)
A regra android_library
adiciona dependências para MainActivity
, arquivos de recursos
e AndroidManifest.xml
.
A regra android_binary
usa a biblioteca Android basic_lib
gerada para
criar um APK binário para instalação no dispositivo Android.
Para criar o app, use o seguinte comando:
bazel build -c opt --config=android_arm64 $APPLICATION_PATH:helloworld
Instale o arquivo APK gerado usando adb install
. Exemplo:
adb install bazel-bin/$APPLICATION_PATH/helloworld.apk
Abra o aplicativo no seu dispositivo. Ele exibirá uma tela com o texto
Hello World!
.
Como usar a câmera via CameraX
Permissões da câmera
Para usar a câmera no nosso aplicativo, precisamos solicitar que o usuário conceda
acesso à câmera. Para solicitar permissões de câmera, adicione o seguinte a
AndroidManifest.xml
:
<!-- For using the camera -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
Altere a versão mínima do SDK para 21
e a versão de destino do SDK para 27
no
mesmo arquivo:
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="27" />
Isso garante que o usuário seja solicitado a solicitar a permissão da câmera e permite usar a biblioteca CameraX para o acesso à câmera.
Para solicitar permissões de câmera, podemos usar um utilitário fornecido pelos componentes do MediaPipe Framework, chamado PermissionHelper
. Para usá-lo, adicione uma dependência
"//mediapipe/java/com/google/mediapipe/components:android_components"
na regra
mediapipe_lib
em BUILD
.
Para usar PermissionHelper
em MainActivity
, adicione a seguinte linha à função onCreate
:
PermissionHelper.checkAndRequestCameraPermissions(this);
Isso solicita que o usuário com uma caixa de diálogo na tela solicite permissões para usar a câmera nesse aplicativo.
Adicione o código a seguir para processar a resposta do usuário:
@Override
public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void onResume() {
super.onResume();
if (PermissionHelper.cameraPermissionsGranted(this)) {
startCamera();
}
}
public void startCamera() {}
Deixaremos o método startCamera()
vazio por enquanto. Quando o usuário responder
à solicitação, o MainActivity
será retomado e onResume()
será chamado.
O código confirmará que as permissões de uso da câmera foram concedidas e, em seguida, iniciará a câmera.
Recrie e instale o aplicativo. Aparecerá uma solicitação solicitando acesso à câmera para o aplicativo.
Acesso à câmera
Com as permissões de câmera disponíveis, podemos iniciar e buscar frames da câmera.
Para visualizar os frames da câmera, usaremos um SurfaceView
. Cada frame
da câmera será armazenado em um objeto SurfaceTexture
. Para usá-los, primeiro precisamos
mudar o layout do nosso aplicativo.
Remova todo o bloco de código TextView
do
$APPLICATION_PATH/res/layout/activity_main.xml
e adicione o seguinte
código:
<FrameLayout
android:id="@+id/preview_display_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">
<TextView
android:id="@+id/no_camera_access_view"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:gravity="center"
android:text="@string/no_camera_access" />
</FrameLayout>
Esse bloco de código tem um novo FrameLayout
chamado preview_display_layout
e um
TextView
aninhado dentro dele, chamado no_camera_access_preview
. Quando as permissões
de acesso à câmera não são concedidas, nosso aplicativo mostra a
TextView
com uma mensagem de string, armazenada na variável no_camera_access
.
Adicione a linha abaixo ao arquivo $APPLICATION_PATH/res/values/strings.xml
:
<string name="no_camera_access" translatable="false">Please grant camera permissions.</string>
Quando o usuário não concede permissão para a câmera, a tela fica assim:
Agora, vamos adicionar os objetos SurfaceTexture
e SurfaceView
a
MainActivity
:
private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;
Na função onCreate(Bundle)
, adicione as duas linhas a seguir antes de solicitar permissões de câmera:
previewDisplayView = new SurfaceView(this);
setupPreviewDisplayView();
Agora, adicione o código que define setupPreviewDisplayView()
:
private void setupPreviewDisplayView() {
previewDisplayView.setVisibility(View.GONE);
ViewGroup viewGroup = findViewById(R.id.preview_display_layout);
viewGroup.addView(previewDisplayView);
}
Definimos um novo objeto SurfaceView
e o adicionamos ao
objeto preview_display_layout
FrameLayout
para que possamos usá-lo para mostrar
os frames da câmera com um objeto SurfaceTexture
chamado previewFrameTexture
.
Para usar a previewFrameTexture
e acessar os frames da câmera, usaremos o CameraX.
O framework fornece um utilitário chamado CameraXPreviewHelper
para usar o CameraX.
Essa classe atualiza um listener quando a câmera é iniciada via
onCameraStarted(@Nullable SurfaceTexture)
.
Para usar esse utilitário, modifique o arquivo BUILD
para adicionar uma dependência a
"//mediapipe/java/com/google/mediapipe/components:android_camerax_helper"
.
Agora importe CameraXPreviewHelper
e adicione a seguinte linha ao MainActivity
:
private CameraXPreviewHelper cameraHelper;
Agora, podemos adicionar nossa implementação a startCamera()
:
public void startCamera() {
cameraHelper = new CameraXPreviewHelper();
cameraHelper.setOnCameraStartedListener(
surfaceTexture -> {
previewFrameTexture = surfaceTexture;
// Make the display view visible to start showing the preview.
previewDisplayView.setVisibility(View.VISIBLE);
});
}
Isso cria um novo objeto CameraXPreviewHelper
e adiciona um listener
anônimo ao objeto. Quando cameraHelper
sinalizar que a câmera foi iniciada
e uma surfaceTexture
para capturar frames está disponível, salvamos esse
surfaceTexture
como previewFrameTexture
e tornamos o previewDisplayView
visível para que possamos começar a ver frames da previewFrameTexture
.
No entanto, antes de iniciar a câmera, precisamos decidir qual câmera queremos
usar. CameraXPreviewHelper
é herdado de CameraHelper
, que fornece duas
opções, FRONT
e BACK
. Podemos transmitir a decisão do arquivo BUILD
como metadados para que nenhuma mudança de código seja necessária para criar outra versão do
app usando uma câmera diferente.
Supondo que queremos usar a câmera BACK
para realizar a detecção da borda em uma cena ao vivo
que visualizamos da câmera, adicione os metadados a AndroidManifest.xml
:
...
<meta-data android:name="cameraFacingFront" android:value="${cameraFacingFront}"/>
</application>
</manifest>
e especifique a seleção em BUILD
na regra binária do Android helloworld
com uma nova entrada em manifest_values
:
manifest_values = {
"applicationId": "com.google.mediapipe.apps.basic",
"appName": "Hello World",
"mainActivity": ".MainActivity",
"cameraFacingFront": "False",
},
Agora, em MainActivity
para recuperar os metadados especificados em manifest_values
,
adicione um objeto ApplicationInfo
:
private ApplicationInfo applicationInfo;
Na função onCreate()
, adicione:
try {
applicationInfo =
getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
Log.e(TAG, "Cannot find application info: " + e);
}
Agora, adicione a seguinte linha ao final da função startCamera()
:
CameraHelper.CameraFacing cameraFacing =
applicationInfo.metaData.getBoolean("cameraFacingFront", false)
? CameraHelper.CameraFacing.FRONT
: CameraHelper.CameraFacing.BACK;
cameraHelper.startCamera(this, cameraFacing, /*unusedSurfaceTexture=*/ null);
Nesse ponto, o aplicativo deve ser criado corretamente. No entanto, ao executar
o aplicativo no dispositivo, você verá uma tela preta, mesmo que as permissões
da câmera tenham sido concedidas. Isso ocorre porque, mesmo que salvemos a
variável surfaceTexture
fornecida pelo CameraXPreviewHelper
, o
previewSurfaceView
ainda não usa a saída e a exibe na tela.
Como queremos usar os frames em um gráfico do MediaPipe, não vamos adicionar código para visualizar a saída da câmera diretamente neste tutorial. Em vez disso, vamos aprender a enviar frames da câmera para processamento em um gráfico do MediaPipe e mostrar a saída do gráfico na tela.
Configuração de ExternalTextureConverter
Uma SurfaceTexture
captura frames de imagem de um stream como uma textura
do OpenGL ES. Para usar um gráfico do MediaPipe, os frames capturados pela câmera precisam ser
armazenados em um objeto de textura Open GL normal. O framework fornece uma classe,
ExternalTextureConverter
, para converter a imagem armazenada em um objeto SurfaceTexture
em um objeto de textura OpenGL normal.
Para usar ExternalTextureConverter
, também precisamos de um EGLContext
, que é
criado e gerenciado por um objeto EglManager
. Adicione uma dependência ao arquivo BUILD
para usar EglManager
, "//mediapipe/java/com/google/mediapipe/glutil"
.
Em MainActivity
, adicione as seguintes declarações:
private EglManager eglManager;
private ExternalTextureConverter converter;
Na função onCreate(Bundle)
, adicione uma instrução para inicializar o objeto
eglManager
antes de solicitar permissões de câmera:
eglManager = new EglManager(null);
Lembre-se de que definimos a função onResume()
em MainActivity
para confirmar
que as permissões da câmera foram concedidas e chamar startCamera()
. Antes dessa
verificação, adicione a seguinte linha em onResume()
para inicializar o objeto
converter
:
converter = new ExternalTextureConverter(eglManager.getContext());
Este converter
agora usa o GLContext
gerenciado por eglManager
.
Também precisamos substituir a função onPause()
no MainActivity
para que,
se o aplicativo entrar em um estado pausado, fechemos o converter
corretamente:
@Override
protected void onPause() {
super.onPause();
converter.close();
}
Para canalizar a saída de previewFrameTexture
para converter
, adicione o
bloco de código abaixo a setupPreviewDisplayView()
:
previewDisplayView
.getHolder()
.addCallback(
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// (Re-)Compute the ideal size of the camera-preview display (the area that the
// camera-preview frames get rendered onto, potentially with scaling and rotation)
// based on the size of the SurfaceView that contains the display.
Size viewSize = new Size(width, height);
Size displaySize = cameraHelper.computeDisplaySizeFromViewSize(viewSize);
// Connect the converter to the camera-preview frames as its input (via
// previewFrameTexture), and configure the output width and height as the computed
// display size.
converter.setSurfaceTextureAndAttachToGLContext(
previewFrameTexture, displaySize.getWidth(), displaySize.getHeight());
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {}
});
Nesse bloco de código, adicionamos um SurfaceHolder.Callback
personalizado a
previewDisplayView
e implementamos a função surfaceChanged(SurfaceHolder holder, int
format, int width, int height)
para calcular um tamanho de exibição adequado
dos frames da câmera na tela do dispositivo e vincular o objeto previewFrameTexture
e enviar frames do displaySize
calculado ao converter
.
Agora já podemos usar os frames da câmera em um gráfico do MediaPipe.
Como usar um gráfico do MediaPipe no Android
Adicionar dependências relevantes
Para usar um gráfico do MediaPipe, é preciso adicionar dependências ao framework MediaPipe
no Android. Primeiro, adicionaremos uma regra de build para criar um cc_binary
usando o código JNI
do framework MediaPipe e, em seguida, criaremos uma regra cc_library
para usar esse binário
no nosso aplicativo. Adicione o seguinte bloco de código ao seu arquivo BUILD
:
cc_binary(
name = "libmediapipe_jni.so",
linkshared = 1,
linkstatic = 1,
deps = [
"//mediapipe/java/com/google/mediapipe/framework/jni:mediapipe_framework_jni",
],
)
cc_library(
name = "mediapipe_jni_lib",
srcs = [":libmediapipe_jni.so"],
alwayslink = 1,
)
Adicione a dependência ":mediapipe_jni_lib"
à regra de build mediapipe_lib
no
arquivo BUILD
.
Em seguida, é preciso adicionar dependências específicas ao gráfico do MediaPipe que queremos usar no aplicativo.
Primeiro, adicione dependências a todo o código da calculadora na regra de build
libmediapipe_jni.so
:
"//mediapipe/graphs/edge_detection:mobile_calculators",
Os gráficos do MediaPipe são arquivos .pbtxt
, mas, para usá-los no aplicativo, precisamos
usar a regra de build mediapipe_binary_graph
para gerar um arquivo .binarypb
.
Na regra de build binário do Android helloworld
, adicione o destino mediapipe_binary_graph
específico ao gráfico como um recurso:
assets = [
"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
],
assets_dir = "",
Na regra de criação assets
, também é possível adicionar outros recursos, como modelos do TensorFlowLite
usados no gráfico.
Além disso, adicione mais manifest_values
para propriedades específicas do
gráfico, que serão recuperadas posteriormente em MainActivity
:
manifest_values = {
"applicationId": "com.google.mediapipe.apps.basic",
"appName": "Hello World",
"mainActivity": ".MainActivity",
"cameraFacingFront": "False",
"binaryGraphName": "mobile_gpu.binarypb",
"inputVideoStreamName": "input_video",
"outputVideoStreamName": "output_video",
},
Observe que binaryGraphName
indica o nome do arquivo do gráfico binário,
determinado pelo campo output_name
no destino mediapipe_binary_graph
.
inputVideoStreamName
e outputVideoStreamName
são os nomes do stream de vídeo de entrada e
saída especificados no gráfico, respectivamente.
Agora, o MainActivity
precisa carregar o framework MediaPipe. Além disso, o
framework usa OpenCV, então MainActvity
também precisa carregar OpenCV
. Use o
código abaixo em MainActivity
(dentro da classe, mas não dentro de qualquer função)
para carregar as duas dependências:
static {
// Load all native libraries needed by the app.
System.loadLibrary("mediapipe_jni");
System.loadLibrary("opencv_java3");
}
Usar o gráfico em MainActivity
Primeiro, precisamos carregar o recurso que contém o .binarypb
compilado no
arquivo .pbtxt
do gráfico. Para isso, podemos usar o utilitário AndroidAssetUtil
do MediaPipe.
Inicialize o gerenciador de recursos no onCreate(Bundle)
antes de inicializar
eglManager
:
// Initialize asset manager so that MediaPipe native libraries can access the app assets, e.g.,
// binary graphs.
AndroidAssetUtil.initializeNativeAssetManager(this);
Agora, precisamos configurar um objeto FrameProcessor
que envie frames da câmera
preparados pelo converter
para o gráfico do MediaPipe e execute o gráfico, prepare
a saída e atualize o previewDisplayView
para mostrar a saída. Adicione
o código a seguir para declarar o FrameProcessor
:
private FrameProcessor processor;
e inicialize-o em onCreate(Bundle)
depois de inicializar eglManager
:
processor =
new FrameProcessor(
this,
eglManager.getNativeContext(),
applicationInfo.metaData.getString("binaryGraphName"),
applicationInfo.metaData.getString("inputVideoStreamName"),
applicationInfo.metaData.getString("outputVideoStreamName"));
O processor
precisa consumir os frames convertidos do converter
para
processamento. Adicione a seguinte linha ao onResume()
depois de inicializar o
converter
:
converter.setConsumer(processor);
O processor
precisa enviar a saída para previewDisplayView
. Para isso, adicione as seguintes definições de função ao SurfaceHolder.Callback
personalizado:
@Override
public void surfaceCreated(SurfaceHolder holder) {
processor.getVideoSurfaceOutput().setSurface(holder.getSurface());
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
processor.getVideoSurfaceOutput().setSurface(null);
}
Quando o SurfaceHolder
é criado, usamos o Surface
no
VideoSurfaceOutput
do processor
. Quando ela é destruída, ela é removida do
VideoSurfaceOutput
do processor
.
Pronto. Agora você pode criar e executar o aplicativo no dispositivo e conferir a detecção do Sobel Edge em execução em um feed de câmera ao vivo. Parabéns!
Se você tiver algum problema, consulte o código completo do tutorial aqui.