Применение гамма-коррекции к упакованному целочисленному пикселю

Я пытаюсь добавить гамма-коррекцию в свой движок рендеринга. У меня две проблемы:

1) Math.pow ОЧЕНЬ медленный (относительно того, что его вызывают тысячи раз в секунду). Поэтому мне нужно будет создать предварительно рассчитанную таблицу гаммы, к которой можно будет получить доступ, вместо расчета на лету. (Это дополнительная информация, а не фактическая проблема).

2) В настоящее время я могу сделать это, только распаковав целые пиксели, применив гамму, заменив каналы RGBA их соответствующими измененными значениями гаммы, а затем переупаковав пиксели и отправив их обратно в буфер изображения. Падение производительности не является ужасным..., но оно снижает фиксированный временной шаг с 60 кадров в секунду примерно до 40 кадров в секунду или около того (при рендеринге нескольких изображений).

Я попытался реализовать целочисленную распаковку/упаковку в собственном коде, но не увидел улучшения производительности и сбоя виртуальной машины (вероятно, ошибки проверки памяти, но сейчас мне все равно, что это исправить).

Есть ли способ применить гамму без распаковки/упаковки пикселей? Если нет, то какой метод вы бы порекомендовали использовать для этого?

Н.Б. Не говорите, что используйте 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 создается с модификатором значения гаммы (0,0–1,0 — темнее, а >1,0 — ярче). Конструктор вызывает внутренний метод, который строит таблицу гаммы для этого значения. Этот метод также используется для сброса гаммы позже:

/**
 * 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, потому что это, казалось, вызывало огромный удар по производительности. создавать новый массив с каждым вызовом, похоже, не влияет на производительность в текущем контексте.


person bgroenks    schedule 12.01.2013    source источник
comment
+1, интересный вопрос. Не могли бы вы показать код, чтобы продемонстрировать, как вы распаковываете/обрабатываете/переупаковываете? Кроме того, это скорее из любопытства, как вы рассчитываете значения гаммы?   -  person GargantuChet    schedule 13.01.2013
comment
Я обновлю пост с обеими частями информации. Спасибо за вопрос.   -  person bgroenks    schedule 13.01.2013


Ответы (2)


Интересно, являются ли математические операции причиной накладных расходов. При каждом вызове 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. Это может быть достаточно легко решено путем передачи типа в качестве параметра или установкой неиспользуемого элемента в 0 в случае TYPE_RGB в unpackInt:

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() выполняется сотни тысяч раз в секунду, но каждый раз, когда цикл выполняется, он выполняется всего несколько итераций. Современный ЦП должен очень хорошо справляться с этим случаем. но, возможно, стоит попробовать.

Вы также можете использовать ограниченный пул потоков для параллельного отображения строк. Привязка одного потока на ядро ​​ЦП может иметь смысл. Дизайн графического оборудования фокусируется на том факте, что операции над каждым пикселем, как правило, похожи, но независимы, и они обеспечивают массивный параллелизм для достижения хорошей производительности.

Также рассмотрите возможность использования отладочной сборки JVM для просмотра в сгенерированных инструкциях для лучшего понимания. В идеале вы должны изменить свой код как можно меньше, внося изменения только там, где JVM упускает возможности для оптимизации.

Если вы в конечном итоге перейдете к нативному коду, вы можете рассмотреть возможность использования некоторых инструкций SSE, где это уместно. Я считаю, что есть операции, которые работают с упакованными целыми числами, в основном применяя одну и ту же операцию к каждому байту в упакованном целом без необходимости распаковывать, вычислять и переупаковывать. Это может сэкономить много времени, но может потребовать изменений в способе расчета гаммы. Преимущество в том, что это быстро — один регистр SSE может позволить вам работать с шестнадцатью байтами в одной инструкции, и такой параллелизм может стоить того, чтобы его использовать.

person GargantuChet    schedule 13.01.2013
comment
Я попробую некоторые из этих предложений и дам вам знать, как они работают. Будет ли создание экземпляра массива действительно вызывать такие большие накладные расходы? - person bgroenks; 13.01.2013
comment
Итак, использование фиксированного массива вместо создания экземпляра вернуло FPS к 50 (просто распаковка и переупаковка int ... без поиска или применения гамма-коррекции). Это все еще 10 кадров в секунду. Поможет ли поместить фиксированный массив в собственный код? Это может потенциально убрать накладные расходы на проверку массива Java, верно? - person bgroenks; 13.01.2013
comment
Чтобы ответить на ваш первый вопрос, современные JVM хорошо справляются с оптимизацией создания множества небольших объектов в общем случае. Но здесь операция делается для каждого пикселя. При низком разрешении, таком как 1024 * 768, это означает 786 432 создания массива на кадр или 47 185 920 операций с пикселями в секунду при 60 кадрах в секунду. На процессоре с тактовой частотой 2 ГГц вам потребуется в среднем больше 42 инструкций на пиксель, чтобы добиться такой частоты кадров. Так что каждая мелочь помогает. - person GargantuChet; 13.01.2013
comment
Обновил ответ, добавив еще несколько мыслей. - person GargantuChet; 13.01.2013
comment
Итак, есть ли поток, работающий над каждой строкой изображения, а затем используйте CyclicBarrier, чтобы предотвратить отрисовку изображения потоком рендеринга до тех пор, пока они не будут выполнены? - person bgroenks; 14.01.2013
comment
Или объект Future, я полагаю. - person bgroenks; 14.01.2013
comment
Я попытался использовать подход многопоточного копирования строк. Он работает и значительно быстрее при прямом рендеринге. Однако, когда я добавляю ColorUtils.packInt(ColorUtils.unpackInt)) в цикл строк рендеринга, он фактически становится ХУЖЕ FPS, чем раньше (~ 40). - person bgroenks; 14.01.2013
comment
Вау, это неожиданно. Убедитесь, что вы не используете synchronized без необходимости. Кроме того, каждый поток получает свой собственный int[] для хранения распакованных значений? Если нет, возможно, между потоками существует конфликт за общий массив. - person GargantuChet; 14.01.2013
comment
По какой-то причине падение производительности исчезает, если вы не вкладываете вызов в место аргумента (например, ColorUTils.packInt(ColorUtils.unpackInt)), это не имеет смысла. - person bgroenks; 15.01.2013

Другой способ сделать это — использовать OpenGL. (Я думаю, что LWJGL разрешит это в Java.) Вы можете загрузить одномерную текстуру, содержащую таблицу с гамма-коррекцией, а затем написать шейдер glsl, который применит таблицу гаммы к вашим пикселям. Не уверен, как это согласуется с вашей текущей моделью обработки, но я постоянно использую его для обработки HD RGBA-кадров 1920x1080 в режиме реального времени.

person user1118321    schedule 13.01.2013
comment
Это блестяще. Вы действительно должны подумать о выборе имени пользователя :-) - person GargantuChet; 13.01.2013
comment
Это интересная идея ... однако я пытался избежать дополнительных библиотек в проекте (и использовать только Java2D ... больно, но полезно для многих вещей). У меня также нет абсолютно никакого опыта написания шейдеров любого типа или использования OpenGL/LWJGL для каких-либо значительных проектов. - person bgroenks; 13.01.2013