【问题标题】:Tinting pixels in Java - Need a faster method在 Java 中着色像素 - 需要一种更快的方法
【发布时间】:2019-05-07 03:40:04
【问题描述】:

我正在制作一款末日风格的伪 3D 游戏。 世界被逐个像素地渲染成一个缓冲的图像,稍后显示在 JPanel 上。我想保留这种方法,以便更轻松地照亮单个像素。

我希望能够将游戏中的纹理着色为多种不同的颜色。 为整个纹理着色并将其存储在单独的缓冲图像中对于我的目的而言需要太多时间和内存。所以我在渲染阶段对纹理的每个像素进行着色。

我遇到的问题是为每个像素着色非常昂贵。当一堵无色墙覆盖整个屏幕时,我的速度约为 65 fps。当彩色墙覆盖屏幕时,我会获得 30 fps。

这是我为像素着色的函数:

//Change the color of the pixel using its brightness.
public static int tintABGRPixel(int pixelColor, Color tintColor) {
    //Calculate the luminance. The decimal values are pre-determined.
    double lum = ((pixelColor>>16 & 0xff) * 0.2126 +
                 (pixelColor>>8 & 0xff) * 0.7152 +
                 (pixelColor & 0xff) * 0.0722) / 255;

    //Calculate the new tinted color of the pixel and return it.
    return ((pixelColor>>24 & 0xff) << 24) |
           ((int)(tintColor.getBlue()*lum) & 0xff) |
           (((int)(tintColor.getGreen()*lum) & 0xff) << 8) |
           (((int)(tintColor.getRed()*lum) & 0xff) << 16);
}

抱歉,代码难以辨认。此函数计算原始像素的亮度,将新颜色乘以亮度,然后将其转换回 int。

它只包含简单的操作,但在最坏的情况下,该函数每帧调用多达一百万次。瓶颈是return语句中的计算。

有没有更有效的方法来计算新颜色? 如果我改变我的方法会更好吗?

谢谢

【问题讨论】:

  • 使用矩阵乘法库可能有助于加快速度。如果您还没有,还可以并行化代码
  • 我尝试在问题上抛出线程,但没有帮助。我将研究矩阵乘法。但考虑到我的渲染算法的工作原理,我认为我只能处理 1x800 矩阵。可能不值得。
  • 为什么不在 GPU 上渲染?这应该是微不足道的。

标签: java image-processing colors


【解决方案1】:

并行工作

线程不一定是并行化代码的唯一方法,cpus 通常具有指令集,例如 SIMD,它允许您一次对多个数字计算相同的算术。 GPU 采用了这个想法并与​​之一起运行,允许您在数百到数千个数字上并行运行相同的函数。我不知道如何在 java 中做到这一点,但我确信通过谷歌搜索可以找到一种有效的方法。

算法 - 做更少的工作

是否可以减少需要调用函数的时间?每帧调用任何函数一百万次都会受到伤害。除非每个函数调用的开销都得到管理(内联它、重用堆栈帧、尽可能缓存结果),否则你会希望做更少的工作。

可能的选择是:

  • 缩小游戏的窗口/分辨率。
  • 使用不同的表示。当像素是 HSV 而不是 RGB 时,您是否正在做很多更容易做的操作?然后仅在您即将渲染像素时转换为 RGB。
  • 为每个像素使用有限数量的颜色。这样一来,您就可以提前计算出可能的色调,并且只需查找即可,而不是函数调用。
  • 尽可能少地着色。也许有一些用户界面是有色的,不应该是。也许灯光效果只能传播这么远。
    • 作为最后的手段,将着色设为默认值。如果对像素进行如此多的着色,那么“不着色”的发生可能会少得多,并且您可以通过这样做获得更好的性能。

性能 - (微)优化代码

如果您可以满足于“近似色调”this SO answer 会给出一个像素的亮度 (lum) 近似值,该像素的计算成本应该更低。 (来自链接的公式是 Y = 0.33 R + 0.5 G + 0.16 B,可以写成 Y = (R+R+B+G+G+G)/6。)

下一步是衡量您的代码(配置文件是谷歌搜索的一个很好的术语)并查看占用最多资源的内容。很可能这里不是这个函数,而是另一段代码。或者等待纹理加载。

从这一点开始,我们将假设问题中提供的功能占用的时间最多。让我们看看它在做什么。我没有你的其余代码,所以我无法对其进行基准测试,但我可以编译它并查看生成的字节码。在包含函数的类上使用 javap 我得到以下内容(字节码已被剪切,有重复)。

public static int tintABGRPixel(int, Color);
    Code:
       0: iload_0
       1: bipush        16
       3: ishr
       4: sipush        255
       7: iand
       8: i2d
       9: ldc2_w        #2                  // double 0.2126d
      12: dmul
      13: iload_0
      ...
      37: dadd
      38: ldc2_w        #8                  // double 255.0d
      41: ddiv
      42: dstore_2
      43: iload_0
      44: bipush        24
      46: ishr
      47: sipush        255
      50: iand
      51: bipush        24
      53: ishl
      54: aload_1
      55: pop
      56: invokestatic  #10                 // Method Color.getBlue:()I
      59: i2d
      60: dload_2
      61: dmul
      62: d2i
      63: sipush        255
      66: iand
      67: ior
      68: aload_1
      69: pop
      ...
      102: ireturn

一开始这看起来很吓人,但 java 字节码很好,因为您可以将每一行(或指令)与函数中的某个点相匹配。它没有做任何疯狂的事情,比如重写或矢量化或任何使其无法识别的事情。

查看更改是否有所改进的一般方法是测量前后的代码。有了这些知识,您就可以决定是否值得保留更改。一旦性能足够好,就停止。

我们的穷人剖析是查看每条指令,并查看(根据在线资料,平均而言)它的成本是多少。这有点幼稚,因为每条指令执行所需的时间可能取决于许多因素,例如运行它的硬件、计算机上的软件版本以及它周围的指令。

我没有每条指令的时间成本的完整列表,因此我将采用一些启发式方法。

  • 整数运算比浮点运算快。
  • 常量比本地内存快,比全局内存快。
  • 2 的幂可以实现强大的优化。

我盯着字节码看了一会儿,我只注意到从第 8 到 42 行有很多浮点运算。这部分代码计算出 lum (亮度)。除此之外,没有什么特别突出的,所以让我们用我们的第一个启发式来重写代码。如果你不关心解释,我会在最后提供最终代码。

让我们只考虑函数结束时蓝色(我们将标记为 B)将是什么。这些更改也将适用于红色和绿色,但为简洁起见,我们将忽略它们。

double lum = ((pixelColor>>16 & 0xff) * 0.2126 +
             (pixelColor>>8 & 0xff) * 0.7152 +
             (pixelColor & 0xff) * 0.0722) / 255;
...
... | ((int)(tintColor.getBlue()*lum) & 0xff) | ...

这可以重写为 int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff); 双a = 0.2126,b = 0.7152,c = 0.0722; 双亮度 = (ax + by + c*z) / 255; int B = (int)(tintColor.getBlue()*lum) & 0xff;

我们不想做那么多浮点运算,所以让我们做一些因式分解。这个想法是0.2126可以写成2126/10000。

int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z;
double temp = (double)(tintColor.getBlue() * top) / 10000 / 255;
int B = (int)temp & 0xff;

所以现在我们执行三个整数乘法 (imul) 而不是三个 dmul。成本是一个额外的浮动除法,仅此一项可能不值得。但是我们可以通过结合两个连续的划分来解决这个问题。我们还可以通过将强制转换和除法移动到一行来设置代码以进行更多优化。

int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z);
int temp = (int)((double)(tintColor.getBlue()*top) / 2550000);
int B = temp & 0xff;

这可能是一个停下来的好地方。但是,如果您需要从这个函数中挤出一点点更多的性能,我们可以优化除以一个常量并将一个 double 转换为一个 int(我认为这是两个昂贵​​的操作)到一个乘法(通过一个 long)和一个换班。

int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z;
int Btemp = (int)(( * top * 1766117501L) >> 52);
int B = temp & 0xff;

当我用 clang 编译代码的 c++ 版本时,魔术数字是两个。我无法解释如何产生这种魔法,但就我测试过的 x、y、z 和 tintColor.getBlue() 的几个值而言,它是有效的。在测试时,我假设所有值都在 0 到 256 的范围内,我只尝试了几个示例。

最终代码如下。请注意,这没有经过很好的测试,并且可能有我错过的边缘情况,所以如果有任何错误,请告诉我。希望它足够快。

public static int tintABGRPixel(int pixelColor, Color tintColor) {
    //Calculate the luminance. The decimal values are pre-determined.
    int x = pixelColor>>16 & 0xff, y = pixelColor>>8 & 0xff, z = pixelColor & 0xff;
    int top = 2126*x + 7252*y + 722*z;
    int Btemp = (int)((tintColor.getBlue() * top * 1766117501L) >> 52);
    int Gtemp = (int)((tintColor.getGreen() * top * 1766117501L) >> 52);
    int Rtemp = (int)((tintColor.getRed() * top * 1766117501L) >> 52);

    //Calculate the new tinted color of the pixel and return it.
    return ((pixelColor>>24 & 0xff) << 24) | Btemp & 0xff | (Gtemp & 0xff) << 8 | (Rtemp & 0xff) << 16;
}

【讨论】:

  • 很好的答案,如果你能在最后总结一个列表中的建议会更好,比如缓存、内联方法、近似而不是绝对、避免浮动等。
  • 这太棒了!我也会尝试使用同样的方法来优化我的照明。
  • 我真的很喜欢使用有限调色板的想法。结合预先计算的每个像素发光的查找表将是完美的。
  • 我发现了一个边缘情况,即着色像素超出范围(例如,将白色像素着色为白色。)我通过将您的幻数更改为 1755488566L 解决了这个问题。现在它完美地映射了从 0 到 255 的颜色。
  • 我不认为一个字节的范围可以像您在回答中所说的那样从 0 到 256。但即使是这样,在某些情况下,神奇的数字也会产生 257。
【解决方案2】:

为了获得更好的性能,您必须在图像处理期间摆脱像 Color 这样的对象,如果您知道一个方法将被调用一百万次(image.width * image.height 次),那么最好内联这个方法.一般来说 JVM 可能会内联这个方法本身,但你不应该冒险。

您可以使用PixelGrabber 将所有像素放入一个数组中。这是一般用法

final int[] pixels = new int[width * height];
final PixelGrabber pixelgrabber = new PixelGrabber(image, 0, 0, width, height, pixels, 0, 0);

for(int i = 0; i < height; i++) {
    for(int j = 0; j < width; j++) {
        int p = pixels[i * width + j]; // same as image.getRGB(j, i);

        int alpha = ( ( p >> 24) & 0xff );
        int red = ( ( p >> 16) & 0xff );
        int green = ( ( p >> 8) & 0xff );
        int blue = ( p  & 0xff );

        //do something i.e. apply luminance
    }
}

以上只是如何迭代行和列索引的示例,但是在您的情况下不需要嵌套循环。这应该会合理地提高性能。

这也可以很容易地使用 Java 8 流进行并行化,但是在处理图像时使用流之前要小心,因为流比普通的旧循环慢得多。

您还可以尝试在适用的情况下将int 替换为byte(即不需要将单个颜色分量存储在int 中)。基本上尝试使用原始数据类型,甚至在原始数据类型中使用适用的最小数据类型。

【讨论】:

  • 我已经在使用类似于 PixelGrabber 的东西了。我检索并存储每个图像的原始数据以避免 getRGB 和 setRGB link。我认为他们在性能上会相似。我将颜色转换为 int,因为我将它写入 TYPE_INT_RGB 缓冲区。但是我可以通过切换到 TYPE_3BYTE_BGR 来专门使用字节。
  • Streams 听起来在这里会有所帮助。但我不确定如何将它集成到我的程序中。我正在考虑存储每个像素应该着色的颜色的缓冲区。然后在渲染帧之后,我会使用流一次为所有必要的像素着色。听起来对吗?
  • 您可以在必须对所有像素应用操作时使用流。只有与并行使用它才会提供更好的性能。即List.of(pixels).stream().parallel().forEach(pixel -&gt; { //tint pixel here});
  • spyr03 提到的一些选项非常棒,你也可以试试。 (近似色调,使用整数运算而不是浮点数)。我认为最好的是缓存结果,每个颜色分量只有255值,所以可以缓存,不需要计算red * lum一百万次,它可以缓存,需要仅计算 255 次。
  • 我假设lum 是一个常量,但事实并非如此,red*lum 不能被缓存。
【解决方案3】:

在这一点上,您在这个计算上真的很接近金属。我认为你必须改变你的方法才能真正改善事情,但一个快速的想法是缓存 lum 计算。这是像素颜色的一个简单函数,你的亮度不依赖于任何东西。如果你缓存它可以为你节省很多计算。在缓存时,您也可以缓存此计算:

((pixelColor>>24 & 0xff) << 24)

我不知道这是否会为您节省大量时间,但我认为目前从微优化的角度来看,这几乎是您所能做的。

现在您可以重构像素循环以使用并行性,并在 CPU 上并行执行这些像素计算,这也可能为您的下一个想法做好准备。

如果上述想法都不起作用,我认为您可能需要尝试将颜色计算推到 GPU 卡上。这都是必须发生数百万次的裸机数学,这是显卡最擅长的。不幸的是,这是一个深刻的话题,必须进行大量教育才能选择最佳选择。以下是一些值得研究的有趣事物:

我知道其中一些是巨大的框架,这不是您所要求的。但它们可能包含其他相对未知的库,您可以使用这些库将这些数学计算推送到 GPU。 @Parrallel 注释看起来可能是最有用的或 JavaCL 绑定。

【讨论】:

  • 我研究过使用 GPU 进行数字运算。我找到的图书馆是rootbeer1。但是使用 GPU 会带来很多开销。 github页面上的文档把我吓跑了。我不确定仅将着色和照明推送到 GPU 是否值得。而且我不想花很多时间来实施它来找出答案。
  • 我明白了。这就是为什么我在其他两个缓存想法之前将其作为最后一个选项提供的原因。我知道改变它会很重要。
猜你喜欢
  • 2017-12-12
  • 1970-01-01
  • 1970-01-01
  • 2022-01-13
  • 2017-03-05
  • 1970-01-01
  • 1970-01-01
  • 2016-04-02
  • 2020-10-11
相关资源
最近更新 更多