Introdução
Este "Hello World!" tutorial usa o MediaPipe Framework para desenvolver um aplicativo Android que executa um gráfico do MediaPipe no Android.
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 transmissão em um dispositivo Android.
Configuração
- Instale o MediaPipe Framework no seu sistema. Consulte Instalação do framework para mais detalhes.
- Instale o SDK de desenvolvimento do Android e o Android NDK. Veja como fazer isso também em [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
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 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 abaixo uma visualização do gráfico:
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 Android exibirá os frames de imagem de saída do
Stream output_video
.
Configuração inicial mínima do aplicativo
Vamos começar com um app Android simples que exibe as palavras "Hello World!"
na tela. Você pode pular esta etapa se estiver familiarizado com a criação de aplicativos
aplicativos usando bazel
.
Crie um novo diretório em que você criará o aplicativo Android. Para
exemplo, o código completo deste tutorial pode ser encontrado em
mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic
: Nós vamos
se referir a 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 em snippets de código neste tutorial, então 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
. Isso mostra
uma TextView
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
, como 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 aplicativo, então precisamos
referências adequadas ao tema. Adicionar 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
e
${appName}
e ${mainActivity}
no manifesto serão substituídos por strings
especificado 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 os arquivos de recursos MainActivity
.
e AndroidManifest.xml
.
A regra android_binary
usa a biblioteca Android basic_lib
gerada para
crie 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. Uma tela com o texto vai aparecer
Hello World!
:
Usando a câmera via CameraX
Permissões da câmera
Para usar a câmera em nosso aplicativo, precisamos solicitar que o usuário forneça
acesso à câmera. Para solicitar permissões de acesso à 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" />
Mude a versão mínima do SDK para 21
e a versão do SDK de destino 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 para usar a câmera e permite usar a biblioteca CameraX para acesso à câmera.
Para solicitar permissões de câmera, podemos usar um utilitário fornecido pelo MediaPipe Framework
componentes, ou seja, PermissionHelper
. Para usá-lo, adicione uma dependência
"//mediapipe/java/com/google/mediapipe/components:android_components"
no
mediapipe_lib
regra em BUILD
.
Para usar o PermissionHelper
no MainActivity
, adicione a linha abaixo à
Função onCreate
:
PermissionHelper.checkAndRequestCameraPermissions(this);
Isso faz com que o usuário abra uma caixa de diálogo na tela para solicitar permissões para a câmera neste aplicativo.
Adicione o seguinte código 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 responde
à solicitação, MainActivity
será retomado e onResume()
será chamado.
O código confirmará que as permissões para usar a câmera foram concedidas
para iniciar a câmera.
Recrie e instale o aplicativo. Agora vai aparecer uma solicitação acesso à câmera para o aplicativo.
Acesso à câmera
Com as permissões de acesso à câmera disponíveis, podemos iniciar e buscar frames do câmera.
Para mostrar os frames da câmera, vamos usar um SurfaceView
. Cada frame
da câmera serão armazenadas em um objeto SurfaceTexture
. Para usá-los,
precisamos primeiro mudar o layout do nosso aplicativo.
Remova todo o bloco de código TextView
(link em inglês) da
$APPLICATION_PATH/res/layout/activity_main.xml
e adicione o seguinte código
como alternativa:
<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>
Este bloco de código tem um novo FrameLayout
chamado preview_display_layout
e um
TextView
aninhado dentro dele, com o nome no_camera_access_preview
. Quando a câmera
de acesso não forem concedidas, nosso aplicativo exibirá os
TextView
por 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 conceder permissão para usar a câmera, a tela vai ficar assim: isso:
Agora, vamos adicionar os objetos SurfaceTexture
e SurfaceView
ao
MainActivity
:
private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;
Na função onCreate(Bundle)
, adicione as duas linhas a seguir antes
solicitando permissões de câmera:
previewDisplayView = new SurfaceView(this);
setupPreviewDisplayView();
Agora, adicione o código definindo 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
preview_display_layout
FrameLayout
para que possamos usá-lo para exibir
os frames da câmera usando um objeto SurfaceTexture
chamado previewFrameTexture
.
Para usar o previewFrameTexture
e receber 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 por meio de
onCameraStarted(@Nullable SurfaceTexture)
:
Para usar esse utilitário, modifique o arquivo BUILD
para adicionar uma dependência
"//mediapipe/java/com/google/mediapipe/components:android_camerax_helper"
.
Agora importe CameraXPreviewHelper
e adicione a linha abaixo ao
MainActivity
:
private CameraXPreviewHelper cameraHelper;
Agora, podemos adicionar nossa implementação ao 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 no objeto. Quando cameraHelper
sinalizar que a câmera foi iniciada
e um surfaceTexture
para capturar frames estiver disponível, ele será salvo.
surfaceTexture
como previewFrameTexture
e tornar previewDisplayView
visível para começar a mostrar os frames da previewFrameTexture
.
No entanto, antes de iniciar a câmera, precisamos decidir qual câmera queremos
usar. CameraXPreviewHelper
herda de CameraHelper
, que fornece dois
opções, FRONT
e BACK
. Podemos transmitir a decisão do arquivo BUILD
como metadados, para que nenhuma alteração no código seja necessária para criar outra versão do
usando uma câmera diferente.
Presumindo que queremos usar a câmera BACK
para realizar detecção de borda em uma cena ao vivo
que mostramos 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 linha abaixo no 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);
Neste ponto, o aplicativo deve ser criado. No entanto, ao executar
o aplicativo no seu dispositivo, você verá uma tela preta (mesmo que a câmera
foram concedidas). Isso porque, embora salvemos
A variável surfaceTexture
fornecida pelo CameraXPreviewHelper
, a
previewSurfaceView
ainda não usa a saída e a mostra na tela.
Como queremos usar os frames em um gráfico do MediaPipe, não vamos adicionar código ao confira a saída da câmera diretamente neste tutorial. Em vez disso, vamos mostrar como podemos enviar quadros de câmera para processamento a um gráfico do MediaPipe e exibir o saída do gráfico na tela.
Configuração de ExternalTextureConverter
Uma SurfaceTexture
captura frames de imagem de um stream como um OpenGL ES
textura. Para usar um gráfico do MediaPipe, os frames capturados da câmera devem ser
armazenadas em um objeto de textura Open GL comum. O Framework oferece uma classe,
ExternalTextureConverter
para converter a imagem armazenada em um SurfaceTexture
.
para um objeto de textura OpenGL normal.
Para usar ExternalTextureConverter
, também precisamos de um EGLContext
, que é
criado e gerenciado por um objeto EglManager
. Adicionar uma dependência ao BUILD
arquivo 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
eglManager
antes de solicitar permissões de câmera:
eglManager = new EglManager(null);
Lembre-se de que definimos a função onResume()
no MainActivity
para confirmar
as permissões da câmera foram concedidas e chame startCamera()
. Antes deste
Adicione a linha abaixo ao arquivo onResume()
para inicializar o converter
.
objeto:
converter = new ExternalTextureConverter(eglManager.getContext());
Este converter
agora usa o GLContext
gerenciado pelo eglManager
.
Também precisamos substituir a função onPause()
no MainActivity
para que
Se o aplicativo entrar em um estado pausado, fecharemos o converter
corretamente:
@Override
protected void onPause() {
super.onPause();
converter.close();
}
Para canalizar a saída de previewFrameTexture
para converter
, adicione o
seguinte bloco de código para 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 para
previewDisplayView
e implemente a função surfaceChanged(SurfaceHolder holder, int
format, int width, int height)
para calcular um tamanho de exibição apropriado.
dos frames da câmera na tela do dispositivo e vincular o previewFrameTexture
e enviar frames do displaySize
calculado para o converter
.
Agora estamos prontos para usar 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 do MediaPipe.
no Android. Primeiro, vamos adicionar uma regra de build para criar um cc_binary
usando o código JNI
do framework do MediaPipe e depois crie uma regra cc_library
para usar esse binário
em nosso aplicativo. Adicione o bloco de código abaixo ao 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
em
o arquivo BUILD
.
Em seguida, precisamos 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 no libmediapipe_jni.so
regra de build:
"//mediapipe/graphs/edge_detection:mobile_calculators",
Os gráficos do MediaPipe são arquivos .pbtxt
, mas, para usá-los no aplicativo, precisamos
para usar a regra de build mediapipe_binary_graph
e gerar um arquivo .binarypb
.
Na regra de build do binário helloworld
do Android, adicione o mediapipe_binary_graph
.
segmentação específica do gráfico como um recurso:
assets = [
"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
],
assets_dir = "",
Na regra de build assets
, também é possível adicionar outros recursos, como o TensorFlowLite.
modelos usados em seu gráfico.
Além disso, adicione mais manifest_values
para propriedades específicas da
gráfico, que será recuperado 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 a entrada e a saída
nome do stream de vídeo especificado no gráfico, respectivamente.
Agora, o MainActivity
precisa carregar o framework do MediaPipe. Além disso,
o framework usa o OpenCV, então MainActvity
também precisa carregar OpenCV
. Use o
o seguinte código 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 no MainActivity
Primeiro, precisamos carregar o recurso que contém o .binarypb
compilado da
o arquivo .pbtxt
do gráfico. Para isso, podemos usar um utilitário MediaPipe,
AndroidAssetUtil
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.
preparado pelo converter
para o gráfico do MediaPipe e executa o gráfico, prepara
na saída e atualiza previewDisplayView
para mostrar a saída. Adicionar
o código abaixo para declarar a 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"));
A processor
precisa consumir os frames convertidos da converter
para
processamento. Adicione a linha abaixo ao onResume()
depois de inicializar o
converter
:
converter.setConsumer(processor);
O processor
precisa enviar a saída para previewDisplayView
. Para fazer isso, adicione
estas definições de função para o 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, os Surface
também são
VideoSurfaceOutput
de processor
. Quando é destruída, ela é removida
a 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 na câmera ao vivo. se alimentam! Parabéns!
Se você tiver algum problema, consulte o código completo do tutorial. aqui.