2011-01-18 19 views
8

Mam aplikację, w której użytkownik powinien móc modyfikować obraz za pomocą suwaków, aby uzyskać odcień, nasycenie i lekkość. Całe przetwarzanie obrazu odbywa się na GPU przy użyciu shaderów fragmentów GLSL.Dopasowania obrazu HSL na GPU

Moim problemem jest to, że RGB -> HSL -> Konwersje RGB są dość kosztowne w Gpu ze względu na rozległe rozgałęzienia.

Moje pytanie brzmi, czy mogę przekonwertować "dostosowanie kolorów" użytkowników do innej przestrzeni kolorów, która może bardziej wydajnie obliczyć dopasowany obraz na GPU.

Odpowiedz

8

Dla jasności i nasycenia można użyć YUV(actually YCbCr). Łatwo jest przekonwertować z RGB iz powrotem. Nie potrzeba rozgałęzień. Nasycenie jest kontrolowane przez zwiększenie lub zmniejszenie wartości Cr i Cb. Lekkość jest Y.

Otrzymujesz coś podobnego do modyfikacji HSL hue poprzez obracanie komponentów Cb i Cr (to praktycznie wektor 3D), ale oczywiście to zależy od twojej aplikacji, jeśli to wystarczy.

alt text

Edycja: Składnik koloru (CB, Cr) jest to punkt w płaszczyźnie kolor jak powyżej. Jeśli weźmiesz dowolny punkt losowy i obrócisz go wokół środka, wynik zmieni się. Ale ponieważ mechanizm jest nieco inny niż w HSL, wyniki nie są dokładnie takie same.

Image jest własnością publiczną z Wikipedii.

+0

Świetna odpowiedź, jednak nadal nie jestem pewien, jak podobne "podobne" dotyczące modyfikacji odcienia. – ronag

+1

Dodałem wyjaśnienie dotyczące problemu odcień. – Virne

1

Można użyć Tabeli przeglądowej 3D do przechowywania transformacji kolorów, tabela byłaby aktualizowana przez zmienne użytkownika, ale może być prostszy approches.

Więcej informacji dostępnych jest pod adresem GPU Gems 2.

1

Wierzę, że konwersja między RGB i HSV/HSL może być zakodowana bez rozgałęziania. Na przykład, jak konwersji RGB -> HSV bez rozgałęzień może wyglądać w GLSL:

vec3 RGBtoHSV(float r, float g, float b) { 
    float minv, maxv, delta; 
    vec3 res = vec3(0.0); 

    minv = min(min(r, g), b); 
    maxv = max(max(r, g), b); 
    res.z = maxv; 
    delta = maxv - minv; 

    // branch1 maxv == 0.0 
    float br1 = 1.0 - abs(sign(maxv)); 
    res.y = mix(delta/maxv, 0.0, br1); 
    res.x = mix(res.x, -1.0, br1); 

    // branch2 r == maxv 
    float br2 = abs(sign(r - maxv)); 
    float br2_or_br1 = max(br2,br1); 
    res.x = mix((g - b)/delta, res.x, br2_or_br1); 

    // branch3 g == maxv 
    float br3 = abs(sign(g - maxv)); 
    float br3_or_br1 = max(br3,br1); 
    res.x = mix(2.0 + (b - r)/delta, res.x, br3_or_br1); 

    // branch4 r != maxv && g != maxv 
    float br4 = 1.0 - br2*br3; 
    float br4_or_br1 = max(br4,br1); 
    res.x = mix(4.0 + (r - g)/delta, res.x, br4_or_br1); 

    res.x = mix(res.x * 60.0, res.x, br1); 

    // branch5 res.x < 0.0 
    float br5 = clamp(sign(res.x),-1.0,0.0) + 1.0; 
    float br5_or_br1 = max(br5,br1); 
    res.x = mix(res.x + 360.0, res.x, br5_or_br1); 

    return res; 
} 

Ale nie wcześniej odwzorować to rozwiązanie. Może się zdarzyć, że niektóre zyski, które wygrywamy bez rozgałęziania się tutaj, mogą zostać zrekompensowane przez utratę wydajności nadmiarowego wykonywania kodu. Potrzebne są tak obszerne testy ...

+0

To nie działa dla mnie i narzeka na niezainicjowany res.x. – kaoD

+0

Inicjalizacja 'res' stała. Co masz na myśli mówiąc "to nie działa"? –

+0

Cały obraz jest podbarwiony na czerwono i ustawienie sinusoidy na odcień po prostu nie działa. Inne implementacje działały dobrze, ale byłem szczególnie zainteresowany wydajnością tego. Co może być przyczyną tego? Czy mogę coś pomóc w debugowaniu? – kaoD

3

Miałem to samo pytanie, ale znalazłem bardzo proste rozwiązanie, które pasuje do moich potrzeb, być może również przydatne dla ciebie. Nasycenie koloru jest w zasadzie rozprzestrzenione, uważam, że jest to odległość euklidesowa między wartościami RGB a ich średnią. Niezależnie od tego, jeśli po prostu weźmiemy średnią z maksymalnej i minimalnej wartości RGB i skalujemy kolory względem tej osi, efektem jest bardzo przyzwoity wzrost (lub spadek) nasycenia.

w cieniującego GLSL byłoby napisać:

float pivot=(min(min(color.x, color.y), color.z)+max(max(color.x, color.y), color.z))/2.0; 
color.xyz -= vec3(pivot); 
color.xyz *= saturationScale; 
color.xyz += vec3(pivot); 
11

Błędem jest założenie, że rozgałęzienia w GPU i rozgałęzienia w kodzie to samo.

Dla prostych warunków nie ma żadnego rozgałęzienia. Procesory graficzne mają warunkowe instrukcje przenoszenia, które są bezpośrednio tłumaczone na wyrażenia potrójne i proste instrukcje if-else.

Tam, gdzie rzeczy stają się problematyczne, gdy masz zagnieżdżone warunkowe lub wiele warunkowo zależnych operacji. Następnie należy rozważyć, czy kompilator GLSL jest wystarczająco inteligentny, aby przetłumaczyć go na cmove. Gdy tylko jest to możliwe, kompilator będzie emitował kod, który wykonuje wszystkie gałęzie i rekombinuje wynik za pomocą ruchów warunkowych, ale nie zawsze może to zrobić.

Musisz wiedzieć, kiedy jej pomóc. Nigdy nie zgaduj, kiedy możesz zmierzyć - użyj procesora AMD GPU Shader Analyzer lub GCG firmy Nvidia, aby wyświetlić dane wyjściowe zespołu. Zestaw instrukcji GPU jest bardzo ograniczony i uproszczony, więc nie bój się słowa "montaż".

Oto para funkcji konwersji RGB/HSL, które zmieniłem, dzięki czemu dobrze się bawią z kompilatorem GLSL AMD, a także z wyjściami zespołu. Podziękowania dla Paula Bourke'a za oryginalny kod konwersji C.

// HSL range 0:1 
vec4 convertRGBtoHSL(vec4 col) 
{ 
    float red = col.r; 
    float green = col.g; 
    float blue = col.b; 

    float minc = min3(col.r, col.g, col.b); 
    float maxc = max3(col.r, col.g, col.b); 
    float delta = maxc - minc; 

    float lum = (minc + maxc) * 0.5; 
    float sat = 0.0; 
    float hue = 0.0; 

    if (lum > 0.0 && lum < 1.0) { 
     float mul = (lum < 0.5) ? (lum) : (1.0-lum); 
     sat = delta/(mul * 2.0); 
    } 

    vec3 masks = vec3(
     (maxc == red && maxc != green) ? 1.0 : 0.0, 
     (maxc == green && maxc != blue) ? 1.0 : 0.0, 
     (maxc == blue && maxc != red) ? 1.0 : 0.0 
    ); 

    vec3 adds = vec3(
       ((green - blue)/delta), 
     2.0 + ((blue - red )/delta), 
     4.0 + ((red - green)/delta) 
    ); 

    float deltaGtz = (delta > 0.0) ? 1.0 : 0.0; 

    hue += dot(adds, masks); 
    hue *= deltaGtz; 
    hue /= 6.0; 

    if (hue < 0.0) 
     hue += 1.0; 

    return vec4(hue, sat, lum, col.a); 
} 

montażowa dla tej funkcji:

1 x: MIN   ____, R0.y, R0.z  
    y: ADD   R127.y, -R0.x, R0.z  
    z: MAX   ____, R0.y, R0.z  
    w: ADD   R127.w, R0.x, -R0.y  
    t: ADD   R127.x, R0.y, -R0.z  
2 y: MAX   R126.y, R0.x, PV1.z  
    w: MIN   R126.w, R0.x, PV1.x  
    t: MOV   R1.w, R0.w  
3 x: ADD   R125.x, -PV2.w, PV2.y  
    y: SETE_DX10 ____, R0.x, PV2.y  
    z: SETNE_DX10 ____, R0.y, PV2.y  
    w: SETE_DX10 ____, R0.y, PV2.y  
    t: SETNE_DX10 ____, R0.z, PV2.y  
4 x: CNDE_INT R123.x, PV3.y, 0.0f, PV3.z  
    y: CNDE_INT R125.y, PV3.w, 0.0f, PS3  
    z: SETNE_DX10 ____, R0.x, R126.y  
    w: SETE_DX10 ____, R0.z, R126.y  
    t: RCP_e  R125.w, PV3.x  
5 x: MUL_e  ____, PS4,  R127.y  
    y: CNDE_INT R123.y, PV4.w, 0.0f, PV4.z  
    z: ADD/2  R127.z, R126.w, R126.y  VEC_021 
    w: MUL_e  ____, PS4,  R127.w  
    t: CNDE_INT R126.x, PV4.x, 0.0f, 1065353216  
6 x: MUL_e  ____, R127.x, R125.w  
    y: CNDE_INT R123.y, R125.y, 0.0f, 1065353216  
    z: CNDE_INT R123.z, PV5.y, 0.0f, 1065353216  
    w: ADD   ____, PV5.x, (0x40000000, 2.0f).y  
    t: ADD   ____, PV5.w, (0x40800000, 4.0f).z  
7 x: DOT4  ____, R126.x, PV6.x  
    y: DOT4  ____, PV6.y, PV6.w  
    z: DOT4  ____, PV6.z, PS6  
    w: DOT4  ____, (0x80000000, -0.0f).x, 0.0f  
    t: SETGT_DX10 R125.w, 0.5,  R127.z  
8 x: ADD   R126.x, PV7.x, 0.0f  
    y: SETGT_DX10 ____, R127.z, 0.0f  
    z: ADD   ____, -R127.z, 1.0f  
    w: SETGT_DX10 ____, R125.x, 0.0f  
    t: SETGT_DX10 ____, 1.0f, R127.z  
9 x: CNDE_INT R127.x, PV8.y, 0.0f, PS8  
    y: CNDE_INT R123.y, R125.w, PV8.z, R127.z  
    z: CNDE_INT R123.z, PV8.w, 0.0f, 1065353216  
    t: MOV   R1.z, R127.z  
10 x: MOV*2  ____, PV9.y  
    w: MUL   ____, PV9.z, R126.x  
11 z: MUL_e  R127.z, PV10.w, (0x3E2AAAAB, 0.1666666716f).x  
    t: RCP_e  ____, PV10.x  
12 x: ADD   ____, PV11.z, 1.0f  
    y: SETGT_DX10 ____, 0.0f, PV11.z  
    z: MUL_e  ____, R125.x, PS11  
13 x: CNDE_INT R1.x, PV12.y, R127.z, PV12.x  
    y: CNDE_INT R1.y, R127.x, 0.0f, PV12.z 

Zauważ, że nie istnieją żadne instrukcje rozgałęzienia. Są to ruchy warunkowe, dokładnie tak, jak je napisałem.

Sprzęt potrzebny do wykonania ruchu warunkowego to po prostu komparator binarny (5 bramek na bit) i kilka śladów. Bardzo szybki.

Kolejną ciekawostką jest brak podziałów. Zamiast tego kompilator użył przybliżonej instrukcji odwrotnej i wielokrotnej. Robi to dla operacji sqrt również przez długi czas. Możesz pobrać te same triki na procesorze za pomocą (na przykład) instrukcji SSC rcpps i rsqrtps.

Teraz operacja odwrotna:

// HSL [0:1] to RGB [0:1] 
vec4 convertHSLtoRGB(vec4 col) 
{ 
    const float onethird = 1.0/3.0; 
    const float twothird = 2.0/3.0; 
    const float rcpsixth = 6.0; 

    float hue = col.x; 
    float sat = col.y; 
    float lum = col.z; 

    vec3 xt = vec3(
     rcpsixth * (hue - twothird), 
     0.0, 
     rcpsixth * (1.0 - hue) 
    ); 

    if (hue < twothird) { 
     xt.r = 0.0; 
     xt.g = rcpsixth * (twothird - hue); 
     xt.b = rcpsixth * (hue  - onethird); 
    } 

    if (hue < onethird) { 
     xt.r = rcpsixth * (onethird - hue); 
     xt.g = rcpsixth * hue; 
     xt.b = 0.0; 
    } 

    xt = min(xt, 1.0); 

    float sat2 = 2.0 * sat; 
    float satinv = 1.0 - sat; 
    float luminv = 1.0 - lum; 
    float lum2m1 = (2.0 * lum) - 1.0; 
    vec3 ct  = (sat2 * xt) + satinv; 

    vec3 rgb; 
    if (lum >= 0.5) 
     rgb = (luminv * ct) + lum2m1; 
    else rgb = lum * ct; 

    return vec4(rgb, col.a); 
} 

(edycja 05/Lipiec/2013. Popełniłem błąd przy tłumaczeniu tej funkcji orignally Zespół został również zaktualizowany).

montażowa:

1 x: ADD   ____, -R2.x, 1.0f  
    y: ADD   ____, R2.x, (0xBF2AAAAB, -0.6666666865f).x  
    z: ADD   R0.z, -R2.x, (0x3F2AAAAB, 0.6666666865f).y  
    w: ADD   R0.w, R2.x, (0xBEAAAAAB, -0.3333333433f).z  
2 x: SETGT_DX10 R0.x, (0x3F2AAAAB, 0.6666666865f).x, R2.x  
    y: MUL   R0.y, PV2.x, (0x40C00000, 6.0f).y  
    z: MOV   R1.z, 0.0f  
    w: MUL   R1.w, PV2.y, (0x40C00000, 6.0f).y  
3 x: MUL   ____, R0.w, (0x40C00000, 6.0f).x  
    y: MUL   ____, R0.z, (0x40C00000, 6.0f).x  
    z: ADD   R0.z, -R2.x, (0x3EAAAAAB, 0.3333333433f).y  
    w: MOV   ____, 0.0f  
4 x: CNDE_INT R0.x, R0.x, R0.y, PV4.x  
    y: CNDE_INT R0.y, R0.x, R1.z, PV4.y  
    z: CNDE_INT R1.z, R0.x, R1.w, PV4.w  
    w: SETGT_DX10 R1.w, (0x3EAAAAAB, 0.3333333433f).x, R2.x  
5 x: MUL   ____, R2.x, (0x40C00000, 6.0f).x  
    y: MUL   ____, R0.z, (0x40C00000, 6.0f).x  
    z: ADD   R0.z, -R2.y, 1.0f  
    w: MOV   ____, 0.0f  
6 x: CNDE_INT R127.x, R1.w, R0.x, PV6.w  
    y: CNDE_INT R127.y, R1.w, R0.y, PV6.x  
    z: CNDE_INT R127.z, R1.w, R1.z, PV6.y  
    w: ADD   R1.w, -R2.z, 1.0f  
7 x: MULADD  R0.x, R2.z, (0x40000000, 2.0f).x, -1.0f  
    y: MIN*2  ____, PV7.x, 1.0f  
    z: MIN*2  ____, PV7.y, 1.0f  
    w: MIN*2  ____, PV7.z, 1.0f  
8 x: MULADD  R1.x, PV8.z, R2.y, R0.z  
    y: MULADD  R127.y, PV8.w, R2.y, R0.z  
    z: SETGE_DX10 R1.z, R2.z,   0.5  
    w: MULADD  R0.w, PV8.y, R2.y, R0.z  
9 x: MULADD  R0.x, R1.w, PV9.x, R0.x  
    y: MULADD  R0.y, R1.w, PV9.y, R0.x  
    z: MUL   R0.z, R2.z, PV9.y  
    w: MULADD  R1.w, R1.w, PV9.w, R0.x  
10 x: MUL   ____, R2.z, R0.w  
    y: MUL   ____, R2.z, R1.x  
    w: MOV   R2.w, R2.w  
11 x: CNDE_INT R2.x, R1.z, R0.z, R0.y  
    y: CNDE_INT R2.y, R1.z, PV11.y, R0.x  
    z: CNDE_INT R2.z, R1.z, PV11.x, R1.w 

Ponownie żadnych oddziałów. Mniam!

+0

Chciałbym móc cię bardziej upomnieć. To jest świetna odpowiedź. Włączenie kodu zespołu cieniowania jest świetnym pomysłem, zawsze powinniśmy go sprawdzić. Sprawia, że ​​czuję się winny, że nie robię tego częściej. Teraz marzę o edytorze, który kompiluje źródło shaderów w tle i wyświetla "na żywo" dane wyjściowe zespołu w osobnym oknie! :) – wil

+1

ten kod ma gdzieś błąd przy zmniejszaniu L.Użyłem: \t 'vec4 color = convertRGBtoHSL (texture2D (map, vUv)); \t color.x = color.x + hue; color.x = color.x - floor (color.x); \t color.y = color.y + nasycenie; color.y = clamp (color.y, 0.0, 1.0); \t color.z = color.z + lightness; color.z = clamp (color.z, 0.0, 1.0); \t gl_FragColor = convertHSLtoRGB (kolor); ' i działa wszędzie, z wyjątkiem sytuacji, gdy jasność jest <0. – makc

+0

faktycznie ... Właśnie zamieniłem kod na inny, a błąd nadal tam jest ... być może HSL nie jest przestrzeń, której szukam? – makc