将伽马校正应用于打包的整数像素。

4
我正在尝试向我的渲染引擎添加伽马校正。我遇到了两个问题:
1)Math.pow非常缓慢(相对于每秒调用数千次)。因此,我需要创建一个预先计算的伽马表,可以访问它而不是即时计算。(这是额外的信息,不是实际问题)。
2)目前,我只能通过拆包整数像素、将伽马应用于替换RGBA通道及其相应的修正值,然后重新打包像素并将其发送回图像缓冲区来完成。性能损失并不可怕,但在渲染多幅图像时,它会将稳定的60fps固定时间步长降至约40fps左右。
我尝试在本地代码中实现整数拆包/打包,但发现没有性能提升,并获得VM崩溃(可能是内存检查错误,但我现在并不关心如何解决它)。
有没有一种方法可以在不解包/打包像素的情况下应用伽马?如果没有,您推荐使用什么方法来解决这个问题?
注:不要说使用BufferedImageOp。它很慢,只能操作整个图像(我需要特定像素)。
附加信息:
像素打包:
public static int[] unpackInt(int argb, int type) {
    int[] vals = null;
    int p1 = 0;
    int p2 = 1;
    int p3 = 2;
    int p4 = 3;
    switch (type) {
    case TYPE_RGB:
        vals = new int[3];
        vals[p1] = argb >> 16 & 0xFF;
        vals[p2] = argb >> 8 & 0xFF;
        vals[p3] = argb & 0xFF;
        break;
    case TYPE_RGBA:
    case TYPE_ARGB:
        vals = new int[4];
        vals[p4] = argb & 0xFF;
        vals[p3] = argb >> 8 & 0xFF;
        vals[p2] = argb >> 16 & 0xFF;
        vals[p1] = argb >> 24 & 0xFF;
        break;
    default:
        throw (new IllegalArgumentException(
                "type must be a valid field defined by ColorUtils class"));
    }
    return vals;
}

public static int packInt(int... rgbs) {

    if (rgbs.length != 3 && rgbs.length != 4) {
        throw (new IllegalArgumentException(
                "args must be valid RGB, ARGB or RGBA value."));
    }
    int color = rgbs[0];
    for (int i = 1; i < rgbs.length; i++) {
        color = (color << 8) + rgbs[i];
    }
    return color;
}

之前我撤掉了这段代码,但我使用了这个伽马校正算法:

protected int correctGamma(int pixel, float gamma) {
    float ginv = 1 / gamma;
    int[] rgbVals = ColorUtils.unpackInt(pixel, ColorUtils.TYPE_ARGB);
    for(int i = 0; i < rgbVals.length; i++) {
        rgbVals[i] = (int) Math.round(255 - Math.pow(rgbVals[i] / 255.0, ginv));
    }
    return ColorUtils.packInt(rgbVals);
}

解决方案

我将GargantuChet提出的很多想法结合起来,创建了一个系统,似乎运行得相当不错(没有性能下降)。

一个叫做GammaTable的类被实例化,它具有一个gamma值修改器(0.0-1.0是变暗,>1.0是变亮)。构造函数调用一个内部方法来为这个值建立gamma表。这个方法也可以用于以后重新设置gamma:

/**
 * Called when a new gamma value is set to rebuild the gamma table.
 */
private synchronized void buildGammaTable() {
    table = new int[TABLE_SIZE];
    float ginv = 1 / gamma;
    double colors = COLORS;
    for(int i=0;i<table.length;i++) {
        table[i] = (int) Math.round(colors * Math.pow(i / colors, ginv)); 
    }
}

应用伽马值时,GammaTable会获取一个整数像素,将其解包,查找修改后的伽马值,并返回重新打包的整数。
/**
 * Applies the current gamma table to the given integer pixel.
 * @param color the integer pixel to which gamma will be applied
 * @param type a pixel type defined by ColorUtils
 * @param rgbArr optional pre-instantiated array to use when unpacking.  May be null.
 * @return the modified pixel value
 */
public int applyGamma(int color, int type, int[] rgbArr) {
    int[] argb = (rgbArr != null) ? ColorUtils.unpackInt(rgbArr, color):ColorUtils.unpackInt(color, type);
    for(int i = 0; i < argb.length; i++) {
        int col = argb[i];
        argb[i] = table[col];
    }
    int newColor = ColorUtils.packInt(argb);
    return newColor;
}

每个屏幕像素都会调用applyGamma方法。

*事实证明,解包和重新打包像素并没有减缓任何速度。由于某种原因,嵌套调用(即ColorUtils.packInt(ColorUtils.unpackInt)))导致方法执行时间大大延长。有趣的是,我还必须停止使用预先实例化的数组与ColorUtils.unpackInt一起使用,因为它似乎导致了巨大的性能损失。在当前上下文中,允许解包方法每次调用创建一个新数组似乎不会影响性能。


3
+1,有趣的问题。你可以展示一些代码来演示你如何进行解压/处理/打包吗?此外,纯属出于好奇,你是如何计算伽马值的? - GargantuChet
我会在帖子中更新这两个信息。谢谢你的询问。 - bgroenks
2个回答

3
我想知道是不是数学运算导致了开销。每次调用unpackInt时,您都会创建一个新数组,JVM必须分配并初始化为零。这可能会导致很多堆活动,其实并不需要。
您可以考虑一种方法,使unpackInt将目标数组作为参数传递。首先,使用示例如下:
int[] rgbVals = new int[4];

protected int correctGamma(int pixel, float gamma) {
    float ginv = 1 / gamma;
    ColorUtils.unpackInt(pixel, ColorUtils.TYPE_ARGB, rgbVals);
    for(int i = 0; i &lt; rgbVals.length; i++) {
        rgbVals[i] = (int) Math.round(255 - Math.pow(rgbVals[i] / 255.0, ginv));
    }
    return ColorUtils.packInt(rgbVals);
}

这将大大减少对象创建的开销,因为您只需创建一次新数组,而不是每次调用unpackInt(通过correctGamma)时都创建一次。唯一的注意事项是,在重新打包int时无法再使用数组长度。可以通过将类型作为参数传递给它或在unpackInt中将未使用的元素设置为0来轻松解决此问题:
case TYPE_RGB:
    vals[p1] = 0;
    vals[p2] = argb >> 16 & 0xFF;
    vals[p3] = argb >> 8 & 0xFF;
    vals[p4] = argb & 0xFF;

这也可能是创建一个更专门的伽马矫正类的良好机会,该类封装了所有这些行为:
class ScreenContent {

    // ...

    GammaCorrector gammaCorrector = new GammaCorrector();

    // ...

    int[][] image;

    void correctGamma() {
        for (int[] row : image) {
            for (int i = 0; i &lt; row.length; i++) {
                row[i] = gammaCorrector.correct(row[i], gamma);
            }
        }
    }
}

class GammaCorrector {
    private int[] unpacked = new int[4];

    public int correct(int pixel, float gamma) {
        float ginv = 1 / gamma;
        ColorUtils.unpackInt(pixel, ColorUtils.TYPE_ARGB, unpacked);
        for(int i = 0; i &lt; rgbVals.length; i++) {
            rgbVals[i] = (int) Math.round(255 - Math.pow(unpacked[i] / 255.0, ginv));
        }
        return ColorUtils.packInt(unpacked);
    }
}

你可以通过创建类似于struct的类来存储解包后的值,以此消除数组和循环。内部的for()循环每秒执行数十万次,但每次循环只运行几次迭代。现代CPU应该能够很好地处理这种情况,但仍然值得尝试。
你还可以使用有界线程池并行处理图像行。每个CPU核心一个线程可能是有意义的选择。图形硬件设计侧重于每个像素操作通常相似但独立,并利用大规模并行性以实现良好的性能。
同时考虑使用JVM的调试版本来查看生成的指令以获得更好的洞察力。理想情况下,您只需要在JVM错过了优化机会的地方进行尽可能少的代码修改。
如果最终转向本地代码,您可以考虑在适当的情况下使用一些SSE指令。我相信有一些操作适用于打包整数,基本上对打包整数中的每个字节应用相同的操作,而无需解包、计算和重新打包。这可以节省大量时间,但可能需要更改计算伽马的方式。好处是它很快--一个SSE寄存器可以让您在单个指令中操作十六个字节,这种并行性值得利用。

我会尝试一些这些建议,并让您知道它们的效果如何。数组实例化真的会造成那么多开销吗? - bgroenks
好的,所以使用固定数组而不是实例化将FPS提高到50(只需解包和重新打包int...没有伽马校正查找或应用)。这仍然是10fps的影响。把固定数组放在本地代码中会有帮助吗?这可能会消除Java数组检查开销,对吧? - bgroenks
更新了答案并加入了一些想法。 - GargantuChet
那么可以让每一行都有一个线程在工作,然后使用循环障栅(CyclicBarrier)来防止渲染线程在它们完成之前绘制图像? - bgroenks
由于某种原因,如果您不将调用嵌套在参数位置中(即ColorUTils.packInt(ColorUtils.unpackInt)),性能下降就会消失,这是没有道理的。 - bgroenks
显示剩余4条评论

0

另一种方法是使用OpenGL。(我认为LWJGL可以在Java中实现。)您可以上传一个包含直接到伽马校正表的1D纹理,然后编写一个glsl着色器将伽马表应用于像素。不确定它如何与您当前的处理模型配合,但我经常使用它来实时处理1920x1080高清RGBA帧。


太棒了。你真的应该考虑选择一个用户名 :-) - GargantuChet
这是一个有趣的想法...然而我一直在尝试避免在项目中使用额外的库(只使用Java2D...虽然很麻烦但对许多事情很有用)。我也完全没有编写任何类型的着色器或在任何重要项目中使用OpenGL/LWJGL的经验。 - bgroenks

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