概览
MediaPipe 支持用于 GPU 计算和渲染的计算器节点,并允许组合多个 GPU 节点,以及将它们与基于 CPU 的计算器节点混合。移动平台上有几个 GPU API(例如,OpenGL ES、Metal 和 Vulkan)。MediaPipe 不会尝试提供单一的跨 API GPU 抽象。单个节点可以使用不同的 API 进行编写,以便它们能够在需要时利用平台特有功能。
GPU 支持对于在移动平台上获得良好性能至关重要,尤其是对于实时视频。借助 MediaPipe,开发者可以编写与 GPU 兼容的计算器,以支持将 GPU 用于以下用途:
- 设备端实时处理,而不仅仅是批处理
- 视频呈现和效果,而不仅仅是分析
以下是 MediaPipe 中 GPU 支持的设计原则
- 基于 GPU 的计算器应该可以在图表中的任何位置出现,不一定用于在屏幕上呈现。
- 帧数据从一个基于 GPU 的计算器到另一个基于 GPU 的计算器应该能够快速传输,而且不会产生高昂的复制操作费用。
- 在 CPU 和 GPU 之间传输帧数据的效率应尽可能高。
- 由于不同平台可能需要不同的技术才能获得最佳性能,因此该 API 应允许在后台实现内容的方式灵活性。
- 计算器应尽可能灵活地使用 GPU 执行全部或部分操作,并在必要时将其与 CPU 结合使用。
OpenGL ES 支持
MediaPipe 在 Android/Linux 上支持最高版本为 3.2 的 OpenGL ES,最高版本为 ES 3.0 。此外,MediaPipe 还支持 iOS 上的 Metal。
需要 OpenGL ES 3.1 或更高版本(在 Android/Linux 系统上)才能运行 机器学习推理计算器和图表。
MediaPipe 允许图在多个 GL 上下文中运行 OpenGL。例如, 在合并较慢的 GPU 推理路径(例如,10 FPS) 以及更快的 GPU 渲染路径(例如,30 FPS):因为一个 GL 上下文 对应于一个有序命令队列,对两者使用相同的上下文 会降低渲染帧速率。
MediaPipe 使用多个上下文解决的难题之一是, 进行通信例如,一个输入视频 同时发送到渲染路径和推理路径,并且渲染需要 访问推理的最新输出。
多个线程无法同时访问 OpenGL 上下文。 此外,在同一线程上切换活跃 GL 上下文也会很慢, 部分 Android 设备因此,我们的方法是使用一个专用线程, 。每个线程都会发出 GL 命令,从而构建串行命令队列 然后由 GPU 异步执行。
GPU 计算器的生命周期
本部分介绍了 GPU 的 Process 方法的基本结构
派生自基类 GlSimpleCalculator 的计算器。GPU 计算器
以 LuminanceCalculator
为例。方法
从 GlSimpleCalculator::Process
调用 LuminanceCalculator::GlRender
。
// Converts RGB images into luminance images, still stored in RGB format.
// See GlSimpleCalculator for inputs, outputs and input side packets.
class LuminanceCalculator : public GlSimpleCalculator {
public:
absl::Status GlSetup() override;
absl::Status GlRender(const GlTexture& src,
const GlTexture& dst) override;
absl::Status GlTeardown() override;
private:
GLuint program_ = 0;
GLint frame_;
};
REGISTER_CALCULATOR(LuminanceCalculator);
absl::Status LuminanceCalculator::GlRender(const GlTexture& src,
const GlTexture& dst) {
static const GLfloat square_vertices[] = {
-1.0f, -1.0f, // bottom left
1.0f, -1.0f, // bottom right
-1.0f, 1.0f, // top left
1.0f, 1.0f, // top right
};
static const GLfloat texture_vertices[] = {
0.0f, 0.0f, // bottom left
1.0f, 0.0f, // bottom right
0.0f, 1.0f, // top left
1.0f, 1.0f, // top right
};
// program
glUseProgram(program_);
glUniform1i(frame_, 1);
// vertex storage
GLuint vbo[2];
glGenBuffers(2, vbo);
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// vbo 0
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), square_vertices,
GL_STATIC_DRAW);
glEnableVertexAttribArray(ATTRIB_VERTEX);
glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, nullptr);
// vbo 1
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), texture_vertices,
GL_STATIC_DRAW);
glEnableVertexAttribArray(ATTRIB_TEXTURE_POSITION);
glVertexAttribPointer(ATTRIB_TEXTURE_POSITION, 2, GL_FLOAT, 0, 0, nullptr);
// draw
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// cleanup
glDisableVertexAttribArray(ATTRIB_VERTEX);
glDisableVertexAttribArray(ATTRIB_TEXTURE_POSITION);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(2, vbo);
return absl::OkStatus();
}
上述设计原则促成了以下设计 MediaPipe GPU 支持选项:
- 我们有一个名为
GpuBuffer
的 GPU 数据类型,用于表示图片数据,并针对 GPU 使用情况进行了优化。此数据类型的确切内容是不透明的,并且特定于平台。 - 基于组合的低级 API,其中任何想要使用 GPU 的计算器都会创建一个
GlCalculatorHelper
类的实例。此类提供了一个与平台无关的 API,用于管理 OpenGL 上下文、为输入和输出设置纹理,等等。 - 一个基于子类的高阶 API,其中简单计算器实现来自
GlSimpleCalculator
的图片滤镜子类,只需要用其特定的 OpenGL 代码替换几个虚拟方法,而父类则负责处理所有管道。 - 需要在所有基于 GPU 的计算器之间共享的数据作为外部输入提供,该输入作为图表服务实现,并由
GlCalculatorHelper
类管理。 - 将特定于计算器的帮助程序和共享图表服务相结合,我们可以极大地灵活地管理 GPU 资源:我们可以为每个计算器提供单独的上下文、共享单个上下文、共享锁或其他同步基元等,所有这些都由帮助程序管理,并且不会显示在各个计算器中。
GpuBuffer 到 ImageFrame 的转换器
我们提供了两个名为 GpuBufferToImageFrameCalculator
和 ImageFrameToGpuBufferCalculator
的计算器。这些计算器可以在 ImageFrame
和 GpuBuffer
之间进行转换,从而构建将 GPU 和 CPU 计算器结合使用的图表。iOS 和 Android 均支持此操作
如果可能,这些计算器会利用平台特有的功能,在不复制数据的情况下在 CPU 和 GPU 之间共享数据。
下图显示了移动应用中的数据流,该应用从相机捕获视频,通过 MediaPipe 图运行视频,并在屏幕上实时渲染输出。虚线指示哪些部分位于 MediaPipe 图本身内。此应用使用 OpenCV 在 CPU 上运行 Canny 边缘检测过滤器,并使用 GPU 将其叠加在原始视频上。
来自相机的视频帧以 GpuBuffer
数据包的形式馈送到图表中。通过
输入流由两个并行计算器访问。
GpuBufferToImageFrameCalculator
将缓冲区转换为 ImageFrame
,
然后,灰度转换器和 Canny 过滤器(两者都是基于
并在 CPU 上运行),其输出随后会转换为
GpuBuffer
。多输入 GPU 计算器 GlOverlayCalculator,
输入原始 GpuBuffer
和来自边缘检测器的另一个,
并使用着色器叠加它们。随后,系统会将输出发送回
以及使用回调计算器来呈现图片
传递给屏幕