Color: from Spectrum to XYZ and beyond


Over the last few years, we have learned how to compute the XYZ tristimulus values from a spectrum. In addition, however, over these years, we have learned how to convert XYZ to RGB on my monitor… how to convert RGB to HSV… how to convert XYZ to CIELab… and how to display colors on the artists’ color wheel (which I distinguish from the printers’ color wheel).

Well, let’s have some fun. Some of us don’t have to learn how to do any of these things – we’ve done them all before – and we can focus on putting them all together.

As it happened, I felt a need to once again compute tristimulus values from a spectrum… and I decided to do everything else I could think of once I had XYZ.

Here I was trying to read chapter 1 of Kang’s “computational color technology”. He is describing how we compute tristimulus values X,Y,Z from a spectrum impinging on the eye of the standard observer. More precisely, he is describing different ways of breaking down that specific computation.

If I had looked carefully at what he did, I would have marked parts of it as careless… and I still will… but, instead, I decided that I needed to have an example handy. I hadn’t spent enough time with him to figure out what I really needed to do.

Now, we’ve done this computation – from spectrum to tristimulus – many, many times. (In fact, I’ve done it far more times than I have published.) I have plenty of examples laying around.

In addition, I thought it might not be a bad idea for me to work a new example, given how long it’s been since I worked one. And to top it off, I can do a couple more things nowadays. That is, since the last time I have worked one of these examples, I have written my own functions to convert XYZ to RGB on my monitor… and to convert XYZ to CIELab… and to display a color on a color wheel. And we know how to convert RGB to HSV… and maybe I could write code to display a color on a rectangular tint-tone-shade grid.

I have to say that I am not entirely happy with all the different things that need to be initialized… maybe I need to reorganize things even more than I already have.

Ultimately, I learned a few things.

Now let us work an example of that old oft-repeated calculation.

We assume that we have an illuminant spectrum, i.e. a light source. We assume that we have a reflectance spectrum, which describes how an equal-energy illuminant is reflected by an object. We assume that we have a standard observer (1931 CIE).

What Kang wrote – correctly – is that we can compute the tristimulus values as a matrix product:

XYZ = k A’E S.

More importantly, he wanted to write alternatives, looking at intermediate products.

XYZ = k (A’E) S = k A’ (ES) = k (A’S) E.

(If you think that doesn’t all work as written… you’re right!)

The way we have done it before does not look as elegant – but it is, in practice; Kang’s elegance is a mirage.

Let me use a screenshot as a table of contents:

Note, in particular, that all the links to relevant posts are at the end of this post.

By the way, not everything in this post is old stuff. In particular, I have two new graphical representations, one for HSB and one for CIELab. If you want to jump ahead to them, you’re looking for images like either of the following:


Let’s get an example.

1st, for no particularly good reason, I am going to choose standard illuminant C. I have it stashed away with a collection of illuminants, and after I execute the appropriate calls, here’s what I get:

Those numbers are every 5 nm… and, yes, I see that some of them are zero at the high wavelengths. Zero is OK.

I also have a collection of reflectance spectra stored away. I chose something called “Blue Sky”, from Appendix G of Glassner’s “Principles of Digital Image Synthesis”, which is a spectrum for one of the MacBeth ColorChecker color swatches. After I execute the appropriate calls, here’s what I get:

While the color of the dots was chosen to be suggestive, that’s all it is at this point: what I used is called “DodgerBlue”.

So far so good: let me work from 380-775 nm. That means I can use the entire “sky” spectrum.

By the way, I’m going to divide the reflectance spectrum by 100, so it is a fraction rather than a percentage, and I’ll call it S.

Next, I adjust the illuminant table so that it covers the same range of wavelengths. I should point out that I use the Interpolation command because it lets me cut down the spectrum by specifying wavelengths, rather than by my having to count entries and drop some number of them from the front and back.

It’s easy enough to look at them – though I don’t feel like adding the wavelengths to the drawing. That’s why the horizontal values run from 1 to 80 or so. (The wavelengths were there in XYZ when I built the A matrix.)

OK, those curves look like what I expect.

How have I computed the tristimulus values in the past?

First, multiply the illuminant and the object spectrum together pointwise. We are saying that the reflectance (object) spectrum times the illuminant spectrum gives us the reflected spectrum – the one that enters the eye of the standard observer.

Multiplying them pointwise, however, I am not doing a matrix multiplication, and I cannot describe my computation as a matrix equation. (That was the promise of Kang’s exposition.)

After computing the pointwise product s = \epsilon\ S\ , I restored the wavelength information, for plotting.

(I see that I should have only run to 770 nm, because one last entry of the C illuminant is zero. But it doesn’t affect the results – precisely because it’s zero.)

We apply AT to it… we also apply AT to the illuminant and take the middle value (the middle column of A = the middle row of AT is the “photopic response”, i.e. the total energy of the illuminant seen by the cones.)… divide:

Those are the tristimulus (XYZ) values.

Now what can we do with them?


Well, first of all I would use my monitor profile to convert to RGB, and draw it.

Ah, my code requires percentages. I had forgotten that. I could have swept this under the rug by not dividing the reflectance spectrum by 100 – but then I wouldn’t have seen places where my code expects percentages instead of fractions at present.

and we see below that colorsync gets .2918, .4407, .7537 . so my conversion is good.

Now that we have RGB, we can ask Mathematica® to draw a disk of that color.

We can also measure the XYZ of that disk:
18.222 18.359 37.982
using the DigitalColor Meter. (Our computed values are repeated as the very next output….)

CIE chromaticity chart

Now let’s put it on the CIE chromaticity chart. (We could have done it immediately, except that I wanted to know the color first.) How do we get chromaticity coordinates? Divide {X,Y,Z} by their sum and keep only the first two…

Since we have the RGB coordinates, we might as well use them to color the point:

(My initialization includes a list called “d31”, which was the original computation of that boundary.)


I can get HSV… we will get the H angle in degrees, but I will also need it as a fraction of 1 (for Mathematica’s Hue function):

Note that H is 220°.

Now let’s display some tint-tone-shades of that hue. I am using GraphicsGrid on a table of rectangles… all having hue H but variable saturation S and brightness B… and I rotated the grid 90°… and I used a “With” command to set the number of rectangles in both directions:

The upper right corner (northeast, NE, if you will) is that hue with S = 1 = V; i.e. the pure hue, fully saturated and maximum brightness.

The x-axis is S, saturation… the top row is maximum brightness (V=1), but saturation ranging from zero to 1 in seven increments of 1/7 That is, the top row is tints of this hue. It certainly looks like we’re mixing white with our pure color.

The y-axis is brightness V (or B)… The far right column is fully saturated (S=1), with brightness ranging from 0 to 1 in seven increments of 1/7. That is, the rightmost column is shades of this hue. It certainly looks like we’re mixing black with our pure color.

The other boxes are neither fully saturated nor fully bright: these look like the effect of mixing both black and white with our pure color.

I am unhappy with the bootom of that picture: too much black. Let’s spread things out:

There are still 8 rows, but the lighter shades and tones have been spread out by taking a square root.. Just to be explicit, the brightness values are

instead of

(And in case you were wondering about 8 rows but 7 increments… I hope that seeing the values settles any doubts.)

I have not figured out how to overlay our blue disk onto, or even next to, the Graphics Grid. I did try using the computed blue color as the background…

The result is a standard optical illusion: all the grid lines are the same color, but they look lighter or darker as they bound darker or lighter boxes, respectively. And (I think) we can see one square where some of the grid lines almost disappear – so that box is the closest to our computed blue background.

on the artists’ color wheel

Recall my artists’ color wheel. (It is characterized by green opposite red, whereas the printers’ color wheel has cyan opposite red.) Like so many things in this post, this image was created when I initialized parts of an “essential color” notebook.

Let me locate our computed blue color on that color wheel. Here’s the computation of angle:

We see that it is at 230° (clockwise from red) instead of at 220°. Good – the artists’ color wheel pushes cyan (and therefore blue) clockwise from its position on the printers’ (or computer’s) color wheel, where cyan is opposite red).

That may not be convincing. Let me get a color wheel with the same S and V as my computed blue.. and let me have 24 subdivisions instead of 12:

I think that looks a lot more plausible.


Next we can convert to CIELab… but I need a white point (called “w1” in the next set of commands). That’s OK, I know my monitor’s white point. By the way, I’m using my monitor white point because that is, after all, what I’m using for the color display; it has nothing to do with the white point of illuminant C.

I also need to multiply my stored XYZ values by 100 again; I know that because I used DigitalColor Meter to measure the CIELab coordinates of that blue disk! I’m really going to have to consider modifying my codes involving XYZ.

I’ll remind us that the first three components are L, a, b; the next two are called “chroma” and “hue”.

If I recall correctly – I’ve checked, I almost did – hue is arctan(b/a) and chroma is Sqrt[a^2 + b^2]…. Let me confirm that. Here are a and b:

Those are indeed the fourth and fifth numbers returned by my XYZtoLab.

Let me do something new. I already know – but I haven’t shown you – that the L values for a color wheel are not uniform… they’re not even smooth. Much as I would like to draw a color wheel for CIELab, I don’t know how to do it. But there is something I can do.

First – had that been “!st”, it would have been the result of dictation not typing – I need a table of HSV:

Those are equally spaced values of h (at full saturation and brightness), so they are the colors of the printers’ color wheel. Now I need to convert those to RGB:

OK. Those look very nice.

Now we go to XYZ:

Finally, we can go from XYZ to CIELab:

Now you see that L – the first column – is, as I said, all over the place… the chroma varies even more… and the hue angle can be negative. Note also that red – the first row – has a hue angle of 40.5°. This all makes it difficult to make a color wheel of equally spaced Lab hue angles – because I don’t know what the corresponding L and chroma values should be.

I want to append the Lab coordinates of our blue color:

Now, but only now, I want to append the RGB coordinates of our blue:

OK, here’s what I propose to do. Put a disk of color at coordinates (a,b). RGB values are in table t2… Lab values are in t4, so let me extract (a,b), which are columns 2 & 3 of table t4:

Here’s what we get (after I play with the Aspect Ratio):

Let me make it clear that the disk at (0, -40) is our computed blue… by stopping the index at 12, I get only the 12 colors of the color wheel.

I want to flip that around a vertical axis, and I want to rotate it so the red dot is at the top. We know the angle of the red dot in Lab…

That is, the angle is 40.5° . In the next commands, the {-1,1} in front of t5 changes the sign of a but not of b… and I put the table inside a Rotate command… and I played with the Aspect Ratio again to get circles instead of ellipses:

Let me explain this again, now that you’ve seen that last drawing. Each of those colored disks is a color from the 12-segment printers’ color wheel… but the coordinates of each disk are the (a,b) CIELab coordinates of that color. I am not using L at all.

As a check, let’s compare those colors to the printers’ color wheel. (This gives me an excuse to include it so you may compare it to the artists’ color wheel if necessary.)

As we would hope, both drawings show three greens, two blues, as well as purple and cyan, and other colors.

Let me pause here and take a breath. We started with 12 hues – equally spaced around the printers’ color wheel. We computed CIELab coordinates for all 12 colors… and then we put a disk of the appropriate color at the point (a,b). Since the hue is the angle, and the chroma is the magnitude, we have effectively plotted our colors in polar coordinates (r, theta) = (chroma, hue).

Well, the obvious next step is to do all that for the artists’ color wheel… but I’m going to stop here… the calculations are getting repetitious. We have to go from HSV to RGB to XYZ to CIELab all over again.


There isn’t much good to say here, but let me include it.

First, I thought that Mathematica could draw a CMYK color correctly – but it cannot tell you what the RGB values are! To be specific, Mathematica appears to use my monitor profile in order to display a CMYKColor specification; but if you ask Mathematica to convert that CMYKColor spec to RGB, it uses a different conversion algorithm.

So if I wanted a CMYK spec for our computed XYZ, I should use the ColorSync Utility to compute it.

No problem. Here’s what ColorSync gets for our XYZ to CMYK:

We’ve already seen what ColorSync got for the conversion of our XYZ to RGB:

We can confirm that ColorSync converts that RGB to that CMYK:

In other words, the two-step conversion XYZ to RGB to CMYK gets the same answer as the one-step conversion from XYZ directly to CMYK.

But, the next conversion is wrong. If I ask ColorSync to convert that CMYK to RGB, it gets a significantly different answer for B:


I found this because I asked Mathematica to draw the CMYK, expecting to get the same as from RGB. I ws shocked to see a different color. Mathematica is still using the monitor profile – the ColorSync Utility – but it’s being given a wrong answer.

That is, if I give Mathematica that CMYK profile…

… here’s what he draws:

The RGB values measured for that disk are:

… which are what ColorSync computed.

So: it appears that Mathematica draws a CMYK spec using the monitor profile, same as ColorSync. Unfortunately, ColorSync itself seems to mess up the conversion from CMYK to RGB.

Damn, indeed.


So why did I do all that?

Oh, because I wanted an example at hand while I was looking at Kang’s equations (p. 5).

He starts by writing

\Upsilon\ = k A’ E S,

where \Upsilon\ are the tristimulus values and k = \frac{100}{\bar{y}'E}\ – so we effectively agree on the scaling, but he multiplies where I divided and the 100 says he wants the answers in percentages rather than fractions. (Well, gee, so did I, although I didn’t remember it at first.)

That is, his E is my illuminant spectrum \epsilon\ , his S is my reflectance spectrum S, his \bar{y}\ is the middle column of A. Like him, I divided A’ES by \bar{y}'\ E\ . We’re good… except that I’m curious about the products E S and S E. I did it pointwise, but he doesn’t.

He defines three pairwise products, which I guess may be of interest each in its own right,

Æ’ = A’E

\eta\ = ES

Q’ = A’S

and writes his original equation three ways:

\Upsilon\ = k Æ’ S = k A’ \eta = k Q’ E.

Let me expand those:

\Upsilon\ = k (A’E) S = k A’ (ES) = k (A’S) E

Note that k A’SE ≠ k A’E S, unless we do something… since matrix multiplication is not commutative in general, S E and E S are not the same in general.

Let’s just look at the dimensions.

There’s no ambiguity about A’: it has 3 rows and n columns.
There’s no ambiguity about \Upsilon : it has 3 rows and 1 column.
Therefore, there’s no ambiguity about the last term in any of the equations: it must have 1 column.

But S and E cannot both be column vectors in the same equation. whichever is not last, must be a diagonal matrix.

That is,

in \Upsilon\ = k (A’E) S = k A’ (ES), S is a column vector and E is diagonal;
and in \Upsilon\ = (k A’S) E, E is a column vector and S is diagonal.

Well, rather than carry around two different forms of E and S, depending on whether I want to compute E S or S E, I would just as soon use one form – column vectors – and multiply them pointwise.

I suppose I should say this another way.

If you need to compute the pairwise product A’S first in (k A’S) E, then you must make S diagonal and E a column… if you need to compute the pairwise product A’E first in k (A’E) S, then you must make S a column and E diagonal.

And if you want to compute ES first in k A’ (ES), then you have two choices:
make S a column and E diagonal,
or make both E and S columns and multiply pointwise.

I have to say that almost all of this entire post was irrelevant to Kang’s equations. Sorting them out only required looking at the mandated sizes of the matrices. I did not need to have an example at hand.

But I had a lot of fun with this – despite some frustration and disappointment about CMYK and ColorSync.


Drawing the CIE chart is here.

This was the first post about going from spectrum to tristimulus… but the introduction to this post may be a good review. Then there was a post in which I illustrated several examples of going from spectrum to tristimulus.

The first post about HSV included the conversion between RGB and HSV. The second post about HSV discussed tint-tone-shade, and showed a rectangular grid of variable S and B for fixed H – but it was largely done “by hand”. The 3rd post about HSV looked more closely at saturation and brightness.

Here was the first discussion of my monitor and the nonlinear conversion between XYZ and RGB. And then there was a final post about my monitor, because the monitor profile changed.

Here was the post about converting between XYZ and CIELab.

Here was the original post about the two different color wheels I keep referring to, namely the artists’ and the printers’. Here is the post about getting color wheels the easy way and overlaying a set of colors on the artists’ color wheel, and the problem with Mathematica and CMYK.

Posted in color. 2 Comments »

2 Responses to “Color: from Spectrum to XYZ and beyond”

  1. kmoxe Says:

    I get very similar tristimulus (XYZ) values: 0.1828378 0.1844421 0.3805857
    But then it gets weird:
    Converting from XYZ to xyY I get: 0.2444795 0.2466247 18.44421
    And the book has: 0.2470 0.2514 19.33

  2. Color Systems — Part 2 - Vanseo Design Says:

    […] Color: from Spectrum to XYZ and beyond […]

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: