Rec. 2020 RGB to Spectrum Conversion for Reflectances

    \[ \begin{comment} <a href="" target="_blank" rel="noopener noreferrer"><img class="alignright size-full wp-image-1801" src="" alt="PDF logo" width="32" height="32" /></a> \end{comment} \]

Published October 1, 2018 by Scott Allen Burns, last updated Nov 3, 2019

Change Log
10/1/18 Initial publication.
10/2/18 Added section of object colors and XYZ color space.
11/3/19 Clarified some points and removed the XYZ space section.


In a previous presentation on generating reflectance curves from sRGB triplets, I presented several algorithms for creating a reasonably realistic spectral reflectance curve associated with a given sRGB color. This publication is a short note on applying the methods to a more recent RGB implementation called Recommendation ITU-R BT.2020-2 (10/2015), or Rec. 2020 for short.

Generating a New T Matrix

The colorimetric specification of this RGB system is:

The main task in applying the reflectance generating algorithms to Rec. 2020 is to create a new T matrix (called T_{2020} here) that relates reflectance, \rho to linear Rec. 2020 rgb. Once this is done, just substitute T_{2020} for T in any of the previous algorithms to make them specific to Rec. 2020. Recall that T is defined as


where A' is a 3×36 matrix of color matching functions (CMFs) that relate a 36×1 reflectance vector, \rho, to XYZ tristimulus values. W is a 36×1 illuminant vector, which is Standard Illuminant D65 in this case. The expression diag(W) refers to a square 36×36 matrix with W on the main diagonal and zeros elsewhere. w is a weighting factor that is calculated as the dot (scalar) product of the second row of A' and W. The matrix M^{-1} here is the tricky part. It relates XYZ to linear rgb and must be custom built for Rec. 2020.

Bruce Lindbloom has a handy page that describes the creation of M. The computation only requires the chromaticity coordinates of the RGB primaries and the tristimulus values of the reference white. For D65, these are (X_W, Y_W, Z_W) = (0.95047, 1.00000, 1.08883).

I made one minor adjustment to the Rec. 2020 specs to compensate for the 10 nm intervals in our calculations. The R/G/B chromaticity coordinates shown in Table 3 above correspond to single wavelength primaries at 630 nm, 532 nm, and 467 nm, respectively. Since the last two wavelengths don’t fall on the 10 nm intervals, I modified them just slightly. The G primary is changed to be the combination of 80% 530 nm light and 20% 540 nm light. The B primary is changed to be 30% 460 nm light and 70% 470 nm light. This is shown graphically to the right (click to enlarge).

The chromaticity coordinates of the slightly modified primaries come out to be (x_r, y_r) = (0.7079, 0.2920), (x_g, y_g) = (0.1718, 0.7941), and (x_b, y_b) = (0.1312, 0.0478). This change greatly improves the stability of the optimization process (by avoiding slight constraint infeasibility very near the spectral envelope) and changes the T_{2020} values by only 0.00074 at most.

Plugging these values into the equations on Bruce Lindbloom’s page yields an M^{-1} matrix of:

    \[ M_{2020}^{-1} =  \left[ \begin{array}{ccc} 1.72466 & -0.36222 & -0.25442 \\ -0.66941 & 1.62275 & 0.01240 \\ 0.01826 &-0.04444 & 0.94329 \end{array} \right], \]

We now have all the ingredients to build our T matrix:


which is available here.

Here are some results from applying a general-purpose optimization code (Matlab’s fmincon) to the nonlinear program (ILSS), using T_{2020} instead of the sRGB version of T,

    \[\begin{split}\mathsf{minimize}\;\;&\sum_{i=1}^{35} (\rho_{i+1} - \rho_i)^2 \\ \mathsf{s.t.}\;\;&T_{2020}\;\rho = rgb \\ &\rho \le 1,\\ &\rho \ge 0.\end{split}\]

As expected, a larger rgb of (1,1,1) produced a flat reflectance curve at 1.

An example of a color like mauve, which is smooth, as expected.

The brightest purely saturated red I could find that worked in the optimizer.

I tried to use a highly saturated rgb, like (1,0,0), but the method failed (the optimizer reported it lost constraint feasibility). I used progressively smaller values for r until I found something that worked, rgb = (0.079, 0, 0). It ended up being a single wavelength spike at the Rec. 2020 r primary (630 nm). Even though the optimizer was trying to find the reflectance curve with least slope squared, it had no recourse but to end up with a very non-smooth solution. This issue is addressed in the next section.

Rec. 2020 and “Real” Reflectances

When I originally developed the methods for converting sRGB values to reflectances, I was pleased to find that they worked for all values of sRGB. Furthermore, the two methods that guarantee reflectance curves in the range 0 to 1 produced a valid answer for all sRGB inputs. This will not be the case with Rec. 2020. Because the primaries are single wavelength lights, there are values of linear Rec. 2020 rgb triplets that have no corresponding reflectance curves in the range 0 to 1.

Consider a reflectance curve with all zeros except at 630 nm, which is given a value of 1. This is the red primary in this color space. If we pre-multiply this reflectance by T_{2020}, we will get the Rec. 2020 rgb triplet corresponding to the reflectance. It comes out to be rgb = (0.0798, 0, 0). This reflectance represents a very dim, but extremely saturated red color. If we try to find a reflectance curve with a larger red rgb component, the only way that is possible is for the reflectance to exceed 1 in some places. This will manifest itself in the optimization methods as lost feasibility in the case of general-purpose optimization codes, or as bad conditioning (singular matrices) in the case of the tightly implemented algorithms like ILLSS, etc.

I was curious to find out just how much of the Rec. 2020 color space can be represented by reflectance curves with 0-1 range. I wrote a program that generated a large set of reflectance curves that contained only 1s and 0s, having one or two contiguous blocks of 1’s and 0s elsewhere. These are “optimal” colors that form the outer surface of the object color solid. All of these reflectance curves were then premultiplied by T_{2020} to get their corresponding rgb values, which were then plotted in Rec. 2020 color space. The rgb values formed the boundary of a region in Rec. 2020 color space (roll mouse over to animate):

Rec. 2020 color space, showing the spectral envelope (black) and the region of rgb values that can be represented by a reflectance curve with magnitudes between 0 and 1. The locations of rgb (1,0,0), (0,1,0), and (0,0,1) are also shown.

Here are some views from specific angles:

Another angle of the rotation of axes gif (hover mouse over it to animate, click to enlarge).

Region of “real” object colors (blue) in Rec. 2020 space, viewed down the r-axis.

Region of “real” object colors (blue) in Rec. 2020 space, viewed down the g-axis.

Region of “real” object colors (blue) in Rec. 2020 space, viewed down the b-axis.

The conclusion here is that if Rec. 2020 is used with any of the optimization-based methods, an additional check to make sure the rgb values are within this “real” color region should be performed to make the implementation robust.

One final observation relates to the spiked reflectance curve for rgb = (0.079, 0, 0), above. If a tiny bit of another rgb component is added, i.e., rgb = (0.079, 0.079, 0), the nature of the curve changes dramatically:

Adding a tiny bit of another rgb component changes the spiked curve into a very smooth one.

Apparently, the additional green component pulls the color far enough away from the spectral envelope to allow the optimizer to find the broad, smooth spectrum reflectance curve it is seeking.


Thanks to Mark (user kram1032), Chris Cook (user smilebags) and Troy Sobotka (troy_s) on a Blender discussion site for the interesting discussions that prompted me to look into this.


Creative Commons License
Rec. 2020 RGB to Spectrum Conversion for Reflectances by Scott Allen Burns is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.