2011-01-18 16 views
8

Tengo una aplicación donde el usuario debería poder modificar una imagen con controles deslizantes para matiz, saturación y claridad. Todo el procesamiento de imágenes se realiza en la GPU usando sombreadores de fragmentos GLSL.Ajustaciones de imagen HSL en la GPU

Mi problema es que las conversiones RGB -> HSL -> RGB son bastante caras en el gpu debido a la extensa bifurcación.

Mi pregunta es si puedo convertir los "ajustes de color" de los usuarios a algún otro espacio de color que pueda calcular más eficientemente la imagen ajustada en la GPU.

Respuesta

8

Para la ligereza y la saturación puede usar YUV(actually YCbCr). Es fácil convertir desde RGB y viceversa. No se necesita ramificación. La saturación se controla aumentando o disminuyendo tanto Cr como Cb. La claridad es Y.

Obtiene algo similar a la modificación de tono HSL al rotar los componentes Cb y Cr (es prácticamente un vector 3D), pero por supuesto depende de su aplicación si eso es suficiente.

alt text

Editar: un componente de color (Cb, Cr) es un punto en un plano de color como la de arriba. Si toma cualquier punto aleatorio y lo rota alrededor del centro, el resultado cambiará de matiz. Pero como el mecanismo es un poco diferente que en HSL, los resultados no son precisamente los mismos.

Image es de dominio público de Wikipedia.

+0

Gran respuesta, sin embargo, todavía no estoy seguro de qué similar "similar" con respecto a la modificación de tono. – ronag

+1

Agregué una aclaración para hue-issue. – Virne

1

Puede usar una tabla de búsqueda 3D para almacenar la transformación de color, la tabla se actualizaría mediante las variables de usuario, pero puede haber accesos más simples.

Más información disponible en GPU Gems 2.

1

Creo que la conversión entre RGB y HSV/HSL podría codificarse sin ramificación alguna. Por ejemplo, la forma en la conversión de RGB -> HSV sin ramificación podría mirar en 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; 
} 

pero no he punto de referencia de esta solución. Puede ser que algunas ganancias de rendimiento que ganamos sin ramificar aquí se puedan compensar con las pérdidas de rendimiento de la ejecución de código redundante. Por lo tanto, se necesitan pruebas exhaustivas ...

+0

No funciona para mí, y se queja de res.x no inicializado. – kaoD

+0

Inicialización de 'res' fija. ¿Qué quiere decir exactamente diciendo "No funciona"? –

+0

Toda la imagen está teñida de rojo y configurar una onda sinusoidal en el tono simplemente no funciona. Otras implementaciones funcionaron bien, pero estaba especialmente interesado en la eficiencia de esta. Que podria estar causando esto? ¿Hay algo que pueda hacer para ayudar a depurar? – kaoD

3

Tenía la misma pregunta, pero encontré una solución muy simple que se adapta a mis necesidades, quizás también sea útil para usted. La saturación de un color es básicamente su propagación, creo que es la distancia euclidiana entre los valores RGB y su promedio. Independientemente de eso, si simplemente toma el promedio del máximo y mínimo de los valores RGB y escala los colores relativos a ese pivote, el efecto es un aumento (o disminución) muy decente en la saturación.

en un sombreado GLSL que iba a escribir:

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

Es un error suponer que la ramificación en la GPU y la ramificaciónen el código son la misma cosa.

Para condicionales simples, nunca hay ninguna bifurcación en absoluto. Las GPU tienen instrucciones de movimiento condicionales que se traducen directamente a expresiones ternarias y declaraciones if-else simples.

Donde las cosas se ponen problemáticas es cuando tiene condicionales anidados o múltiples operaciones condicionalmente dependientes. Luego debe considerar si el compilador GLSL es lo suficientemente inteligente como para traducirlo todo en cmoves. Siempre que sea posible, el compilador emitirá un código que ejecutará todas las ramas y recombinará el resultado con movimientos condicionales, pero no siempre puede hacerlo.

Tienes que saber cuándo ayudarlo. Nunca adivine cuándo puede medir: use el GPU Shader Analyzer de AMD o el GCG de Nvidia para ver la salida del conjunto. El conjunto de instrucciones de una GPU es muy limitado y simplista, así que no tengas miedo de la palabra 'ensamblado'.

Aquí hay un par de funciones de conversión RGB/HSL que he modificado para que funcionen bien con el compilador GLSL de AMD, junto con la salida de ensamblaje. El crédito va para Paul Bourke por el código de conversión C original.

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

salida Asamblea para esta función:

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 

en cuenta que no hay instrucciones de ramificación. Son movimientos condicionales todo el tiempo, más o menos exactamente como los escribí.

El hardware necesario para un movimiento condicional es solo un comparador binario (5 puertas por bit) y un montón de rastros. Muy rapido.

Otra cosa divertida de notar es que no hay divisiones. En cambio, el compilador usó una instrucción aproximada recíproca y una instrucción multiplicada. Hace esto también para operaciones sqrt muchas veces. Puede utilizar los mismos trucos en una CPU con (por ejemplo) las instrucciones SSE rcpps y rsqrtps.

Ahora la operación inversa:

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

(editado 05/Julio/2013:. He cometido un error al traducir esta función ¡Originalmente El conjunto también ha sido actualizada).

salida de Asamblea:

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 

Una vez más no hay ramas. ¡Yum!

+0

Ojalá pudiera votarte más. Esta es una respuesta genial. Incluir el código de montaje del sombreador es una excelente idea, siempre debemos inspeccionarlo. Me hace sentir culpable de no hacerlo más a menudo. ¡Ahora sueño con un editor que compila la fuente del sombreador en el fondo y muestra "en vivo" el resultado del ensamblaje en una ventana separada! :) – wil

+1

este código tiene un error en algún lugar al reducir L.Usé: \t 'vec4 color = convertRGBtoHSL (texture2D (map, vUv)); \t color.x = color.x + hue; color.x = color.x - piso (color.x); \t color.y = color.y + saturación; color.y = abrazadera (color.y, 0.0, 1.0); \t color.z = color.z + ligereza; color.z = abrazadera (color.z, 0.0, 1.0); \t gl_FragColor = convertHSLtoRGB (color); ' y está funcionando en todas partes excepto cuando la luminosidad es <0. – makc

+0

en realidad ... Acabo de reemplazar el código con otro, y el error sigue ahí ... tal vez HSL no es el espacio que estoy buscando? – makc