## Color – RGB and XYZ on my monitor, again

I set out to do something very simple: let Mathematica do the transformations between RGB and XYZ. Since CIELab is defined in terms of XYZ, but my monitor (“Color LCD” on a MacBook) is RGB, I’d like to be able to convert between RGB and CIELab.

Well, I showed us how to convert between RGB and XYZ, in a previous post about the nonlinearity of my monitor.

Recall the purple disk that figured so prominently in that post. It was defined by:

$RGB = \{0.5666,0.3877,0.6864\}$

and drawn by Mathematica® as:

Its original tristimulus values were

$XYZpur = \{28.891,24.93,37.567\}\$

But now they are:

$newPur = \{22.586,19.125,31.4\}$

The percent differences are: $\{-21.8234,-23.2852,-16.416\}\$.

Those are quite different. The smallest change is more than 16%! My monitor profile seems to have changed!

Let me say that I don’t particularly trust the new profile… I don’t know why it was changed, how often it was changed, when it will change next. I will probably have to make a habit of checking the profile after I install system updates. And just to be safe, I should probably check the XYZ values for that purple disk whenever I start playing with color.

The profile is now dated 6/24/11; but its immediate predecessor was dated earlier this year – which was still newer than the one I used for the previous measurements.

Anyway, it appears that I need to work out a new relationship between RGB and XYZ on this monitor.

Let me remark that the numbers listed for the “PCS Illuminant” are almost exactly what I measured for the white in the previous post. (Strictly speaking, divide w1 by 100.)

$w1 = \{96.42,100.003,82.489\}\$.

I think I know what the following does (and, yes, there are two additional identical graphs, presumably for green and blue)…

so let’s just code up their function f(x) with those parameters:

I’m a little unhappy with this function: it is not continuous at the break point. That is, the power term… evaluated at the point .0393, has the value

… while the linear term… evaluated at the point .0393, has the value

The values are not the same; the function is not continuous at .0393. I’m still mulling over whether this is a problem for inverting the function.

The function, however, does have one mandatory property: it maps 0 and 1 to 0 and 1. This means that the phosphors will not be affected by the nonlinear function. (That will make more sense very soon.)

Recall my artists’ color wheel and recall the measured RGB…

Here are the measured numbers, all in one long list (because I’m going to apply the nonlin function to each number):

Let me check the ColorSync computations. That is, I open the ColorSync Utility and I ask it to calculate tristimulus values (perceptual to perceptual) for each of the measured RGB values. Here are the answers:

$csTri = \left(\begin{array}{ccc} 36.15 & 20.8 & 2.65 \\ 38.41 & 24.29 & 3.26 \\ 45.49 & 35.24 & 5.15 \\ 59.08 & 56.26 & 8.79 \\ 80.15 & 88.85 & 14.44 \\ 51.67 & 72.46 & 12.35 \\ 44. & 68.05 & 11.78 \\ 60.27 & 79.2 & 79.84 \\ 16.27 & 11.15 & 68.05 \\ 23.94 & 15.57 & 68.62 \\ 52.42 & 31.95 & 70.71 \\ 39.6 & 23.17 & 17.09\end{array}\right)$

Let’s compare those to the measured tristimuus values:

$measTri = \left(\begin{array}{ccc} 36.151 & 20.801 & 2.655 \\ 38.409 & 24.292 & 3.259 \\ 45.496 & 35.254 & 5.157 \\ 59.079 & 56.259 & 8.792 \\ 80.151 & 88.846 & 14.438 \\ 51.678 & 72.464 & 12.347 \\ 44. & 68.045 & 11.783 \\ 60.269 & 79.199 & 79.834 \\ 16.269 & 11.154 & 68.051 \\ 23.947 & 15.573 & 68.616 \\ 52.42 & 31.955 & 70.706 \\ 39.606 & 23.169 & 17.108\end{array}\right)$

The two sets of numbers are pretty close. Here are the absolute errors…

$\left(\begin{array}{ccc} 0.001 & 0.001 & 0.005 \\ -0.001 & 0.002 & -0.001 \\ 0.006 & 0.014 & 0.007 \\ -0.001 & -0.001 & 0.002 \\ 0.001 & -0.004 & -0.002 \\ 0.008 & 0.004 & -0.003 \\ 0 & -0.005 & 0.003 \\ -0.001 & -0.001 & -0.006 \\ -0.001 & 0.004 & 0.001 \\ 0.007 & 0.003 & -0.004 \\ 0 & 0.005 & -0.004 \\ 0.006 & -0.001 & 0.018\end{array}\right)$

… and here are the percent errors:

$\left(\begin{array}{ccc} 0.003 & 0.005 & 0.188 \\ -0.003 & 0.008 & -0.031 \\ 0.013 & 0.04 & 0.136 \\ -0.002 & -0.002 & 0.023 \\ 0.001 & -0.005 & -0.014 \\ 0.015 & 0.006 & -0.024 \\ 0 & -0.007 & 0.025 \\ -0.002 & -0.001 & -0.008 \\ -0.006 & 0.036 & 0.001 \\ 0.029 & 0.019 & -0.006 \\ 0 & 0.016 & -0.006 \\ 0.015 & -0.004 & 0.105\end{array}\right)$

For the present, I’m OK with these measured and ColorSync numbers. (There’s nothing I can do about them, either.)

From the phosphor coordinates, we will need (and, once again, there are two additional identical graphs, labelled for green and blue):

… and I arrange those nine values in a 3×3 matrix M (and I define the transpose MT):

$M = \left(\begin{array}{ccc} 0.362 & 0.44 & 0.163 \\ 0.208 & 0.68 & 0.112 \\ 0.027 & 0.118 & 0.681\end{array}\right)$

Now I’m going to do what I did before:

1. apply the nonlinear function to the measured RGB
2. arrange those as 3-element vectors
3. apply M to each vector (actually, apply 100 times M)

The results are:

$compTri2 = \left(\begin{array}{ccc} 36.2 & 20.8 & 2.7 \\ 38.4557 & 24.2862 & 3.30495 \\ 45.5359 & 35.2282 & 5.20371 \\ 59.1219 & 56.2247 & 8.84723 \\ 80.2 & 88.8 & 14.5 \\ 51.6809 & 72.4133 & 12.3729 \\ 44. & 68. & 11.8 \\ 60.3 & 79.2 & 79.9 \\ 16.3 & 11.2 & 68.1 \\ 23.9809 & 15.6133 & 68.6729 \\ 52.5 & 32. & 70.8 \\ 39.6585 & 23.1764 & 17.1494\end{array}\right)$

How close are they to either the measured or ColorSync-computed values? Here are the differences from the measured values, rounded to .001 .

$\left(\begin{array}{ccc} -0.049 & 0.001 & -0.045 \\ -0.047 & 0.006 & -0.046 \\ -0.04 & 0.026 & -0.047 \\ -0.043 & 0.034 & -0.055 \\ -0.049 & 0.046 & -0.062 \\ -0.003 & 0.051 & -0.026 \\ 0 & 0.045 & -0.017 \\ -0.031 & -0.001 & -0.066 \\ -0.031 & -0.046 & -0.049 \\ -0.034 & -0.04 & -0.057 \\ -0.08 & -0.045 & -0.094 \\ -0.053 & -0.007 & -0.041\end{array}\right)$

Here are the % differences:

$\left(\begin{array}{ccc} -0.136 & 0.005 & -1.695 \\ -0.122 & 0.024 & -1.41 \\ -0.088 & 0.073 & -0.906 \\ -0.073 & 0.061 & -0.628 \\ -0.061 & 0.052 & -0.429 \\ -0.006 & 0.07 & -0.21 \\ 0 & 0.066 & -0.144 \\ -0.051 & -0.001 & -0.083 \\ -0.191 & -0.412 & -0.072 \\ -0.141 & -0.259 & -0.083 \\ -0.153 & -0.141 & -0.133 \\ -0.133 & -0.032 & -0.242\end{array}\right)$

Hmm. Red is off by quite a bit.

But that’s silly. My pure red is supposed to match the XYZ coordinates of the red phosphor.

So, I measure red to have… divide by 100… and if I round it to 3 places, I get… and then I recall the first column of M (actually, the first row of M transpose).

which is what the profile showed. It appears that the profile should have more digits than it shows. I’m going to check green and blue, but I’m already sure that they problem is simply that the phosphor values have been rounded off too far. What I’m checking is that the measured XYZ for red, green, and blue do in fact round off to the values I used for M.

What about green? I take the measured XYZ… divide by 100… round to 3 places… and compare it to the second row of $M^T\$:

The same.

Blue?

Yes again. Let me define a new matrix M; call it M2. Actually, let me initialize its transpose, i.e. $M2^T = M^T\$, and then redefine the transpose MT2 and then M2. M2 will be M with more digits.

Let’s try the new matrix M2 on the color wheel.

$compTri3 = \left(\begin{array}{ccc} 36.15 & 20.8 & 2.65 \\ 38.4057 & 24.2887 & 3.25392 \\ 45.4859 & 35.2388 & 5.14946 \\ 59.0719 & 56.2508 & 8.78681 \\ 80.15 & 88.85 & 14.43 \\ 51.6703 & 72.4633 & 12.3423 \\ 44. & 68.05 & 11.78 \\ 60.27 & 79.2 & 79.83 \\ 16.27 & 11.15 & 68.05 \\ 23.9403 & 15.5633 & 68.6123 \\ 52.42 & 31.95 & 70.7 \\ 39.6021 & 23.1658 & 17.0888\end{array}\right)$

Here are the differences between the newly computed XYZ and the ColorSync computations:

$\left(\begin{array}{ccc} 0 & 0 & 0 \\ -0.004 & -0.001 & -0.006 \\ -0.004 & -0.001 & -0.001 \\ -0.008 & -0.009 & -0.003 \\ 0 & 0 & -0.01 \\ 0 & 0.003 & -0.008 \\ 0 & 0 & 0 \\ 0 & 0 & -0.01 \\ 0 & 0 & 0 \\ 0 & -0.007 & -0.008 \\ 0 & 0 & -0.01 \\ 0.002 & -0.004 & -0.001\end{array}\right)$

Those are looking pretty good. You might note that precisely three rows are made up of three zeros. Guess what? Those are rows 1, 7, 9, i.e. red, green, and blue.

Here are the percent differences between the newly computed XYZ and the ColorSync computations:

$\left(\begin{array}{ccc} 0 & 0 & 0 \\ -0.011 & -0.005 & -0.186 \\ -0.009 & -0.004 & -0.01 \\ -0.014 & -0.016 & -0.036 \\ 0 & 0 & -0.069 \\ 0.001 & 0.005 & -0.063 \\ 0 & 0 & 0 \\ 0 & 0 & -0.013 \\ 0 & 0 & 0 \\ 0.001 & -0.043 & -0.011 \\ 0 & 0 & -0.014 \\ 0.005 & -0.018 & -0.007\end{array}\right)$

Here are the differences between the newly computed XYZ and the measured XYZ:

$\left(\begin{array}{ccc} -0.001 & -0.001 & -0.005 \\ -0.003 & -0.003 & -0.005 \\ -0.01 & -0.015 & -0.008 \\ -0.007 & -0.008 & -0.005 \\ -0.001 & 0.004 & -0.008 \\ -0.008 & -0.001 & -0.005 \\ 0 & 0.005 & -0.003 \\ 0.001 & 0.001 & -0.004 \\ 0.001 & -0.004 & -0.001 \\ -0.007 & -0.01 & -0.004 \\ 0 & -0.005 & -0.006 \\ -0.004 & -0.003 & -0.019\end{array}\right)$

And here are the percent differences between the newly computed XYZ and the measured XYZ:

$\left(\begin{array}{ccc} -0.003 & -0.005 & -0.188 \\ -0.008 & -0.014 & -0.156 \\ -0.022 & -0.043 & -0.146 \\ -0.012 & -0.015 & -0.059 \\ -0.001 & 0.005 & -0.055 \\ -0.015 & -0.001 & -0.038 \\ 0 & 0.007 & -0.025 \\ 0.002 & 0.001 & -0.005 \\ 0.006 & -0.036 & -0.001 \\ -0.028 & -0.062 & -0.005 \\ 0 & -0.016 & -0.008 \\ -0.01 & -0.014 & -0.112\end{array}\right)$

OK, I think the more precise M2 will do just fine.

Here’s the module I wrote for RGB to XYZ.

It computes a vector (r,g,b) = ( f(R), f(G), f(B) ) from the input vector (R,G,B), and then it applies 100 M to (r,g,b). Yes, this local copy of M is actually M2.

$y = 0.0774 x\$ becomes $x = y / .0774\$ .

The nonlinear portion becomes

$y = (.0521 + .9479 x)^{2.4}$

$y^{1/2.4} = .0521 + .9479 x$

$(y^{1/2.4} - .0521) / .9479 = x\$.

In place of (X,Y,Z) = 100 M (r,g,b), we will need

$(r,g,b) = M^{-1} (X,Y,Z) / 100\$.

Thus I write

It inverts a local copy of M… applies it to the given (X,Y,Z) and divides by 100… then applies the inverse of the nonlin function to each r,g,b value… and returns (R,G,B).

Now I need to check the modules.

Start with measured RGB, and partition it now, because the modules take 3 values at a time:

use the newly defined module to get XYZ:

Compare those to the previous computations:

$testTri-compTri3 = \left(\begin{array}{ccc} 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0. \\ 0. & 0. & 0.\end{array}\right)$

Call the inverse:

Did we recover what we started with?

Yes. So I have checked that the module RGBtoXYZ matches the original calculations, which in turn very nearly matched the ColorSync computations… and the inverse module XYZtoRGB recovers the original measured RGB.

One last thing. Let’s check the purple disk while we’re at it. That’s what started this whole thing!

Take the RGB… apply the module and get answers in line 2… and print the new measurements as line 3:

… so the newly computed XYZ are close to the newly measured XYZ.

So. I now have a pair of functions that will transform between RGB and XYZ on my monitor.

And the last post gave us functions that would transform between XYZ and CIELab… so now I can move between RGB on my monitor and CIELab.

And we have long been able to transform between HSB and RGB on my monitor, so I can now move between HSB and CIELab.

The one bad thing is that Mathematica cannot move between CMYK and RGB – as I keep saying, Mathematica draws the RGB given CMYK, just fine, but he can’t correctly describe the RGB coordinates of what he drew!

If I could bring myself to type in RGB coordinates of a color printed in a book, everything would be perfect… but I prefer to type in the CMYK coordinates of printed matter.

Still, we’ve come a long way.

Somewhere down the road I will look at CIECAM02, a true color appearance model… and I’ll probably look at hue in HSB and CIELab. (They’re not the same; worse, constant h in HSB is not constant h in CIELab. But HSB is still extraordinarily convenient for choosing colors on my monitor.)