javascriptcolorscolor-spacesrgblab-color-space

Converting XYZ to sRGB


I am attempting to convert from CIE XYZ to sRGB colourspace, as an intermediary between CIELAB and sRGB colour space. I am using the formlas in http://www.brucelindbloom.com/ and https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ.

The values produced by my current attempt are incorrect (e.g. compare https://www.nixsensor.com/free-color-converter/), and I had to absolute the linear RGB values so that the formula would work. Can anyone tell me how this is supposed to be done or where I am going wrong?

// Convert XYZ to sRGB
function xyz2rgb(xyz){
    const r =  3.2404542*xyz.x + -1.5371385*xyz.y + -0.4985314*xyz.z
    const g = -0.9692660*xyz.x + 1.8760108*xyz.y + 0.0415560*xyz.z
    const b =  0.0556434*xyz.x + -0.2040259*xyz.y + 1.0572252*xyz.z

    //convert to srgb
    function adj(C) {
        C = Math.abs(C)
        let v
        if (C <= 0.0031308) {
            v = 12.92 * C
        } else {
            v = 1.055 * C**(1/24) - 0.055
        }
        return Math.round(v*255)
    }

    const R = adj(r)
    const G = adj(g)
    const B = adj(b)

    return { 'r':R, 'g':G, 'b':B }
}

const xyz = {x:0.02, y:0.18, z:1.08}
const rgb = xyz2rgb(xyz)
console.log(rgb)

Edit: jsfiddle: https://jsfiddle.net/058e36ac/2/


Solution

  • There are couple of issue in your code while doing the conversion. First is you applied gamma correction logic without clamping the linear RGB values.

    Another issue is you didn't handled the negative RGB values and as you know the gamma correction should only be applied to values within the [0, 1] range and negative values need special handling.

    Corrections that I've made:

    1. Added a clamp function to clamp values between 0 to 1
    var clamp = function (value) {
      return Math.max(0, Math.min(1, value));
    };
    
    1. Added the gamma correction function to clamp linear RGB values:
    var transfer = function (c) {
     if (c <= 0.0031308) {
       return 12.92 * c;
     } else {
       return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
     }
    };
    
    1. Applied Gamma Correction and Clamping and then scaled to 0 - 255 range:
    var R = clamp(transfer(rLinear));
    var G = clamp(transfer(gLinear));
    var B = clamp(transfer(bLinear));
    
    
    return {
      r: Math.round(R * 255),
      g: Math.round(G * 255),
      b: Math.round(B * 255)
    };
    
    

    Here is the full function of converting xyz to srgb:

    function xyz_to_srgb(xyz) {
    
      var xsm = [
        [3.2406255, -1.537208, -0.4986286],
        [-0.9689307, 1.8757561, 0.0415175],
        [0.0557101, -0.2040211, 1.0569959]
      ];
    
      var rLinear = xyz.x * xsm[0][0] + xyz.y * xsm[0][1] + xyz.z * xsm[0][2];
      var gLinear = xyz.x * xsm[1][0] + xyz.y * xsm[1][1] + xyz.z * xsm[1][2];
      var bLinear = xyz.x * xsm[2][0] + xyz.y * xsm[2][1] + xyz.z * xsm[2][2];
    
      // Clamp values between 0 and 1
      var clamp = function (value) {
        return Math.max(0, Math.min(1, value));
      };
    
      // Apply gamma correction
      var transfer = function (c) {
        if (c <= 0.0031308) {
          return 12.92 * c;
        } else {
          return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
        }
      };
    
      // Apply gamma correction and clamp
      var R = clamp(transfer(rLinear));
      var G = clamp(transfer(gLinear));
      var B = clamp(transfer(bLinear));
    
      // Convert to 0-255 range
      return {
        r: Math.round(R * 255),
        g: Math.round(G * 255),
        b: Math.round(B * 255)
      };
    }
    
    const xyz = { x: 0.02, y: 0.18, z: 1.08 };
    const rgb = xyz_to_srgb(xyz);
    console.log(rgb);
    

    Here's the output of above code:

    {
      b: 255,
      g: 162,
      r: 0
    }
    

    Refer to this gist for further clarity of how to implement the conversion: xyz to srgb conversion