Color Science Snippets

Practical guides and mathematical foundations for color science, color spaces, and image processing.


Tristimulus Values & Color Matching Functions

Color perception is based on three types of cone cells in the human eye. The CIE defined standard color matching functions $\bar{x}(\lambda)$, $\bar{y}(\lambda)$, $\bar{z}(\lambda)$ that represent the response of a standard observer.

CIE 1931 2° Standard Observer

The tristimulus values $X$, $Y$, $Z$ for a spectral power distribution (SPD) $S(\lambda)$ are calculated by:

$$ X = k \int_{380}^{780} S(\lambda) \bar{x}(\lambda) d\lambda $$

$$ Y = k \int_{380}^{780} S(\lambda) \bar{y}(\lambda) d\lambda $$

$$ Z = k \int_{380}^{780} S(\lambda) \bar{z}(\lambda) d\lambda $$

Where:

  • $S(\lambda)$ is the spectral power distribution (light source or reflectance × illuminant)
  • $\bar{x}(\lambda)$, $\bar{y}(\lambda)$, $\bar{z}(\lambda)$ are the CIE 1931 color matching functions
  • $k$ is a normalization constant (typically chosen so that $Y = 100$ for a perfect white reflector)

Color Matching Functions (CIE 1931 2° Observer)

Key observations:

  • $\bar{x}(\lambda)$ peaks in the red region (~600nm)
  • $\bar{y}(\lambda)$ peaks in the green region (~555nm) and represents luminance
  • $\bar{z}(\lambda)$ peaks in the blue region (~445nm)
  • The functions overlap, meaning pure spectral colors activate multiple cone types

Standard Illuminant D65 (Daylight)

D65 represents average daylight with a correlated color temperature of 6504K. It's the standard illuminant for sRGB and most digital imaging.

D65 Spectral Power Distribution

Characteristics:

  • Relatively flat in the visible range
  • Slight peak in the blue region (shorter wavelengths)
  • Used as the standard for sRGB, Adobe RGB, and most color spaces

Calculating Tristimulus Values: Example

For a perfect white reflector under D65 illumination:

$$ X_{D65} = k \int_{380}^{780} D65(\lambda) \bar{x}(\lambda) d\lambda \approx 95.047 $$

$$ Y_{D65} = k \int_{380}^{780} D65(\lambda) \bar{y}(\lambda) d\lambda = 100.0 $$

$$ Z_{D65} = k \int_{380}^{780} D65(\lambda) \bar{z}(\lambda) d\lambda \approx 108.883 $$

Where $k$ is chosen so that $Y = 100$.

Numerical Integration (Discrete Approximation)

For sampled data at wavelength intervals $\Delta\lambda$:

$$ X \approx k \sum_{i} S(\lambda_i) \bar{x}(\lambda_i) \Delta\lambda $$

$$ Y \approx k \sum_{i} S(\lambda_i) \bar{y}(\lambda_i) \Delta\lambda $$

$$ Z \approx k \sum_{i} S(\lambda_i) \bar{z}(\lambda_i) \Delta\lambda $$

Typically $\Delta\lambda = 1$nm, 5nm, or 10nm depending on required accuracy.


Standard Illuminant A (Incandescent)

Illuminant A represents a tungsten-filament lamp at 2856K.

Illuminant A Spectral Power Distribution

Characteristics:

  • Strong red/orange component (warm light)
  • Increases with wavelength (more power at longer wavelengths)
  • Lower blue content compared to daylight

Chromaticity Diagram Concept

The chromaticity coordinates $(x, y)$ normalize tristimulus values:

$$ x = \frac{X}{X + Y + Z}, \quad y = \frac{Y}{X + Y + Z}, \quad z = \frac{Z}{X + Y + Z} $$

Note: $x + y + z = 1$, so only two coordinates are needed.

Standard white points:

  • D65: $(x, y) \approx (0.3127, 0.3290)$
  • D50: $(x, y) \approx (0.3457, 0.3585)$
  • Illuminant A: $(x, y) \approx (0.4476, 0.4074)$

Practical Python Example

 1import numpy as np
 2
 3# Load CIE 1931 color matching functions (380-780nm, 1nm steps)
 4# Format: wavelength, x_bar, y_bar, z_bar
 5cmf = np.loadtxt('CIE_xyz_1931_2deg.csv', delimiter=',')
 6wavelengths = cmf[:, 0]
 7x_bar = cmf[:, 1]
 8y_bar = cmf[:, 2]
 9z_bar = cmf[:, 3]
10
11# Load D65 illuminant SPD
12d65 = np.loadtxt('CIE_std_illum_D65.csv', delimiter=',')
13d65_wavelengths = d65[:, 0]
14d65_spd = d65[:, 1]
15
16# Interpolate to match wavelengths if needed
17from scipy.interpolate import interp1d
18d65_interp = interp1d(d65_wavelengths, d65_spd, bounds_error=False, fill_value=0)
19d65_matched = d65_interp(wavelengths)
20
21# Calculate tristimulus values
22delta_lambda = wavelengths[1] - wavelengths[0]  # Usually 1nm
23X = np.sum(d65_matched * x_bar * delta_lambda)
24Y = np.sum(d65_matched * y_bar * delta_lambda)
25Z = np.sum(d65_matched * z_bar * delta_lambda)
26
27# Normalize so Y = 100
28k = 100.0 / Y
29X *= k
30Y *= k
31Z *= k
32
33print(f"D65 White Point: X={X:.3f}, Y={Y:.3f}, Z={Z:.3f}")
34# Expected: X≈95.047, Y=100.0, Z≈108.883
35
36# Calculate chromaticity coordinates
37x_chrom = X / (X + Y + Z)
38y_chrom = Y / (X + Y + Z)
39print(f"Chromaticity: x={x_chrom:.4f}, y={y_chrom:.4f}")
40# Expected: x≈0.3127, y≈0.3290

Data Sources

High-resolution spectral data used in these examples:

  • CIE 1931 2° Standard Observer color matching functions (1nm resolution, 380-780nm)
  • CIE Standard Illuminants (D65, D50, A) spectral power distributions

Further Reading

Snippets