简介
你好!世界教程使用 MediaPipe 框架来开发 Android 应用, 在 Android 上运行 MediaPipe 图。
构建内容
一款简单的相机应用,可对实时视频进行实时 Sobel 边缘检测 如何在 Android 设备上流式传输
设置
- 在系统上安装 MediaPipe Framework,请参阅框架安装 指南了解详情。
- 安装 Android Development SDK 和 Android NDK。您也可以在 [框架安装指南]。
- 在 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
数据流。
初始最小应用设置
我们先从显示“Hello World!”的简单 Android 应用开始。
。如果您熟悉如何构建 Android 应用,则可以跳过此步骤。
使用 bazel
的应用。
创建一个将在其中创建 Android 应用的新目录。对于
本教程的完整代码可以在以下位置找到:
mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic
。我们将
在整个 Codelab 中,此路径称为 $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>
向 $APPLICATION_PATH
添加一个简单的 MainActivity.java
,用于加载内容
的 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
使用 adb install
安装生成的 APK 文件。例如:
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" />
在21
27
同一个文件:
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="27" />
这样可以确保系统提示用户请求相机权限, 以使用 CameraX 库访问相机。
如需请求相机权限,我们可以使用 MediaPipe 框架提供的实用程序
组件,即 PermissionHelper
。如需使用它,请添加依赖项
"//mediapipe/java/com/google/mediapipe/components:android_components"
在
BUILD
中的mediapipe_lib
规则。
如需在 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
对象中。为了使用这些功能,
首先需要更改应用的布局
从以下位置移除整个 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>
此代码块有一个名为 preview_display_layout
的新 FrameLayout
,
其中嵌套的 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
对象,以便我们可以用它来显示
使用名为 previewFrameTexture
的 SurfaceTexture
对象构建相机取景框。
如需使用 previewFrameTexture
获取相机帧,我们将使用 CameraX。
框架提供了一个名为 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>
并在 helloworld
Android 二进制文件规则的 BUILD
中指定选择
并在 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 图,从相机捕获的帧应
存储在常规 OpenGL 纹理对象中框架提供了一个类,
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);
回想一下,我们在 MainActivity
中定义 onResume()
函数来确认
已授予相机权限并调用 startCamera()
。在此之前
请在 onResume()
中添加以下代码行,以初始化 converter
对象:
converter = new ExternalTextureConverter(eglManager.getContext());
此 converter
现在使用由 eglManager
管理的 GLContext
。
我们还需要替换 MainActivity
中的 onPause()
函数,以便
如果应用进入暂停状态,我们会正确关闭 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 图中使用相机帧了。
在 Android 中使用 MediaPipe 图
添加相关依赖项
如需使用 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,
)
将依赖项 ":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
表示二进制图的文件名,
取决于 mediapipe_binary_graph
目标中的 output_name
字段。
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;
并在初始化 eglManager
后在 onCreate(Bundle)
中对其进行初始化:
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
processor
的 VideoSurfaceOutput
。销毁后,我们会将其从以下位置移除:
processor
的 VideoSurfaceOutput
。
这样就大功告成了!现在,您应该能够成功构建并运行 并查看在实时摄像头上运行的 Sobel 边缘检测功能 Feed!恭喜!
如果您遇到任何问题,请参阅本教程的完整代码 此处。