22

I'm having problems converting colours from RGB to LAB space It should be straight forward using the formulas from here, only I'm getting back the wrong values

  • RGB = 56,79,132

  • X = 8.592

  • Y = 8.099
  • Z = 22.940

and CIE-L*ab as

  • L* 34.188
  • a* 8.072
  • b* -32.478

This is my code; but I can't see where I'm going wrong. It maybe due to floating points like this fella before me. Thank you.

// user colour
var Red   = 56;
var Green = 79;
var Blue  = 132;

// user colour converted to XYZ space
XYZ = RGBtoXYZ(Red,Green,Blue)
var colX = XYZ[0];
var colY = XYZ[1];
var colZ = XYZ[2];

// alert(XYZ)

LAB = XYZtoLAB(colX, colY, colZ)

alert(LAB)

function RGBtoXYZ(R, G, B)
{
    var_R = parseFloat( R / 255 )        //R from 0 to 255
    var_G = parseFloat( G / 255 )        //G from 0 to 255
    var_B = parseFloat( B / 255 )        //B from 0 to 255

    if ( var_R > 0.04045 ) var_R = ( ( var_R + 0.055 ) / 1.055 ) ^ 2.4
    else                   var_R = var_R / 12.92
    if ( var_G > 0.04045 ) var_G = ( ( var_G + 0.055 ) / 1.055 ) ^ 2.4
    else                   var_G = var_G / 12.92
    if ( var_B > 0.04045 ) var_B = ( ( var_B + 0.055 ) / 1.055 ) ^ 2.4
    else                   var_B = var_B / 12.92

    var_R = var_R * 100
    var_G = var_G * 100
    var_B = var_B * 100

    //Observer. = 2°, Illuminant = D65
    X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
    Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
    Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
    return [X, Y, Z]
}


function XYZtoLAB(x, y, z)
{
    var ref_X =  95.047;
    var ref_Y = 100.000;
    var ref_Z = 108.883;

    var_X = x / ref_X          //ref_X =  95.047   Observer= 2°, Illuminant= D65
    var_Y = y / ref_Y          //ref_Y = 100.000
    var_Z = z / ref_Z          //ref_Z = 108.883

    if ( var_X > 0.008856 ) var_X = var_X ^ ( 1/3 )
    else                    var_X = ( 7.787 * var_X ) + ( 16 / 116 )
    if ( var_Y > 0.008856 ) var_Y = var_Y ^ ( 1/3 )
    else                    var_Y = ( 7.787 * var_Y ) + ( 16 / 116 )
    if ( var_Z > 0.008856 ) var_Z = var_Z ^ ( 1/3 )
    else                    var_Z = ( 7.787 * var_Z ) + ( 16 / 116 )

    CIE_L = ( 116 * var_Y ) - 16
    CIE_a = 500 * ( var_X - var_Y )
    CIE_b = 200 * ( var_Y - var_Z )

return [CIE_L, CIE_a, CIE_b]
}
0

5 Answers 5

25

I'm pretty sure ^ is bitwise xor in javascript not a power operator. I think Math.pow is what you are looking for.

1
  • or **, eg 4**2 = 16
    – rednoyz
    Commented Dec 27, 2023 at 18:41
7
/**
 * Converts RGB color to CIE 1931 XYZ color space.
 * https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz
 * @param  {string} hex
 * @return {number[]}
 */
export function rgbToXyz(hex) {
    const [r, g, b] = hexToRgb(hex).map(_ => _ / 255).map(sRGBtoLinearRGB)
    const X =  0.4124 * r + 0.3576 * g + 0.1805 * b
    const Y =  0.2126 * r + 0.7152 * g + 0.0722 * b
    const Z =  0.0193 * r + 0.1192 * g + 0.9505 * b
    // For some reason, X, Y and Z are multiplied by 100.
    return [X, Y, Z].map(_ => _ * 100)
}

/**
 * Undoes gamma-correction from an RGB-encoded color.
 * https://en.wikipedia.org/wiki/SRGB#Specification_of_the_transformation
 * https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
 * @param  {number}
 * @return {number}
 */
function sRGBtoLinearRGB(color) {
    // Send this function a decimal sRGB gamma encoded color value
    // between 0.0 and 1.0, and it returns a linearized value.
    if (color <= 0.04045) {
        return color / 12.92
    } else {
        return Math.pow((color + 0.055) / 1.055, 2.4)
    }
}

/**
 * Converts hex color to RGB.
 * https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
 * @param  {string} hex
 * @return {number[]} [rgb]
 */
function hexToRgb(hex) {
    const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
    if (match) {
        match.shift()
        return match.map(_ => parseInt(_, 16))
    }
}

/**
 * Converts CIE 1931 XYZ colors to CIE L*a*b*.
 * The conversion formula comes from <http://www.easyrgb.com/en/math.php>.
 * https://github.com/cangoektas/xyz-to-lab/blob/master/src/index.js
 * @param   {number[]} color The CIE 1931 XYZ color to convert which refers to
 *                           the D65/2° standard illuminant.
 * @returns {number[]}       The color in the CIE L*a*b* color space.
 */
// X, Y, Z of a "D65" light source.
// "D65" is a standard 6500K Daylight light source.
// https://en.wikipedia.org/wiki/Illuminant_D65
const D65 = [95.047, 100, 108.883]
export function xyzToLab([x, y, z]) {
  [x, y, z] = [x, y, z].map((v, i) => {
    v = v / D65[i]
    return v > 0.008856 ? Math.pow(v, 1 / 3) : v * 7.787 + 16 / 116
  })
  const l = 116 * y - 16
  const a = 500 * (x - y)
  const b = 200 * (y - z)
  return [l, a, b]
}

/**
 * Converts Lab color space to Luminance-Chroma-Hue color space.
 * http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html
 * @param  {number[]}
 * @return {number[]}
 */
export function labToLch([l, a, b]) {
    const c = Math.sqrt(a * a + b * b)
    const h = abToHue(a, b)
    return [l, c, h]
}

/**
 * Converts a and b of Lab color space to Hue of LCH color space.
 * https://stackoverflow.com/questions/53733379/conversion-of-cielab-to-cielchab-not-yielding-correct-result
 * @param  {number} a
 * @param  {number} b
 * @return {number}
 */
function abToHue(a, b) {
    if (a >= 0 && b === 0) {
        return 0
    }
    if (a < 0 && b === 0) {
        return 180
    }
    if (a === 0 && b > 0) {
        return 90
    }
    if (a === 0 && b < 0) {
        return 270
    }
    let xBias
    if (a > 0 && b > 0) {
        xBias = 0
    } else if (a < 0) {
        xBias = 180
    } else if (a > 0 && b < 0) {
        xBias = 360
    }
    return radiansToDegrees(Math.atan(b / a)) + xBias
}

function radiansToDegrees(radians) {
    return radians * (180 / Math.PI)
}

function degreesToRadians(degrees) {
    return degrees * Math.PI / 180
}

1
  • 1
    "// For some reason, X, Y and Z are multiplied by 100." - yeah, probably the wrong place for that XYZ should be 0 to 1. Lab has luminance from 0 to 100, which is probably why they're doing that. Commented Sep 1, 2021 at 23:51
2

Here are some functions for RGB -> XYZ, XYZ -> LAB, LAB -> XYZ, XYZ -> RGB.

function RGBtoXYZ([R, G, B]) {
    const [var_R, var_G, var_B] = [R, G, B]
        .map(x => x / 255)
        .map(x => x > 0.04045
            ? Math.pow(((x + 0.055) / 1.055), 2.4)
            : x / 12.92)
        .map(x => x * 100)

    // Observer. = 2°, Illuminant = D65
    X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
    Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
    Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
    return [X, Y, Z]
}

function XYZtoRGB([X, Y, Z]) {
    //X, Y and Z input refer to a D65/2° standard illuminant.
    //sR, sG and sB (standard RGB) output range = 0 ÷ 255

    let var_X = X / 100
    let var_Y = Y / 100
    let var_Z = Z / 100

    var_R = var_X *  3.2406 + var_Y * -1.5372 + var_Z * -0.4986
    var_G = var_X * -0.9689 + var_Y *  1.8758 + var_Z *  0.0415
    var_B = var_X *  0.0557 + var_Y * -0.2040 + var_Z *  1.0570

    return [var_R, var_G, var_B]
        .map(n => n > 0.0031308
            ? 1.055 * Math.pow(n, (1 / 2.4)) - 0.055
            : 12.92 * n)
        .map(n => n * 255)
}

const ref_X =  95.047;
const ref_Y = 100.000;
const ref_Z = 108.883;

function XYZtoLAB([x, y, z]) {
    const [ var_X, var_Y, var_Z ] = [ x / ref_X, y / ref_Y, z / ref_Z ]
        .map(a => a > 0.008856
            ? Math.pow(a, 1 / 3)
            : (7.787 * a) + (16 / 116))

    CIE_L = (116 * var_Y) - 16
    CIE_a = 500 * (var_X - var_Y)
    CIE_b = 200 * (var_Y - var_Z)

    return [CIE_L, CIE_a, CIE_b]
}

function LABtoXYZ([l, a, b]) {

    const var_Y = (l + 16) / 116
    const var_X = a / 500 + var_Y
    const var_Z = var_Y - b / 200

    const [X, Y, Z] = [var_X, var_Y, var_Z]
        .map(n => Math.pow(n, 3) > 0.008856
            ? Math.pow(n, 3)
            : (n - 16 / 116) / 7.787)

    return [X * ref_X, Y * ref_Y, Z * ref_Z]
}

Reference: http://www.easyrgb.com/en/math.php

1

The JavaScript function rgb_to_lab convert color values from the RGB (Red, Green, Blue) color space to the CIELab color space :

const rgb_to_lab = (...rgb) => {
    const lab = new Array(3), tmp = new Array(3)
    for (let i = 0; i < 3; ++i) {
        lab[i] = rgb[i] * .00392156862745098
        if (.04045 < lab[i])
            lab[i] = 100 * Math.pow((lab[i] + .055) * .9478672985781991, 2.4)
        else
            lab[i] *= 7.739938080495357
    }
    tmp[0] = (lab[0] * .4124 + lab[1] * .3576 + lab[2] * .1805) * .010521110608435826
    tmp[1] = (lab[0] * .2126 + lab[1] * .7152 + lab[2] * .0722) * .01
    tmp[2] = (lab[0] * .0193 + lab[1] * .1192 + lab[2] * .9505) * .009184170164304804
    for (let i = 0; i < 3; ++i)
        if (.008856 < tmp[i])
            tmp[i] = Math.cbrt(tmp[i])
        else
            tmp[i] = 7.787 * tmp[i] + .13793103448275862
    lab[0] = 116 * tmp[1] - 16
    lab[1] = 500 * (tmp[0] - tmp[1])
    lab[2] = 200 * (tmp[1] - tmp[2])
    return lab
}

The function receive 3 parameters in [0, 255] for R/G/B, its result is a set of three CIELab values (L*, a*, b*) that accurately represent the color.

const rgb_to_lab = (...rgb) => {
    const lab = new Array(3), tmp = new Array(3)
    for (let i = 0; i < 3; ++i) {
        lab[i] = rgb[i] / 255
        if (.04045 < lab[i])
            lab[i] = Math.pow((lab[i] + .055) / 1.055, 2.4)
        else
            lab[i] /= 12.92
        lab[i] *= 100
    }
    tmp[0] = (lab[0] * .4124 + lab[1] * .3576 + lab[2] * .1805) / 95.047
    tmp[1] = (lab[0] * .2126 + lab[1] * .7152 + lab[2] * .0722) / 100
    tmp[2] = (lab[0] * .0193 + lab[1] * .1192 + lab[2] * .9505) / 108.883
    for (let i = 0; i < 3; ++i)
        if (.008856 < tmp[i])
            tmp[i] = Math.cbrt(tmp[i])
        else
            tmp[i] = 7.787 * tmp[i] + .13793103448275862
    lab[0] = 116 * tmp[1] - 16
    lab[1] = 500 * (tmp[0] - tmp[1])
    lab[2] = 200 * (tmp[1] - tmp[2])
    return lab
}

console.log(rgb_to_lab(0, 0, 0)) // 1. Pure Black
console.log(rgb_to_lab(255, 255, 255)) // 2. Pure White
console.log(rgb_to_lab(128, 128, 128)) // 3. Neutral Gray
console.log(rgb_to_lab(255, 0, 0)) // 4. Pure Red
console.log(rgb_to_lab(0, 255, 0)) // 5. Pure Green
console.log(rgb_to_lab(0, 0, 255)) // 6. Pure Blue
console.log(rgb_to_lab(0, 255, 255)) // 7. Cyan

1. Gamma Correction (sRGB to Linear RGB)

It’s essential that the 12.92 division and 2.4 exponentiation accurately represent the conversion.

2. Conversion to XYZ

  • The conversion matrix coefficients used are :
    • X = 0.4124R + 0.3576G + 0.1805B
    • Y = 0.2126R + 0.7152G + 0.0722B
    • Z = 0.0193R + 0.1192G + 0.9505B

Dividing the resulting XYZ values by 95.047, 100, and 108.883 respectively is standard practice. These are the reference white D65 values.

3. XYZ to CIELab

  • The condition .008856 corresponds to the cubic root threshold.
  • The factor 7.787 corresponds to the slope of the linear part of the function when f(t) = t^(1/3) for values below 0.008856.
  • The offset .13793103448275862 is 16/116 ensure that f(t) is continuous.
0

function xyzc(c){return ((c/255)>0.04045)?Math.pow((((c/255)+0.055)/1.055),2.4)*100:(c/255)/12.92*100;}

This line will convert a rgb channel to XYZ

1
  • No, it will not, also this is sRGB, not RGB (12.92 number). This just converts R'G'B' to linear RGB using sRGB curve. XYZ requires special matrix depending on what primaries that linear light (linear RGB) now is. Commented Dec 26, 2021 at 2:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.