Introducción
¡Hola, mundo! usa el framework MediaPipe para desarrollar una aplicación para Android que ejecuta un gráfico de MediaPipe en Android.
Qué compilarás
Una app de cámara simple para la detección de bordes de Sobel en tiempo real aplicada a un video en vivo transmitir en un dispositivo Android.
Configuración
- Instala el framework de MediaPipe en tu sistema; consulta Instalación del framework guía para obtener más información.
- Instala el SDK de desarrollo de Android y el NDK de Android. Consulta cómo hacerlo también en [Guía de instalación del framework].
- Habilita las opciones para desarrolladores en tu dispositivo Android.
- Configura Bazel en tu sistema para compilar e implementar la app para Android.
Gráfico de detección de bordes
Usaremos el siguiente 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"
}
A continuación, se muestra una visualización del gráfico:
Este gráfico tiene un solo flujo de entrada llamado input_video
para todos los fotogramas entrantes
que proporcionará la cámara del dispositivo.
El primer nodo del gráfico, LuminanceCalculator
, toma un solo paquete (imagen
fotograma) y aplica un cambio de luminancia usando un sombreador OpenGL. El resultado
marco de imagen se envía al flujo de salida luma_video
.
El segundo nodo, SobelEdgesCalculator
, aplica la detección de borde a las llamadas
paquetes en la transmisión luma_video
y los resultados se generan en output_video
en tiempo real.
Nuestra aplicación para Android mostrará los marcos de la imagen de salida de la
Novedades de output_video
.
Configuración inicial mínima de la aplicación
Primero, empezamos con una aplicación para Android simple que muestra el mensaje "Hello World!".
en la pantalla. Puedes omitir este paso si tienes conocimientos sobre la compilación de Android.
aplicaciones que usan bazel
.
Crea un directorio nuevo en el que crearás tu aplicación para Android. Para
ejemplo, puedes encontrar el código completo de este instructivo en
mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic
Más tarde
hacer referencia a esta ruta de acceso como $APPLICATION_PATH
en todo el codelab.
Observa lo siguiente en la ruta a la aplicación:
- La aplicación se llama
helloworld
. - El
$PACKAGE_PATH
de la aplicación escom.google.mediapipe.apps.basic
. Esto se usa en los fragmentos de código de este instructivo, así que recuerda usar tu propio$PACKAGE_PATH
cuando copies o uses los fragmentos de código
Agrega un archivo activity_main.xml
a $APPLICATION_PATH/res/layout
. Aparecerá
un TextView
en la pantalla completa de la aplicación con la cadena 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>
Agrega un MainActivity.java
simple a $APPLICATION_PATH
que cargue el contenido.
del diseño activity_main.xml
, como se muestra a continuación:
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);
}
}
Agrega un archivo de manifiesto, AndroidManifest.xml
a $APPLICATION_PATH
, que
inicia MainActivity
cuando se inicia la aplicación:
<?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>
En nuestra aplicación, usamos un tema Theme.AppCompat
en la app, por lo que necesitamos
referencias temáticas adecuadas. Agregar 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>
Agrega 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 compilar la aplicación, agrega un archivo BUILD
a $APPLICATION_PATH
.
Se reemplazarán ${appName}
y ${mainActivity}
en el manifiesto por cadenas
especificado en BUILD
, como se muestra a continuación.
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",
],
)
La regla android_library
agrega dependencias para MainActivity
, archivos de recursos
y AndroidManifest.xml
.
La regla android_binary
usa la biblioteca de Android basic_lib
generada para
compilarás un APK binario para instalarlo en tu dispositivo Android.
Para compilar la app, usa el siguiente comando:
bazel build -c opt --config=android_arm64 $APPLICATION_PATH:helloworld
Instala el archivo APK generado con adb install
. Por ejemplo:
adb install bazel-bin/$APPLICATION_PATH/helloworld.apk
Abre la aplicación en tu dispositivo. Debería aparecer una pantalla con el texto
Hello World!
Usar la cámara mediante CameraX
Permisos de cámara
Para usar la cámara en nuestra aplicación, debemos pedirle al usuario que proporcione
acceso a la cámara. Para solicitar permisos de la cámara, agrega lo siguiente a
AndroidManifest.xml
:
<!-- For using the camera -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
Cambia la versión mínima del SDK a 21
y la versión de destino del SDK a 27
en el
mismo archivo:
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="27" />
De esta manera, se garantiza que se solicite al usuario el permiso para acceder a la cámara y se habilita usemos la biblioteca de CameraX para acceder a la cámara.
Para solicitar permisos de cámara, podemos usar una utilidad proporcionada por el framework de MediaPipe
componentes, en concreto, PermissionHelper
. Para usarlo, agrega una dependencia.
"//mediapipe/java/com/google/mediapipe/components:android_components"
en
mediapipe_lib
regla en BUILD
.
Para usar PermissionHelper
en MainActivity
, agrega la siguiente línea al
Función onCreate
:
PermissionHelper.checkAndRequestCameraPermissions(this);
Se le mostrará al usuario un diálogo en la pantalla para solicitar permisos para usa la cámara en esta aplicación.
Agrega el siguiente código para controlar la respuesta del usuario:
@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() {}
Dejaremos el método startCamera()
vacío por ahora. Cuando el usuario responde
al mensaje, se reanudará MainActivity
y se llamará a onResume()
.
El código confirmará que se otorgaron los permisos para usar la cámara.
y, luego, encenderá la cámara.
Vuelve a compilar e instala la aplicación. Ahora deberías ver un mensaje en el que se solicita acceso a la cámara para la aplicación.
Acceso a la cámara
Con los permisos de la cámara disponibles, podemos iniciar y recuperar fotogramas del cámara.
Para ver los fotogramas de la cámara, usaremos un objeto SurfaceView
. Cada marco
de la cámara se almacenará en un objeto SurfaceTexture
. Para usarlos,
primero debemos cambiar el diseño de la aplicación.
Quita todo el bloque de código TextView
de
$APPLICATION_PATH/res/layout/activity_main.xml
y agrega el siguiente código
en su lugar:
<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 bloque de código tiene un FrameLayout
nuevo llamado preview_display_layout
y un
TextView
anidado en su interior, llamado no_camera_access_preview
. Cuando se usa la cámara
no se otorgan los permisos de acceso, nuestra aplicación mostrará el
TextView
con un mensaje de cadena, almacenado en la variable no_camera_access
.
Agrega la siguiente línea al archivo $APPLICATION_PATH/res/values/strings.xml
:
<string name="no_camera_access" translatable="false">Please grant camera permissions.</string>
Cuando el usuario no otorgue el permiso de cámara, la pantalla se verá de la siguiente manera: esto:
Ahora, agregaremos los objetos SurfaceTexture
y SurfaceView
a
MainActivity
:
private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;
En la función onCreate(Bundle)
, agrega las siguientes dos líneas antes
solicitud de permisos para usar la cámara:
previewDisplayView = new SurfaceView(this);
setupPreviewDisplayView();
Ahora, agrega el código que define setupPreviewDisplayView()
:
private void setupPreviewDisplayView() {
previewDisplayView.setVisibility(View.GONE);
ViewGroup viewGroup = findViewById(R.id.preview_display_layout);
viewGroup.addView(previewDisplayView);
}
Definimos un nuevo objeto SurfaceView
y lo agregamos al
preview_display_layout
FrameLayout
de modo que podamos usarlo para mostrar
los encuadres de la cámara usando un objeto SurfaceTexture
llamado previewFrameTexture
.
Si quieres usar previewFrameTexture
para obtener marcos de cámara, usaremos CameraX.
Framework proporciona una utilidad llamada CameraXPreviewHelper
para usar CameraX.
Esta clase actualiza un objeto de escucha cuando la cámara se inicia mediante
onCameraStarted(@Nullable SurfaceTexture)
Para usar esta utilidad, modifica el archivo BUILD
para agregar una dependencia
"//mediapipe/java/com/google/mediapipe/components:android_camerax_helper"
Ahora importa CameraXPreviewHelper
y agrega la siguiente línea a
MainActivity
:
private CameraXPreviewHelper cameraHelper;
Ahora, podemos agregar nuestra implementación 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);
});
}
Esto crea un objeto CameraXPreviewHelper
nuevo y agrega un objeto anónimo
objeto de escucha en el objeto. Cuando cameraHelper
indique que la cámara se inició
y un surfaceTexture
para tomar fotogramas está disponible, lo guardamos
surfaceTexture
como previewFrameTexture
y hacer que el previewDisplayView
visible para que podamos comenzar a ver marcos de previewFrameTexture
.
Sin embargo, antes de iniciar la cámara, debemos decidir qué cámara queremos
usar. CameraXPreviewHelper
hereda de CameraHelper
, que proporciona dos
FRONT
y BACK
. Podemos pasar la decisión del archivo BUILD
.
como metadatos, de modo que no sea necesario cambiar el código para compilar otra versión del
usando una cámara diferente.
Suponiendo que queremos usar la cámara BACK
para realizar la detección de bordes en una escena en vivo
que vemos desde la cámara, agrega los metadatos a AndroidManifest.xml
:
...
<meta-data android:name="cameraFacingFront" android:value="${cameraFacingFront}"/>
</application>
</manifest>
y especifica la selección en BUILD
en la regla binaria de Android helloworld
.
con una nueva entrada en manifest_values
:
manifest_values = {
"applicationId": "com.google.mediapipe.apps.basic",
"appName": "Hello World",
"mainActivity": ".MainActivity",
"cameraFacingFront": "False",
},
Ahora, en MainActivity
, para recuperar los metadatos especificados en manifest_values
, haz lo siguiente:
agrega un objeto ApplicationInfo
:
private ApplicationInfo applicationInfo;
En la función onCreate()
, agrega lo siguiente:
try {
applicationInfo =
getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
Log.e(TAG, "Cannot find application info: " + e);
}
Ahora agrega la siguiente línea al final de la función startCamera()
:
CameraHelper.CameraFacing cameraFacing =
applicationInfo.metaData.getBoolean("cameraFacingFront", false)
? CameraHelper.CameraFacing.FRONT
: CameraHelper.CameraFacing.BACK;
cameraHelper.startCamera(this, cameraFacing, /*unusedSurfaceTexture=*/ null);
En este punto, la aplicación debería compilarse con éxito. Sin embargo, cuando ejecutas
la aplicación en tu dispositivo, verás una pantalla negra (aunque la cámara
permisos otorgados). Esto se debe a que, si bien guardamos
La variable surfaceTexture
proporcionada por CameraXPreviewHelper
, el
previewSurfaceView
aún no usa su resultado y lo muestra en la pantalla.
Como queremos usar los marcos en un gráfico de MediaPipe, no agregaremos código al mira la salida de la cámara directamente en este instructivo. En cambio, pasaremos a cómo podemos enviar fotogramas de cámara para su procesamiento a un gráfico de MediaPipe y mostrar el salida del grafo en la pantalla.
Configuración de ExternalTextureConverter
Un SurfaceTexture
captura marcos de imagen de una transmisión como OpenGL ES
textura. Para usar un gráfico de MediaPipe, los fotogramas capturados desde la cámara deben ser
almacenado en un objeto de textura Open GL normal. El framework proporciona una clase,
ExternalTextureConverter
para convertir la imagen almacenada en un SurfaceTexture
a un objeto de textura OpenGL normal.
Para usar ExternalTextureConverter
, también necesitamos un EGLContext
, que es
creado y administrado por un objeto EglManager
. Agrega una dependencia a BUILD
.
para usar EglManager
, "//mediapipe/java/com/google/mediapipe/glutil"
.
En MainActivity
, agrega las siguientes declaraciones:
private EglManager eglManager;
private ExternalTextureConverter converter;
En la función onCreate(Bundle)
, agrega una sentencia para inicializar el
eglManager
antes de solicitar permisos de la cámara:
eglManager = new EglManager(null);
Recuerda que definimos la función onResume()
en MainActivity
para confirmar.
Se otorgaron permisos para la cámara y llamar a startCamera()
. Antes de esto
marca, agrega la siguiente línea en onResume()
para inicializar converter
objeto:
converter = new ExternalTextureConverter(eglManager.getContext());
Este converter
ahora usa el GLContext
administrado por eglManager
.
También necesitamos anular la función onPause()
en MainActivity
para que
Si la aplicación entra en un estado de pausa, cerramos la converter
correctamente:
@Override
protected void onPause() {
super.onPause();
converter.close();
}
Para canalizar el resultado de previewFrameTexture
a converter
, agrega el elemento
siguiente bloque de código 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) {}
});
En este bloque de código, agregamos un SurfaceHolder.Callback
personalizado al
previewDisplayView
e implementa la función surfaceChanged(SurfaceHolder holder, int
format, int width, int height)
para calcular un tamaño de visualización adecuado
de los marcos de la cámara en la pantalla del dispositivo y para vincular el previewFrameTexture
y envía fotogramas del displaySize
calculado al converter
.
Ya está todo listo para usar marcos de cámara en un gráfico de MediaPipe.
Cómo usar un gráfico de MediaPipe en Android
Agrega las dependencias relevantes
Para usar un gráfico de MediaPipe, debemos agregar dependencias al framework de MediaPipe
en Android. Primero, agregaremos una regla de compilación para compilar un cc_binary
usando el código JNI
del framework MediaPipe y, luego, compilarás una regla cc_library
para usar este objeto binario
en nuestra aplicación. Agrega el siguiente bloque de código a tu archivo 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,
)
Agrega la dependencia ":mediapipe_jni_lib"
a la regla de compilación mediapipe_lib
en
el archivo BUILD
.
A continuación, debemos agregar dependencias específicas al gráfico de MediaPipe que queremos usar en la aplicación.
Primero, agrega dependencias a todo el código de la calculadora en libmediapipe_jni.so
.
regla de compilación:
"//mediapipe/graphs/edge_detection:mobile_calculators",
Los gráficos de MediaPipe son archivos .pbtxt
, pero, para usarlos en la aplicación, necesitamos
para usar la regla de compilación mediapipe_binary_graph
para generar un archivo .binarypb
.
En la regla de compilación del objeto binario de Android helloworld
, agrega mediapipe_binary_graph
.
objetivo específico del gráfico como un recurso:
assets = [
"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
],
assets_dir = "",
En la regla de compilación assets
, también puedes agregar otros elementos, como TensorFlowLite.
modelos usados en tu grafo.
Además, agrega manifest_values
adicionales para las propiedades específicas del
gráfico que luego se recuperará en 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",
},
Ten en cuenta que binaryGraphName
indica el nombre de archivo del gráfico binario.
determinados por el campo output_name
en el objetivo mediapipe_binary_graph
.
inputVideoStreamName
y outputVideoStreamName
son las entradas y salidas
nombre de transmisión de video por Internet especificado en el gráfico, respectivamente.
Ahora, MainActivity
debe cargar el framework de MediaPipe. Además, el
El framework usa OpenCV, por lo que MainActvity
también debe cargar OpenCV
. Usa el
El siguiente código en MainActivity
(dentro de la clase, pero no dentro de ninguna función)
para cargar ambas dependencias:
static {
// Load all native libraries needed by the app.
System.loadLibrary("mediapipe_jni");
System.loadLibrary("opencv_java3");
}
Cómo usar el gráfico en MainActivity
Primero, debemos cargar el recurso que contiene el .binarypb
compilado de
el archivo .pbtxt
del gráfico. Para ello, podemos usar la utilidad MediaPipe,
AndroidAssetUtil
Inicializa el administrador de recursos en 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);
Ahora, debemos configurar un objeto FrameProcessor
que envíe marcos de cámara.
preparado por converter
para el gráfico de MediaPipe y lo ejecuta, prepara
el resultado y, luego, actualiza previewDisplayView
para mostrar el resultado. Agrega
el siguiente código para declarar FrameProcessor
:
private FrameProcessor processor;
Luego, inicialízala en onCreate(Bundle)
después de inicializar eglManager
:
processor =
new FrameProcessor(
this,
eglManager.getNativeContext(),
applicationInfo.metaData.getString("binaryGraphName"),
applicationInfo.metaData.getString("inputVideoStreamName"),
applicationInfo.metaData.getString("outputVideoStreamName"));
processor
debe consumir los fotogramas convertidos de converter
para
el procesamiento de datos. Agrega la siguiente línea a onResume()
después de inicializar el elemento
converter
converter.setConsumer(processor);
processor
debe enviar su resultado a previewDisplayView
. Para ello, agrega
las siguientes definiciones de función a nuestra SurfaceHolder.Callback
personalizada:
@Override
public void surfaceCreated(SurfaceHolder holder) {
processor.getVideoSurfaceOutput().setSurface(holder.getSurface());
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
processor.getVideoSurfaceOutput().setSurface(null);
}
Cuando se creaba SurfaceHolder
, teníamos la Surface
en la
VideoSurfaceOutput
de processor
Cuando se destruye, lo quitamos de
el VideoSurfaceOutput
de processor
.
Eso es todo. Ahora deberías poder compilar y ejecutar correctamente en el dispositivo y ver la detección de borde de Sobel ejecutándose en una cámara en vivo. feed. ¡Felicitaciones!
Si tuviste algún problema, consulta el código completo del instructivo aquí.