Color.Spectral (Color v0.11.0)

Copy Markdown

Spectral power distributions, spectral reflectances, and spectrum → XYZ integration.

A Color.Spectral struct holds a list of wavelengths (in nm) and a matching list of values. For emissive sources the values are absolute or relative spectral power. For reflective samples the values are the per-wavelength reflectance in [0.0, 1.0].

The module provides:

  • illuminant/1 — well-known CIE illuminants (:D65, :D50, :A, :B, :C, :E, :F2, :F7, :F11) as ready-to-use SPDs.

  • blackbody/1 — a Planckian (blackbody) SPD at any colour temperature.

  • cmf/1 — the CIE 1931 2° (default) and CIE 1964 10° standard observer colour matching functions.

  • to_xyz/1,2 — integrates an SPD against a CMF to produce a Color.XYZ struct. The Y component is normalised to 1.0 for the chosen illuminant's reference white (so this matches the rest of the library's convention).

  • reflectance_to_xyz/3 — multiplies a reflectance SPD by an illuminant SPD, then integrates. This is what you use for paint samples, fabric, printed swatches, etc.

  • metamerism/4 — compares two spectral samples under two illuminants to detect metamers: pairs that match under one light and diverge under another.

The standard tables are at 5 nm intervals from 380 nm to 780 nm (81 samples). All the built-in data lives in Color.Spectral.Tables. If you load a sample with a different wavelength grid, to_xyz/2 will linearly interpolate it onto the 5 nm grid before integrating.

Summary

Functions

Returns a Planckian (blackbody) SPD at the given colour temperature.

Returns the CIE standard observer colour matching functions as three Color.Spectral structs ({x_bar, y_bar, z_bar}).

Returns a Color.Spectral struct for a named CIE illuminant.

Computes a metamerism index between two spectral reflectance samples under two different illuminants.

Converts a spectral reflectance sample under a specific illuminant to a CIE XYZ tristimulus.

Resamples a spectral struct onto a new wavelength grid via linear interpolation. Samples outside the source range are extrapolated as zero.

Converts a spectral power distribution to a CIE XYZ tristimulus.

Types

illuminant_name()

@type illuminant_name() :: :D65 | :D50 | :A | :B | :C | :E | :F2 | :F7 | :F11

t()

@type t() :: %Color.Spectral{values: [number()], wavelengths: [number()]}

Functions

blackbody(temperature)

@spec blackbody(number()) :: t()

Returns a Planckian (blackbody) SPD at the given colour temperature.

The relative spectral power is computed from Planck's law normalised to 100.0 at 560 nm, matching the convention used by the CIE's Illuminant A definition (which is itself a Planckian at 2856 K).

Arguments

  • temperature is the colour temperature in kelvin. Must be positive. Typical values: 2700 (warm white LED), 3200 (tungsten), 4000 (neutral), 5000 (horizon), 6504 (D65 equivalent), 10_000 (cool).

Returns

Examples

iex> spd = Color.Spectral.blackbody(2856)
iex> length(spd.values)
81

iex> spd = Color.Spectral.blackbody(6504)
iex> {:ok, xyz} = Color.Spectral.to_xyz(spd)
iex> Float.round(xyz.y, 4)
1.0

cmf(observer_angle \\ 2)

@spec cmf(2 | 10) :: {t(), t(), t()}

Returns the CIE standard observer colour matching functions as three Color.Spectral structs ({x_bar, y_bar, z_bar}).

Arguments

  • observer_angle is 2 (CIE 1931) or 10 (CIE 1964). Defaults to 2.

Returns

Examples

iex> {x, y, z} = Color.Spectral.cmf()
iex> {length(x.values), length(y.values), length(z.values)}
{81, 81, 81}

illuminant(name)

@spec illuminant(illuminant_name()) :: t()

Returns a Color.Spectral struct for a named CIE illuminant.

:D65, :D50 and :A are tabulated natively at 5 nm. :B, :C, :F2, :F7 and :F11 originate from 10 nm published values and are linearly interpolated onto the 5 nm grid, so they should be treated as reference-grade but not sub-10 nm accurate — in particular the F-series emission spikes are under-represented. For higher precision, pass a user-supplied SPD directly to to_xyz/2.

Arguments

  • name is one of :D65, :D50, :A, :B, :C, :E, :F2, :F7, :F11.

Returns

Examples

iex> spd = Color.Spectral.illuminant(:D65)
iex> length(spd.wavelengths)
81

iex> spd = Color.Spectral.illuminant(:E)
iex> Enum.uniq(spd.values)
[100.0]

iex> spd = Color.Spectral.illuminant(:F7)
iex> length(spd.values)
81

metamerism(sample_a, sample_b, reference, test)

@spec metamerism(t(), t(), illuminant_name(), illuminant_name()) ::
  {:ok, float()} | {:error, Exception.t()}

Computes a metamerism index between two spectral reflectance samples under two different illuminants.

Two samples that match under illuminant a (same XYZ or very close) may diverge visibly under illuminant b. The returned value is the CIEDE2000 ΔE between their appearances under the second illuminant — larger is "more metameric".

Arguments

  • sample_a is a Color.Spectral reflectance.

  • sample_b is a Color.Spectral reflectance.

  • reference is the illuminant under which the samples match (for example :D65).

  • test is the illuminant under which to measure divergence (for example :A — tungsten light — is the classic test illuminant).

Returns

  • {:ok, delta_e} with delta_e as a non-negative float (CIEDE2000).

Examples

iex> a = %Color.Spectral{
...>   wavelengths: Color.Spectral.Tables.wavelengths(),
...>   values: List.duplicate(0.5, 81)
...> }
iex> {:ok, de} = Color.Spectral.metamerism(a, a, :D65, :A)
iex> de
0.0

reflectance_to_xyz(reflectance, illuminant_name \\ :D65, options \\ [])

@spec reflectance_to_xyz(t(), illuminant_name(), keyword()) :: {:ok, Color.XYZ.t()}

Converts a spectral reflectance sample under a specific illuminant to a CIE XYZ tristimulus.

This is the formula used for paint chips, fabric swatches, printed samples, and anything else that reflects rather than emits light:

X = k · Σ R(λ) · I(λ) · (λ)
Y = k · Σ R(λ) · I(λ) · ȳ(λ)
Z = k · Σ R(λ) · I(λ) · (λ)

where the normalising constant k = 1 / Σ I(λ) · ȳ(λ) so Y = 1.0 for a perfect (100% reflective) diffuser under the same illuminant.

Arguments

  • reflectance is a Color.Spectral struct whose values are in [0.0, 1.0].

  • illuminant_name is one of the atoms accepted by illuminant/1 (:D65, :D50, :A, :B, :C, :E, :F2, :F7, :F11). Defaults to :D65.

  • options is a keyword list. Supports :observer (2 or 10, default 2).

Returns

  • {:ok, %Color.XYZ{}} tagged with the chosen illuminant.

Examples

iex> perfect_diffuser = %Color.Spectral{
...>   wavelengths: Color.Spectral.Tables.wavelengths(),
...>   values: List.duplicate(1.0, 81)
...> }
iex> {:ok, xyz} = Color.Spectral.reflectance_to_xyz(perfect_diffuser, :D65)
iex> {Float.round(xyz.x, 4), Float.round(xyz.y, 4), Float.round(xyz.z, 4)}
{0.9504, 1.0, 1.0888}

iex> {:ok, xyz} = Color.Spectral.reflectance_to_xyz(%Color.Spectral{
...>   wavelengths: Color.Spectral.Tables.wavelengths(),
...>   values: List.duplicate(1.0, 81)
...> }, :D50)
iex> {Float.round(xyz.x, 4), Float.round(xyz.y, 4), Float.round(xyz.z, 4)}
{0.9642, 1.0, 0.8251}

resample(spd, grid)

@spec resample(t(), [number()]) :: [number()]

Resamples a spectral struct onto a new wavelength grid via linear interpolation. Samples outside the source range are extrapolated as zero.

Arguments

Returns

  • A list of values corresponding to each grid point.

to_xyz(spd, options \\ [])

@spec to_xyz(
  t(),
  keyword()
) :: {:ok, Color.XYZ.t()}

Converts a spectral power distribution to a CIE XYZ tristimulus.

This is the general formula used for emissive sources (monitors, LEDs, light bulbs):

X = k · Σ S(λ) · (λ)
Y = k · Σ S(λ) · ȳ(λ)
Z = k · Σ S(λ) · (λ)

where the normalising constant k = 1 / Σ S(λ) · ȳ(λ) so Y = 1.0 at the source's own white point.

Arguments

  • spd is a Color.Spectral struct representing the source's spectral power distribution.

  • options is a keyword list.

Options

  • :observer is 2 or 10. Defaults to 2.

  • :illuminant tags the resulting Color.XYZ struct. Defaults to :D65. This does not normalise the result against that illuminant — use reflectance_to_xyz/3 for that case.

Returns

  • {:ok, %Color.XYZ{}}.

Examples

iex> d65 = Color.Spectral.illuminant(:D65)
iex> {:ok, xyz} = Color.Spectral.to_xyz(d65)
iex> {Float.round(xyz.x, 4), Float.round(xyz.y, 4), Float.round(xyz.z, 4)}
{0.9504, 1.0, 1.0888}

iex> a = Color.Spectral.illuminant(:A)
iex> {:ok, xyz} = Color.Spectral.to_xyz(a, illuminant: :A)
iex> {Float.round(xyz.x, 4), Float.round(xyz.y, 4), Float.round(xyz.z, 4)}
{1.0985, 1.0, 0.3558}