Введение
Этот Привет, Мир! Учебное пособие использует MediaPipe Framework для разработки приложения Android, которое запускает граф MediaPipe на Android.
Что вы построите
Простое приложение камеры для обнаружения границ Собела в режиме реального времени, применяемое к живому видеопотоку на устройстве Android.
Настраивать
- Установите MediaPipe Framework в вашей системе. Подробности см. в руководстве по установке Framework .
- Установите Android Development SDK и Android NDK. См. также, как это сделать, в [Руководстве по установке Framework].
- Включите параметры разработчика на своем устройстве Android.
- Настройте 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"
}
Визуализация графика показана ниже:
Этот граф имеет один входной поток с именем input_video
для всех входящих кадров, которые будут предоставлены камерой вашего устройства.
Первый узел графа, LuminanceCalculator
, принимает один пакет (кадр изображения) и применяет изменение яркости с помощью шейдера OpenGL. Результирующий кадр изображения отправляется в выходной поток luma_video
.
Второй узел, SobelEdgesCalculator
применяет обнаружение границ к входящим пакетам в потоке luma_video
и выводит результаты в выходной поток output_video
.
Наше приложение для Android будет отображать выходные кадры изображения потока output_video
.
Начальная минимальная настройка приложения
Сначала мы начнем с простого приложения для Android, которое отображает «Hello World!» на экране. Вы можете пропустить этот шаг, если знакомы с созданием приложений Android с использованием bazel
.
Создайте новый каталог, в котором вы будете создавать свое приложение для Android. Например, полный код этого руководства можно найти по адресу mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic
. В кодовой лаборатории мы будем называть этот путь $APPLICATION_PATH
.
Обратите внимание, что в пути к приложению:
- Приложение называется
helloworld
. -
$PACKAGE_PATH
приложения —com.google.mediapipe.apps.basic
. Он используется во фрагментах кода в этом руководстве, поэтому не забудьте использовать свой собственный$PACKAGE_PATH
при копировании/использовании фрагментов кода.
Добавьте файл activity_main.xml
в $APPLICATION_PATH/res/layout
. При этом TextView
отображается на полном экране приложения со строкой 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>
Добавьте простой MainActivity.java
в $APPLICATION_PATH
, который загружает содержимое макета activity_main.xml
, как показано ниже:
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);
}
}
Добавьте файл манифеста AndroidManifest.xml
в $APPLICATION_PATH
, который запускает MainActivity
при запуске приложения:
<?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
, поэтому нам нужны соответствующие ссылки на темы. Добавьте colors.xml
в $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>
Добавьте 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 basic_lib
, созданную для создания двоичного APK для установки на ваше устройство Android.
Чтобы создать приложение, используйте следующую команду:
bazel build -c opt --config=android_arm64 $APPLICATION_PATH:helloworld
Установите сгенерированный APK-файл с помощью adb install
. Например:
adb install bazel-bin/$APPLICATION_PATH/helloworld.apk
Откройте приложение на своем устройстве. Должен появиться экран с текстом Hello World!
.
Использование камеры через 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
. Чтобы использовать его, добавьте зависимость "//mediapipe/java/com/google/mediapipe/components:android_components"
в правило mediapipe_lib
в BUILD
.
Чтобы использовать PermissionHelper
в MainActivity
, добавьте следующую строку в функцию 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
. Чтобы использовать их, нам сначала нужно изменить макет нашего приложения.
Удалите весь блок кода TextView
из $APPLICATION_PATH/res/layout/activity_main.xml
и добавьте вместо него следующий код:
<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>
Этот блок кода имеет новый FrameLayout
с preview_display_layout
и вложенный в него TextView
с именем no_camera_access_preview
. Если права доступа к камере не предоставлены, наше приложение отобразит TextView
со строковым сообщением, хранящимся в переменной no_camera_access
. Добавьте следующую строку в файл $APPLICATION_PATH/res/values/strings.xml
:
<string name="no_camera_access" translatable="false">Please grant camera permissions.</string>
Если пользователь не предоставляет разрешение камере, экран теперь будет выглядеть следующим образом:
Теперь мы добавим объекты SurfaceTexture
и SurfaceView
в 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);
}
Мы определяем новый объект SurfaceView
и добавляем его к объекту preview_display_layout
FrameLayout
, чтобы можно было использовать его для отображения кадров камеры с помощью объекта SurfaceTexture
с именем previewFrameTexture
.
Чтобы использовать previewFrameTexture
для получения кадров камеры, мы будем использовать CameraX . Framework предоставляет утилиту под названием CameraXPreviewHelper
для использования CameraX . Этот класс обновляет прослушиватель при запуске камеры через 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
для захвата кадров, мы сохраняем этот surfaceTexture
как previewFrameTexture
и делаем previewDisplayView
видимым, чтобы мы могли начать видеть кадры из previewFrameTexture
.
Однако прежде чем запустить камеру, нам нужно решить, какую камеру мы хотим использовать. CameraXPreviewHelper
наследует от CameraHelper
, который предоставляет два параметра: FRONT
и BACK
. Мы можем передать решение из файла BUILD
в виде метаданных, чтобы не требовалось никаких изменений кода для создания другой версии приложения с использованием другой камеры.
Предполагая, что мы хотим использовать BACK
камеру для обнаружения границ на живой сцене, которую мы просматриваем с камеры, добавьте метаданные в AndroidManifest.xml
:
...
<meta-data android:name="cameraFacingFront" android:value="${cameraFacingFront}"/>
</application>
</manifest>
и укажите выбор в BUILD
в двоичном правиле helloworld
для Android с новой записью в manifest_values
:
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);
На этом этапе приложение должно быть успешно построено. Однако когда вы запустите приложение на своем устройстве, вы увидите черный экран (хотя разрешения камеры были предоставлены). Это связано с тем, что, хотя мы сохраняем переменную surfaceTexture
, предоставленную CameraXPreviewHelper
, previewSurfaceView
еще не использует ее выходные данные и не отображает их на экране.
Поскольку мы хотим использовать кадры в графике MediaPipe, мы не будем добавлять код для просмотра выходных данных камеры непосредственно в этом руководстве. Вместо этого мы перейдем к тому, как мы можем отправлять кадры камеры для обработки в граф MediaPipe и отображать выходные данные графика на экране.
Настройка ExternalTextureConverter
SurfaceTexture
захватывает кадры изображения из потока в виде текстуры OpenGL ES. Чтобы использовать график MediaPipe, кадры, снятые с камеры, должны храниться в обычном объекте текстуры Open GL. Платформа предоставляет класс ExternalTextureConverter
для преобразования изображения, хранящегося в объекте SurfaceTexture
, в обычный объект текстуры OpenGL.
Чтобы использовать ExternalTextureConverter
, нам также нужен EGLContext
, который создается и управляется объектом EglManager
. Добавьте зависимость к файлу BUILD
для использования EglManager
"//mediapipe/java/com/google/mediapipe/glutil"
.
В MainActivity
добавьте следующие объявления:
private EglManager eglManager;
private ExternalTextureConverter converter;
В функцию onCreate(Bundle)
добавьте оператор для инициализации объекта eglManager
перед запросом разрешений камеры:
eglManager = new EglManager(null);
Напомним, что мы определили функцию onResume()
в MainActivity
для подтверждения предоставления разрешений камере и вызова startCamera()
. Перед этой проверкой добавьте следующую строку в onResume()
для инициализации объекта converter
:
converter = new ExternalTextureConverter(eglManager.getContext());
Этот converter
теперь использует GLContext
управляемый eglManager
.
Нам также необходимо переопределить функцию onPause()
в MainActivity
, чтобы, если приложение перейдет в состояние паузы, мы могли правильно закрыть converter
:
@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.Callback
к previewDisplayView
и реализуем функцию surfaceChanged(SurfaceHolder holder, int format, int width, int height)
для вычисления соответствующего размера отображения кадров камеры на экране устройства и привязки previewFrameTexture
и отправьте кадры вычисленного displaySize
в converter
.
Теперь мы готовы использовать кадры камеры в графе MediaPipe.
Использование графика MediaPipe в Android
Добавьте соответствующие зависимости
Чтобы использовать граф MediaPipe, нам нужно добавить зависимости к платформе MediaPipe на Android. Сначала мы добавим правило сборки для создания cc_binary
с использованием кода JNI платформы MediaPipe, а затем создадим правило 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,
)
Добавьте зависимость ":mediapipe_jni_lib"
к правилу сборки mediapipe_lib
в файле BUILD
.
Далее нам нужно добавить зависимости, специфичные для графа 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, используемые в вашем графике.
Кроме того, добавьте дополнительные manifest_values
для свойств, специфичных для графа, которые позже будут получены в 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",
},
Обратите внимание, binaryGraphName
указывает имя файла двоичного графа, определяемое полем output_name
в цели mediapipe_binary_graph
. inputVideoStreamName
и outputVideoStreamName
— это имя входного и выходного видеопотока, указанное на графике соответственно.
Теперь MainActivity
необходимо загрузить платформу MediaPipe. Кроме того, фреймворк использует OpenCV, поэтому MainActvity
также должен загружать OpenCV
. Используйте следующий код в MainActivity
(внутри класса, но не внутри какой-либо функции), чтобы загрузить обе зависимости:
static {
// Load all native libraries needed by the app.
System.loadLibrary("mediapipe_jni");
System.loadLibrary("opencv_java3");
}
Используйте график в MainActivity
Сначала нам нужно загрузить ресурс, содержащий .binarypb
скомпилированный из файла .pbtxt
графика. Для этого мы можем использовать утилиту MediaPipe AndroidAssetUtil
.
Инициализируйте менеджер активов в onCreate(Bundle)
перед инициализацией eglManager
:
// Initialize asset manager so that MediaPipe native libraries can access the app assets, e.g.,
// binary graphs.
AndroidAssetUtil.initializeNativeAssetManager(this);
Теперь нам нужно настроить объект FrameProcessor
, который отправляет кадры камеры, подготовленные converter
, в граф MediaPipe, запускает график, подготавливает выходные данные, а затем обновляет previewDisplayView
для отображения выходных данных. Добавьте следующий код, чтобы объявить FrameProcessor
:
private FrameProcessor processor;
и инициализируйте его в onCreate(Bundle)
после инициализации eglManager
:
processor =
new FrameProcessor(
this,
eglManager.getNativeContext(),
applicationInfo.metaData.getString("binaryGraphName"),
applicationInfo.metaData.getString("inputVideoStreamName"),
applicationInfo.metaData.getString("outputVideoStreamName"));
processor
необходимо использовать преобразованные кадры из converter
для обработки. Добавьте следующую строку в onResume()
после инициализации converter
:
converter.setConsumer(processor);
processor
должен отправить свои выходные данные в previewDisplayView
Для этого добавьте следующие определения функций в наш собственный SurfaceHolder.Callback
:
@Override
public void surfaceCreated(SurfaceHolder holder) {
processor.getVideoSurfaceOutput().setSurface(holder.getSurface());
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
processor.getVideoSurfaceOutput().setSurface(null);
}
Когда SurfaceHolder
был создан, у нас была Surface
для VideoSurfaceOutput
processor
. Когда он уничтожен, мы удаляем его из VideoSurfaceOutput
processor
.
И все! Теперь вы сможете успешно создать и запустить приложение на устройстве и увидеть, как обнаружение границ Sobel работает в прямом эфире с камеры! Поздравляю!
Если у вас возникли какие-либо проблемы, ознакомьтесь с полным кодом руководства здесь .