Hello World! no Android

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.

edge_detection_android_gpu_gif

Configuração

  1. Instale o MediaPipe Framework no seu sistema. Consulte Instalação do framework para mais detalhes.
  2. Instale o SDK de desenvolvimento do Android e o Android NDK. Veja como fazer isso também em [Guia de instalação do framework].
  3. Ative as opções do desenvolvedor no seu dispositivo Android.
  4. 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:

edge_detection_mobile_gpu

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!:

bazel_hello_world_android

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:

missing_camera_permission_android

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!

edge_detection_android_gpu_gif

Se você tiver algum problema, consulte o código completo do tutorial. aqui.