Chromaticity Color Spaces#

Images often represent color using three channels: (R, G, B) - red, green, and blue. For this document, we assume the range of each channel is [0.0, 1.0].

Normalized Chromaticity#

Formulas#

Output (r, g, b)
(1.0, 0.0, 0.0):

100% red

(0.0, 1.0, 0.0):

100% green

(0.0, 0.0, 1.0):

100% blue

Output (r, g)
(1.0, 0.0):

100% red

(0.0, 1.0):

100% green

(0.0, 0.0):

100% blue (implied)

Normalized RG/RGB#

\[\begin{split}r = \frac{R}{R+G+B} \\ g = \frac{G}{R+G+B} \\ b = \frac{B}{R+G+B} \\ \\ r+g+b = 1\end{split}\]

Normalized RG/RGB White-Point#

\[\begin{split}R = 1 \\ G = 1 \\ B = 1 \\ \\ r = \frac{R}{R+G+B} \\ g = \frac{G}{R+G+B} \\ b = \frac{B}{R+G+B} \\ \\ r+g+b = 1\end{split}\]
float3 RGBtoChromaticityRGB(float3 Color)
{
   // Optimizes 2 ADD instructions into 1 DP3 instruction.
   float SumRGB = dot(Color, 1.0);
   float3 Chromaticity = saturate(Color / SumRGB);
   // Output the chromaticity's white point if the divisor is 0.0.
   // Prevents undefined behavior when dividing by 0.
   Chromaticity = (SumRGB == 0.0) ? float3(1.0 / 3.0) : Chromaticity;
   return Chromaticity;
}

Spherical Chromaticity#

This section introduces a color space that represents chromaticity using angles.

Precision Loss in RG Chromaticity#

A significant drawback of RG chromaticity is precision loss. All possible values map to a right triangle, effectively halving the precision in integer buffers.

Spherical chromaticity encodes data that utilizes the entire RG8 range by calculating angles between the color channels.

/*
   This code is based on the algorithm described in the following paper:
   Author(s): Joost van de Weijer, T. Gevers
   Title: "Robust optical flow from photometric invariants"
   Year: 2004
   DOI: 10.1109/ICIP.2004.1421433
   Link: https://www.researchgate.net/publication/4138051_Robust_optical_flow_from_photometric_invariants
*/

float2 RGBtoSphericalRG(float3 Color)
{
   const float HalfPi = 1.0 / acos(0.0); // 1 / (pi/2) = 2 / pi

   // Precalculate (x*x + y*y)^0.5 and (x*x + y*y + z*z)^0.5
   float L1 = length(Color.rg);
   float L2 = length(Color.rgb);

   float2 Angles = 0.0;
   Angles[0] = (L1 == 0.0) ? 1.0 / sqrt(2.0) : Color.g / L1;
   Angles[1] = (L2 == 0.0) ? 1.0 / sqrt(3.0) : L1 / L2;

   return saturate(asin(abs(Angles)) * HalfPi);
}