Hello World! en Android

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.

edge_detection_android_gpu_gif

Configuración

  1. Instala el framework de MediaPipe en tu sistema; consulta Instalación del framework guía para obtener más información.
  2. 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].
  3. Habilita las opciones para desarrolladores en tu dispositivo Android.
  4. 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:

edge_detection_mobile_gpu

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 es com.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!

bazel_hello_world_android

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:

missing_camera_permission_android

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!

edge_detection_android_gpu_gif

Si tuviste algún problema, consulta el código completo del instructivo aquí.