MediaPipe加速流程和原理

时间:2024-02-21 10:29:16

1. MediaPipe加速流程

1.1 OpenGL ES准备

(1) OpenGL上下文(Context)
在调用任何OpenGL指令之前,需要创建一个OpenGL上下文,该上下文是一个非常庞大的状态机,保存了OpenGL的各种状态。由于OpenGL上下文是一个巨大的状态机,因此切换上下文需要较大的开销,但是不同的绘制模块需要完全独立的状态管理。因此可以通过在应用程序中分别创建多个不同上下文,在不同线程中使用不同上下文,同时上下文之间共享纹理,缓冲区等资源,避免反复切换上下文,造成较大开销。(OpenGL的一个上下文不能被多个线程同时访问。此外,在同一线程上切换上下文可能会很慢。因此更有效的做法是为每个上下文设置一个专用线程,每个线程发出GL命令,在其上下文建立一个串行命令队列,然后由GPU异步执行)

(2) 帧缓冲区(FrameBuffer)
OpenGL可以理解成图形API,因此所有运算和结果输出都需要通过图像进行输出。绘图需要有一块画板,那么帧缓冲区就是这块画板。

(3) 附着(Attachment)
附着可以理解成画板上的夹子,夹住哪块画布,就往对应的画布上输出数据。

(4) 纹理(Texture)和渲染缓冲区(RenderBuffer)
帧缓冲区并不是实际存储数据的地方,实际存储图像数据数据的对象就是纹理和渲染缓冲区。(注意,一般来说渲染缓冲区和纹理不能同时挂载在同一帧缓冲区上)

(5) 顶点数组(VertexArray)和顶点缓冲区(VertexBuffer)
有了画板,夹子,画布就可以开始绘画了,画图先画好图像骨架,然后往骨架里面添加颜色,顶点数据就是要画的图像的骨架,OpenGL图像都是由图元组成的(点,线,三角形),顶点数据预先传入显存当中,这部分显存称为顶点缓冲区。

(6) 索引数组(ElementArray)和索引缓冲区(ElementBuffer)
索引数据的目的是为了实现顶点复用,在绘制图像时,总是会有一些顶点被多个图元共享,避免反复对这个顶点进行计算。索引数据存储在显存中,这部分显存称为索引缓冲区。

(7) 着色器程序(Shader)

  • 顶点着色器(VertexShader)
    顶点着色器是OpenGL中用于计算顶点属性的程序。顶点着色器是逐顶点运算的程序,每个顶点数据都会执行一次顶点着色器,每个顶点执行时并行的,并且顶点着色器运算过程中无法访问其他顶点数据。

  • 片段着色器(FragmentShader)
    片段着色器是OpenGL中用于计算像素颜色的程序。每个像素都会执行一次片段着色器,每个像素运行时同样也是并行的。

(8) 逐片段操作(Per-Fragment Operation)

  • 测试(Test)
    着色器程序完成后,我们就得到了像素数据,这些数据必须通过测试才能最终绘制到画布,也就是帧缓冲上的颜色附着上。

  • 混合(Blending)

  • 抖动(Dithering)
    抖动是针对可用颜色较少的系统,可以牺牲分辨率为代价,通过颜色值的抖动来增加可用颜色数量的技术。机器分辨率足够高的情况下,激活抖动操作没有太大意义。

(9) 渲染到纹理
OpenGL程序并不希望渲染出来的图像立即显示在屏幕上,而是需要多次渲染。其中一次的渲染结果作为下一次渲染的输入。如果帧缓冲区的颜色附着设置为一张纹理,那么渲染完成之后,可以重新构造新的帧缓冲区,并将上次渲染出来的纹理作为输入,重复上述流程。

(10) 渲染上屏/交换缓冲区(SwapBuffer)

1.2 CameraX 准备

在 Android 应用中要实现 Camera 功能还是比较困难的,为了保证在各品牌手机设备上的兼容性、响应速度等体验细节,Camera 应用的开发者往往需要花很大的时间和精力进行测试,甚至需要手动在数百种不同设备上进行测试。CameraX 正是为解决这个痛点而诞生的。
优势:

  • CameraX和生命周期结合在一起,方便开发者管理生命周期,相比camera2减少了大量的样板代码的使用
  • CameraX是基于Camera2 API实现的,兼容Andorid L(API 21),保证兼容市面上的绝大多数手机
  • 开发者可以通过扩展形式使用和原生摄像头应用相同的功能(人像,夜间模式,HDR,滤镜,美颜)
  • Google对CameraX进行了深度测试,确保能够给覆盖到更加广泛的设备中。

(1) 添加CameraX依赖

(2) 显示相机预览

(3) 拍照和存储图片

(4) 实时分析图像帧

1.3 Android平台上加速流程

(1) 设置

  • 系统中安装MediaPipe
  • 安装Android Development SDK和Android NDK
  • Android设备开发模式
  • 设置Bazel编译部署Android应用

(2) 特定功能的图结构

(3) 初始最小应用程序设置

(4) 调用CameraX相机驱动

  • 相机权限
  • 相机访问

(5) 设置外部纹理转换器
表面纹理(SurfaceTexture)从流中捕获图像帧作为OpenGL ES纹理。要使用MediaPipe图形,从摄像机捕获的帧应该存储在一个常规的Open GL纹理对象中。MediaPipe提供了一个名为ExternalTextureConverter的类,用于将存储在SurfaceTexture对象中的图像转换为常规OpenGL纹理对象。要使用ExternalTextureConverter,我们需要EglManager对象创建和管理EGLContext。向构建(BUILD)文件添加依赖项以使用EglManager

  1. 计算摄像头的帧在设备屏幕上的适当的显示尺寸
  2. 传入previewFrameTexturedisplaySizeconvert现在摄像头获取到的图像帧已传输到MediaPipe graph中了。

(6) 在Android中调用MediaPipe图结构

  • 添加相关依赖
  • 主活动MainActivity中使用图

2. MediaPipe加速原理

2.1 MediaPipe源码结构

MediaPipe整个技术栈如图所示

MediaPipe中核心源码的结构如下,BUILD为Bazel编译文件、calculators为图结构的计算单元、docs为开发文档、examples为mediapipe的应用示例、framework为框架包含计算单元属性,上下文环境,数据流管理,调度队列,线程池,时间戳等、gpu为OpenGL的依赖文件、graphs为mediapipe各项示例的图结构(边缘检测,人脸检测,姿态追踪等等)、java为安卓应用开发的依赖项、MediaPipe.tulsiproj为相关配置文件、models为各个应用的tflite模型,modules为示例组件、objc为 objective-c语言相关文件、util为工具代码。

2.2 框架加速组件和原理

框架的加速部分主要在framework中,源码中包括计算单元基类,计算单元数据类型定义、计算单元的状态控制、计算单元的上下文环境管理、图结构中输入流和输出流、调度器队列、线程池、时间戳同步等。下面主要分析调度器队列(scheduler_queue),线程池(thread_pool),时间戳(timestamp)怎样通过调度数据流实现时数据流时间戳同步,再GPU计算渲染,从而达到mediapipe管线的最大数据吞吐量。

  • 调度器机制
    优先级队列、线程池的原理可以看这个链接:https://www.cnblogs.com/zhongzhaoxie/p/13630795.html

    MediaPipe图是由计算单元构成的,整个调度机制决定何时运行每个计算单元。每个图至少有一个调度队列,每个调度队列只有一个执行器。默认情况下,执行器是一个线程池,根据系统的能力决定线程数量。每个计算单元作为一个节点都有一个调度状态(未就绪,就绪或者正在运行)。
    对于源节点,没有数据流输入的节点成为源节点,源节点总是处于准备运行的状态,一直到整个图结构没有数据输出,源节点才会关闭。对于非源节点有要处理的输入时,根据节点的输入策略(下面将讨论),形成一个有效的输出集,此时非源节点保持准备状态。当一个节点准备就绪时,意味着一个任务添加到优先调度程序队列中,优先级函数目前时固定的,考虑到节点的静态属性及其图中的拓扑排序。靠近图输出端的节点具有更高的优先级,而源节点具有最低的优先级。

  • 时间戳同步
    MediaPipe图结构执行时去中心化的:没有全局锁,不同的节点能够在同一时间处理不同时间戳的数据。这允许管道有更高的吞吐量。然而时间信息对于许多感知工作流非常重要。同时接收多个输入流的节点需要以某种方式取协调它们。例如,一个目标检测器可能产生一系列候选框,然后再将这个信息输送到渲染节点,该节点应该与原始帧一起处理。
    因此MediaPipe主要功能之一就是让节点输入同步。就框架而言,时间戳的主要作用是充当同步键。此外,MediaPipe被设计为支持确定性操作,这在许多场景(测试、模拟、批处理等)中非常重要,同时允许图设计者在需要满足实时约束的地方放松确定性。
    同步和决定论这两个目标是几种设计选择的基础。值得注意的是,推入给定流的数据包必须有单调递增的时间戳:这不仅是对许多节点有用的假设,而且同步逻辑也依赖于此。每个数据流有时间戳限制,这是数据流上新包允许的最低时间戳。当一个时间戳为T的数据包到达时,边界自动推进到T+1,反映了单调要求。这允许框架确定没有时间戳小于T的数据包会到达。

  • 输入策略
    DefaultInputStreamHandler定义的默认输入策略提供了确定性的输入同步,可以保证多个输入流上具有相同时间戳的数据包,输入数据流严格按照时间戳升序处理。基于计算单元的方法使图可以控制在哪里丢弃数据包,并允许根据资源约束灵活的适应和定制图行为。

  • GPU计算和渲染
    MediaPipe支持用于GPU计算和渲染的计算单元节点,并允许合并多个GPU节点,以及将它们与基于CPU的计算单元节点混合。MediaPipe中GPU设计原则是保证GPU计算单元可以出现在图的任何地方,帧数据在GPU计算单元到另一个计算单元应该不需要复制操作,CPU和GPU之间的数据传输应该高效。
    MediaPipe允许图在多个GL上下文中运行OpenGL。举例来说,这可能是在图结构中非常有用,结合较慢的GPU推理路径(例如,在10帧/秒)和更快的GPU渲染路径(如30 FPS):因为一个GL上下文对应于一个连续的命令队列,所以这两个任务使用相同的上下文将会减少渲染的帧速率。
    MediaPipe使用多个上下文解决的一个挑战是跨它们进行通信的能力。比如这样一个示例场景,同时发送输入视频到显示和推理路径,显示需要先访问推理的结果。
    一个OpenGL上下文不能被多个线程同时访问。此外,在某些Android设备上,在同一线程上切换活动GL上下文可能会很慢。因此,我们的方法是为每个上下文设置一个专用线程。每个线程发出GL命令,在其上下文上建立一个串行命令队列,然后由GPU异步执行。

2.3 人手姿态估计示例

参考文献:
[1] https://zhuanlan.zhihu.com/p/56693625
[2] OPENGL ES 3.0编程指南
[3] https://codelabs.developers.google.com/codelabs/camerax-getting-started/#0
[4] https://zhuanlan.zhihu.com/p/110411044