"""
ATD (1995) Colour Vision Model
==============================
Define the *ATD (1995)* colour vision model objects:
- :class:`colour.CAM_Specification_ATD95`
- :func:`colour.XYZ_to_ATD95`
Notes
-----
- According to *CIE TC1-34* definition of a colour appearance model, the
*ATD (1995)* model cannot be considered as a colour appearance model.
It was developed with different aims and is described as a model of colour
vision.
References
----------
- :cite:`Fairchild2013v` : Fairchild, M. D. (2013). ATD Model. In Color
Appearance Models (3rd ed., pp. 5852-5991). Wiley. ISBN:B00DAYO8E2
- :cite:`Guth1995a` : Guth, S. L. (1995). Further applications of the ATD
model for color vision. In E. Walowit (Ed.), Proc. SPIE 2414,
Device-Independent Color Imaging II (Vol. 2414, pp. 12-26).
doi:10.1117/12.206546
"""
from __future__ import annotations
from dataclasses import dataclass, field
import numpy as np
from colour.algebra import spow, vecmul
from colour.hints import ArrayLike, NDArrayFloat
from colour.utilities import (
MixinDataclassArithmetic,
as_float,
as_float_array,
from_range_degrees,
to_domain_100,
tsplit,
tstack,
)
__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__ = [
"CAM_ReferenceSpecification_ATD95",
"CAM_Specification_ATD95",
"XYZ_to_ATD95",
"luminance_to_retinal_illuminance",
"XYZ_to_LMS_ATD95",
"opponent_colour_dimensions",
"final_response",
]
@dataclass
class CAM_ReferenceSpecification_ATD95(MixinDataclassArithmetic):
"""
Define the *ATD (1995)* colour vision model reference specification.
This specification has field names consistent with *Fairchild (2013)*
reference.
Parameters
----------
H
*Hue* angle :math:`H` in degrees.
C
Correlate of *saturation* :math:`C`. *Guth (1995)* incorrectly uses the
terms saturation and chroma interchangeably. However, :math:`C` is here
a measure of saturation rather than chroma since it is measured
relative to the achromatic response for the stimulus rather than that
of a similarly illuminated white.
Br
Correlate of *brightness* :math:`Br`.
A_1
First stage :math:`A_1` response.
T_1
First stage :math:`T_1` response.
D_1
First stage :math:`D_1` response.
A_2
Second stage :math:`A_2` response.
T_2
Second stage :math:`A_2` response.
D_2
Second stage :math:`D_2` response.
References
----------
:cite:`Fairchild2013v`, :cite:`Guth1995a`
"""
H: float | NDArrayFloat | None = field(default_factory=lambda: None)
C: float | NDArrayFloat | None = field(default_factory=lambda: None)
Br: float | NDArrayFloat | None = field(default_factory=lambda: None)
A_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
T_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
D_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
A_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
T_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
D_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
[docs]
@dataclass
class CAM_Specification_ATD95(MixinDataclassArithmetic):
"""
Define the *ATD (1995)* colour vision model specification.
This specification has field names consistent with the remaining colour
appearance models in :mod:`colour.appearance` but diverge from
*Fairchild (2013)* reference.
Parameters
----------
h
*Hue* angle :math:`H` in degrees.
C
Correlate of *saturation* :math:`C`. *Guth (1995)* incorrectly uses the
terms saturation and chroma interchangeably. However, :math:`C` is here
a measure of saturation rather than chroma since it is measured
relative to the achromatic response for the stimulus rather than that
of a similarly illuminated white.
Q
Correlate of *brightness* :math:`Br`.
A_1
First stage :math:`A_1` response.
T_1
First stage :math:`T_1` response.
D_1
First stage :math:`D_1` response.
A_2
Second stage :math:`A_2` response.
T_2
Second stage :math:`A_2` response.
D_2
Second stage :math:`D_2` response.
Notes
-----
- This specification is the one used in the current model implementation.
References
----------
:cite:`Fairchild2013v`, :cite:`Guth1995a`
"""
h: float | NDArrayFloat | None = field(default_factory=lambda: None)
C: float | NDArrayFloat | None = field(default_factory=lambda: None)
Q: float | NDArrayFloat | None = field(default_factory=lambda: None)
A_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
T_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
D_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
A_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
T_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
D_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
[docs]
def XYZ_to_ATD95(
XYZ: ArrayLike,
XYZ_0: ArrayLike,
Y_0: ArrayLike,
k_1: ArrayLike,
k_2: ArrayLike,
sigma: ArrayLike = 300,
) -> CAM_Specification_ATD95:
"""
Compute the *ATD (1995)* colour vision model correlates.
Parameters
----------
XYZ
*CIE XYZ* tristimulus values of test sample / stimulus.
XYZ_0
*CIE XYZ* tristimulus values of reference white.
Y_0
Absolute adapting field luminance in :math:`cd/m^2`.
k_1
Application specific weight :math:`k_1`.
k_2
Application specific weight :math:`k_2`.
sigma
Constant :math:`\\sigma` varied to predict different types of data.
Returns
-------
:class:`colour.CAM_Specification_ATD95`
*ATD (1995)* colour vision model specification.
Notes
-----
+------------+-----------------------+---------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===============+
| ``XYZ`` | [0, 100] | [0, 1] |
+------------+-----------------------+---------------+
| ``XYZ_0`` | [0, 100] | [0, 1] |
+------------+-----------------------+---------------+
+-------------------------------+-----------------------+---------------+
| **Range** | **Scale - Reference** | **Scale - 1** |
+===============================+=======================+===============+
| ``CAM_Specification_ATD95.h`` | [0, 360] | [0, 1] |
+-------------------------------+-----------------------+---------------+
- For unrelated colors, there is only self-adaptation and :math:`k_1` is
set to 1.0 while :math:`k_2` is set to 0.0. For related colors such as
typical colorimetric applications, :math:`k_1` is set to 0.0 and
:math:`k_2` is set to a value between 15 and 50 *(Guth, 1995)*.
References
----------
:cite:`Fairchild2013v`, :cite:`Guth1995a`
Examples
--------
>>> XYZ = np.array([19.01, 20.00, 21.78])
>>> XYZ_0 = np.array([95.05, 100.00, 108.88])
>>> Y_0 = 318.31
>>> k_1 = 0.0
>>> k_2 = 50.0
>>> XYZ_to_ATD95(XYZ, XYZ_0, Y_0, k_1, k_2) # doctest: +ELLIPSIS
CAM_Specification_ATD95(h=1.9089869..., C=1.2064060..., Q=0.1814003..., \
A_1=0.1787931... T_1=0.0286942..., D_1=0.0107584..., A_2=0.0192182..., \
T_2=0.0205377..., D_2=0.0107584...)
"""
XYZ = to_domain_100(XYZ)
XYZ_0 = to_domain_100(XYZ_0)
Y_0 = as_float_array(Y_0)
k_1 = as_float_array(k_1)
k_2 = as_float_array(k_2)
sigma = as_float_array(sigma)
XYZ = luminance_to_retinal_illuminance(XYZ, Y_0)
XYZ_0 = luminance_to_retinal_illuminance(XYZ_0, Y_0)
# Computing adaptation model.
LMS = XYZ_to_LMS_ATD95(XYZ)
XYZ_a = k_1[..., None] * XYZ + k_2[..., None] * XYZ_0
LMS_a = XYZ_to_LMS_ATD95(XYZ_a)
LMS_g = LMS * (sigma[..., None] / (sigma[..., None] + LMS_a))
# Computing opponent colour dimensions.
A_1, T_1, D_1, A_2, T_2, D_2 = tsplit(opponent_colour_dimensions(LMS_g))
# Computing the correlate of *brightness* :math:`Br`.
Br = spow(A_1**2 + T_1**2 + D_1**2, 0.5)
# Computing the correlate of *saturation* :math:`C`.
C = spow(T_2**2 + D_2**2, 0.5) / A_2
# Computing the *hue* :math:`H`. Note that the reference does not take the
# modulus of the :math:`H`, thus :math:`H` can exceed 360 degrees.
H = T_2 / D_2
return CAM_Specification_ATD95(
as_float(from_range_degrees(H)),
C,
Br,
A_1,
T_1,
D_1,
A_2,
T_2,
D_2,
)
def luminance_to_retinal_illuminance(XYZ: ArrayLike, Y_c: ArrayLike) -> NDArrayFloat:
"""
Convert from luminance in :math:`cd/m^2` to retinal illuminance in
trolands.
Parameters
----------
XYZ
*CIE XYZ* tristimulus values.
Y_c
Absolute adapting field luminance in :math:`cd/m^2`.
Returns
-------
:class:`numpy.ndarray`
Converted *CIE XYZ* tristimulus values in trolands.
Examples
--------
>>> XYZ = np.array([19.01, 20.00, 21.78])
>>> Y_0 = 318.31
>>> luminance_to_retinal_illuminance(XYZ, Y_0) # doctest: +ELLIPSIS
array([ 479.4445924..., 499.3174313..., 534.5631673...])
"""
XYZ = as_float_array(XYZ)
Y_c = as_float_array(Y_c)
return 18 * spow(Y_c[..., None] * XYZ / 100, 0.8)
def XYZ_to_LMS_ATD95(XYZ: ArrayLike) -> NDArrayFloat:
"""
Convert from *CIE XYZ* tristimulus values to *LMS* cone responses.
Parameters
----------
XYZ
*CIE XYZ* tristimulus values.
Returns
-------
:class:`numpy.ndarray`
*LMS* cone responses.
Examples
--------
>>> XYZ = np.array([19.01, 20.00, 21.78])
>>> XYZ_to_LMS_ATD95(XYZ) # doctest: +ELLIPSIS
array([ 6.2283272..., 7.4780666..., 3.8859772...])
"""
LMS = vecmul(
[
[0.2435, 0.8524, -0.0516],
[-0.3954, 1.1642, 0.0837],
[0.0000, 0.0400, 0.6225],
],
XYZ,
)
LMS *= np.array([0.66, 1.0, 0.43])
LMS_p = spow(LMS, 0.7)
LMS_p += np.array([0.024, 0.036, 0.31])
return LMS_p
def opponent_colour_dimensions(LMS_g: ArrayLike) -> NDArrayFloat:
"""
Return opponent colour dimensions from given post adaptation cone signals.
Parameters
----------
LMS_g
Post adaptation cone signals.
Returns
-------
:class:`numpy.ndarray`
Opponent colour dimensions.
Examples
--------
>>> LMS_g = np.array([6.95457922, 7.08945043, 6.44069316])
>>> opponent_colour_dimensions(LMS_g) # doctest: +ELLIPSIS
array([ 0.1787931..., 0.0286942..., 0.0107584..., 0.0192182..., ...])
"""
L_g, M_g, S_g = tsplit(LMS_g)
A_1i = 3.57 * L_g + 2.64 * M_g
T_1i = 7.18 * L_g - 6.21 * M_g
D_1i = -0.7 * L_g + 0.085 * M_g + S_g
A_2i = 0.09 * A_1i
T_2i = 0.43 * T_1i + 0.76 * D_1i
D_2i = D_1i
A_1 = final_response(A_1i)
T_1 = final_response(T_1i)
D_1 = final_response(D_1i)
A_2 = final_response(A_2i)
T_2 = final_response(T_2i)
D_2 = final_response(D_2i)
return tstack([A_1, T_1, D_1, A_2, T_2, D_2])
def final_response(value: ArrayLike) -> NDArrayFloat:
"""
Return the final response of given opponent colour dimension.
Parameters
----------
value
Opponent colour dimension.
Returns
-------
:class:`numpy.ndarray`
Final response of opponent colour dimension.
Examples
--------
>>> final_response(43.54399695501678) # doctest: +ELLIPSIS
0.1787931...
"""
value = as_float_array(value)
return as_float(value / (200 + np.abs(value)))