RGB/XYZ Conversion Matrix Accuracy

Eric Lengyel   •   December 1, 2019

The second volume of the Foundations of Game Engine Development series (FGED2) contains a derivation of the 3×3 matrices that transform between the standard RGB color space and the CIE XYZ color space. The resulting matrix entries that I published are a little different from the numbers published in both Real-Time Rendering, 3rd edition (RTR3) and Physically Based Rendering, 2nd edition (PBR2). This post is a quick note about how I arrived at my numbers and why they are correct.

On page 14 of FGED2, the following matrix is given for converting XYZ colors to sRGB colors.

\(\mathbf M_{\mathrm{sRGB}} = \begin{bmatrix}\phantom{-}3.240970 & -1.537383 & -0.498611 \\ -0.969244 & \phantom{-}1.875968 & \phantom{-}0.041555 \\ \phantom{-}0.055630 & -0.203977 & \phantom{-}1.056972\end{bmatrix}\)

And the following matrix is the inverse given for converting back from sRGB to XYZ.

\(\mathbf M_{\mathrm{sRGB}}^{-1} = \begin{bmatrix}0.412391 & 0.357584 & 0.180481 \\ 0.212639 & 0.715169 & 0.072192 \\ 0.019331 & 0.119195 & 0.950532\end{bmatrix}\)

However, on page 215 of RTR3, the same matrix \(\mathbf M_{\mathrm{sRGB}}\) has the following entries. (It appears that this matrix was altogether removed from the 4th edition of RTR.)

\(\mathbf M_{\mathrm{sRGB}} = \begin{bmatrix}\phantom{-}3.240{\color{red} 479} & -1.537{\color{red} 150} & -0.498{\color{red} 535} \\ -0.9692{\color{red} 56} & \phantom{-}1.8759{\color{red} 92} & \phantom{-}0.04155{\color{red} 6} \\ \phantom{-}0.0556{\color{red} 48} & -0.20{\color{red} 4043} & \phantom{-}1.05{\color{red} 7311}\end{bmatrix}\)

And the entries of its inverse are the following.

\(\mathbf M_{\mathrm{sRGB}}^{-1} = \begin{bmatrix}0.412{\color{red} 453} & 0.35758{\color{red} 0} & 0.1804{\color{red} 23} \\ 0.2126{\color{red} 71} & 0.71516{\color{red} 0} & 0.0721{\color{red} 69} \\ 0.01933{\color{red} 4} & 0.11919{\color{red} 3} & 0.950{\color{red} 227}\end{bmatrix}\)

The digits colored red are the ones that differ from the digits published in FGED2. On page 273 of PBR2, the matrix \(\mathbf M_{\mathrm{sRGB}}\) is the same as the one in RTR3 except that the center entry has the slightly different value 1.875991. The inverse matrix in PBR2 is identical to the one in RTR3. The fact that RTR3 and PBR2 agree so well suggests that they got their numbers from the same source, but I have not been able to identify that source. (The two sources cited in RTR3 don’t actually provide these matrices.)

So why the difference? FGED2 disagrees with both RTR3 and PBR2 in the final digit of every matrix entry, and the disagreement extends to as many as four decimal places in some entries. To demonstrate that the numbers given by FGED2 are correct, I have carried out their calculation in exact rational arithmetic below. I suspect that the numbers given by RTR3 and PBR2 were calculated in some limited-precision fashion that accumulated rounding errors at each step.

The sRGB specification defines the red, green, and blue primaries as having the exact chromaticity coordinates \((x_{\mathrm R}, y_{\mathrm R}) = (0.64, 0.33),\) \((x_{\mathrm G}, y_{\mathrm G}) = (0.30, 0.60),\) and \((x_{\mathrm B}, y_{\mathrm B}) = (0.15, 0.06).\) The D65 white point used by sRGB has the exact chromaticity coordinates \((x_{\mathrm W}, y_{\mathrm W}) = (0.3127, 0.3290).\) Using the relationship \(z = 1 - x - y,\) we also have \(z_{\mathrm R} = 0.03,\) \(z_{\mathrm G} = 0.10,\) \(z_{\mathrm B} = 0.79,\) and \(z_{\mathrm W} = 0.3583.\) When we plug all of these numbers into Equation (5.9) in FGED2 as exact fractions, we get the following equation.

\(\begin{bmatrix}\frac{3127}{3290} \\ 1 \\ \frac{3583}{3290}\end{bmatrix} = \begin{bmatrix}\frac{64}{33} & \frac{1}{2} & \frac{5}{2} \\ 1 & 1 & 1 \\ \frac{1}{11} & \frac{1}{6} & \frac{79}{6}\end{bmatrix}\begin{bmatrix}Y_R \vphantom{\frac{1}{1}} \\ Y_G \vphantom{\frac{1}{1}} \\ Y_B \vphantom{\frac{1}{1}}\end{bmatrix}\)

Solving for the luminances Y produces the exact values shown below.

\(\begin{bmatrix}Y_R \vphantom{\frac{1}{1}} \\ Y_G \vphantom{\frac{1}{1}} \\ Y_B \vphantom{\frac{1}{1}}\end{bmatrix} = \begin{bmatrix}\frac{87098}{409605} \\ \frac{175762}{245763} \\ \frac{12673}{175545}\end{bmatrix}\)

Using these values in Equation (5.11) gives us the following matrix.

\(\mathbf M_{\mathrm{sRGB}} = \begin{bmatrix}\frac{12831}{3959} & -\frac{329}{214} & -\frac{1974}{3959} \\ -\frac{851781}{878810} & \frac{1648619}{878810} & \frac{36519}{878810} \\ \frac{705}{12673} & -\frac{2585}{12673} & \frac{705}{667}\end{bmatrix}\)

The exact inverse of this matrix is the following.

\(\mathbf M_{\mathrm{sRGB}}^{-1} = \begin{bmatrix}\frac{506752}{1228815} & \frac{87881}{245763} & \frac{12673}{70218} \\ \frac{87098}{409605} & \frac{175762}{245763} & \frac{12673}{175545} \\ \frac{7918}{409605} & \frac{87881}{737289} & \frac{1001167}{1053270}\end{bmatrix}\)

These matrices were calculated with Mathematica. When we carry out the division for each entry and round to six decimal places, we get the values that are published in FGED2. Because no rounding occurred before these final divisions, we know with certainty that the values are accurate through as many decimal places as we decide to keep.