I’m trying to convert sRGB values (in Photoshop) to L*ab values (also for use in Photoshop) I’m in the ballpark, but not getting the same results as EasyRGB which is where I got the conversion formulas from. I expect there’s rounding errors (not at the end, where I round to three places). Only I can’t see them.
- Input: sRGB 255, 0, 255
- Output:L*ab: 60.32 , 98.254, -60.843
- Expected Output L*ab: 60.324 98.234 -60.825
To make it easier I’ve lifted out any Photoshop code, so it’s just JavaScript
var msg = "";
// RGB
// var Red = foregroundColor.rgb.red;
// var Green = foregroundColor.rgb.green;
// var Blue = foregroundColor.rgb.blue;
var Red = 255;
var Green = 0;
var Blue = 255;
msg = "RGB: " + Red + ", " + Green + ", " + Blue + "<br>";
printy(msg);
// user colour converted to XYZ space
var myColXYZ = RGB_to_XYZ(Red, Green, Blue)
var colX = myColXYZ[0];
var colY = myColXYZ[1];
var colZ = myColXYZ[2];
// using CIE L-ab* colour space
var myLab = XYZ_to_LAB(colX, colY, colZ)
//msg = "L*ab: " + myLab[0] + ", " + myLab[1] + ", " + myLab[2] + "<br>";
// b4 rounding: 60.319933664076004, 98.25421868616108, -60.84298422386232
// round to three places
for (var i = 0; i < 3; i++) {
myLab[i] = round_nicely(myLab[i], 3);
}
msg = "L*ab: " + myLab[0] + ", " + myLab[1] + ", " + myLab[2] + "<br>";
printy(msg);
// results
// RGB: 255, 0, 255
// L*ab: 60.32, 98.254, -60.843
// should be
// CIE-L*ab = 60.324 98.234 -60.825
// function RGB TO XYZ (R, G, B)
// --------------------------------------------------------
// http://www.easyrgb.com/index.php?X=MATH&H=02#text2
function RGB_to_XYZ(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 = Math.pow(((var_R + 0.055) / 1.055), 2.4)
else var_R = var_R / 12.92
if (var_G > 0.04045) var_G = Math.pow(((var_G + 0.055) / 1.055), 2.4)
else var_G = var_G / 12.92
if (var_B > 0.04045) var_B = Math.pow(((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 XYZ TO LAB (x, y, z)
// --------------------------------------------------------
// http://www.easyrgb.com/index.php?X=MATH&H=16#text16
function XYZ_to_LAB(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 = Math.pow(var_X, (1 / 3))
else var_X = (7.787 * var_X) + (16 / 116)
if (var_Y > 0.008856) var_Y = Math.pow(var_Y, (1 / 3))
else var_Y = (7.787 * var_Y) + (16 / 116)
if (var_Z > 0.008856) var_Z = Math.pow(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]
}
// function ROUND NICELY (num number, number of decimal places)
// --------------------------------------------------------
function round_nicely(num, places) {
// if(! places) places = 3;
var p = Math.pow(10, places)
return Math.round(num * p) / p
}
// function PRINTY (str)
// --------------------------------------------------------
function printy(message) {
var outputDiv = document.getElementById('myPage');
outputDiv.innerHTML += message;
}
<!-- page content -->
<p id="myPage"></p>
3
Answers
Obviously the EasyRBG
RGB
toXYZ
conversion is flawed. It doesn’t obey it’s own rules.Let’s do this step by step:
The input data is
R: 255 G: 0 B: 255
The first step is to map those to the zero to one range by dividing by 255. yielding this
var_R: 1 var_G: 0 var_B: 1
The next step is some manipulations, which seem to be some Gamut adjustments. But those manipulations have fix points at the extremes, meaning input values
1
or0
don’t change in this manipulation. So we are still atvar_R: 1 var_G: 0 var_B: 1
Next step is scale everything by 100
var_R: 100 var_G: 0 var_B: 100
Then there is a matrix multiplication of the vector
(R,G,B)
to(X,Y,Z)
which is a weighted linear combination of RGB vectorX = 42.24 + 18.05 = 60.29
Y = 21.26 + 7.22 = 28.48
Y = 1.93 + 95.05 = 96.98
EasyRGB has some different result for this conversion from
RGB
toXYZ
X: 59.289, Y: 28.485, Z: 96.964
Consider utilizing Photoshop’s ExtendScript/JS API instead of EasyRGB for an accurate color transformation.
The custom
rgbToLab
function in the gist below demonstrates how theSolidColor
class can be utilized to firstly create a new RGB color (using the given color components; R, G, B) and how to subsequently obtain the corresponding Lab values.This discrepancy occurs because Photoshop uses the D50 illuminant for Lab conversions (source) while the EasyRGB Color Converter uses D65 when converting from sRGB (notice the mention of "D65/2°" in the sRGB and XYZ rows of the conversion results, also the D65 illuminant is hardcoded in their pseudocode for this conversion). As far as I know there’s no way to make EasyRGB use a different illuminant when starting with sRGB.
As for the provided JavaScript code, I can tell that you’re using values that correspond to a D65 illuminant, so your code will generate Lab values that match those given by EasyRGB but not those given by Photoshop.
If you want to write code supporting both D50 and D65 illuminants, you need to use different values. The following values are from this source. In the JavaScript code above, the "D65 Illuminant, RGB to XYZ" set of these values is hardcoded into the
RGB_to_XYZ
function.D65 Illuminant, sRGB working space, RGB to XYZ:
0.4124564 0.3575761 0.1804375
0.2126729 0.7151522 0.0721750
0.0193339 0.1191920 0.9503041
D65 Illuminant, sRGB working space, XYZ to RGB:
3.2404542 -1.5371385 -0.4985314
-0.9692660 1.8760108 0.0415560
0.0556434 -0.2040259 1.0572252
D50 Illuminant, sRGB (Bradford-adapted) working space, RGB to XYZ:
0.4360747 0.3850649 0.1430804
0.2225045 0.7168786 0.0606169
0.0139322 0.0971045 0.7141733
D50 Illuminant, sRGB (Bradford-adapted) working space, XYZ to RGB:
3.1338561 -1.6168667 -0.4906146
-0.9787684 1.9161415 0.0334540
0.0719453 -0.2289914 1.4052427
There are also different white point values to use during the XYZ to Lab and Lab to XYZ conversions, depending on your illuminant. In the JavaScript code above these variables are named
ref_X
,ref_Y
, andref_Z
.D65 Illuminant, used for XYZ to Lab and Lab to XYZ conversions (source)
X: 95.047
Y: 100.0
Z: 108.883
D50 Illuminant, used for XYZ to Lab and Lab to XYZ conversions (source 1, source 2)
X: 96.42
Y: 100.0
Z: 82.49
I have tested conversions with these values myself and they do resolve the discrepancy between EasyRGB’s conversion and Photoshop’s conversion that you noticed. Using either set of values, you can go from RGB -> XYZ -> Lab -> XYZ -> RGB successfully (the original RGB values are recovered after converting). If the illuminant is D65, the XYZ and Lab values will match those given by EasyRGB. If the illuminant is D50, the Lab values will match those given by Photoshop.
Lastly, a little bit of context about the conversion algorithms I am referencing: