我正在尝试计算我在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上计算前缀和的常用方法是在两次传递(垂直/水平,如此处讨论的on this thread或or here)中进行,但不是'这里有一个问题,因为每个单元格与前一个(顶部或左侧)有一个数据依赖关系?
我似乎无法理解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尚未被计算)?
另外,据我所知,片段不是像素(如果我没有弄错),如果我使用的话,我在第一遍中的一个纹理中存储的值是否会在另一个传递中相同从顶点着色器传递的相同坐标,还是以某种方式插值?
答案 0 :(得分:5)
Tommy和Bartvbl解决了有关求和区域表的问题,但您的自适应阈值的核心问题可能不需要。
作为我的开源GPUImage框架的一部分,我使用OpenGL ES对optimizing blurs over large radii进行了一些实验。通常,增加模糊半径会导致每个像素的纹理采样和计算显着增加,并伴随着减速。
然而,我发现对于大多数模糊操作,您可以应用令人惊讶的有效优化来限制模糊样本的数量。如果在模糊之前对图像进行下采样,在较小的像素半径(半径/下采样因子)处进行模糊,然后线性上采样,则可以得到模糊的图像,该图像相当于在更大的像素半径处模糊的图像。在我的测试中,这些下采样,模糊,然后上采样的图像看起来几乎与基于原始图像分辨率模糊的图像相同。实际上,精度限制可能导致在原始分辨率下以较大的图像质量破坏大半径模糊,其中下采样的图像保持适当的图像质量。
通过调整下采样因子以使下采样模糊半径保持恒定,可以在模糊半径增加的情况下实现接近恒定时间的模糊速度。对于自适应阈值,图像质量应该足以用于比较。
我在上面链接的框架的最新版本中使用高斯和盒子模糊的方法,所以如果你在Mac,iOS或Linux上运行,你可以通过尝试其中一个来评估结果示例应用程序。我有一个基于使用此优化的盒子模糊的自适应阈值操作,因此您可以查看结果是否符合您的要求。
答案 1 :(得分:3)
根据上述内容,它在GPU上不会很棒。但是假设GPU和CPU之间分流数据的成本更令人不安,那么它仍然值得坚持不懈。
最明显的表面解决方案是如所讨论的那样分割水平/垂直。使用添加剂混合模式,创建一个绘制整个源图像的四边形,然后例如对于宽度为n
的位图上的水平步骤,发出请求四元组的调用n
次,x = 0
的第0次,x = m
的第m次。然后通过FBO乒乓,将水平绘图的缓冲区目标切换为垂直的源纹理。
内存访问可能是O(n ^ 2)(即你可能会很好地缓存,但这不是一个完全缓解)所以这是一个相当差的解决方案。你可以通过在乐队中做同样的事情来划分和征服来改善它 - 例如对于垂直步骤,独立地对8个单独的行进行求和,之后在最终的每一行中的错误是未能包括该行上的任何总和。所以执行第二遍传播。
然而,在帧缓冲区中累积的问题是钳位以避免溢出 - 如果您在积分图像中的任何位置预期值大于255,那么您运气不好,因为加法混合会钳位并{{1在3.0之前不会达到ES。
我可以想到的最佳解决方案是,在不使用任何特定于供应商的扩展的情况下,将源图像的位分开并在事后合并通道。假设您的源图像是4位且图像在两个方向上小于256像素,您将在R,G,B和A通道中各放一位,执行正常的加法步骤,然后运行快速重组着色器{ {1}}。如果纹理的大小或位深度更大或更小,则相应地向上或向下缩放。
(特定于平台的观察:如果您在iOS上,那么您希望循环中已经有GL_RG32I
,这意味着您可以访问同一纹理存储区的CPU和GPU,因此您可能更喜欢将此步骤移至GCD.iOS是支持value = A + (B*2) + (G*4) + (R*8)
的平台之一;如果您可以访问它,那么您可以编写任何您喜欢的旧混合功能,并至少放弃组合步骤。此外,您可以保证在绘制之前已经完成了前面的几何体,所以如果每个条带将其总数写入它应该的位置以及下面的行,则可以执行理想的双像素条带解决方案,没有中间缓冲区或状态更改)
答案 2 :(得分:2)
您尝试执行的操作无法在片段着色器中完成。 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计算着色器中。不过,这可能是你的出发点。
答案 3 :(得分:2)
对于初学者来说可能看起来令人惊讶,但前缀sum或SAT计算适合于并行化。由于Hensley算法最直观易懂(也在OpenGL中实现),因此可以使用更加高效的并行方法,请参阅CUDA scan。来自Sengupta的论文讨论了并行方法,该方法似乎是具有减少和减少交换阶段的最先进的有效方法。这些是有价值的材料,但它们没有详细输入OpenGL着色器实现。最接近的文档是您找到的presentation(它指的是Hensley发布),因为它有一些着色器片段。这是FBO Ping-Pong片段着色器完全可以完成的工作。请注意,FBO及其纹理需要将内部格式设置为高精度 - GL_RGB32F最好,但我不确定OpenGL ES 2.0是否支持它。