GLSL 和 GPU 片段着色器执行中的求和区域表



我正在尝试计算GPU内存(相机捕获(中纹理的积分图像(又名求和面积表(,目标是计算所述图像的自适应阈值。我正在使用OpenGL ES 2.0,并且仍在学习:)。

我用一个简单的高斯模糊着色器(垂直/水平通道(做了一个测试,它工作正常,但我需要一个更大的可变平均面积才能给出令人满意的结果。

我之前确实在 CPU 上实现了该算法的一个版本,但我对如何在 GPU 上实现它有点困惑。我试图对每个片段进行(完全不正确的(测试:

#version 100
#extension GL_OES_EGL_image_external : require
precision highp float;
uniform sampler2D           u_Texture;      // The input texture.
varying lowp vec2           v_TexCoordinate;    // Interpolated texture     coordinate per fragment.
uniform vec2                u_PixelDelta;           // Pixel delta
void main()
{
    // get neighboring pixels values
    float center = texture2D(u_Texture, v_TexCoordinate).r;
    float a = texture2D(u_Texture, v_TexCoordinate + vec2(u_PixelDelta.x * -1.0, 0.0)).r;
    float b = texture2D(u_Texture, v_TexCoordinate + vec2(0.0, u_PixelDelta.y * 1.0)).r;
    float c = texture2D(u_Texture, v_TexCoordinate + vec2(u_PixelDelta.x * -1.0, u_PixelDelta.y * 1.0)).r;
    // compute value
    float pixValue = center + a + b - c;

    // Result stores value (R) and original gray value (G)
    gl_FragColor = vec4(pixValue, center, center, 1.0);
}

然后是另一个着色器来获得我想要的区域,然后得到平均值。这显然是错误的,因为有多个执行单元同时运行。

我知道在 GPU 上计算前缀总和的常用方法是分两次进行(垂直/水平,如此线程或此处所述(,但这里没有问题,因为每个单元格都有数据依赖性来自上一个(顶部或左侧(一个?

我似乎无法理解 GPU 上的多个执行单元处理不同片段的顺序,以及两次传递过滤器如何解决这个问题。例如,如果我有一些这样的值:

2 1 5
0 3 2
4 4 7

两次传递应给出(第一列然后是行(:

2 1 5          2 3 8
2 4 7     ->   2 6 13
6 8 14         6 14 28

例如,我如何确定值 [0;2] 将计算为 6 (2 + 4( 而不是 4(0 + 4,如果尚未计算 0(?

另外,据我了解片段不是像素(如果我没记错的话(,如果我使用从顶点着色器传递的完全相同的坐标,我在第一次传递中存储回我的一个纹理中的值在另一次传递中是否相同,或者它们会以某种方式插值?

Tommy 和 Bartvbl 解决了你关于求和面积表的问题,但你的自适应阈值的核心问题可能不需要这个。

作为我的开源 GPU mage 框架的一部分,我做了一些使用 OpenGL ES 优化大半径模糊的实验。通常,增加模糊半径会导致每像素的纹理采样和计算显著增加,并随之减慢。

但是,我发现对于大多数模糊操作,您可以应用令人惊讶的有效优化来限制模糊样本的数量。如果在模糊之前对图像进行缩减像素采样,以较小的像素半径(半径/缩减像素采样因子(进行模糊,然后线性上采样,则可以得到相当于在更大像素半径下模糊的图像的模糊图像。在我的测试中,这些下采样、模糊和上采样的图像看起来与基于原始图像分辨率模糊的图像几乎相同。事实上,精度限制可能导致在原始分辨率下完成的较大半径模糊在图像质量超过一定尺寸时被破坏,而缩减采样的图像质量保持适当的图像质量。

通过调整缩减采样因子以保持缩减采样的模糊半径恒定,您可以在模糊半径增加的情况下实现接近恒定时间的模糊速度。对于自适应阈值,图像质量应足够好,可用于比较。

我在上述链接框架的最新版本中的高斯和框模糊中使用此方法,因此,如果您在 Mac、iOS 或 Linux 上运行,您可以通过尝试其中一个示例应用程序来评估结果。我有一个基于使用此优化的框模糊的自适应阈值操作,因此您可以查看那里的结果是否符合您的要求。

如上所述,它在 GPU 上不会很棒。但是,假设在GPU和CPU之间分流数据的成本更令人不安,它可能仍然值得坚持。

最明显的初步解决方案是按照所讨论的水平/垂直拆分。使用加法混合模式,创建一个绘制整个源图像的四边形,然后例如,对于宽度位图上的水平步长,n发出一个调用,请求绘制四边形n次,第 0 次在 x = 0 次,第 m 次在 x = m 次。然后通过FBO乒乓球,将水平绘制的缓冲区目标切换到垂直的源纹理中。

内存访问可能是 O(n^2((即您可能会缓存得很好,但这很难完全缓解(,因此这是一个相当糟糕的解决方案。您可以通过在带中执行相同的操作来划分和征服来改进它 - 例如,对于垂直步骤,独立地对 8 行的各个行求和,之后最终下方每一行的错误是未能包含该行上的任何总和。因此,执行第二遍来传播这些内容。

然而,在帧缓冲区中累积的一个问题是夹紧以避免溢出——如果你期望积分图像中的任何位置的值大于 255,那么你就不走运了,因为加法混合会夹紧并且GL_RG32I等人不会在 3.0 之前达到 ES。

我能想到的最佳解决方案是,不使用任何特定于供应商的扩展,即拆分源映像的位并在事后合并通道。假设您的源图像是 4 位,并且您的图像在两个方向上都小于 256 像素,您需要在 R、G、B 和 A 通道中各放置一位,执行正常的加法步骤,然后运行快速重新组合着色器作为value = A + (B*2) + (G*4) + (R*8)。如果您的纹理在大小或位深度上更大或更小,请相应地放大或缩小。

(特定于平台的观察:如果你在iOS上,那么你希望已经有一个循环中的CVOpenGLESTextureCache,这意味着你可以CPU和GPU访问相同的纹理存储,所以你可能更愿意从GCD开始这一步。 iOS是支持EXT_shader_framebuffer_fetch的平台之一;如果你可以访问它,那么你可以编写任何你喜欢的旧混合函数,至少放弃组合步骤。此外,您还可以保证在绘制之前已完成前面的几何图形,因此如果每个条带将其总计写入应有的位置以及下面的行,那么您可以执行理想的双像素条带解决方案,而无需中间缓冲区或状态更改(

您尝试执行的操作无法在片段着色器中完成。GPU 本质上与 CPU 非常不同,它们同时并行执行大量指令。因此,OpenGL不对执行顺序做出任何保证,因为硬件在物理上不允许这样做。

因此,除了"无论 GPU 线程块调度程序决定什么"之外,实际上没有任何定义的顺序。

片段是像素,有点像。它们是可能最终出现在屏幕上的像素。如果另一个三角形最终出现在另一个三角形的前面,则先前计算的颜色值将被丢弃。无论之前在颜色缓冲区中的该像素上存储了什么颜色,都会发生这种情况。

至于在GPU上创建求和面积表,我想你可能首先想看看GLSL"计算着色器",它是专门为这种事情制作的。

我认为您可以通过为表中的每一行像素创建一个线程来使其工作,然后让每个线程与上一行相比"滞后"1 个像素。

在伪代码中:

int row_id = thread_id()
for column_index in (image.cols + image.rows):
    int my_current_column_id = column_index - row_id
    if my_current_column_id >= 0 and my_current_column_id < image.width:
        // calculate sums

这种方法的优点是,应该保证所有线程同时执行它们的指令,而不会彼此领先。这在 CUDA 中是有保证的,但我不确定它是否在 OpenGL 计算着色器中。不过,这可能是您的起点。

对于初学者来说,这可能看起来很令人惊讶,但前缀和或 SAT 计算适用于并行化。由于Hensley算法是最直观的理解(也在OpenGL中实现(,因此可以使用更高效的并行方法,请参阅CUDA扫描。Sengupta的论文讨论了并行方法,该方法似乎是具有减少和向下交换阶段的最先进的有效方法。这些是有价值的材料,但它们不会详细进入 OpenGL 着色器实现。最接近的文档是您找到的演示文稿(它指的是 Hensley 出版物(,因为它有一些着色器片段。这是完全可以在带有FBO乒乓球的片段着色器中完成的工作。请注意,FBO 及其纹理需要将内部格式设置为高精度 - GL_RGB32F最好,但我不确定 OpenGL ES 2.0 是否支持它。

最新更新