【问题标题】:Zero-copy Camera Processing and Rendering Pipeline on AndroidAndroid 上的零拷贝相机处理和渲染管道
【发布时间】:2016-06-02 13:13:28
【问题描述】:

我需要对实时摄像机数据(仅来自 Y 平面)进行 CPU 端只读处理,然后在 GPU 上进行渲染。在处理完成之前不应渲染帧(因此我并不总是想渲染来自相机的最新帧,只是 CPU 端已完成处理的最新帧)。渲染与相机处理分离,目标是 60 FPS,即使相机帧的到达速度低于此速度。

有一个相关但更高级别的问题:Lowest overhead camera to CPU to GPU approach on android

更详细地描述当前设置:我们有一个用于相机数据的应用程序端缓冲池,其中的缓冲区可以是“空闲”、“显示中”或“待显示”。当来自相机的新帧到达时,我们获取一个空闲缓冲区,将帧(或者如果实际数据在某个系统提供的缓冲池中,则为它的引用)存储在那里,进行处理并将结果存储在缓冲区中,然后设置缓冲区“待显示”。在渲染器线程中,如果在渲染循环开始时有任何缓冲区“待显示”,我们将其锁定为“显示中”的缓冲区,渲染相机,并使用从相同计算的处理信息渲染其他内容相机框架。

感谢@fadden 对上面链接的问题的回复,我现在了解 android camera2 API 的“并行输出”功能在各种输出队列之间共享缓冲区,因此不应该涉及任何数据副本,至少在现代机器人。

在评论中,有人建议我可以同时锁定 SurfaceTexture 和 ImageReader 输出,然后“坐在缓冲区上”直到处理完成。不幸的是,我认为这不适用于我的情况,因为我们仍然希望以 60 FPS 的速度驱动解耦渲染,并且在处理新帧时仍然需要访问前一帧以确保事情不会得到不同步。

想到的一个解决方案是拥有多个 SurfaceTexture - 在我们的每个应用端缓冲区中都有一个(我们目前使用 3 个)。使用该方案,当我们获得一个新的相机帧时,我们将从我们的应用端池中获得一个空闲缓冲区。然后我们在 ImageReader 上调用 acquireLatestImage() 以获取数据进行处理,并在空闲缓冲区中的 SurfaceTexture 上调用 updateTexImage()。在渲染时,我们只需要确保“显示中”缓冲区中的 SufaceTexture 是绑定到 GL 的缓冲区,并且大部分时间都应该同步(正如 @fadden 评论的那样,调用 updateTexImage() 之间存在竞争和acquireLatestImage(),但该时间窗口应该足够小以使其罕见,并且无论如何使用缓冲区中的时间戳可能是可检测和可修复的)。

我在文档中注意到 updateTexImage() 只能在 SurfaceTexture 绑定到 GL 上下文时调用,这表明我在相机处理线程中也需要一个 GL 上下文,以便相机线程可以执行 updateTexImage()在“空闲”缓冲区中的 SurfaceTexture 上,而渲染线程仍然能够从“显示中”缓冲区中的 SurfaceTexture 进行渲染。

所以,对于问题:

  1. 这看起来是一种明智的做法吗?
  2. SurfaceTextures 基本上是共享缓冲池周围的轻量级包装器,还是它们消耗一些有限的硬件资源并且应该谨慎使用?
  3. SurfaceTexture 调用是否都足够便宜,以至于使用多个调用仍然比仅复制数据更胜一筹?
  4. 是否计划让两个线程具有不同的 GL 上下文并在每个线程中绑定不同的 SurfaceTexture 可能会工作,还是我要求一个充满痛苦和错误驱动程序的世界?

这听起来很有希望,我会试一试;但认为值得在这里问一下,以防有人(基本上是@fadden!)知道我忽略的任何内部细节,这会使这是一个坏主意。

【问题讨论】:

    标签: android c++ opengl-es android-camera2


    【解决方案1】:

    有趣的问题。

    背景资料

    拥有多个具有独立上下文的线程是很常见的。每个使用硬件加速视图渲染的应用在主线程上都有一个 GLES 上下文,因此任何使用 GLSurfaceView(或使用 SurfaceView 或 TextureView 和独立渲染线程滚动自己的 EGL)的应用都在积极使用多个上下文。

    每个 TextureView 内部都有一个 SurfaceTexture,因此任何使用多个 TextureView 的应用程序在单个线程上都有多个 SurfaceTexture。 (实际上,框架had a bug 在其实现中导致多个TextureView 出现问题,但这是一个高级问题,而不是驱动程序问题。)

    SurfaceTexture,a/k/a GLConsumer,不会做很多处理。当帧从源(在您的情况下为相机)到达时,它使用一些 EGL 函数将缓冲区“包装”为“外部”纹理。在没有 EGL 上下文的情况下,您无法执行这些 EGL 操作,这就是为什么必须将 SurfaceTexture 附加到其中的原因,以及如果当前存在错误的上下文,您不能将新帧放入纹理的原因。你可以从the implementation of updateTexImage() 看到,它使用缓冲区队列、纹理和栅栏做了很多神秘的事情,但都不需要复制像素数据。您真正占用的唯一系统资源是 RAM,如果您要捕获高分辨率图像,这并不是小事。

    连接

    EGL 上下文可以在线程之间移动,但一次只能在一个线程上“当前”。来自多个线程的同时访问将需要大量不需要的同步。给定线程只有一个“当前”上下文。 OpenGL API 从具有全局状态的单线程演变为多线程,而不是重写 API,他们只是将状态推送到线程本地存储中……因此有了“当前”的概念。

    可以创建在它们之间共享某些内容(包括纹理)的 EGL 上下文,但如果这些上下文位于不同的线程上,则在更新纹理时必须非常小心。 Grafika 提供了一个很好的 getting it wrong 示例。

    SurfaceTextures 建立在 BufferQueues 之上,BufferQueues 具有生产者-消费者结构。 SurfaceTextures 的有趣之处在于它们包含两侧,因此您可以在一个进程中将数据从一侧输入并从另一侧拉出(与 SurfaceView 不同,其中消费者距离很远)。像所有 Surface 的东西一样,它们构建在 Binder IPC 之上,因此您可以从一个线程提供 Surface,并在不同的线程(或进程)中安全地 updateTexImage()。 API 的安排是让您在消费者端(您的进程)创建 SurfaceTexture,然后将引用传递给生产者(例如相机,它主要在 mediaserver 进程中运行)。

    实施

    如果您不断地连接和断开 BufferQueue,您会产生大量开销。所以如果你想让三个 SurfaceTexture 接收缓冲区,你需要将所有三个连接到 Camera2 的输出,并让它们都接收“缓冲区广播”。然后你updateTexImage() 以循环方式进行。由于 SurfaceTexture 的 BufferQueue 在“异步”模式下运行,因此您应该始终在每次调用时获取最新帧,而无需“排空”队列。

    直到 Lollipop 时代的 BufferQueue 多输出发生变化和 Camera2 的引入,这种安排才真正可行,所以我不知道之前是否有人尝试过这种方法。

    所有 SurfaceTexture 都将附加到同一个 EGL 上下文,理想情况下是在 View UI 线程之外的线程中,因此您不必为当前的内容而争吵。如果您想从不同线程中的第二个上下文访问纹理,则需要使用 SurfaceTexture attach/detach API 调用,它明确支持这种方法:

    创建了一个新的 OpenGL ES 纹理对象,并使用上次调用 detachFromGLContext() 时当前的 SurfaceTexture 图像帧填充。

    请记住,切换 EGL 上下文是消费者端的操作,与摄像头的连接无关,它是生产者端的操作。在上下文之间移动 SurfaceTexture 所涉及的开销应该很小——小于updateTexImage()——但你需要采取通常的步骤来确保线程之间通信时的同步。

    太糟糕了 ImageReader 缺少 getTimestamp() 调用,因为这将大大简化从相机匹配缓冲区的过程。

    结论

    使用多个 SurfaceTexture 来缓冲输出是可能的,但很棘手。我可以看到乒乓缓冲区方法的潜在优势,其中一个 ST 用于接收线程/上下文 A 中的帧,而另一个 ST 用于在线程/上下文 B 中渲染,但因为您是在真实操作时间我认为额外的缓冲没有任何价值,除非你试图填补时间。

    一如既往,推荐阅读Android System-Level Graphics Architecture doc

    【讨论】:

    • 感谢您的精彩解释,这听起来确实是一种可行的方法。我可能会在周末嘲笑它。可能会把它放在github上
    • 通过“确保同步的通常步骤”,您只是指调用updateTexImage() 的线程必须在渲染线程尝试附加它以进行渲染之前调用detachFromGLContext()?如果将上下文设置为共享您链接的 grafika 错误中提到的纹理,则不需要 glFinish() 要求?
    • API 19 的 Image 上有 getTimestamp()。我希望它返回与 SurfaceTexture 相同的时间戳,但会在我测试该方法时进行检查。
    • “常规步骤”我的意思是synchronize/wait/notify 或类似的,以避免 SMP 事故。 updateTexImage(),分离,通知其他线程;其他线程唤醒,将 ST 附加到上下文,使用纹理。尝试在 EGL 上下文之间共享纹理会使事情变得丑陋……这对于静态内容来说很好,但是当纹理发生变化时,您必须采取额外的步骤来避免出现问题。
    • ImageReader 没有 getTimestamp(),但这是因为它位于从 ImageReader 获取的单个图像上:developer.android.com/reference/android/media/…。与 SurfaceTexture 不同,ImageReader 允许应用程序一次访问多个缓冲区,因此元数据与每个图像相关联,而不是与阅读器本身相关联。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-02-15
    • 2016-08-14
    • 1970-01-01
    • 2017-07-06
    • 1970-01-01
    • 2019-01-22
    • 2012-04-12
    相关资源
    最近更新 更多