Прилагане на гама корекция към опакован цял пиксел

Опитвам се да добавя гама корекция към моя механизъм за изобразяване. Имам два проблема:

1) Math.pow е НАИСТИНА бавен (в сравнение с това, че се извиква хиляди пъти в секунда). Така че ще трябва да създам предварително изчислена гама таблица, която да бъде достъпна, вместо да изчислявам в движение. (Това е допълнителна информация, а не действителният проблем).

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

Опитах се да внедря целочисленото разопаковане/опаковане в собствения код, само за да не видя подобрение на производителността и да получа VM да се срива (вероятно грешки при проверка на паметта, но не ми се иска да го поправя сега).

Има ли начин да се приложи гамата без разопаковане/опаковане на пиксели? Ако не, какъв метод бихте препоръчали да използвате за това?

N.B. Не казвайте да използвате 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 fps. При 2GHz CPU ще трябва да имате средно по-добро от около 42 инструкции на пиксел, за да постигнете тази честота на кадрите. Така че всяко малко помага. - person GargantuChet; 13.01.2013
comment
Актуализира отговора с още няколко мисли. - person GargantuChet; 13.01.2013
comment
Така че има нишка, работеща върху всеки ред от изображението, след което използвайте CyclicBarrier, за да попречите на нишката за изобразяване да изчертае изображението, докато не свърши? - person bgroenks; 14.01.2013
comment
Или обект от бъдещето, предполагам. - 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.) Можете да качите 1D текстура, съдържаща правилната към гама коригирана таблица, и след това да напишете glsl шейдър, който прилага гама таблицата към вашите пиксели. Не съм сигурен как това ще се впише в текущия ви модел на обработка, но го използвам за обработка на 1920x1080 HD RGBA кадри в реално време през цялото време.

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