#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tristimulus Values
==================
Defines objects for tristimulus values computation from spectral data:
- :func:`tristimulus_weighting_factors_ASTME202211`
- :func:`spectral_to_XYZ_integration`
- :func:`spectral_to_XYZ_tristimulus_weighting_factors_ASTME30815`
- :func:`spectral_to_XYZ_ASTME30815`
- :func:`spectral_to_XYZ`
- :func:`wavelength_to_XYZ`
The default implementation is based on practise *ASTM E308-15* method [2]_.
References
----------
.. [1] ASTM International. (2011). ASTM E2022–11 - Standard Practice for
Calculation of Weighting Factors for Tristimulus Integration, i, 1–10.
doi:10.1520/E2022-11
.. [2] ASTM International. (2015). ASTM E308–15 - Standard Practice for
Computing the Colors of Objects by Using the CIE System, 1–47.
doi:10.1520/E0308-15
See Also
--------
`Colour Matching Functions Jupyter Notebook
<http://nbviewer.jupyter.org/github/colour-science/colour-notebooks/\
blob/master/notebooks/colorimetry/cmfs.ipynb>`_
`Spectrum Jupyter Notebook
<http://nbviewer.jupyter.org/github/colour-science/colour-notebooks/\
blob/master/notebooks/colorimetry/spectrum.ipynb>`_
"""
from __future__ import division, unicode_literals
import numpy as np
from colour.algebra import (
CubicSplineInterpolator,
LinearInterpolator,
PchipInterpolator,
SpragueInterpolator,
lagrange_coefficients)
from colour.colorimetry import (
DEFAULT_SPECTRAL_SHAPE,
SpectralShape,
STANDARD_OBSERVERS_CMFS, ones_spd)
from colour.utilities import (
CaseInsensitiveMapping,
filter_kwargs,
is_string,
tsplit,
warning)
__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2017 - Colour Developers'
__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'
__all__ = ['ASTME30815_PRACTISE_SHAPE',
'lagrange_coefficients_ASTME202211',
'tristimulus_weighting_factors_ASTME202211',
'adjust_tristimulus_weighting_factors_ASTME30815',
'spectral_to_XYZ_integration',
'spectral_to_XYZ_tristimulus_weighting_factors_ASTME30815',
'spectral_to_XYZ_ASTME30815',
'SPECTRAL_TO_XYZ_METHODS',
'spectral_to_XYZ',
'wavelength_to_XYZ']
ASTME30815_PRACTISE_SHAPE = DEFAULT_SPECTRAL_SHAPE
"""
*ASTM E308–15* practise shape: (360, 780, 1).
ASTME30815_PRACTISE_SHAPE : SpectralShape
"""
_LAGRANGE_INTERPOLATING_COEFFICIENTS_CACHE = None
_TRISTIMULUS_WEIGHTING_FACTORS_CACHE = None
[docs]def lagrange_coefficients_ASTME202211(
interval=10,
interval_type='inner'):
"""
Computes the *Lagrange Coefficients* for given interval size using practise
*ASTM E2022-11* method [1]_.
Parameters
----------
interval : int
Interval size in nm.
interval_type : unicode, optional
**{'inner', 'boundary'}**,
If the interval is an *inner* interval *Lagrange Coefficients* are
computed for degree 4. Degree 3 is used for a *boundary* interval.
Returns
-------
ndarray
*Lagrange Coefficients*.
See Also
--------
colour.lagrange_coefficients
Examples
--------
>>> lagrange_coefficients_ASTME202211( # doctest: +ELLIPSIS
... 10, 'inner')
array([[-0.028..., 0.940..., 0.104..., -0.016...],
[-0.048..., 0.864..., 0.216..., -0.032...],
[-0.059..., 0.773..., 0.331..., -0.045...],
[-0.064..., 0.672..., 0.448..., -0.056...],
[-0.062..., 0.562..., 0.562..., -0.062...],
[-0.056..., 0.448..., 0.672..., -0.064...],
[-0.045..., 0.331..., 0.773..., -0.059...],
[-0.032..., 0.216..., 0.864..., -0.048...],
[-0.016..., 0.104..., 0.940..., -0.028...]])
>>> lagrange_coefficients_ASTME202211( # doctest: +ELLIPSIS
... 10, 'boundary')
array([[ 0.85..., 0.19..., -0.04...],
[ 0.72..., 0.36..., -0.08...],
[ 0.59..., 0.51..., -0.10...],
[ 0.48..., 0.64..., -0.12...],
[ 0.37..., 0.75..., -0.12...],
[ 0.28..., 0.84..., -0.12...],
[ 0.19..., 0.91..., -0.10...],
[ 0.12..., 0.96..., -0.08...],
[ 0.05..., 0.99..., -0.04...]])
"""
global _LAGRANGE_INTERPOLATING_COEFFICIENTS_CACHE
if _LAGRANGE_INTERPOLATING_COEFFICIENTS_CACHE is None:
_LAGRANGE_INTERPOLATING_COEFFICIENTS_CACHE = CaseInsensitiveMapping()
name_lica = ', '.join((str(interval), interval_type))
if name_lica in _LAGRANGE_INTERPOLATING_COEFFICIENTS_CACHE:
return _LAGRANGE_INTERPOLATING_COEFFICIENTS_CACHE[name_lica]
r_n = np.linspace(1 / interval, 1 - (1 / interval), interval - 1)
d = 3
if interval_type.lower() == 'inner':
r_n += 1
d = 4
lica = _LAGRANGE_INTERPOLATING_COEFFICIENTS_CACHE[name_lica] = (
np.asarray([lagrange_coefficients(r, d) for r in r_n]))
return lica
[docs]def tristimulus_weighting_factors_ASTME202211(cmfs, illuminant, shape):
"""
Returns a table of tristimulus weighting factors for given colour matching
functions and illuminant using practise *ASTM E2022-11* method [1]_.
The computed table of tristimulus weighting factors should be used with
spectral data that has been corrected for spectral bandpass dependence.
Parameters
----------
cmfs : XYZ_ColourMatchingFunctions
Standard observer colour matching functions.
illuminant : SpectralPowerDistribution
Illuminant spectral power distribution.
shape : SpectralShape
Shape used to build the table, only the interval is needed.
Returns
-------
ndarray
Tristimulus weighting factors table.
Raises
------
ValueError
If the colour matching functions or illuminant intervals are not equal
to 1 nm.
Warning
-------
- The tables of tristimulus weighting factors are cached in
:attr:`_TRISTIMULUS_WEIGHTING_FACTORS_CACHE` attribute. Their
identifier key is defined by the colour matching functions and
illuminant names along the current shape such as:
`CIE 1964 10 Degree Standard Observer, A, (360.0, 830.0, 10.0)`
Considering the above, one should be mindful that using similar colour
matching functions and illuminant names but with different spectral
data will lead to unexpected behaviour.
Notes
-----
- Input colour matching functions and illuminant intervals are expected
to be equal to 1 nm. If the illuminant data is not available at 1 nm
interval, it needs to be interpolated using *CIE* recommendations:
The method developed by *Sprague (1880)* should be used for
interpolating functions having a uniformly spaced independent variable
and a *Cubic Spline* method for non-uniformly spaced independent
variable.
Examples
--------
>>> from colour import (
... CMFS,
... CIE_standard_illuminant_A_function,
... SpectralPowerDistribution,
... SpectralShape)
>>> cmfs = CMFS['CIE 1964 10 Degree Standard Observer']
>>> wl = cmfs.shape.range()
>>> A = SpectralPowerDistribution(
... 'A (360, 830, 1)',
... dict(zip(wl, CIE_standard_illuminant_A_function(wl))))
>>> tristimulus_weighting_factors_ASTME202211( # doctest: +ELLIPSIS
... cmfs, A, SpectralShape(360, 830, 20))
array([[ -2.9816934...e-04, -3.1709762...e-05, -1.3301218...e-03],
[ -8.7154955...e-03, -8.9154168...e-04, -4.0743684...e-02],
[ 5.9967988...e-02, 5.0203497...e-03, 2.5650183...e-01],
[ 7.7342255...e-01, 7.7983983...e-02, 3.6965732...e+00],
[ 1.9000905...e+00, 3.0370051...e-01, 9.7554195...e+00],
[ 1.9707727...e+00, 8.5528092...e-01, 1.1486732...e+01],
[ 7.1836236...e-01, 2.1457000...e+00, 6.7845806...e+00],
[ 4.2666758...e-02, 4.8985328...e+00, 2.3208000...e+00],
[ 1.5223302...e+00, 9.6471138...e+00, 7.4306714...e-01],
[ 5.6770329...e+00, 1.4460970...e+01, 1.9581949...e-01],
[ 1.2445174...e+01, 1.7474254...e+01, 5.1826979...e-03],
[ 2.0553577...e+01, 1.7583821...e+01, -2.6512696...e-03],
[ 2.5331538...e+01, 1.4895703...e+01, 0.0000000...e+00],
[ 2.1571157...e+01, 1.0079661...e+01, 0.0000000...e+00],
[ 1.2178581...e+01, 5.0680655...e+00, 0.0000000...e+00],
[ 4.6675746...e+00, 1.8303239...e+00, 0.0000000...e+00],
[ 1.3236117...e+00, 5.1296946...e-01, 0.0000000...e+00],
[ 3.1753258...e-01, 1.2300847...e-01, 0.0000000...e+00],
[ 7.4634128...e-02, 2.9024389...e-02, 0.0000000...e+00],
[ 1.8299016...e-02, 7.1606335...e-03, 0.0000000...e+00],
[ 4.7942065...e-03, 1.8888730...e-03, 0.0000000...e+00],
[ 1.3293045...e-03, 5.2774591...e-04, 0.0000000...e+00],
[ 4.2546928...e-04, 1.7041978...e-04, 0.0000000...e+00],
[ 9.6251115...e-05, 3.8955295...e-05, 0.0000000...e+00]])
"""
if cmfs.shape.interval != 1:
raise ValueError('"{0}" shape "interval" must be 1!'.format(cmfs))
if illuminant.shape.interval != 1:
raise ValueError(
'"{0}" shape "interval" must be 1!'.format(illuminant))
global _TRISTIMULUS_WEIGHTING_FACTORS_CACHE
if _TRISTIMULUS_WEIGHTING_FACTORS_CACHE is None:
_TRISTIMULUS_WEIGHTING_FACTORS_CACHE = CaseInsensitiveMapping()
name_twf = ', '.join((cmfs.name, illuminant.name, str(shape)))
if name_twf in _TRISTIMULUS_WEIGHTING_FACTORS_CACHE:
return _TRISTIMULUS_WEIGHTING_FACTORS_CACHE[name_twf]
Y = cmfs.values
S = illuminant.values
W = S[::shape.interval, np.newaxis] * Y[::shape.interval, :]
# First and last measurement intervals *Lagrange Coefficients*.
c_c = lagrange_coefficients_ASTME202211(shape.interval, 'boundary')
# Intermediate measurement intervals *Lagrange Coefficients*.
c_b = lagrange_coefficients_ASTME202211(shape.interval, 'inner')
# Total wavelengths count.
w_c = len(Y)
# Measurement interval interpolated values count.
r_c = c_b.shape[0]
# Last interval first interpolated wavelength.
w_lif = w_c - (w_c - 1) % shape.interval - 1 - r_c
# Intervals count.
i_c = W.shape[0]
i_cm = i_c - 1
for i in range(3):
# First interval.
for j in range(r_c):
for k in range(3):
W[k, i] = W[k, i] + c_c[j, k] * S[j + 1] * Y[j + 1, i]
# Last interval.
for j in range(r_c):
for k in range(i_cm, i_cm - 3, -1):
W[k, i] = (W[k, i] + c_c[r_c - j - 1, i_cm - k] *
S[j + w_lif] * Y[j + w_lif, i])
# Intermediate intervals.
for j in range(i_c - 3):
for k in range(r_c):
w_i = (r_c + 1) * (j + 1) + 1 + k
W[j, i] = W[j, i] + c_b[k, 0] * S[w_i] * Y[w_i, i]
W[j + 1, i] = W[j + 1, i] + c_b[k, 1] * S[w_i] * Y[w_i, i]
W[j + 2, i] = W[j + 2, i] + c_b[k, 2] * S[w_i] * Y[w_i, i]
W[j + 3, i] = W[j + 3, i] + c_b[k, 3] * S[w_i] * Y[w_i, i]
# Extrapolation of potential incomplete interval.
for j in range(int(w_c - ((w_c - 1) % shape.interval)), w_c, 1):
W[i_cm, i] = W[i_cm, i] + S[j] * Y[j, i]
W *= 100 / np.sum(W, axis=0)[1]
_TRISTIMULUS_WEIGHTING_FACTORS_CACHE[name_twf] = W
return W
[docs]def adjust_tristimulus_weighting_factors_ASTME30815(W, shape_r, shape_t):
"""
Adjusts given table of tristimulus weighting factors to account for a
shorter wavelengths range of the test spectral shape compared to the
reference spectral shape using practise *ASTM E308-15* method [2]_:
Weights at the wavelengths for which data are not available are added to
the weights at the shortest and longest wavelength for which spectral data
are available.
Parameters
----------
W : array_like
Tristimulus weighting factors table.
shape_r : SpectralShape
Reference spectral shape.
shape_t : SpectralShape
Test spectral shape.
Returns
-------
ndarray
Adjusted tristimulus weighting factors.
Examples
--------
>>> from colour import (
... CMFS,
... CIE_standard_illuminant_A_function,
... SpectralPowerDistribution,
... SpectralShape)
>>> cmfs = CMFS['CIE 1964 10 Degree Standard Observer']
>>> wl = cmfs.shape.range()
>>> A = SpectralPowerDistribution(
... 'A (360, 830, 1)',
... dict(zip(wl, CIE_standard_illuminant_A_function(wl))))
>>> W = tristimulus_weighting_factors_ASTME202211(
... cmfs, A, SpectralShape(360, 830, 20))
>>> adjust_tristimulus_weighting_factors_ASTME30815( # doctest: +ELLIPSIS
... W, SpectralShape(360, 830, 20), SpectralShape(400, 700, 20))
array([[ 5.0954324...e-02, 4.0970982...e-03, 2.1442802...e-01],
[ 7.7342255...e-01, 7.7983983...e-02, 3.6965732...e+00],
[ 1.9000905...e+00, 3.0370051...e-01, 9.7554195...e+00],
[ 1.9707727...e+00, 8.5528092...e-01, 1.1486732...e+01],
[ 7.1836236...e-01, 2.1457000...e+00, 6.7845806...e+00],
[ 4.2666758...e-02, 4.8985328...e+00, 2.3208000...e+00],
[ 1.5223302...e+00, 9.6471138...e+00, 7.4306714...e-01],
[ 5.6770329...e+00, 1.4460970...e+01, 1.9581949...e-01],
[ 1.2445174...e+01, 1.7474254...e+01, 5.1826979...e-03],
[ 2.0553577...e+01, 1.7583821...e+01, -2.6512696...e-03],
[ 2.5331538...e+01, 1.4895703...e+01, 0.0000000...e+00],
[ 2.1571157...e+01, 1.0079661...e+01, 0.0000000...e+00],
[ 1.2178581...e+01, 5.0680655...e+00, 0.0000000...e+00],
[ 4.6675746...e+00, 1.8303239...e+00, 0.0000000...e+00],
[ 1.3236117...e+00, 5.1296946...e-01, 0.0000000...e+00],
[ 4.1711096...e-01, 1.6181949...e-01, 0.0000000...e+00]])
"""
W = np.copy(W)
start_index = int((shape_t.start - shape_r.start) / shape_r.interval)
for i in range(start_index):
W[start_index] += W[i]
end_index = int((shape_r.end - shape_t.end) / shape_r.interval)
for i in range(end_index):
W[-end_index - 1] += W[-i - 1]
return W[start_index:-end_index or None, ...]
[docs]def spectral_to_XYZ_integration(
spd,
cmfs=STANDARD_OBSERVERS_CMFS[
'CIE 1931 2 Degree Standard Observer'],
illuminant=ones_spd(
STANDARD_OBSERVERS_CMFS[
'CIE 1931 2 Degree Standard Observer'].shape)):
"""
Converts given spectral power distribution to *CIE XYZ* tristimulus values
using given colour matching functions and illuminant accordingly to
classical integration method.
Parameters
----------
spd : SpectralPowerDistribution
Spectral power distribution.
cmfs : XYZ_ColourMatchingFunctions
Standard observer colour matching functions.
illuminant : SpectralPowerDistribution, optional
Illuminant spectral power distribution.
Returns
-------
ndarray, (3,)
*CIE XYZ* tristimulus values.
Warning
-------
The output range of that definition is non standard!
Notes
-----
- Output *CIE XYZ* tristimulus values are in range [0, 100].
References
----------
.. [3] Wyszecki, G., & Stiles, W. S. (2000). Integration Replace by
Summation. In Color Science: Concepts and Methods, Quantitative
Data and Formulae (pp. 158–163). Wiley. ISBN:978-0471399186
Examples
--------
>>> from colour import (
... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution)
>>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer']
>>> data = {
... 400: 0.0641,
... 420: 0.0645,
... 440: 0.0562,
... 460: 0.0537,
... 480: 0.0559,
... 500: 0.0651,
... 520: 0.0705,
... 540: 0.0772,
... 560: 0.0870,
... 580: 0.1128,
... 600: 0.1360,
... 620: 0.1511,
... 640: 0.1688,
... 660: 0.1996,
... 680: 0.2397,
... 700: 0.2852}
>>> spd = SpectralPowerDistribution('Sample', data)
>>> illuminant = ILLUMINANTS_RELATIVE_SPDS['D50']
>>> spectral_to_XYZ_integration( # doctest: +ELLIPSIS
... spd, cmfs, illuminant)
array([ 11.5296285..., 9.9499467..., 4.7066079...])
"""
if illuminant.shape != cmfs.shape:
warning('Aligning "{0}" illuminant shape to "{1}" colour matching '
'functions shape.'.format(illuminant, cmfs))
illuminant = illuminant.clone().align(cmfs.shape)
if spd.shape != cmfs.shape:
warning('Aligning "{0}" spectral power distribution shape to "{1}" '
'colour matching functions shape.'.format(spd, cmfs))
spd = spd.clone().align(cmfs.shape)
S = illuminant.values
x_bar, y_bar, z_bar = tsplit(cmfs.values)
R = spd.values
dw = cmfs.shape.interval
k = 100 / (np.sum(y_bar * S) * dw)
X_p = R * x_bar * S * dw
Y_p = R * y_bar * S * dw
Z_p = R * z_bar * S * dw
XYZ = k * np.sum(np.array([X_p, Y_p, Z_p]), axis=-1)
return XYZ
[docs]def spectral_to_XYZ_tristimulus_weighting_factors_ASTME30815(
spd,
cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'],
illuminant=ones_spd(ASTME30815_PRACTISE_SHAPE)):
"""
Converts given spectral power distribution to *CIE XYZ* tristimulus values
using given colour matching functions and illuminant using a table
of tristimulus weighting factors accordingly to practise
*ASTM E308-15* method [2]_.
Parameters
----------
spd : SpectralPowerDistribution
Spectral power distribution.
cmfs : XYZ_ColourMatchingFunctions
Standard observer colour matching functions.
illuminant : SpectralPowerDistribution, optional
Illuminant spectral power distribution.
Returns
-------
ndarray, (3,)
*CIE XYZ* tristimulus values.
Warning
-------
The output range of that definition is non standard!
Notes
-----
- Output *CIE XYZ* tristimulus values are in range [0, 100].
Examples
--------
>>> from colour import (
... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution)
>>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer']
>>> data = {
... 400: 0.0641,
... 420: 0.0645,
... 440: 0.0562,
... 460: 0.0537,
... 480: 0.0559,
... 500: 0.0651,
... 520: 0.0705,
... 540: 0.0772,
... 560: 0.0870,
... 580: 0.1128,
... 600: 0.1360,
... 620: 0.1511,
... 640: 0.1688,
... 660: 0.1996,
... 680: 0.2397,
... 700: 0.2852}
>>> spd = SpectralPowerDistribution('Sample', data)
>>> illuminant = ILLUMINANTS_RELATIVE_SPDS['D50']
>>> spectral_to_XYZ_tristimulus_weighting_factors_ASTME30815(
... spd, cmfs, illuminant) # doctest: +ELLIPSIS
array([ 11.5296311..., 9.9505845..., 4.7098037...])
"""
if illuminant.shape != cmfs.shape:
warning('Aligning "{0}" illuminant shape to "{1}" colour matching '
'functions shape.'.format(illuminant, cmfs))
illuminant = illuminant.clone().align(cmfs.shape)
if spd.shape.boundaries != cmfs.shape.boundaries:
warning('Trimming "{0}" spectral power distribution shape to "{1}" '
'colour matching functions shape.'.format(illuminant, cmfs))
spd = spd.clone().trim_wavelengths(cmfs.shape)
W = tristimulus_weighting_factors_ASTME202211(
cmfs, illuminant, SpectralShape(
cmfs.shape.start, cmfs.shape.end, spd.shape.interval))
start_w = cmfs.shape.start
end_w = cmfs.shape.start + spd.shape.interval * (W.shape[0] - 1)
W = adjust_tristimulus_weighting_factors_ASTME30815(
W, SpectralShape(start_w, end_w, spd.shape.interval), spd.shape)
R = spd.values
XYZ = np.sum(W * R[..., np.newaxis], axis=0)
return XYZ
[docs]def spectral_to_XYZ_ASTME30815(
spd,
cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'],
illuminant=ones_spd(ASTME30815_PRACTISE_SHAPE),
use_practice_range=True,
mi_5nm_omission_method=True,
mi_20nm_interpolation_method=True):
"""
Converts given spectral power distribution to *CIE XYZ* tristimulus values
using given colour matching functions and illuminant accordingly to
practise *ASTM E308-15* method [2]_.
Parameters
----------
spd : SpectralPowerDistribution
Spectral power distribution.
cmfs : XYZ_ColourMatchingFunctions
Standard observer colour matching functions.
illuminant : SpectralPowerDistribution, optional
Illuminant spectral power distribution.
use_practice_range : bool, optional
Practise *ASTM E308-15* working wavelengths range is [360, 780],
if `True` this argument will trim the colour matching functions
appropriately.
mi_5nm_omission_method : bool, optional
5 nm measurement intervals spectral power distribution conversion to
tristimulus values will use a 5 nm version of the colour matching
functions instead of a table of tristimulus weighting factors.
mi_20nm_interpolation_method : bool, optional
20 nm measurement intervals spectral power distribution conversion to
tristimulus values will use a dedicated interpolation method instead
of a table of tristimulus weighting factors.
Returns
-------
ndarray, (3,)
*CIE XYZ* tristimulus values.
Warning
-------
- The tables of tristimulus weighting factors are cached in
:attr:`_TRISTIMULUS_WEIGHTING_FACTORS_CACHE` attribute. Their
identifier key is defined by the colour matching functions and
illuminant names along the current shape such as:
`CIE 1964 10 Degree Standard Observer, A, (360.0, 830.0, 10.0)`
Considering the above, one should be mindful that using similar colour
matching functions and illuminant names but with different spectral
data will lead to unexpected behaviour.
- The output range of that definition is non standard!
Notes
-----
- Output *CIE XYZ* tristimulus values are in range [0, 100].
Examples
--------
>>> from colour import (
... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution)
>>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer']
>>> data = {
... 400: 0.0641,
... 420: 0.0645,
... 440: 0.0562,
... 460: 0.0537,
... 480: 0.0559,
... 500: 0.0651,
... 520: 0.0705,
... 540: 0.0772,
... 560: 0.0870,
... 580: 0.1128,
... 600: 0.1360,
... 620: 0.1511,
... 640: 0.1688,
... 660: 0.1996,
... 680: 0.2397,
... 700: 0.2852}
>>> spd = SpectralPowerDistribution('Sample', data)
>>> illuminant = ILLUMINANTS_RELATIVE_SPDS['D50']
>>> spectral_to_XYZ_ASTME30815(
... spd, cmfs, illuminant) # doctest: +ELLIPSIS
array([ 11.5290265..., 9.9502091..., 4.7098882...])
"""
if spd.shape.interval not in (1, 5, 10, 20):
raise ValueError(
'Tristimulus values conversion from spectral data accordingly to '
'practise "ASTM E308-15" should be performed on spectral data '
'with measurement interval of 1, 5, 10 or 20nm!')
if use_practice_range:
cmfs = cmfs.clone().trim_wavelengths(ASTME30815_PRACTISE_SHAPE)
method = spectral_to_XYZ_tristimulus_weighting_factors_ASTME30815
if spd.shape.interval == 1:
method = spectral_to_XYZ_integration
elif spd.shape.interval == 5 and mi_5nm_omission_method:
if cmfs.shape.interval != 5:
cmfs = cmfs.clone().interpolate(SpectralShape(interval=5))
method = spectral_to_XYZ_integration
elif spd.shape.interval == 20 and mi_20nm_interpolation_method:
spd = spd.clone()
if spd.shape.boundaries != cmfs.shape.boundaries:
warning(
'Trimming "{0}" spectral power distribution shape to "{1}" '
'colour matching functions shape.'.format(illuminant, cmfs))
spd.trim_wavelengths(cmfs.shape)
# Extrapolation of additional 20nm padding intervals.
spd.align(SpectralShape(spd.shape.start - 20, spd.shape.end + 20, 10))
for i in range(2):
spd[spd.wavelengths[i]] = (3 * spd.values[i + 2] -
3 * spd.values[i + 4] +
spd.values[i + 6])
i_e = len(spd) - 1 - i
spd[spd.wavelengths[i_e]] = (spd.values[i_e - 6] -
3 * spd.values[i_e - 4] +
3 * spd.values[i_e - 2])
# Interpolating every odd numbered values.
# TODO: Investigate code vectorisation.
for i in range(3, len(spd) - 3, 2):
spd[spd.wavelengths[i]] = (-0.0625 * spd.values[i - 3] +
0.5625 * spd.values[i - 1] +
0.5625 * spd.values[i + 1] -
0.0625 * spd.values[i + 3])
# Discarding the additional 20nm padding intervals.
spd.trim_wavelengths(SpectralShape(spd.shape.start + 20,
spd.shape.end - 20,
10))
XYZ = method(spd, cmfs, illuminant)
return XYZ
SPECTRAL_TO_XYZ_METHODS = CaseInsensitiveMapping(
{'ASTM E308-15': spectral_to_XYZ_ASTME30815,
'Integration': spectral_to_XYZ_integration})
"""
Supported spectral power distribution to *CIE XYZ* tristimulus values
conversion methods
SPECTRAL_TO_XYZ_METHODS : CaseInsensitiveMapping
**{'ASTM E308-15', 'Integration'}**
Aliases:
- 'astm2015': 'ASTM E308-15'
"""
SPECTRAL_TO_XYZ_METHODS['astm2015'] = (
SPECTRAL_TO_XYZ_METHODS['ASTM E308-15'])
[docs]def spectral_to_XYZ(
spd,
cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'],
illuminant=ones_spd(ASTME30815_PRACTISE_SHAPE),
method='ASTM E308-15',
**kwargs):
"""
Converts given spectral power distribution to *CIE XYZ* tristimulus values
using given colour matching functions, illuminant and method.
Parameters
----------
spd : SpectralPowerDistribution
Spectral power distribution.
cmfs : XYZ_ColourMatchingFunctions
Standard observer colour matching functions.
illuminant : SpectralPowerDistribution, optional
Illuminant spectral power distribution.
method : unicode, optional
**{'ASTM E308-15', 'Integration'}**,
Computation method.
Other Parameters
----------------
use_practice_range : bool, optional
{:func:`spectral_to_XYZ_ASTME30815`},
Practise *ASTM E308-15* working wavelengths range is [360, 780],
if `True` this argument will trim the colour matching functions
appropriately.
mi_5nm_omission_method : bool, optional
{:func:`spectral_to_XYZ_ASTME30815`},
5 nm measurement intervals spectral power distribution conversion to
tristimulus values will use a 5 nm version of the colour matching
functions instead of a table of tristimulus weighting factors.
mi_20nm_interpolation_method : bool, optional
{:func:`spectral_to_XYZ_ASTME30815`},
20 nm measurement intervals spectral power distribution conversion to
tristimulus values will use a dedicated interpolation method instead
of a table of tristimulus weighting factors.
Returns
-------
ndarray, (3,)
*CIE XYZ* tristimulus values.
Warning
-------
The output range of that definition is non standard!
Notes
-----
- Output *CIE XYZ* tristimulus values are in range [0, 100].
Examples
--------
>>> from colour import (
... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution)
>>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer']
>>> data = {
... 400: 0.0641,
... 420: 0.0645,
... 440: 0.0562,
... 460: 0.0537,
... 480: 0.0559,
... 500: 0.0651,
... 520: 0.0705,
... 540: 0.0772,
... 560: 0.0870,
... 580: 0.1128,
... 600: 0.1360,
... 620: 0.1511,
... 640: 0.1688,
... 660: 0.1996,
... 680: 0.2397,
... 700: 0.2852}
>>> spd = SpectralPowerDistribution('Sample', data)
>>> illuminant = ILLUMINANTS_RELATIVE_SPDS['D50']
>>> spectral_to_XYZ( # doctest: +ELLIPSIS
... spd, cmfs, illuminant)
array([ 11.5290265..., 9.9502091..., 4.7098882...])
>>> spectral_to_XYZ( # doctest: +ELLIPSIS
... spd, cmfs, illuminant, use_practice_range=False)
array([ 11.5291275..., 9.9502369..., 4.7098811...])
>>> spectral_to_XYZ( # doctest: +ELLIPSIS
... spd, cmfs, illuminant, method='Integration')
array([ 11.5296285..., 9.9499467..., 4.7066079...])
"""
function = SPECTRAL_TO_XYZ_METHODS[method]
filter_kwargs(function, **kwargs)
return function(spd, cmfs, illuminant, **kwargs)
[docs]def wavelength_to_XYZ(wavelength,
cmfs=STANDARD_OBSERVERS_CMFS[
'CIE 1931 2 Degree Standard Observer'],
method=None):
"""
Converts given wavelength :math:`\lambda` to *CIE XYZ* tristimulus values
using given colour matching functions.
If the wavelength :math:`\lambda` is not available in the colour matching
function, its value will be calculated using *CIE* recommendations:
The method developed by *Sprague (1880)* should be used for interpolating
functions having a uniformly spaced independent variable and a
*Cubic Spline* method for non-uniformly spaced independent variable.
Parameters
----------
wavelength : numeric or array_like
Wavelength :math:`\lambda` in nm.
cmfs : XYZ_ColourMatchingFunctions, optional
Standard observer colour matching functions.
method : unicode, optional
{None, 'Cubic Spline', 'Linear', 'Pchip', 'Sprague'},
Enforce given interpolation method.
Returns
-------
ndarray
*CIE XYZ* tristimulus values.
Raises
------
RuntimeError
If *Sprague (1880)* interpolation method is forced with a
non-uniformly spaced independent variable.
ValueError
If the interpolation method is not defined or if wavelength
:math:`\lambda` is not contained in the colour matching functions
domain.
Notes
-----
- Output *CIE XYZ* tristimulus values are in range [0, 1].
- If *scipy* is not unavailable the *Cubic Spline* method will fallback
to legacy *Linear* interpolation.
- *Sprague (1880)* interpolator cannot be used for interpolating
functions having a non-uniformly spaced independent variable.
Warning
-------
- If *scipy* is not unavailable the *Cubic Spline* method will fallback
to legacy *Linear* interpolation.
- *Cubic Spline* interpolator requires at least 3 wavelengths
:math:`\lambda_n` for interpolation.
- *Linear* interpolator requires at least 2 wavelengths :math:`\lambda_n`
for interpolation.
- *Pchip* interpolator requires at least 2 wavelengths :math:`\lambda_n`
for interpolation.
- *Sprague (1880)* interpolator requires at least 6 wavelengths
:math:`\lambda_n` for interpolation.
Examples
--------
Uniform data is using *Sprague (1880)* interpolation by default:
>>> from colour import CMFS
>>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer']
>>> wavelength_to_XYZ(480, cmfs) # doctest: +ELLIPSIS
array([ 0.09564 , 0.13902 , 0.812950...])
>>> wavelength_to_XYZ(480.5, cmfs) # doctest: +ELLIPSIS
array([ 0.0914287..., 0.1418350..., 0.7915726...])
Enforcing *Cubic Spline* interpolation:
>>> wavelength_to_XYZ(480.5, cmfs, 'Cubic Spline') # doctest: +ELLIPSIS
array([ 0.0914288..., 0.1418351..., 0.7915729...])
Enforcing *Linear* interpolation:
>>> wavelength_to_XYZ(480.5, cmfs, 'Linear') # doctest: +ELLIPSIS
array([ 0.0914697..., 0.1418482..., 0.7917337...])
Enforcing *Pchip* interpolation:
>>> wavelength_to_XYZ(480.5, cmfs, 'Pchip') # doctest: +ELLIPSIS
array([ 0.0914280..., 0.1418341..., 0.7915711...])
"""
cmfs_shape = cmfs.shape
if (np.min(wavelength) < cmfs_shape.start or
np.max(wavelength) > cmfs_shape.end):
raise ValueError(
'"{0} nm" wavelength is not in "[{1}, {2}]" domain!'.format(
wavelength, cmfs_shape.start, cmfs_shape.end))
if wavelength not in cmfs:
wavelengths, values, = cmfs.wavelengths, cmfs.values
if is_string(method):
method = method.lower()
is_uniform = cmfs.is_uniform()
if method is None:
if is_uniform:
interpolator = SpragueInterpolator
else:
interpolator = CubicSplineInterpolator
elif method == 'cubic spline':
interpolator = CubicSplineInterpolator
elif method == 'linear':
interpolator = LinearInterpolator
elif method == 'pchip':
interpolator = PchipInterpolator
elif method == 'sprague':
if is_uniform:
interpolator = SpragueInterpolator
else:
raise RuntimeError(
('"Sprague" interpolator can only be used for '
'interpolating functions having a uniformly spaced '
'independent variable!'))
else:
raise ValueError(
'Undefined "{0}" interpolator!'.format(method))
interpolators = [interpolator(wavelengths, values[..., i])
for i in range(values.shape[-1])]
XYZ = np.dstack([i(np.ravel(wavelength)) for i in interpolators])
else:
XYZ = cmfs[wavelength]
XYZ = np.reshape(XYZ, np.asarray(wavelength).shape + (3,))
return XYZ