Hello World!(Android)

소개

이 Hello World! 튜토리얼에서는 MediaPipe 프레임워크를 사용하여 Android에서 MediaPipe 그래프를 실행하는 Android 애플리케이션을 개발합니다.

빌드 대상

Android 기기의 실시간 동영상 스트림에 적용된 실시간 Sobel 에지 감지를 위한 간단한 카메라 앱.

edge_detection_android_gpu_gif

설정

  1. 시스템에 MediaPipe 프레임워크를 설치합니다. 자세한 내용은 프레임워크 설치 가이드를 참고하세요.
  2. Android 개발 SDK와 Android NDK를 설치합니다. 설치 방법은 [프레임워크 설치 가이드]를 참고하세요.
  3. Android 기기에서 개발자 옵션을 사용 설정합니다.
  4. 시스템에 Bazel을 설정하여 Android 앱을 빌드하고 배포합니다.

가장자리 감지 그래프

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"
}

그래프의 시각화는 다음과 같습니다.

edge_detection_mobile_gpu

이 그래프에는 기기의 카메라에서 제공하는 모든 수신 프레임에 input_video라는 단일 입력 스트림이 있습니다.

그래프의 첫 번째 노드인 LuminanceCalculator는 단일 패킷 (이미지 프레임)을 취하고 OpenGL 셰이더를 사용하여 휘도 변경을 적용합니다. 결과 이미지 프레임은 luma_video 출력 스트림으로 전송됩니다.

두 번째 노드 SobelEdgesCalculatorluma_video 스트림의 수신 패킷에 에지 감지를 적용하고 output_video 출력 스트림을 출력합니다.

Android 애플리케이션은 output_video 스트림의 출력 이미지 프레임을 표시합니다.

초기 최소 애플리케이션 설정

먼저 화면에 'Hello World!'를 표시하는 간단한 Android 애플리케이션부터 시작해 보겠습니다. bazel를 사용하여 Android 애플리케이션을 빌드하는 데 익숙한 경우 이 단계를 건너뛰어도 됩니다.

Android 애플리케이션을 만들 새 디렉터리를 만듭니다. 예를 들어 이 튜토리얼의 전체 코드는 mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic에서 찾을 수 있습니다. Codelab 전체에서 이 경로를 $APPLICATION_PATH라고 합니다.

애플리케이션 경로에서 다음을 확인합니다.

  • 애플리케이션 이름은 helloworld입니다.
  • 애플리케이션의 $PACKAGE_PATHcom.google.mediapipe.apps.basic입니다. 이는 이 가이드의 코드 스니펫에 사용되므로 코드 스니펫을 복사/사용할 때 자체 $PACKAGE_PATH를 사용해야 합니다.

activity_main.xml 파일을 $APPLICATION_PATH/res/layout에 추가합니다. 이렇게 하면 애플리케이션의 전체 화면에 Hello World! 문자열과 함께 TextView가 표시됩니다.

<?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>

아래와 같이 activity_main.xml 레이아웃의 콘텐츠를 로드하는 간단한 MainActivity.java$APPLICATION_PATH에 추가합니다.

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);
  }
}

애플리케이션 시작 시 MainActivity를 실행하는 매니페스트 파일 AndroidManifest.xml$APPLICATION_PATH에 추가합니다.

<?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>

애플리케이션에서 앱에 Theme.AppCompat 테마를 사용하므로 적절한 테마 참조가 필요합니다. $APPLICATION_PATH/res/values/colors.xml를 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
</resources>

styles.xml$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>

애플리케이션을 빌드하려면 BUILD 파일을 $APPLICATION_PATH에 추가합니다. 그러면 매니페스트의 ${appName}${mainActivity}가 아래와 같이 BUILD에 지정된 문자열로 대체됩니다.

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",
    ],
)

android_library 규칙은 MainActivity, 리소스 파일, AndroidManifest.xml의 종속 항목을 추가합니다.

android_binary 규칙은 Android 기기에 설치할 바이너리 APK를 빌드하기 위해 생성된 basic_lib Android 라이브러리를 사용합니다.

앱을 빌드하려면 다음 명령어를 사용합니다.

bazel build -c opt --config=android_arm64 $APPLICATION_PATH:helloworld

adb install를 사용하여 생성된 APK 파일을 설치합니다. 예를 들면 다음과 같습니다.

adb install bazel-bin/$APPLICATION_PATH/helloworld.apk

기기에서 애플리케이션을 엽니다. Hello World! 텍스트가 포함된 화면이 표시됩니다.

bazel_hello_world_android

CameraX 앱을 통해 카메라 사용 중

카메라 권한

애플리케이션에서 카메라를 사용하려면 사용자에게 카메라 액세스 권한을 제공하도록 요청해야 합니다. 카메라 권한을 요청하려면 다음을 AndroidManifest.xml에 추가합니다.

<!-- For using the camera -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />

같은 파일에서 최소 SDK 버전을 21로, 타겟 SDK 버전을 27로 변경합니다.

<uses-sdk
    android:minSdkVersion="21"
    android:targetSdkVersion="27" />

이렇게 하면 사용자에게 카메라 권한을 요청하라는 메시지가 표시되고 카메라 액세스에 CameraX 라이브러리를 사용할 수 있습니다.

카메라 권한을 요청하려면 MediaPipe Framework 구성요소에서 제공하는 유틸리티(PermissionHelper)를 사용하면 됩니다. 사용하려면 BUILDmediapipe_lib 규칙에 종속 항목 "//mediapipe/java/com/google/mediapipe/components:android_components"를 추가합니다.

MainActivity에서 PermissionHelper를 사용하려면 다음 줄을 onCreate 함수에 추가합니다.

PermissionHelper.checkAndRequestCameraPermissions(this);

이렇게 하면 사용자에게 이 애플리케이션에서 카메라를 사용할 권한을 요청하는 대화상자가 표시됩니다.

사용자 응답을 처리하려면 다음 코드를 추가합니다.

@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() {}

지금은 startCamera() 메서드를 비워 둡니다. 사용자가 메시지에 응답하면 MainActivity가 재개되고 onResume()가 호출됩니다. 이 코드는 카메라 사용 권한이 부여되었음을 확인한 후 카메라를 시작합니다.

애플리케이션을 다시 빌드하고 설치합니다. 이제 애플리케이션의 카메라 액세스를 요청하는 메시지가 표시됩니다.

카메라 액세스

카메라 권한을 사용할 수 있으면 카메라에서 프레임을 시작하고 가져올 수 있습니다.

카메라의 프레임을 보기 위해 SurfaceView를 사용합니다. 카메라의 각 프레임은 SurfaceTexture 객체에 저장됩니다. 이를 사용하려면 먼저 애플리케이션의 레이아웃을 변경해야 합니다.

$APPLICATION_PATH/res/layout/activity_main.xml에서 전체 TextView 코드 블록을 삭제하고 대신 다음 코드를 추가합니다.

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

이 코드 블록에는 preview_display_layout라는 새로운 FrameLayout와 그 안에 중첩된 TextViewno_camera_access_preview가 있습니다. 카메라 액세스 권한이 부여되지 않으면 애플리케이션에서 no_camera_access 변수에 저장된 문자열 메시지와 함께 TextView를 표시합니다. $APPLICATION_PATH/res/values/strings.xml 파일에 다음 줄을 추가합니다.

<string name="no_camera_access" translatable="false">Please grant camera permissions.</string>

사용자가 카메라 권한을 부여하지 않으면 화면이 다음과 같이 표시됩니다.

missing_camera_permission_android

이제 SurfaceTextureSurfaceView 객체를 MainActivity에 추가합니다.

private SurfaceTexture previewFrameTexture;
private SurfaceView previewDisplayView;

onCreate(Bundle) 함수에서 카메라 권한을 요청하기 전에 다음 두 줄을 추가합니다.

previewDisplayView = new SurfaceView(this);
setupPreviewDisplayView();

이제 setupPreviewDisplayView()를 정의하는 코드를 추가합니다.

private void setupPreviewDisplayView() {
  previewDisplayView.setVisibility(View.GONE);
  ViewGroup viewGroup = findViewById(R.id.preview_display_layout);
  viewGroup.addView(previewDisplayView);
}

previewFrameTexture라는 SurfaceTexture 객체를 사용하여 카메라 프레임을 표시할 수 있도록 새 SurfaceView 객체를 정의하고 preview_display_layout FrameLayout 객체에 추가합니다.

previewFrameTexture를 사용하여 카메라 프레임을 가져오기 위해 CameraX를 사용합니다. Framework는 CameraX를 사용할 수 있는 CameraXPreviewHelper라는 유틸리티를 제공합니다. 이 클래스는 onCameraStarted(@Nullable SurfaceTexture)를 통해 카메라가 시작되면 리스너를 업데이트합니다.

이 유틸리티를 사용하려면 BUILD 파일을 수정하여 "//mediapipe/java/com/google/mediapipe/components:android_camerax_helper"의 종속 항목을 추가합니다.

이제 CameraXPreviewHelper를 가져오고 다음 줄을 MainActivity에 추가합니다.

private CameraXPreviewHelper cameraHelper;

이제 구현을 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);
    });
}

그러면 새 CameraXPreviewHelper 객체가 생성되고 객체에 익명 리스너가 추가됩니다. cameraHelper가 카메라가 시작되었다고 신호를 보내고 프레임을 포착하는 surfaceTexture를 사용할 수 있으면 surfaceTexturepreviewFrameTexture로 저장하고 previewFrameTexture의 프레임을 볼 수 있도록 previewDisplayView를 표시합니다.

그러나 카메라를 시작하기 전에 사용할 카메라를 결정해야 합니다. CameraXPreviewHelperFRONTBACK의 두 가지 옵션을 제공하는 CameraHelper에서 상속받습니다. 다른 카메라를 사용하여 다른 버전의 앱을 빌드하는 데 코드를 변경하지 않아도 되도록 BUILD 파일의 결정을 메타데이터로 전달할 수 있습니다.

카메라에서 보는 실시간 장면에서 BACK 카메라를 사용하여 에지 감지를 실행하려고 한다고 가정하고 메타데이터를 AndroidManifest.xml에 추가합니다.

      ...
      <meta-data android:name="cameraFacingFront" android:value="${cameraFacingFront}"/>
  </application>
</manifest>

manifest_values에 새 항목이 있는 helloworld Android 바이너리 규칙의 BUILD에서 선택 항목을 지정합니다.

manifest_values = {
    "applicationId": "com.google.mediapipe.apps.basic",
    "appName": "Hello World",
    "mainActivity": ".MainActivity",
    "cameraFacingFront": "False",
},

이제 MainActivity에서 manifest_values에 지정된 메타데이터를 검색하기 위해 ApplicationInfo 객체를 추가합니다.

private ApplicationInfo applicationInfo;

onCreate() 함수에 다음을 추가합니다.

try {
  applicationInfo =
      getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
  Log.e(TAG, "Cannot find application info: " + e);
}

이제 startCamera() 함수 끝에 다음 줄을 추가합니다.

CameraHelper.CameraFacing cameraFacing =
    applicationInfo.metaData.getBoolean("cameraFacingFront", false)
        ? CameraHelper.CameraFacing.FRONT
        : CameraHelper.CameraFacing.BACK;
cameraHelper.startCamera(this, cameraFacing, /*unusedSurfaceTexture=*/ null);

이제 애플리케이션이 성공적으로 빌드됩니다. 그러나 기기에서 애플리케이션을 실행하면 카메라 권한이 부여되더라도 검은색 화면이 표시됩니다. 이는 CameraXPreviewHelper에서 제공하는 surfaceTexture 변수를 저장하지만 previewSurfaceView가 아직 출력을 사용하지 않아 화면에 표시하지 않기 때문입니다.

MediaPipe 그래프에서 프레임을 사용하려고 하므로 이 튜토리얼에서는 카메라 출력을 보는 코드를 직접 추가하지 않습니다. 대신 처리할 카메라 프레임을 MediaPipe 그래프로 전송하고 그래프의 출력을 화면에 표시하는 방법으로 건너뜁니다.

ExternalTextureConverter 설정

SurfaceTexture는 스트림의 이미지 프레임을 OpenGL ES 텍스처로 캡처합니다. MediaPipe 그래프를 사용하려면 카메라에서 캡처한 프레임을 일반 Open GL 텍스처 객체에 저장해야 합니다. 프레임워크는 SurfaceTexture 객체에 저장된 이미지를 일반 OpenGL 텍스처 객체로 변환하는 ExternalTextureConverter 클래스를 제공합니다.

ExternalTextureConverter를 사용하려면 EglManager 객체에서 만들고 관리하는 EGLContext도 필요합니다. EglManager, "//mediapipe/java/com/google/mediapipe/glutil"를 사용하도록 BUILD 파일에 종속 항목을 추가합니다.

MainActivity에서 다음 선언을 추가합니다.

private EglManager eglManager;
private ExternalTextureConverter converter;

onCreate(Bundle) 함수에서 카메라 권한을 요청하기 전에 eglManager 객체를 초기화하는 문을 추가합니다.

eglManager = new EglManager(null);

카메라 권한이 부여되었는지 확인하기 위해 MainActivityonResume() 함수를 정의하고 startCamera()를 호출했습니다. 이 확인 전에 onResume()에 다음 줄을 추가하여 converter 객체를 초기화합니다.

converter = new ExternalTextureConverter(eglManager.getContext());

이제 이 convertereglManager에서 관리하는 GLContext를 사용합니다.

또한 애플리케이션이 일시중지 상태가 되면 converter를 올바르게 닫도록 MainActivity에서 onPause() 함수를 재정의해야 합니다.

@Override
protected void onPause() {
  super.onPause();
  converter.close();
}

previewFrameTexture의 출력을 converter로 파이핑하려면 다음 코드 블록을 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) {}
     });

이 코드 블록에서는 맞춤 SurfaceHolder.CallbackpreviewDisplayView에 추가하고 surfaceChanged(SurfaceHolder holder, int format, int width, int height) 함수를 구현하여 기기 화면에서 카메라 프레임의 적절한 표시 크기를 계산하고 previewFrameTexture 객체를 연결하고 계산된 displaySize의 프레임을 converter에 전송합니다.

이제 MediaPipe 그래프에서 카메라 프레임을 사용할 준비가 되었습니다.

Android에서 MediaPipe 그래프 사용

관련 종속 항목 추가

MediaPipe 그래프를 사용하려면 Android의 MediaPipe 프레임워크에 종속 항목을 추가해야 합니다. 먼저 MediaPipe 프레임워크의 JNI 코드를 사용하여 cc_binary를 빌드하는 빌드 규칙을 추가한 다음, 애플리케이션에서 이 바이너리를 사용하도록 cc_library 규칙을 빌드합니다. 다음 코드 블록을 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,
)

BUILD 파일의 mediapipe_lib 빌드 규칙에 종속 항목 ":mediapipe_jni_lib"를 추가합니다.

다음으로, 애플리케이션에서 사용할 MediaPipe 그래프와 관련된 종속 항목을 추가해야 합니다.

먼저 libmediapipe_jni.so 빌드 규칙의 모든 계산기 코드에 종속 항목을 추가합니다.

"//mediapipe/graphs/edge_detection:mobile_calculators",

MediaPipe 그래프는 .pbtxt 파일이지만 애플리케이션에서 사용하려면 mediapipe_binary_graph 빌드 규칙을 사용하여 .binarypb 파일을 생성해야 합니다.

helloworld Android 바이너리 빌드 규칙에서 그래프 전용 mediapipe_binary_graph 타겟을 애셋으로 추가합니다.

assets = [
  "//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",
],
assets_dir = "",

assets 빌드 규칙에서 그래프에 사용된 TensorFlowLite 모델과 같은 다른 애셋을 추가할 수도 있습니다.

또한 나중에 MainActivity에서 검색할 수 있도록 그래프와 관련된 속성에 manifest_values를 추가합니다.

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",
},

binaryGraphNamemediapipe_binary_graph 타겟의 output_name 필드에 의해 결정되는 바이너리 그래프의 파일 이름을 나타냅니다. inputVideoStreamNameoutputVideoStreamName은 각각 그래프에 지정된 입력 및 출력 동영상 스트림 이름입니다.

이제 MainActivity는 MediaPipe 프레임워크를 로드해야 합니다. 또한 프레임워크는 OpenCV를 사용하므로 MainActvityOpenCV를 로드해야 합니다. MainActivity에서 다음 코드를 사용하여 (클래스 내, 함수 내부가 아님) 두 종속 항목을 모두 로드합니다.

static {
  // Load all native libraries needed by the app.
  System.loadLibrary("mediapipe_jni");
  System.loadLibrary("opencv_java3");
}

MainActivity의 그래프 사용

먼저 그래프의 .pbtxt 파일에서 컴파일된 .binarypb가 포함된 애셋을 로드해야 합니다. MediaPipe 유틸리티 AndroidAssetUtil를 사용하면 됩니다.

eglManager를 초기화하기 전에 onCreate(Bundle)에서 Asset Manager를 초기화합니다.

// Initialize asset manager so that MediaPipe native libraries can access the app assets, e.g.,
// binary graphs.
AndroidAssetUtil.initializeNativeAssetManager(this);

이제 converter에서 준비된 카메라 프레임을 MediaPipe 그래프로 전송하고 그래프를 실행하고 출력을 준비한 다음 previewDisplayView를 업데이트하여 출력을 표시하는 FrameProcessor 객체를 설정해야 합니다. 다음 코드를 추가하여 FrameProcessor를 선언합니다.

private FrameProcessor processor;

eglManager를 초기화한 후 onCreate(Bundle)에서 초기화합니다.

processor =
    new FrameProcessor(
        this,
        eglManager.getNativeContext(),
        applicationInfo.metaData.getString("binaryGraphName"),
        applicationInfo.metaData.getString("inputVideoStreamName"),
        applicationInfo.metaData.getString("outputVideoStreamName"));

processor는 처리를 위해 converter에서 변환된 프레임을 사용해야 합니다. converter를 초기화한 후 onResume()에 다음 줄을 추가합니다.

converter.setConsumer(processor);

processorpreviewDisplayView에 출력을 전송해야 합니다. 이렇게 하려면 맞춤 SurfaceHolder.Callback에 다음 함수 정의를 추가하세요.

@Override
public void surfaceCreated(SurfaceHolder holder) {
  processor.getVideoSurfaceOutput().setSurface(holder.getSurface());
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
  processor.getVideoSurfaceOutput().setSurface(null);
}

SurfaceHolder가 생성되면 processorVideoSurfaceOutput에 관한 Surface가 있었습니다. 소멸되면 processorVideoSurfaceOutput에서 삭제됩니다.

이상입니다 이제 기기에서 애플리케이션을 성공적으로 빌드하고 실행할 수 있으며 실시간 카메라 피드에서 Sobel 에지 감지가 실행되는 것을 확인할 수 있습니다. 축하합니다!

edge_detection_android_gpu_gif

문제가 발생한 경우 여기에서 가이드의 전체 코드를 확인하세요.