Why Displays Cannot Show Pure Emission Lines
So how can the observed colour from spectra be displayed? An exploration of the CIE 1931 color space and how that relates to sRGB. Python code included.
Introduction
Okay, you got yourself a spectrometer and you now have a spectra. As a sensible person, you did not stare into the end of the fiber optic cable to see what colour that spectra represents. (And seriously, do not do this with your remaining eye either!) So you decide to simulate what that colour is from the spectra you obtained but as it turns out, this task is not trivial. This was the story of how this blog post came to be.
In this blog post, we will discuss human vision and the colour spaces that are necessary to represent spectra in the "right" colour - in particular the CIE 1931 XYZ colour space. In doing so, we will discover that there are colours that we can see but are not possible to display on screens and how to work around this. Lastly, as a demonstration, we will see how spectra can be coloured.
Human Vision
The ordinary human eye has three types of cone cells that are sensitive to different spectral ranges under bright conditions. (When it is dark, rod cells dominate and they give us monochromatic vision.) They are the short "S", middle "M", and long "L" cones and their sensitivity is shown in Figure 1. Together, this forms the LMS colour space and is good for simulating various human eye visual receptions but is otherwise not very useful for computer displays. We will get to why later but the fundamental reason lies in the overlap between the L, M, and S sensitivities.
Displays and the sRGB Colour Space
According to this website, the definition of colours that can be displayed on screens fall under the Standard RGB (sRGB) standard. So in order to represent a colour to show on our screens, we must produce an sRGB representation from a spectra. As it turns out, this conversion between from spectra to sRGB cannot be done directly and we must turn to a standard reference that defines other colour spaces called the CIE XYZ colour space.
The CIE XYZ Colour Space
The Commission Internationale de l'éclairage (CIE) 1931 XYZ colour space defines all colour sensations an average human can experience.
Matching Functions
The matching functions were obtained by the standard observers and is shown below (produced from an analytic approximation). Overall, these are similar to the LMS plot in Figure 1 with a notable difference - the bumb in X($\lambda$) around 450 nm. I haven't found conclusive evidence why that is but after experimenting in code, this bump is needed to get conversions to indigo and violet for wavelengths close to 400 nm. Without this bump, the displayed colour would just be blue and if you've seen a rainbow, the colours of the rainbow do not go from red to green to blue but includes a shade of purple too.
plot_cie_xyz_matching_functions()
Here is a rainbow for good measure.
In Figure 3, the boundary of the coloured shark fin represent the colour of each pure wavelength. By definition, the sRGB colour space is a subset of the CIE XYZ. Any chromaticity that lies outside the sRGB triangle will have some R, G, or B values that are negative and cannot be displayed. Let's look at why this is the case using an example.
The white point is marked as D65. What is the reasoning behind this naming?
D65 and the CIE Standard Illuminant
D65 is known as the CIE standard illuminant and is roughly equivalent to daylight at midday Europe. The 65 refers to a colour temperature of 6500 K (actually 6504 K after physical constants were revised). There are other standard illuminants defined by different colour temperatures.
How to display a pure green spectral line?
Let's use the CIE XYZ space for the sake of this argument. The same reasoning can be applied to the LMS colour space. In our example, we assume that green on the display is emitted at 532 nm (will depend on your display!). Then this pure spectral line will have components in X, Y, and Z but most of the contribution is towards Y.
wavelength2xyz(532) # X, Y, and Z contributions at 532 nm
If we treated these values as the same as sRGB and that the possible sRGB values lie between 0 (min) and 1 (max), then what colour will be displayed?
plot_colour(wavelength2xyz(532))
Then the colour will less green than expected, whitened by adding blue and red, and shifted a little more red when it should just display only green components! Can you tell the difference bewteen the green above and below?
plot_colour([0,1.,0])
The same can be said for any other pure wavelength.
How to deal with negative sRGB values
Here, we will see two ways we can deal with negative sRGB values. They are: clamping all negatives to zero, and whitening. In whitening, we add to all channels until all numbers are non-negative then rescale the result such that the original max sRGB value is unchanged. The following plots will show you what the difference is.
plot_wavelength_colours("CLAMP")
plot_wavelength_colours("WHITEN")
bb = Blackbody(5778)
bb.plot()
def show_blackbody_colour(T_K):
bb = Blackbody(T_K,np.linspace(380,750))
sRGB = spectra2sRGB(bb.λ_nm,bb.B_λT)
# Due to normalisation choices, the brightness can change depending on the spectra
# show the same hue at max brightness
HSV = cv2.cvtColor(np.reshape(sRGB,(1,1,3)), cv2.COLOR_RGB2HSV_FULL)
HSV[0,0,2] = 255
RGB = cv2.cvtColor(HSV, cv2.COLOR_HSV2RGB_FULL)
plot_colour(RGB)
show_blackbody_colour(5778)
show_blackbody_colour(11000) # what about the star Rigel?
plot_hsv_LUT_spectrum()
This is a simulated HgAr spectra.
lines_nm = [254,436,546,764,405,365,578,750,738,697,812,772,912,801,842,795,706,826,852,727] # approx sorted by emission strength
img = np.zeros((100,1000))
wavelengths = np.linspace(350,850,1000)
strength = 1.
for line in lines_nm:
indx = np.sum(wavelengths<line)
if indx > 0 and indx < 1000:
img[:,indx-2:indx+2] = strength
strength -= 0.04
plt.imshow(img,cmap="gray",extent=[np.min(wavelengths),np.max(wavelengths),0,np.shape(img)[0]])
plt.xlabel("wavelength (nm)")
colour_hyperspectral_line(wavelengths,img)
Conclusion
In this blog post, we learned that displays cannot show all the colours we can see. We saw how the CIE 1931 XYZ colour space can be used to generate suitable sRGB values to display including ways to mitigate negative sRGB values. As a cool demo, we ended on colouring in the HgAr emission spectra.