在Java中着色像素-需要更快的方法

时间:2018-12-05 14:27:03

标签: java image-processing colors

我正在制作厄运风格的伪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语句中的计算。

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

谢谢

3 个答案:

答案 0 :(得分:2)

并行进行工作

线程不一定是并行化代码的唯一方法,cpus通常具有诸如SIMD之类的指令集,该指令集可让您一次对多个数字计算相同的算术。 GPU采纳了这一想法并将其运行,从而使您可以在数百至数千个数字上并行运行相同的功能。我不知道如何在Java中执行此操作,但是我确信可以通过某种搜索找到可行的方法。

算法-减少工作量

是否可以减少需要调用该函数的时间?每帧调用任何功能一百万次会很痛苦。除非管理每个函数调用的开销(内联它,重用堆栈帧,尽可能缓存结果),否则您将希望减少工作量。

可能的选项可能是:

  • 减小游戏的窗口/分辨率。
  • 使用其他表示形式。当像素是HSV而不是RGB时,您是否正在做很多更容易做的操作?然后仅在要渲染像素时才转换为RGB。
  • 为每个像素使用有限数量的颜色。这样,您就可以提前计算出可能的色彩,而只需查找即可,而不是函数调用。
  • 尽量减少色调。也许有些UI是有色的,不应该是。也许灯光效果只能传播那么远。
    • 作为最后的选择,将着色设置为默认值。如果对像素进行了太多着色,那么“不着色”的发生率可能会大大降低,并且这样做可以获得更好的性能。

性能-(微观)优化代码

如果您可以选择“近似色”,则this SO answer给出应该更便宜地计算的像素亮度(lum)的近似值。 (链接中的公式为Y = 0.33 R + 0.5 G + 0.16 B,可以写成Y =(R + R + B + G + G + G)/ 6。)

下一步是评估您的代码(配置文件是了解Google信息的一个好名词),并查看占用了最多资源的资源。很有可能不是这里的功能,而是另一段代码。或等待纹理加载。

从现在开始,我们将假设问题中提供的功能占用的时间最多。让我们看看它在花时间在做什么。我没有剩下的代码,因此无法对其进行基准测试,但可以编译它并查看生成的字节码。在包含该函数的类上使用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;     double lum =(a x + b y + 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(我认为这是两个昂贵​​的运算),再将其转换为乘法(长整)和a移。

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;
}

答案 1 :(得分:1)

要获得更好的性能,您必须在图像处理过程中摆脱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中)。基本上尝试使用原始数据类型,甚至在原始数据类型中也使用适用的最小数据类型。

答案 2 :(得分:0)

在这一点上,您实际上已经接近金属。我认为您必须更改方法才能真正改善性能,但是一个快速的主意是缓存lum计算。这是像素颜色的简单功能,您的光明不依赖于任何其他东西。如果您缓存它,可以节省大量计算。缓存时,您也可以缓存此calc:

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

我不知道这样是否可以为您节省大量时间,但是从现在看来,从微优化的角度来看,这几乎就是您可以做的一切。

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

如果以上两种想法均无效,我认为您可能需要尝试将颜色计算推到GPU卡上。这是数以百万计的所有裸机数学运算,这是图形卡最出色的表现。不幸的是,这是一个很深的话题,为了选择最佳选择,必须进行大量的教育。这里有一些有趣的事情需要研究:

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