Введение
Этот Привет, Мир! Учебное пособие использует 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 = [
"@maven//:androidx_appcompat_appcompat",
"@maven//:androidx_constraintlayout_constraintlayout",
],
)
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 работает в прямом эфире с камеры! Поздравляю!

Если у вас возникли какие-либо проблемы, ознакомьтесь с полным кодом руководства здесь .