Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color conversion does not make the round trip: RGB - XYZ - LAB - LCH - LAB - XYZ - RGB #295

Open
tdbe opened this issue Apr 7, 2022 · 4 comments
Milestone

Comments

@tdbe
Copy link

tdbe commented Apr 7, 2022

Hi, I ported your code to GLSL because I need to do gpu hue adjustments on CIE colors, so I needed to convert to LCH to change the hue, but I noticed that, without making any changes to the color, doing RGB - XYZ - LAB - LCH - LAB - XYZ - RGB I get a completely different result from the starting color. I am still fairly new to CIE so maybe I am not understanding it right? Is it not meant to work in a round trip? Just want to confirm at least if it's indeed supposed to work.

I did notice that if I do RGB - XYZ - Lab - XYZ - RGB, everything is correctly identical. When I involve LCH which is the only way I know how to change the Hue, I get broken results whether I change the hue or not.

My ported code, everything is done right AFAIK:

float atan2(float x, float y)
{
    bool s = (abs(x) > abs(y));
    return mix(_PI/2.0 - atan(x,y), atan(y,x), s);
}
//<--https://github.com/gka/chroma.js-->

//LAB Constants
// Corresponds roughly to RGB brighter/darker
const float _Kn = 18;

// D65 standard referent
const float _LAB_CONSTANTS_Xn = 0.950470;
const float _LAB_CONSTANTS_Yn = 1;
const float _LAB_CONSTANTS_Zn = 1.088830;

const float _LAB_CONSTANTS_t0 = 0.137931034;  // 4 / 29
const float _LAB_CONSTANTS_t1 = 0.206896552;  // 6 / 29
const float _LAB_CONSTANTS_t2 = 0.12841855;   // 3 * t1 * t1
const float _LAB_CONSTANTS_t3 = 0.008856452;  // t1 * t1 * t1

float rgb_xyz (float r) {
    if ((r /= 255) <= 0.04045) return r / 12.92;
    return pow((r + 0.055) / 1.055, 2.4);
}

float xyz_lab (float t) {
    if (t > _LAB_CONSTANTS_t3) return pow(t, 1 / 3);
    return t / _LAB_CONSTANTS_t2 + _LAB_CONSTANTS_t0;
}

vec3 rgb2xyz (float r,float g,float b) {
    r = rgb_xyz(r);
    g = rgb_xyz(g);
    b = rgb_xyz(b);
    float x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / _LAB_CONSTANTS_Xn);
    float y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.0721750 * b) / _LAB_CONSTANTS_Yn);
    float z = xyz_lab((0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / _LAB_CONSTANTS_Zn);
    return vec3(x,y,z);
}

vec3 rgb2lab(float r,float g,float b) {
    vec3 xyz = rgb2xyz(r,g,b);
    float l = 116 * xyz.y - 16;
    return vec3(l < 0 ? 0 : l, 500 * (xyz.x - xyz.y), 200 * (xyz.y - xyz.z));
}

//-----

float xyz_rgb (float r) {
    return (255 * (r <= 0.00304 ? 12.92 * r : 1.055 * pow(r, 1 / 2.4) - 0.055));
}

float lab_xyz (float t) {
    return t > _LAB_CONSTANTS_t1 ? t * t * t : _LAB_CONSTANTS_t2 * (t - _LAB_CONSTANTS_t0);
}

/*
 * L* [0..100]
 * a [-100..100]
 * b [-100..100]
 */
vec3 lab2rgb (float l,float a,float b) {
    
    float x,y,z, r,g,b_;

    y = (l + 16) / 116;
    x = isnan(a) ? y : y + a / 500;
    z = isnan(b) ? y : y - b / 200;

    y = _LAB_CONSTANTS_Yn * lab_xyz(y);
    x = _LAB_CONSTANTS_Xn * lab_xyz(x);
    z = _LAB_CONSTANTS_Zn * lab_xyz(z);

    r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z);  // D65 -> sRGB
    g = xyz_rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z);
    b_ = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z);

    return vec3(r,g,b_);
};

/**
Convert CIE Lab values of a color to CIE LCH(ab) values
The nominal ranges are as follows:
    1) Input: 0 to 100 for `L`; ±128 for `a` and `b`
    2) Output: 0 to 100 for `L`; 0 to 128√2 for `C` and 0 to 360° for `h`
Note: `L` is unchanged
*/
vec3 lab2lch(vec3 lab)
{
    vec3 temp = vec3(lab.x, 0, -1);
    
    //https://github.com/gka/chroma.js/blob/75ea5d8a5480c90ef1c7830003ac63c2d3a15c03/src/io/lch/lab2lch.js
    if (lab.y != 0)
    {
        float l = lab.x;
        float a = lab.y;
        float b = lab.z;
        float c = sqrt(a * a + b * b);
        float h = mod((atan2(b, a) * _RAD2DEG + 360), 360);//check atan2 definition up top
        //if (round(c*10000) == 0.0) h = Number.NaN;
        temp.y = c;
        temp.z = h;
    }

    return temp;
}

/**
Convert CIE LCH(ab) values of a color to CIE Lab values
The nominal ranges are as follows:
    1) Input: 0 to 100 for `L`; 0 to 128√2 for `C` and 0 to 360° for `h`
    2) Output: 0 to 100 for `L`; ±128 for `a` and `b`
Note: `L` is unchanged
*/
vec3 lch2lab(vec3 lch)
{
    vec3 temp = vec3(lch.x, 0, 0);

    //https://github.com/gka/chroma.js/blob/75ea5d8a5480c90ef1c7830003ac63c2d3a15c03/src/io/lch/lch2lab.js
    float h = lch.z;
    float c = lch.y;
    float l = lch.x;
    //if (isnan(h)) h = 0;
    h = h * _DEG2RAD;
    temp.y = cos(h) * c;
    temp.z = sin(h) * c;

    return temp;
}

//</--https://github.com/gka/chroma.js-->

    vec3 lab = rgb2lab(color.x, color.y, color.z);
    vec3 lch = lab2lch(lab);
    lch.z = lch.z + hue;
    lab = lch2lab(lch);
    vec3 rgb = lab2rgb(lab.x, lab.y, lab.z);

[EDIT]

I found this shadertoy in the meantime, and the code here makes the round trip and I can change hue. But the LCH code there is baked in with the LAB conversion, uses a lot of magic numbers etc, so I can't tell what's wrong here.

Anyway, at least this answers the question of "Is it not meant to work in a round trip?" I guess it is, and I am assuming (hoping) it's my fault somewhere and not the repo's

@brandonmcconnell
Copy link

I've heard that chroma does NOT support "round trip", but I personally think they should as well.

+1 from me 👍🏼

@thomas-cruz
Copy link

Had some base colors defined in HEX, got their LCH values and then setting new colors based on the same LCH values and converting it to HEX and the HEX values would be different to the expected values.

@tdbe
Copy link
Author

tdbe commented May 6, 2022

Nice to see I wasn't crazy 🙂 I -guess- the problem here is there is loss of information going through all these color spaces and a lot of assumptions or approximations must happen to make a round trip.

But it would be amazing if they actually wrote this in like the start of their readme.md: "hey I know you're expecting to get color conversions out of this, but they don't make the round trip because of reasons" 🙂

@gka
Copy link
Owner

gka commented Aug 18, 2024

makes sense to add this to the docs!

@gka gka added this to the 3.1 milestone Aug 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants