# Why does ColorConvert give a different answer than other websites when converting from RGB to XYZ?

I was recently making a function that needed to convert between several different colorspaces, and I decided to do it manually. @J.M. used many built-in functions to accomplish the same goal. When I was trying to delve in and see if they produced the same results, I found that visually they did, but numerically there were differences, and so I tried to track them down.

In my code, I needed a function to convert from RGB to XYZ colorspaces, and I used the recipe described on this Wikipedia page, which is the same as is used by EasyRGB, and appears in this paper.

The recipe goes as follows: first, the RGB values are converted to "linear RGB" via

$$C_\mathrm{linear}= \begin{cases}\frac{C_\mathrm{srgb}}{12.92}, & C_\mathrm{srgb}\le0.04045\\ \left(\frac{C_\mathrm{srgb}+a}{1+a}\right)^{2.4}, & C_\mathrm{srgb}>0.04045 \end{cases}$$

and then the XYZ values are obtained from a linear transformation,

$$\begin{bmatrix} X\\Y\\Z\end{bmatrix}= \begin{bmatrix} 0.4124&0.3576&0.1805\\ 0.2126&0.7152&0.0722\\ 0.0193&0.1192&0.9505 \end{bmatrix} \begin{bmatrix} R_\mathrm{linear}\\ G_\mathrm{linear}\\ B_\mathrm{linear}\end{bmatrix}$$

So I define this as a function rgb2xyz,

rgb2xyz[r_, g_, b_] := Module[
{transm, rl, gl, bl},
{rl, gl, bl} = If[# > .04045,
((# + 0.055)/1.055)^2.4,
#/12.92] & /@ {r, g, b};
transm = {{0.4124, 0.3576, 0.1805},
{0.2126, 0.7152, 0.0722},
{0.0193, 0.1192, 0.9505}};
transm.{rl, gl, bl}
];


and then I can apply it to a test color,

testcolor = {43, 15, 155}/255.;
rgb2xyz @@ testcolor
(* {0.0708348, 0.032218, 0.312589} *)


Then I realized there is a ColorConvert function, and test it against my function.

List @@ ColorConvert[RGBColor @@ testcolor, XYZColor]
(* {0.0592725, 0.0286686, 0.234891} *)


which seems to be a reasonably large discrepancy. Visually, I can see a very small difference

XYZColor @@@ {%, %%}


When I go to other websites, like ColorMine or EasyRGB their results agree with mine completely (although they scale their results by a factor of 100).

What is going on here? Is Mathematica using a different RGB colorspace than sRGB?

• Just a comment, on last EWTC one of the developers said sRGB is what MMA is using. – Kuba Dec 10 '15 at 13:00
• @Kuba, Thanks - I just found that in the documentation for ColorConvert – Jason B. Dec 10 '15 at 13:06

Just for reference purposes, here is a manual re-implementation of ColorConvert[color, "RGB" -> "XYZ"]:

(* whitepoints *)
d65 = {0.95047, 1., 1.08883}; d50 = {0.96422, 1., 0.82521};

(* cone response domain *)
{-0.7502, 1.7135, 0.0367},
{0.0389, -0.0685, 1.0296}};

(* inverse companding function *)
InversesRGBGamma = Function[x, Piecewise[{{x/12.92, x <= 0.04045}},
((x + 0.055)/1.055)^2.4], Listable];

(* chromaticity coordinates of primaries (RGB) *)
srgb = {{0.64, 0.33}, {0.3, 0.6}, {0.15, 0.06}};

(* chromatic adaptation matrix, D65 to D50 *)

(* RGB -> XYZ matrix *)
r2x = With[{xyz = Transpose[{#1/#2, 1., (1 - #1 - #2)/#2} & @@@ srgb]},
xyz.DiagonalMatrix[LinearSolve[xyz, d65]]];

rgb2xyz[col_RGBColor] := d65tod50.r2x.InversesRGBGamma[List @@ col]


Example:

rgb2xyz /@ ColorData[61, "ColorList"]
{{0.199667, 0.106547, 0.00721723}, {0.073949, 0.056383, 0.147498},
{0.823078, 0.923995, 0.234938}, {0.226765, 0.233149, 0.365262},
{0.099347, 0.136449, 0.027346}, {0.521978, 0.511432, 0.0633633},
{0.0405846, 0.033663, 0.113543}, {0.3308, 0.169661, 0.0191982},
{0.671893, 0.718828, 0.33747}}

List @@@ ColorConvert[ColorData[61, "ColorList"], XYZColor]
{{0.199667, 0.106547, 0.00721724}, {0.0739489, 0.0563829, 0.147498},
{0.823078, 0.923995, 0.234938}, {0.226765, 0.233149, 0.365262},
{0.099347, 0.136449, 0.027346}, {0.521978, 0.511432, 0.0633633},
{0.0405845, 0.033663, 0.113543}, {0.3308, 0.16966, 0.0191983},
{0.671893, 0.718828, 0.33747}}


There are discrepancies, but they seem slight.

For completeness, here is the manual "XYZ" -> "RGB" conversion:

sRGBGamma = Function[x, With[{z = Abs[x]},
Sign[x] Piecewise[{{12.92 z, z <= 0.0031308}},
1.055 z^(1/2.4) - 0.055]],
Listable];

xyz2rgb[col_XYZColor] :=
Clip[sRGBGamma[LinearSolve[r2x, d50tod65.(List @@ col)]], {0., 1.}]

• Nice! I had something similar here, yours is easier to follow what is happening though (and I multiplied the answer by 100 in trying to match the recipe in Moreland's paper) – Jason B. Jun 15 '16 at 13:20
• I figured a Mathematica summary of the stuff in Lindbloom's website would be useful for those trying to debug their color conversions. I believe we've both been bitten by mistaken whitepoint assumptions, and I'm sure we won't be the only ones. – J. M.'s ennui Jun 15 '16 at 13:23

Hopefully someone else can shed more light on this, someone with more expertise on colorspaces, but I was able to find out a little bit more. I found Bruce Lindbloom's website which lists many different transformation matrices for going from RGB to XYZ. The matrix used in the above-linked wiki page corresponds to converting between sRGB and XYZ, using a reference white of D65. I know not what the reference white value means, but it will apparently be important here.

I tried the hard way to find out what matrix Mathematica uses to convert between RGB and XYZ, just by taking 3 random colors and converting them and then solving for the matrix,

varmat =
Array[ToExpression[
"m" <> IntegerString[#1] <> IntegerString[#2]] &, {3, 3}];
eqn = {x, y, z} == varmat.(If[# > .04045,
((# + 0.055)/1.055)^2.4,
#/12.92] & /@ {r, g, b});
eqns = Table[
rgb = RandomReal[1, 3];
xyz = List @@ ColorConvert[RGBColor @@ rgb, XYZColor];
eqn /. (#1 -> #2 & @@@ Transpose@{{r, g, b}, rgb}) /. (#1 -> #2 & @@@
Transpose@{{x, y, z}, xyz})
, {3}];
varmat /. First@Solve[eqns, Flatten@varmat] // MatrixForm


And this matches exactly the transformation listed on Lindbloom's site for sRGB with a reference white of D50. I had not seen anything listed on the help pages for RGBColor or XYZColor about a reference white. But then I found the following on the page for ColorConvert

ColorConvert automatically performs chromatic (white point) adaptation. D50 white point is assumed for "XYZ", "LAB", "LUV", and "LCH" and D65 for "RGB", "CMYK", "HSB", and "Grayscale".

So that solves this problem, I post it here in case anyone in the future is trying to convert colors and notices the discrepancy.

I do wonder why Mathematica uses a different reference white than seems to be the norm for sRGB.

Edit: I was also able to find that Mathematica uses the D50 Reference White when converting to CIELAB from XYZ, which is again different than many other sites that have conversion utilities.

• Ah, you've seen Lindbloom's site already... another thing to take into account would be chromatic adaptation when converting between systems with different white points. – J. M.'s ennui Dec 10 '15 at 14:50
• About the white point: it's just a way to fix where "white" ought to be in the chromaticity diagram; you might want to look at ChromaticityPlot[]. – J. M.'s ennui Dec 10 '15 at 14:52
• >I do wonder why Mathematica uses a different reference white than seems to be the norm for sRGB Given that sRGB is defined respect to D65 and CIEXYZ respect to D50 that's exactly the matrix you want to use. – Batracos Mar 16 '16 at 16:22
• As far as I know CIELAB inherits the white point from the XYZ it originates from. – Batracos Mar 16 '16 at 16:24
• TL;DR is that RGB is a colour encoding model, not a colour space. That means that there are an infinite number of RGB colour spaces. Indeed sRGB, AdobeRGB, etc. are a couple of many. Defining a colour space in RGB requires transfer function(s), RGB primaries, and white point. colour-science.org/posts/… Accepted answer is wrong, this answer is closer, but still leaves a bit to be desired. Answering your last question, see ICC D50. ICC's working space is D50, so the matrices dumped from the ICC is D50 based. – troy_s Dec 20 '19 at 22:37