Here is some .csv spectral data of high interest (they are in meters): https://github.com/colour-science/colour-ocean
%matplotlib inline
import numpy as np
import colour
COLOUR_RENDITION_CHART = colour.COLOURCHECKERS_SPDS['ColorChecker N Ohta']
SAMPLE_SPD = COLOUR_RENDITION_CHART['neutral 5 (.70 D)']
CMFS = colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']
ILLUMINANT = colour.ILLUMINANTS_RELATIVE_SPDS['D65']
XYZ = colour.spectral_to_XYZ(SAMPLE_SPD, CMFS, ILLUMINANT)
print(XYZ)
[ 19.31022942 20.30535098 22.15676876]
colour.spectral_to_XYZ definition is self-contained and doesn't have any dependencies on other objects except for some methods of colour.SpectralPowerDistribution class.
note: The computation domain is [0, 100].
def spectral_to_XYZ(spd,
cmfs=STANDARD_OBSERVERS_CMFS.get(
'CIE 1931 2 Degree Standard Observer'),
illuminant=None):
"""
Converts given spectral power distribution to *CIE XYZ* tristimulus values
using given colour matching functions and illuminant.
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 domain of that definition is non standard!
Notes
-----
- Output *CIE XYZ* tristimulus values are in domain [0, 100].
References
----------
.. [1] 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 # noqa
>>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer')
>>> data = {380: 0.0600, 390: 0.0600}
>>> spd = SpectralPowerDistribution('Custom', data)
>>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50')
>>> spectral_to_XYZ(spd, cmfs, illuminant) # doctest: +ELLIPSIS
array([ 4.5764852...e-04, 1.2964866...e-05, 2.1615807...e-03])
"""
# http://nbviewer.jupyter.org/github/colour-science/colour-notebooks/blob/master/notebooks/colorimetry/spectrum.ipynb#CIE-XYZ-Tristimulus-Values
# This first block ensures that we have all the spectral data available and with matching
# spectral shape (start, end, steps), `CMFS` will be the spectral shape reference.
shape = cmfs.shape
if spd.shape != cmfs.shape:
# If `spd` argument has a different shape than `CMFS` argument, we fill it with *0*.
# We clone it before because a lot of operations on spectral power distributions
# happen in place and we want to keep the original one vanilla.
spd = spd.clone().zeros(shape)
if illuminant is None:
# No `illuminant` argument has been provided thus we use a *1* filled illuminant instead.
# Cases where you don't provide an illuminant are when your input `spd` is an
# actual light source or illuminant.
illuminant = ones_spd(shape)
else:
if illuminant.shape != cmfs.shape:
# If `illuminant` argument has a different shape than `CMFS` argument, we fill it with *0*.
illuminant = illuminant.clone().zeros(shape)
# We retrieve the actual spectral data that is now aligned with the *CMFS* shape.
spd = spd.values
x_bar, y_bar, z_bar = (cmfs.x_bar.values,
cmfs.y_bar.values,
cmfs.z_bar.values)
illuminant = illuminant.values
# Follows the integral implementation as a summation.
# Products.
x_products = spd * x_bar * illuminant
y_products = spd * y_bar * illuminant
z_products = spd * z_bar * illuminant
# *CIE* uses a [0, 100] computation domain for the spectral to tristimulus values conversion.
# and normalises the Luminance with a *100* factor.
normalising_factor = 100 / np.sum(y_bar * illuminant)
# Summation.
XYZ = np.array([normalising_factor * np.sum(x_products),
normalising_factor * np.sum(y_products),
normalising_factor * np.sum(z_products)])
return XYZ
RGB = colour.XYZ_to_sRGB(XYZ / 100, transfer_function=False)
print(RGB)
[ 0.20319374 0.20296181 0.20354896]
colour.XYZ_to_sRGB definition is a convenient wrapper around colour.XYZ_to_RGB.
note: The computation domain is [0, 1].
def XYZ_to_sRGB(XYZ,
illuminant=RGB_COLOURSPACES.get('sRGB').whitepoint,
chromatic_adaptation_transform='CAT02',
transfer_function=True):
"""
Converts from *CIE XYZ* tristimulus values to *sRGB* colourspace.
Parameters
----------
XYZ : array_like
*CIE XYZ* tristimulus values.
illuminant : array_like, optional
Source illuminant chromaticity coordinates.
chromatic_adaptation_transform : unicode, optional
{'CAT02', 'XYZ Scaling', 'Von Kries', 'Bradford', 'Sharp', 'Fairchild,
'CMCCAT97', 'CMCCAT2000', 'CAT02_BRILL_CAT', 'Bianco', 'Bianco PC'},
*Chromatic adaptation* transform.
transfer_function : bool, optional
Apply *sRGB* *opto-electronic conversion function*.
Returns
-------
ndarray
*sRGB* colour array.
Notes
-----
- Input *CIE XYZ* tristimulus values are in domain [0, 1].
Examples
--------
>>> import numpy as np
>>> XYZ = np.array([0.07049534, 0.10080000, 0.09558313])
>>> XYZ_to_sRGB(XYZ) # doctest: +ELLIPSIS
array([ 0.1750135..., 0.3881879..., 0.3216195...])
"""
sRGB = RGB_COLOURSPACES.get('sRGB')
# Nothing fancy here, just passing relevant arguments to `colour.XYZ_to_RGB`.
# The `illuminant` argument is used to perform chromatic adaptation between
# the illuminant used for the spectral to tristimulus values conversion and
# sRGB *D65* whitepoint. Default is to assume that `illuminant` argument is
# *D65*, thus discounting chromatic adaptation entirely.
return XYZ_to_RGB(XYZ,
illuminant,
sRGB.whitepoint,
sRGB.XYZ_to_RGB_matrix,
chromatic_adaptation_transform,
sRGB.transfer_function if transfer_function else None)
def XYZ_to_RGB(XYZ,
illuminant_XYZ,
illuminant_RGB,
XYZ_to_RGB_matrix,
chromatic_adaptation_transform='CAT02',
transfer_function=None):
"""
Converts from *CIE XYZ* tristimulus values to given *RGB* colourspace.
Parameters
----------
XYZ : array_like
*CIE XYZ* tristimulus values.
illuminant_XYZ : array_like
*CIE XYZ* tristimulus values *illuminant* *xy* chromaticity
coordinates.
illuminant_RGB : array_like
*RGB* colourspace *illuminant* *xy* chromaticity coordinates.
XYZ_to_RGB_matrix : array_like
*Normalised primary matrix*.
chromatic_adaptation_transform : unicode, optional
{'CAT02', 'XYZ Scaling', 'Von Kries', 'Bradford', 'Sharp', 'Fairchild,
'CMCCAT97', 'CMCCAT2000', 'CAT02_BRILL_CAT', 'Bianco', 'Bianco PC'},
*Chromatic adaptation* transform.
transfer_function : object, optional
*Opto-electronic conversion function*.
Returns
-------
ndarray
*RGB* colourspace array.
Notes
-----
- Input *CIE XYZ* tristimulus values are in domain [0, 1].
- Input *illuminant_XYZ* *xy* chromaticity coordinates are in domain
[0, 1].
- Input *illuminant_RGB* *xy* chromaticity coordinates are in domain
[0, 1].
- Output *RGB* colourspace array is in domain [0, 1].
Examples
--------
>>> XYZ = np.array([0.07049534, 0.10080000, 0.09558313])
>>> illuminant_XYZ = np.array([0.34567, 0.35850])
>>> illuminant_RGB = np.array([0.31271, 0.32902])
>>> chromatic_adaptation_transform = 'Bradford'
>>> XYZ_to_RGB_matrix = np.array([
... [3.24100326, -1.53739899, -0.49861587],
... [-0.96922426, 1.87592999, 0.04155422],
... [0.05563942, -0.20401120, 1.05714897]])
>>> XYZ_to_RGB(
... XYZ,
... illuminant_XYZ,
... illuminant_RGB,
... XYZ_to_RGB_matrix,
... chromatic_adaptation_transform) # doctest: +ELLIPSIS
array([ 0.0110360..., 0.1273446..., 0.1163103...])
"""
# The first step for conversion from *CIE XYZ* to *RGB* is to compute
# the chromatic adaptation matrix, I would actually suggest to always
# account for it in any *CIE XYZ* to *RGB* conversion code even if
# you don't need it, thus the day the whitepoints / illuminants are
# different, nothing will break.
M = chromatic_adaptation_matrix_VonKries(
xy_to_XYZ(illuminant_XYZ),
xy_to_XYZ(illuminant_RGB),
transform=chromatic_adaptation_transform)
XYZ_a = dot_vector(M, XYZ)
# XYZ_to_RGB_matrix is the inverse of the NPM and can be computed using
# `colour.normalised_primary_matrix` or taking the inverse of
# https://www.colour-science.org/cgi-bin/rgb_colourspace_models_derivation.cgi
RGB = dot_vector(XYZ_to_RGB_matrix, XYZ_a)
if transfer_function is not None:
RGB = transfer_function(RGB)
return RGB
def chromatic_adaptation_matrix_VonKries(XYZ_w, XYZ_wr, transform='CAT02'):
"""
Computes the *chromatic adaptation* matrix from test viewing conditions
to reference viewing conditions.
Parameters
----------
XYZ_w : array_like
Test viewing condition *CIE XYZ* tristimulus values of whitepoint.
XYZ_wr : array_like
Reference viewing condition *CIE XYZ* tristimulus values of whitepoint.
transform : unicode, optional
{'CAT02', 'XYZ Scaling', 'Von Kries', 'Bradford', 'Sharp', 'Fairchild,
'CMCCAT97', 'CMCCAT2000', 'CAT02_BRILL_CAT', 'Bianco', 'Bianco PC'},
Chromatic adaptation transform.
Returns
-------
ndarray
Chromatic adaptation matrix.
Raises
------
KeyError
If chromatic adaptation method is not defined.
Examples
--------
>>> XYZ_w = np.array([1.09846607, 1.00000000, 0.35582280])
>>> XYZ_wr = np.array([0.95042855, 1.00000000, 1.08890037])
>>> chromatic_adaptation_matrix_VonKries(XYZ_w, XYZ_wr) # noqa # doctest: +ELLIPSIS
array([[ 0.8687653..., -0.1416539..., 0.3871961...],
[-0.1030072..., 1.0584014..., 0.1538646...],
[ 0.0078167..., 0.0267875..., 2.9608177...]])
Using Bradford method:
>>> XYZ_w = np.array([1.09846607, 1.00000000, 0.35582280])
>>> XYZ_wr = np.array([0.95042855, 1.00000000, 1.08890037])
>>> method = 'Bradford'
>>> chromatic_adaptation_matrix_VonKries(XYZ_w, XYZ_wr, method) # noqa # doctest: +ELLIPSIS
array([[ 0.8446794..., -0.1179355..., 0.3948940...],
[-0.1366408..., 1.1041236..., 0.1291981...],
[ 0.0798671..., -0.1349315..., 3.1928829...]])
"""
# http://nbviewer.jupyter.org/github/colour-science/colour-notebooks/blob/master/notebooks/adaptation/vonkries.ipynb
# http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
M = CHROMATIC_ADAPTATION_TRANSFORMS.get(transform)
if M is None:
raise KeyError(
'"{0}" chromatic adaptation transform is not defined! Supported '
'methods: "{1}".'.format(transform,
CHROMATIC_ADAPTATION_TRANSFORMS.keys()))
rgb_w = np.einsum('...i,...ij->...j', XYZ_w, np.transpose(M))
rgb_wr = np.einsum('...i,...ij->...j', XYZ_wr, np.transpose(M))
D = rgb_wr / rgb_w
D = row_as_diagonal(D)
cat = dot_matrix(np.linalg.inv(M), D)
cat = dot_matrix(cat, M)
return cat
RGB = (colour.RGB_COLOURSPACES['sRGB'].transfer_function(RGB) * 255).astype(np.int_)
print(RGB)
[124 124 124]