今天开始给大家分享mediapipe学习,踩坑过程.
我现在使用的是windown系统, 也跑过centos上,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
helloworld!android
- 介绍
- 你会学到什么
- 你将建造什么
- 设置
- 边缘检测图
- 初始最小应用程序设置
- 通过使用相机CameraX
- 相机权限
- 相机访问
- ExternalTextureConverter设置
- 在 Android 中使用 MediaPipe 图
- 添加相关依赖
- 使用图表MainActivity
介绍
此 Codelab 在 Android 设备上使用 MediaPipe。
你会学到什么
如何开发使用 MediaPipe 的 Android 应用程序并在 Android 上运行 MediaPipe 图。
你将建造什么
用于实时 Sobel 边缘检测的简单相机应用程序,适用于 Android 设备上的实时视频流。
设置
- 在您的系统上安装 MediaPipe,有关详细信息,请参阅MediaPipe 安装指南。
- 安装 Android 开发 SDK 和 Android NDK。另请参阅 [MediaPipe 安装指南] 中的操作方法。
- 在您的 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 应用程序开始。屏幕上。如果您熟悉使用bazel
.
创建一个新目录,您将在其中创建 Android 应用程序。例如,本教程的完整代码可以在mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic
. $APPLICATION_PATH
我们将在整个 Codelab 中引用此路径。
请注意,在应用程序的路径中:
- 该应用程序名为
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
to $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
规则使用basic_lib
生成的 Android 库构建二进制 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 组件提供的实用程序,即PermissionHelper. 要使用它,请"//mediapipe/java/com/google/mediapipe/components:android_components"
在.mediapipe_lib
BUILD
要使用PermissionHelper
in 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()
调用。该代码将确认已授予使用相机的权限,然后将启动相机。
重建并安装应用程序。您现在应该会看到请求访问应用程序的摄像头的提示。
注意:如果没有对话框提示,请卸载并重新安装应用程序。如果您没有更改文件中的minSdkVersion
and也可能发生这种情况。targetSdkVersion
AndroidManifest.xml
相机访问
有了可用的相机权限,我们就可以开始并从相机中获取帧。
要查看来自相机的帧,我们将使用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。MediaPipe 提供了一个名为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继承 from CameraHelperwhich 提供两个选项,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
设置
ASurfaceTexture从流中捕获图像帧作为 OpenGL ES 纹理。要使用 MediaPipe 图,从相机捕获的帧应存储在常规 Open GL 纹理对象中。MediaPipe 提供了一个类,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 图中使用相机帧。
在 Android 中使用 MediaPipe 图
添加相关依赖
要使用 MediaPipe 图,我们需要将依赖项添加到 Android 上的 MediaPipe 框架。我们将首先添加一个构建规则来构建一个cc_binary
使用 MediaPipe 框架的 JNI 代码,然后构建一个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 边缘检测运行!恭喜!