"""
CIE 1994 Chromatic Adaptation Model
===================================
Define the *CIE 1994* chromatic adaptation model objects:
- :func:`colour.adaptation.chromatic_adaptation_CIE1994`
References
----------
- :cite:`CIETC1-321994b` : CIE TC 1-32. (1994). CIE 109-1994 A Method of
Predicting Corresponding Colours under Different Chromatic and Illuminance
Adaptations. Commission Internationale de l'Eclairage.
ISBN:978-3-900734-51-0
"""
from __future__ import annotations
import numpy as np
from colour.adaptation import CAT_VON_KRIES
from colour.algebra import sdiv, sdiv_mode, spow, vecmul
from colour.hints import ArrayLike, NDArrayFloat
from colour.utilities import (
as_float_array,
from_range_100,
to_domain_100,
tsplit,
tstack,
usage_warning,
)
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"MATRIX_XYZ_TO_RGB_CIE1994",
"MATRIX_RGB_TO_XYZ_CIE1994",
"chromatic_adaptation_CIE1994",
"XYZ_to_RGB_CIE1994",
"RGB_to_XYZ_CIE1994",
"intermediate_values",
"effective_adapting_responses",
"beta_1",
"beta_2",
"exponential_factors",
"K_coefficient",
"corresponding_colour",
]
MATRIX_XYZ_TO_RGB_CIE1994: NDArrayFloat = CAT_VON_KRIES
"""
*CIE 1994* colour appearance model *CIE XYZ* tristimulus values to cone
responses matrix.
"""
MATRIX_RGB_TO_XYZ_CIE1994: NDArrayFloat = np.linalg.inv(MATRIX_XYZ_TO_RGB_CIE1994)
"""
*CIE 1994* colour appearance model cone responses to *CIE XYZ* tristimulus
values matrix.
"""
[docs]
def chromatic_adaptation_CIE1994(
XYZ_1: ArrayLike,
xy_o1: ArrayLike,
xy_o2: ArrayLike,
Y_o: ArrayLike,
E_o1: ArrayLike,
E_o2: ArrayLike,
n: ArrayLike = 1,
) -> NDArrayFloat:
"""
Adapt given stimulus *CIE XYZ_1* tristimulus values from test viewing
conditions to reference viewing conditions using *CIE 1994* chromatic
adaptation model.
Parameters
----------
XYZ_1
*CIE XYZ* tristimulus values of test sample / stimulus.
xy_o1
Chromaticity coordinates :math:`x_{o1}` and :math:`y_{o1}` of test
illuminant and background.
xy_o2
Chromaticity coordinates :math:`x_{o2}` and :math:`y_{o2}` of reference
illuminant and background.
Y_o
Luminance factor :math:`Y_o` of achromatic background as percentage
normalised to domain [18, 100] in **'Reference'** domain-range scale.
E_o1
Test illuminance :math:`E_{o1}` in :math:`cd/m^2`.
E_o2
Reference illuminance :math:`E_{o2}` in :math:`cd/m^2`.
n
Noise component in fundamental primary system.
Returns
-------
:class:`numpy.ndarray`
Adapted *CIE XYZ_2* tristimulus values of test stimulus.
Notes
-----
+------------+-----------------------+---------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===============+
| ``XYZ_1`` | [0, 100] | [0, 1] |
+------------+-----------------------+---------------+
| ``Y_o`` | [0, 100] | [0, 1] |
+------------+-----------------------+---------------+
+------------+-----------------------+---------------+
| **Range** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===============+
| ``XYZ_2`` | [0, 100] | [0, 1] |
+------------+-----------------------+---------------+
References
----------
:cite:`CIETC1-321994b`
Examples
--------
>>> XYZ_1 = np.array([28.00, 21.26, 5.27])
>>> xy_o1 = np.array([0.4476, 0.4074])
>>> xy_o2 = np.array([0.3127, 0.3290])
>>> Y_o = 20
>>> E_o1 = 1000
>>> E_o2 = 1000
>>> chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2)
... # doctest: +ELLIPSIS
array([ 24.0337952..., 21.1562121..., 17.6430119...])
"""
XYZ_1 = to_domain_100(XYZ_1)
Y_o = as_float_array(to_domain_100(Y_o))
E_o1 = as_float_array(E_o1)
E_o2 = as_float_array(E_o2)
if np.any(Y_o < 18) or np.any(Y_o > 100):
usage_warning(
'"Y_o" luminance factor must be in [18, 100] domain, '
"unpredictable results may occur!"
)
RGB_1 = XYZ_to_RGB_CIE1994(XYZ_1)
xez_1 = intermediate_values(xy_o1)
xez_2 = intermediate_values(xy_o2)
RGB_o1 = effective_adapting_responses(xez_1, Y_o, E_o1)
RGB_o2 = effective_adapting_responses(xez_2, Y_o, E_o2)
bRGB_o1 = exponential_factors(RGB_o1)
bRGB_o2 = exponential_factors(RGB_o2)
K = K_coefficient(xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, n)
RGB_2 = corresponding_colour(RGB_1, xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, K, n)
XYZ_2 = RGB_to_XYZ_CIE1994(RGB_2)
return from_range_100(XYZ_2)
def XYZ_to_RGB_CIE1994(XYZ: ArrayLike) -> NDArrayFloat:
"""
Convert from *CIE XYZ* tristimulus values to cone responses.
Parameters
----------
XYZ
*CIE XYZ* tristimulus values.
Returns
-------
:class:`numpy.ndarray`
Cone responses.
Examples
--------
>>> XYZ = np.array([28.00, 21.26, 5.27])
>>> XYZ_to_RGB_CIE1994(XYZ) # doctest: +ELLIPSIS
array([ 25.8244273..., 18.6791422..., 4.8390194...])
"""
return vecmul(MATRIX_XYZ_TO_RGB_CIE1994, XYZ)
def RGB_to_XYZ_CIE1994(RGB: ArrayLike) -> NDArrayFloat:
"""
Convert from cone responses to *CIE XYZ* tristimulus values.
Parameters
----------
RGB
Cone responses.
Returns
-------
:class:`numpy.ndarray`
*CIE XYZ* tristimulus values.
Examples
--------
>>> RGB = np.array([25.82442730, 18.67914220, 4.83901940])
>>> RGB_to_XYZ_CIE1994(RGB) # doctest: +ELLIPSIS
array([ 28. , 21.26, 5.27])
"""
return vecmul(MATRIX_RGB_TO_XYZ_CIE1994, RGB)
def intermediate_values(xy_o: ArrayLike) -> NDArrayFloat:
"""
Return the intermediate values :math:`\\xi`, :math:`\\eta`,
:math:`\\zeta`.
Parameters
----------
xy_o
Chromaticity coordinates :math:`x_o` and :math:`y_o` of whitepoint.
Returns
-------
:class:`numpy.ndarray`
Intermediate values :math:`\\xi`, :math:`\\eta`, :math:`\\zeta`.
Examples
--------
>>> xy_o = np.array([0.4476, 0.4074])
>>> intermediate_values(xy_o) # doctest: +ELLIPSIS
array([ 1.1185719..., 0.9329553..., 0.3268087...])
"""
x_o, y_o = tsplit(xy_o)
# Computing :math:`\\xi` :math:`\\eta`, :math:`\\zeta` values.
xi = (0.48105 * x_o + 0.78841 * y_o - 0.08081) / y_o
eta = (-0.27200 * x_o + 1.11962 * y_o + 0.04570) / y_o
zeta = (0.91822 * (1 - x_o - y_o)) / y_o
xez = tstack([xi, eta, zeta])
return xez
def effective_adapting_responses(
xez: ArrayLike, Y_o: ArrayLike, E_o: ArrayLike
) -> NDArrayFloat:
"""
Derive the effective adapting responses in the fundamental primary system
of the test or reference field.
Parameters
----------
xez
Intermediate values :math:`\\xi`, :math:`\\eta`, :math:`\\zeta`.
Y_o
Luminance factor :math:`Y_o` of achromatic background as percentage
normalised to domain [18, 100] in **'Reference'** domain-range scale.
E_o
Test or reference illuminance :math:`E_{o}` in lux.
Returns
-------
:class:`numpy.ndarray`
Effective adapting responses.
Examples
--------
>>> xez = np.array([1.11857195, 0.93295530, 0.32680879])
>>> E_o = 1000
>>> Y_o = 20
>>> effective_adapting_responses(xez, Y_o, E_o) # doctest: +ELLIPSIS
array([ 71.2105020..., 59.3937790..., 20.8052937...])
"""
xez = as_float_array(xez)
Y_o = as_float_array(Y_o)
E_o = as_float_array(E_o)
RGB_o = ((Y_o[..., None] * E_o[..., None]) / (100 * np.pi)) * xez
return RGB_o
def beta_1(x: ArrayLike) -> NDArrayFloat:
"""
Compute the exponent :math:`\\beta_1` for the middle and long-wavelength
sensitive cones.
Parameters
----------
x
Middle and long-wavelength sensitive cone response.
Returns
-------
:class:`numpy.ndarray`
Exponent :math:`\\beta_1`.
Examples
--------
>>> beta_1(318.323316315) # doctest: +ELLIPSIS
4.6106222...
"""
x_p = spow(x, 0.4495)
return (6.469 + 6.362 * x_p) / (6.469 + x_p)
def beta_2(x: ArrayLike) -> NDArrayFloat:
"""
Compute the exponent :math:`\\beta_2` for the short-wavelength sensitive
cones.
Parameters
----------
x
Short-wavelength sensitive cone response.
Returns
-------
:class:`numpy.ndarray`
Exponent :math:`\\beta_2`.
Examples
--------
>>> beta_2(318.323316315) # doctest: +ELLIPSIS
4.6522416...
"""
x_p = spow(x, 0.5128)
return 0.7844 * (8.414 + 8.091 * x_p) / (8.414 + x_p)
def exponential_factors(RGB_o: ArrayLike) -> NDArrayFloat:
"""
Return the chromatic adaptation exponential factors :math:`\\beta_1(R_o)`,
:math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)` of given cone responses.
Parameters
----------
RGB_o
Cone responses.
Returns
-------
:class:`numpy.ndarray`
Chromatic adaptation exponential factors :math:`\\beta_1(R_o)`,
:math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)`.
Examples
--------
>>> RGB_o = np.array([318.32331631, 318.30352317, 318.23283482])
>>> exponential_factors(RGB_o) # doctest: +ELLIPSIS
array([ 4.6106222..., 4.6105892..., 4.6520698...])
"""
R_o, G_o, B_o = tsplit(RGB_o)
bR_o = beta_1(R_o)
bG_o = beta_1(G_o)
bB_o = beta_2(B_o)
bRGB_o = tstack([bR_o, bG_o, bB_o])
return bRGB_o
def K_coefficient(
xez_1: ArrayLike,
xez_2: ArrayLike,
bRGB_o1: ArrayLike,
bRGB_o2: ArrayLike,
Y_o: ArrayLike,
n: ArrayLike = 1,
) -> NDArrayFloat:
"""
Compute the coefficient :math:`K` for correcting the difference between
the test and references illuminances.
Parameters
----------
xez_1
Intermediate values :math:`\\xi_1`, :math:`\\eta_1`, :math:`\\zeta_1`
for the test illuminant and background.
xez_2
Intermediate values :math:`\\xi_2`, :math:`\\eta_2`, :math:`\\zeta_2`
for the reference illuminant and background.
bRGB_o1
Chromatic adaptation exponential factors :math:`\\beta_1(R_{o1})`,
:math:`\\beta_1(G_{o1})` and :math:`\\beta_2(B_{o1})` of test sample.
bRGB_o2
Chromatic adaptation exponential factors :math:`\\beta_1(R_{o2})`,
:math:`\\beta_1(G_{o2})` and :math:`\\beta_2(B_{o2})` of reference
sample.
Y_o
Luminance factor :math:`Y_o` of achromatic background as percentage
normalised to domain [18, 100] in **'Reference'** domain-range scale.
n
Noise component in fundamental primary system.
Returns
-------
:class:`numpy.ndarray`
Coefficient :math:`K`.
Examples
--------
>>> xez_1 = np.array([1.11857195, 0.93295530, 0.32680879])
>>> xez_2 = np.array([1.00000372, 1.00000176, 0.99999461])
>>> bRGB_o1 = np.array([3.74852518, 3.63920879, 2.78924811])
>>> bRGB_o2 = np.array([3.68102374, 3.68102256, 3.56557351])
>>> Y_o = 20
>>> K_coefficient(xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o)
1.0
"""
xi_1, eta_1, _zeta_1 = tsplit(xez_1)
xi_2, eta_2, _zeta_2 = tsplit(xez_2)
bR_o1, bG_o1, _bB_o1 = tsplit(bRGB_o1)
bR_o2, bG_o2, _bB_o2 = tsplit(bRGB_o2)
Y_o = as_float_array(Y_o)
n = as_float_array(n)
K = spow((Y_o * xi_1 + n) / (20 * xi_1 + n), (2 / 3) * bR_o1) / spow(
(Y_o * xi_2 + n) / (20 * xi_2 + n), (2 / 3) * bR_o2
)
K *= spow((Y_o * eta_1 + n) / (20 * eta_1 + n), (1 / 3) * bG_o1) / spow(
(Y_o * eta_2 + n) / (20 * eta_2 + n), (1 / 3) * bG_o2
)
return K
def corresponding_colour(
RGB_1: ArrayLike,
xez_1: ArrayLike,
xez_2: ArrayLike,
bRGB_o1: ArrayLike,
bRGB_o2: ArrayLike,
Y_o: ArrayLike,
K: ArrayLike,
n: ArrayLike = 1,
) -> NDArrayFloat:
"""
Compute the corresponding colour cone responses of given test sample cone
responses :math:`RGB_1`.
Parameters
----------
RGB_1
Test sample cone responses :math:`RGB_1`.
xez_1
Intermediate values :math:`\\xi_1`, :math:`\\eta_1`, :math:`\\zeta_1`
for the test illuminant and background.
xez_2
Intermediate values :math:`\\xi_2`, :math:`\\eta_2`, :math:`\\zeta_2`
for the reference illuminant and background.
bRGB_o1
Chromatic adaptation exponential factors :math:`\\beta_1(R_{o1})`,
:math:`\\beta_1(G_{o1})` and :math:`\\beta_2(B_{o1})` of test sample.
bRGB_o2
Chromatic adaptation exponential factors :math:`\\beta_1(R_{o2})`,
:math:`\\beta_1(G_{o2})` and :math:`\\beta_2(B_{o2})` of reference
sample.
Y_o
Luminance factor :math:`Y_o` of achromatic background as percentage
normalised to domain [18, 100] in **'Reference'** domain-range scale.
K
Coefficient :math:`K`.
n
Noise component in fundamental primary system.
Returns
-------
:class:`numpy.ndarray`
Corresponding colour cone responses of given test sample cone
responses.
Examples
--------
>>> RGB_1 = np.array([25.82442730, 18.67914220, 4.83901940])
>>> xez_1 = np.array([1.11857195, 0.93295530, 0.32680879])
>>> xez_2 = np.array([1.00000372, 1.00000176, 0.99999461])
>>> bRGB_o1 = np.array([3.74852518, 3.63920879, 2.78924811])
>>> bRGB_o2 = np.array([3.68102374, 3.68102256, 3.56557351])
>>> Y_o = 20
>>> K = 1
>>> corresponding_colour(RGB_1, xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, K)
... # doctest: +ELLIPSIS
array([ 23.1636901..., 20.0211948..., 16.2001664...])
"""
R_1, G_1, B_1 = tsplit(RGB_1)
xi_1, eta_1, zeta_1 = tsplit(xez_1)
xi_2, eta_2, zeta_2 = tsplit(xez_2)
bR_o1, bG_o1, bB_o1 = tsplit(bRGB_o1)
bR_o2, bG_o2, bB_o2 = tsplit(bRGB_o2)
Y_o = as_float_array(Y_o)
K = as_float_array(K)
n = as_float_array(n)
def RGB_c(
x_1: NDArrayFloat,
x_2: NDArrayFloat,
y_1: NDArrayFloat,
y_2: NDArrayFloat,
z: NDArrayFloat,
n: NDArrayFloat,
) -> NDArrayFloat:
"""Compute the corresponding colour cone responses component."""
with sdiv_mode():
return (Y_o * x_2 + n) * spow(K, sdiv(1, y_2)) * spow(
(z + n) / (Y_o * x_1 + n), sdiv(y_1, y_2)
) - n
R_2 = RGB_c(xi_1, xi_2, bR_o1, bR_o2, R_1, n)
G_2 = RGB_c(eta_1, eta_2, bG_o1, bG_o2, G_1, n)
B_2 = RGB_c(zeta_1, zeta_2, bB_o1, bB_o2, B_1, n)
RGB_2 = tstack([R_2, G_2, B_2])
return RGB_2