什么是在OpenGL ES 2中用于卷积着色器的最佳方法,使其足够快以实时运行?

3

注意:目前我在模拟器中进行测试。但我的想法是在iPhone 4s上获得可接受的性能。(我知道,我应该在设备上进行测试,但我还要等几天才能拿到设备)。

我在尝试制作一个卷积着色器,它可以允许用支持3x3、5x5或7x7的滤波器对图像进行卷积,并具有多次传递的选项。我想这个着色器本身是有效的。但我注意到了以下几点:

  1. 一个简单的盒式滤波器3x3,单次传递,只能轻微地模糊图像。所以为了获得更明显的模糊效果,我必须使用3x3 2次通行或5x5。
  2. 最简单的情况(即3x3,1次传递)已经足够慢,以至于无法在30fps的情况下使用。

到目前为止,我已经尝试了两种方法(这是我为iPhone设计的一些OGLES2插件,所以使用了这些方法):

- (NSString *)vertexShader
{

    return SHADER_STRING
    (
     attribute vec4 aPosition;
     attribute vec2 aTextureCoordinates0;

     varying vec2 vTextureCoordinates0;

     void main(void)
     {
         vTextureCoordinates0 = aTextureCoordinates0;
         gl_Position = aPosition;
     }

     );
}

- (NSString *)fragmentShader
{
    return SHADER_STRING
    (
     precision highp float;

     uniform sampler2D uTextureUnit0;
     uniform float uKernel[49];
     uniform int uKernelSize;
     uniform vec2 uTextureUnit0Offset[49];
     uniform vec2 uTextureUnit0Step;

     varying vec2 vTextureCoordinates0;

     void main(void)
     {
         vec4 outputFragment = texture2D(uTextureUnit0, vTextureCoordinates0 + uTextureUnit0Offset[0] * uTextureUnit0Step) * uKernel[0];
         for (int i = 0; i < uKernelSize; i++) {
             outputFragment += texture2D(uTextureUnit0, vTextureCoordinates0 + uTextureUnit0Offset[i] * uTextureUnit0Step) * uKernel[i];
         }

         gl_FragColor = outputFragment;
     }

     );
}

在这种方法中,过滤器值和提取纹理像素的偏移坐标在客户端/App领域预先计算,并设置为uniforms。然后,着色器程序会随时可用。请注意,uniform数组的大小(49)很大是因为潜在地我可以使用7x7内核。 该方法每次消耗0.46秒。 然后,我尝试了以下方法:
- (NSString *)vertexShader
{

    return SHADER_STRING
    (
     // Default pass-thru vertex shader:
     attribute vec4 aPosition;
     attribute vec2 aTextureCoordinates0;

     varying highp vec2 vTextureCoordinates0;

     void main(void)
     {
         vTextureCoordinates0 = aTextureCoordinates0;
         gl_Position = aPosition;
     }

     );
}

- (NSString *)fragmentShader
{
    return SHADER_STRING
    (
     precision highp float;

     uniform sampler2D uTextureUnit0;
     uniform vec2 uTextureUnit0Step;
     uniform float uKernel[49];
     uniform float uKernelRadius;

     varying vec2 vTextureCoordinates0;

     void main(void)
     {
         vec4 outputFragment = vec4(0., 0., 0., 0.);
         int kRadius = int(uKernelRadius);
         int kSupport  = 2 * kRadius + 1;
         for (int t = -kRadius; t <= kRadius; t++) {
             for (int s = -kRadius; s <= kRadius; s++) {
                 int kernelIndex = (s + kRadius) + ((t + kRadius) * kSupport);
                 outputFragment += texture2D(uTextureUnit0, vTextureCoordinates0 + (vec2(s,t) * uTextureUnit0Step)) * uKernel[kernelIndex];
             }
         }

         gl_FragColor = outputFragment;
     }

     );
}

在这里,我仍然通过uniform将预计算的内核传递到片段着色器中。但是现在,我在着色器中计算像素偏移和内核索引。我期望这种方法会更慢,因为我不仅有两个循环,而且对于每个单独的片段,我还要进行一堆额外的计算。

有趣的是,这种方法只需要0.42秒。 实际上更快了...

此时,我能想到的唯一其他事情就是通过将2D内核视为两个可分离的1D内核,将卷积拆分为两个步骤。还没有尝试过。

仅供比较,并且知道以下示例是盒式滤波的特定实现,A-几乎是硬编码的,B-实际上并不符合经典nxn线性滤波器的理论定义(它不是矩阵,也不加起来等于1),我尝试了OpenGL ES 2.0编程指南中的这种方法:

- (NSString *)fragmentShader
{
    return SHADER_STRING
    (
     // Default pass-thru fragment shader:
     precision mediump float;

     // Input texture:
     uniform sampler2D uTextureUnit0;

     // Texel step:
     uniform vec2 uTextureUnit0Step;


     varying vec2 vTextureCoordinates0;

     void main() {
         vec4 sample0;
         vec4 sample1;
         vec4 sample2;
         vec4 sample3;
         float step = uTextureUnit0Step.x;
         sample0 = texture2D(uTextureUnit0, vec2(vTextureCoordinates0.x - step, vTextureCoordinates0.y - step));
         sample1 = texture2D(uTextureUnit0, vec2(vTextureCoordinates0.x + step, vTextureCoordinates0.y + step));
         sample2 = texture2D(uTextureUnit0, vec2(vTextureCoordinates0.x + step, vTextureCoordinates0.y - step));
         sample3 = texture2D(uTextureUnit0, vec2(vTextureCoordinates0.x - step, vTextureCoordinates0.y + step));
         gl_FragColor = (sample0 + sample1 + sample2 + sample3) / 4.0;
     }
     );
}

这种方法每次处理需要0.06秒。 请注意,上述是我自己修改的内容,其中步骤基本上与我在实现中使用的像素偏移相同。使用这个步骤,结果非常类似于我的实现,但OpenGL指南中的原始着色器使用了更大的步长,会模糊更多。

因此,总的来说,我的问题实际上有两个:

  1. 我将步骤/像素偏移计算为vec2(1 /图像宽度,1 /图像高度)。使用此偏移量,如我所说,3x3的盒状滤波器几乎不可见。这是正确的吗?还是我误解了步骤的计算或其他什么东西?
  2. 除了像OpenGL示例那样简化之外,我是否可以尝试其他任何方法使“一般情况下的卷积”方法足够快以实时运行?还是我必须选择简化的方法?
2个回答

3
如果您在Instruments的OpenGL ES Analysis工具或Xcode的Frame Debugger中运行它们,您可能会看到一个关于依赖纹理读取的提示 - 您正在计算片段着色器中的texcoords,这意味着硬件无法获取纹素数据直到在评估着色器时到达该点。如果纹理坐标在进入片段着色器之前已知,则硬件可以与其他任务并行预取您的纹素数据,以便在片段着色器需要时立即使用。
通过在顶点着色器中预先计算纹理坐标,您可以大大加快速度。Brad Larson在此回答中提供了一个很好的类似问题的示例。

你的意思是只有纹素偏移量还是包括偏移量和原始纹理坐标在内的整个东西? 我尝试在顶点着色器中仅使用偏移量,但没有看到任何改进。在0.06秒内,这仍然不足以实时处理(假设30fps,我每帧大约有1/30的时间来处理)。 - SaldaVonSchwartz
换句话说:如果传递给 texture2D 的 texcoord 参数是在片段着色器中进行的任何计算的结果,它都会很慢。这包括在顶点着色器中计算的偏移量。如果您在顶点着色器中完成所有的texcoord计算,就像我链接的答案中那样,您应该会看到大大提高的性能。 - rickster

0

关于你的具体问题,我没有答案,但你应该看一下GPUImage框架 - 它实现了几个盒状模糊滤镜(参见这个SO问题) - 其中包括一个两遍9x9滤镜 - 你还可以看看这篇文章,比较不同方法的实时FPS:vImage VS GPUImage vs CoreImage


我会看一下这篇文章,谢谢!昨天我确实查看了GPUImage的代码,其中盒状模糊的方法与我上一个例子中从OpenGL ES 2编程指南中获取的方法基本相同。但仍然不够快以支持实时处理 :/ - SaldaVonSchwartz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接