4

我正在尝试将 Gamma 校正添加到我的渲染引擎中。我有两个问题:

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

为了应用 gamma,GammaTable 获取一个整数像素,将其解包,查找修改后的 gamma 值,并返回重新打包的整数*

/**
 * 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因为它似乎会造成巨大的性能损失。允许解包方法创建一个每次调用的新数组似乎不会影响当前上下文中的性能。

4

2 回答 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 中的 TYPE_RGB 情况下将未使用的元素设置为 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 指令。我相信有些操作适用于打包整数,基本上对打包整数中的每个字节应用相同的操作,而无需解包、计算和重新打包。这可以节省大量时间,但可能会改变您计算 gamma 的方式。好处是它很快——单个 SSE 寄存器可以让您在单个指令中操作 16 个字节,这种并行性值得努力利用。

于 2013-01-13T00:40:57.410 回答
0

另一种方法是使用 OpenGL。(我认为 LWJGL 会在 Java 中允许它。)您可以上传包含直接到伽马校正表的一维纹理,然后编写一个 glsl 着色器,将伽马表应用于您的像素。不确定这是否适合您当前的处理模型,但我一直使用它实时处理 1920x1080 HD RGBA 帧。

于 2013-01-13T01:36:50.283 回答