27

Is there a way to efficiently change hue of a 2D OpenGL texture using GLSL (fragment shader)?

Do someone have some code for it?

UPDATE: This is the code resulting from user1118321 suggestion:

uniform sampler2DRect texture;
const mat3 rgb2yiq = mat3(0.299, 0.587, 0.114, 0.595716, -0.274453, -0.321263, 0.211456, -0.522591, 0.311135);
const mat3 yiq2rgb = mat3(1.0, 0.9563, 0.6210, 1.0, -0.2721, -0.6474, 1.0, -1.1070, 1.7046);
uniform float hue;

void main() {

vec3 yColor = rgb2yiq * texture2DRect(texture, gl_TexCoord[0].st).rgb; 

float originalHue = atan(yColor.b, yColor.g);
float finalHue = originalHue + hue;

float chroma = sqrt(yColor.b*yColor.b+yColor.g*yColor.g);

vec3 yFinalColor = vec3(yColor.r, chroma * cos(finalHue), chroma * sin(finalHue));
gl_FragColor    = vec4(yiq2rgb*yFinalColor, 1.0);
}

And this is the result compared with a reference:

enter image description here

I have tried to switch I with Q inside atan but the result is wrong even around 0°

Have you got any hint?

If needed for comparison, this is the original unmodified image: enter image description here

Andrea3000
  • 1,018
  • 1
  • 11
  • 26
  • 5
    It's a rotation of the RGB triple around the (1,1,1) vector - you can express that as a matrix multiplication in the shader – Flexo Feb 10 '12 at 21:00
  • If your hue shift is constant then you can skip the atan, sqrt, sin, and cos. What you're doing is converting them to polar coordinates, adding to the angle, and converting back. You can do this math with a 2x2 rotation matrix. If you don't need to do any other math in the yiq space you can precalculate the whole transform. Multiply your rgb2yiq with the 2d rotation (padded out to a 3x3) and then with the yiq2rgb to get one big 3x3 matrix that does the whole process. Which, like @Flexo says is just a rotation around the (1,1,1) vector. – John K Sep 07 '12 at 18:19

3 Answers3

30

While what @awoodland says is correct, that method may cause issues with changes in luminance, I believe.

HSV and HLS color systems are problematic for a number of reasons. I talked with a color scientist about this recently, and his recommendation was to convert to YIQ or YCbCr space and adjust the the chroma channels (I&Q, or Cb&Cr) accordingly. (You can learn how to do that here and here.)

Once in one of those spaces, you can get the hue from the angle formed by the chroma channels, by doing hue = atan(cr/cb) (watching for cb == 0). This gives you a value in radians. Simply rotate it by adding the hue rotation amount. Once you've done that, you can calculate the magnitude of the chroma with chroma = sqrt(cr*cr+cb*cb). To get back to RGB, calculate the new Cb and Cr (or I & Q) using Cr = chroma * sin (hue), Cb = chroma * cos (hue). Then convert back to RGB as described on the above web pages.

EDIT: Here's a solution that I've tested and seems to give me the same results as your reference. You can probably collapse some of the dot products into matrix multiplies:

uniform sampler2DRect inputTexture;
uniform float   hueAdjust;
void main ()
{
    const vec4  kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);
    const vec4  kRGBToI     = vec4 (0.596, -0.275, -0.321, 0.0);
    const vec4  kRGBToQ     = vec4 (0.212, -0.523, 0.311, 0.0);

    const vec4  kYIQToR   = vec4 (1.0, 0.956, 0.621, 0.0);
    const vec4  kYIQToG   = vec4 (1.0, -0.272, -0.647, 0.0);
    const vec4  kYIQToB   = vec4 (1.0, -1.107, 1.704, 0.0);

    // Sample the input pixel
    vec4    color   = texture2DRect (inputTexture, gl_TexCoord [ 0 ].xy);

    // Convert to YIQ
    float   YPrime  = dot (color, kRGBToYPrime);
    float   I      = dot (color, kRGBToI);
    float   Q      = dot (color, kRGBToQ);

    // Calculate the hue and chroma
    float   hue     = atan (Q, I);
    float   chroma  = sqrt (I * I + Q * Q);

    // Make the user's adjustments
    hue += hueAdjust;

    // Convert back to YIQ
    Q = chroma * sin (hue);
    I = chroma * cos (hue);

    // Convert back to RGB
    vec4    yIQ   = vec4 (YPrime, I, Q, 0.0);
    color.r = dot (yIQ, kYIQToR);
    color.g = dot (yIQ, kYIQToG);
    color.b = dot (yIQ, kYIQToB);

    // Save the result
    gl_FragColor    = color;
}
user1118321
  • 25,567
  • 4
  • 55
  • 86
  • Thank you, I've just implemented it but it doesn't seem to work correctly.. It actually gives a hue change but it is completely different from Photoshop hue adjustment (for example). I've decided to convert it to YIQ because with YCbCr I was having even worst results. Therefore I have calculated `hue = atan2(Q,I)`, `chroma = sqrt(I*I + Q*Q)` and then `I = chroma * sin(hue)`, `Q = chorma * cos(hue)`. Is it correct? Am I missing something? – Andrea3000 Feb 11 '12 at 15:00
  • That seems correct to me. What results are you getting? (Is just the hue wrong, or are the luminance and saturation getting messed up, too?) Do the results look more like what you expect if you reverse I and Q in the `atan2()` call? Maybe if you posted some code, we could double-check it for you. – user1118321 Feb 11 '12 at 17:25
  • Thank you for your attention, I have updated the question with code and a snapshot of the result. – Andrea3000 Feb 11 '12 at 18:31
  • It looks to me like maybe you need to reverse the hue angle you're passing in for a start. That would at least get you closer. It seems like maybe your saturation is increasing, for some reason. Also, you may not get the exact same results as Photoshop, as they may be working in a different color space than you. – user1118321 Feb 11 '12 at 19:05
  • Reversing hue doesn't seem to get me closer actually. I tried to apply a saturation filter but I can't get even closer to the reference. I know that I can't have the exact same results as Photoshop, but this seems to be a huge difference at the moment, isn't it? – Andrea3000 Feb 11 '12 at 19:13
  • A couple things - I assume you're passing the angle in radians, right? Also, does switching the `sin` and `cos` calls in the second to last line change anything? I'd have put them in the reverse order. – user1118321 Feb 11 '12 at 19:17
  • Yes, I'm passing the angle in radians from +3.14 to -3.14. For what is about `sin` and `cos` calls, if I reverse them I get an image that is altered even if angle is equal to 0 radians. – Andrea3000 Feb 11 '12 at 19:41
  • This function works great, but something happens to the alpha channel in a way that even after just taking the .rgb of the created vec4 and the .a of the original vec4 the alpha appears black. – Pochi Dec 31 '15 at 03:06
  • That's a good point. I'm completely ignoring the alpha in this example, and if you're working pre-multiplied, you'll need to un-pre-multiply the input before doing these operations and re-pre-multiply them after. Since it's not clear what any given viewer of this answer will do, I'm not sure which to write up. If you have suggestions, I'm happy to hear them. – user1118321 Dec 31 '15 at 03:37
  • 1
    @user1118321 I try to convert pure R (1,0,0), G (0,1,0), B (0,0,1) to YCrCb and then compute saturation. However the saturation is not 1 in either of the cases, infact it is totally different for each of the primary colors. What am I missing? – Deepak Sharma May 29 '18 at 12:22
4

Andrea3000, in comparing YIQ examples on the net, i came across your posting, but i think there is an issue with the 'updated' version of your code.... i'm sure your 'mat3' definitions are flip/flopped on the column/row ordering... (maybe that's why you were still having troubles)...

FYI: OpenGL matrix ordering: "For more values, matrices are filled in in column-major order. That is, the first X values are the first column, the second X values are the next column, and so forth." See: http://www.opengl.org/wiki/GLSL_Types

mat2(
float, float,   //first column
float, float);  //second column
Joe
  • 51
  • 2
1

MSL(Metal Shader Language) version for changing the hue of a texture. I would recommend the MetalPetal pod. This pod will make life a lot easier.

#include <metal_stdlib>
using namespace metal;

typedef struct {
    float4 position [[ position ]];
    float2 textureCoordinate;
} VertexOut;


fragment float4 hue_adjust_filter(
                            VertexOut vertexIn [[stage_in]],
                            texture2d<float, access::sample> inTexture [[texture(0)]],
                            sampler inSampler [[sampler(0)]],
                            constant float &hueAdjust [[ buffer(0) ]])
{
    const float4  kRGBToYPrime = float4 (0.299, 0.587, 0.114, 0.0);
    const float4  kRGBToI     =  float4 (0.596, -0.275, -0.321, 0.0);
    const float4  kRGBToQ     =  float4 (0.212, -0.523, 0.311, 0.0);
    
    const float4  kYIQToR   =    float4 (1.0, 0.956, 0.621, 0.0);
    const float4  kYIQToG   =    float4 (1.0, -0.272, -0.647, 0.0);
    const float4  kYIQToB   =    float4 (1.0, -1.107, 1.704, 0.0);
    
    // Sample the input pixel
    float2 uv = vertexIn.textureCoordinate;
    float4 color = inTexture.sample(inSampler, uv);
    
    // Convert to YIQ
    float   YPrime  = dot (color, kRGBToYPrime);
    float   I      = dot (color, kRGBToI);
    float   Q      = dot (color, kRGBToQ);
    
    // Calculate the hue and chroma
    float   hue     = atan2 (Q, I);
    float   chroma  = sqrt (I * I + Q * Q);
    
    // Make the user's adjustments
    hue += hueAdjust;
    
    // Convert back to YIQ
    Q = chroma * sin (hue);
    I = chroma * cos (hue);
    
    // Convert back to RGB
    float4    yIQ   = float4 (YPrime, I, Q, 0.0);
    color.r = dot (yIQ, kYIQToR);
    color.g = dot (yIQ, kYIQToG);
    color.b = dot (yIQ, kYIQToB);
    
    return color;
}
Asif Newaz
  • 557
  • 7
  • 17