🌈 Building a rainbow

Published on

We are going to generate an image of the human visible color spectrum. Not quite a rainbow, but very colorful either way!

But first: what determines the perceived color of light? That would be cone cells in your eyes, these are cells sensitive to light. A typical human has three different kinds of cone cells, that respond differently based on the wavelength of the light that hits the cell. The combined activation of these cone cells determines the perceived color.

Get human color vision data

The human color vision data we will use is publicly available and was originally published in 1931 (almost 100 years ago!). It is a mapping of wavelengths (in nm) to three values defining the CIE XYZ color space and was gathered through a series of experiments where people had to match colors.

Note that the three values are not activation values of the three different cone cells. You can still think of the values as some other kind of light sensitive sensors. The values produced by the mapping are called tristimulus values X, Y and Z. Y is an important value as it encodes the brightness.

Normalize XYZ values

We will normalize our tristimulus relative to a standard illuminant. There are different such illuminants, but we will use D65 (usually assumed choice). Each standard illuminant is defined by a spectral power distribution which basically describes how much light at each wavelength that illuminant emits. This data is also available for download.

We are going to integrate over this spectral power distribution to find the Y value for the D65 illuminant. This Y value encodes the brightness of D65 and we will accept it as unit brightness. To integrate we take all the D65 powers between 360 nm and 830 nm and multiply them with the y values from the CIE 1931 data between 360 nm and 830 nm, and then sum them.

Interestingly, the XYZ data goes over a range of 360 to 830 nm, but the D65 data starts at 300 nm. So if you want to integrate yourself, make sure to map to the right wavelengths and not assume that both start at the same wavelength.

Also, even though the CIE XYZ data goes from 360 nm to 830 nm, we can only see wavelengths between approximately 380 nm and 700 nm. This means that the sides of our spectrum should be black in the end.

Convert XYZ to sRGB

Lucky for us, a CIE XYZ to sRGB mapping is readily available in the form of a matrix! According to Wikipedia, the X, Y, and Z values must be scaled such that the Y of D65 is 1.0. Good thing we already did this ☺️

Also, the RGB color after the matrix multiplication is "linear", but RGB colors are not actually saved in this format for sRGB. You still need to apply so called gamma correction. This changes how brightness values are distributed: instead of evenly spaced (linearly), more precision is given to dark values and less precision to bright values.

Lo and behold

If we now render our spectrum it would be black, because the wavelength emissions are too weak. Remember that we are rendering light of a single wavelength, each with only a power of 1. This is very weak compared to our reference D65 illuminant. In order to get something visible we just have to increase the power. I have chosen 4,000 because it gives a nice, bright image without capping out any RGB channels. Now, we can finally see our beautiful, spectral rainbow in all its glory!

Wavelengths from 360 nm to 830 nm rendered as RGB colors.

Corrections

Actually, this image is not an accurate representation of the perceived color of each wavelength. Almost no purely spectral color can be displayed with sRGB. After the mapping from XYZ to RGB some of the channels will be negative, but we cannot display negative RGB values. A little disappointing πŸ˜” These values I clamped to 0, so that they are as close as possible to being accurate, but they will not look the same as real monochromatic light.

Afterthoughts

You can use this Python script to generate the image yourself. Data required for the script: CIE XYZ data and D65 standard illuminant data.

This colorspace stuff is pretty complicated, man.