Color.Spectral (Color v0.4.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, :E) as ready-to-use SPDs.

  • 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 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

t()

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

Functions

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(:D65 | :D50 | :A | :E) :: t()

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

Arguments

  • name is one of :D65, :D50, :A, :E.

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]

metamerism(sample_a, sample_b, reference, test)

@spec metamerism(t(), t(), :D65 | :D50 | :A | :E, :D65 | :D50 | :A | :E) ::
  {: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(), :D65 | :D50 | :A | :E, 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 :D65, :D50, :A, or :E. 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}