Coverage for appearance/atd95.py: 51%
81 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2ATD (1995) Colour Vision Model
3==============================
5Define the *ATD (1995)* colour vision model.
7- :class:`colour.CAM_Specification_ATD95`
8- :func:`colour.XYZ_to_ATD95`
10Notes
11-----
12- According to *CIE TC1-34* definition of a colour appearance model, the
13 *ATD (1995)* model cannot be considered as a colour appearance model.
14 It was developed with different aims and is described as a model of
15 colour vision.
17References
18----------
19- :cite:`Fairchild2013v` : Fairchild, M. D. (2013). ATD Model. In Color
20 Appearance Models (3rd ed., pp. 5852-5991). Wiley. ISBN:B00DAYO8E2
21- :cite:`Guth1995a` : Guth, S. L. (1995). Further applications of the ATD
22 model for color vision. In E. Walowit (Ed.), Proc. SPIE 2414,
23 Device-Independent Color Imaging II (Vol. 2414, pp. 12-26).
24 doi:10.1117/12.206546
25"""
27from __future__ import annotations
29from dataclasses import dataclass, field
31import numpy as np
33from colour.algebra import spow, vecmul
34from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat # noqa: TC001
35from colour.utilities import (
36 MixinDataclassArithmetic,
37 as_float,
38 as_float_array,
39 from_range_degrees,
40 to_domain_100,
41 tsplit,
42 tstack,
43)
45__author__ = "Colour Developers"
46__copyright__ = "Copyright 2013 Colour Developers"
47__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
48__maintainer__ = "Colour Developers"
49__email__ = "colour-developers@colour-science.org"
50__status__ = "Production"
52__all__ = [
53 "CAM_ReferenceSpecification_ATD95",
54 "CAM_Specification_ATD95",
55 "XYZ_to_ATD95",
56 "luminance_to_retinal_illuminance",
57 "XYZ_to_LMS_ATD95",
58 "opponent_colour_dimensions",
59 "final_response",
60]
63@dataclass
64class CAM_ReferenceSpecification_ATD95(MixinDataclassArithmetic):
65 """
66 Define the *ATD (1995)* colour vision model reference specification.
68 This specification contains field names consistent with the *Fairchild
69 (2013)* reference.
71 Parameters
72 ----------
73 H
74 *Hue* angle :math:`H` in degrees.
75 C
76 Correlate of *saturation* :math:`C`. *Guth (1995)* incorrectly uses
77 the terms saturation and chroma interchangeably. However, :math:`C`
78 represents a measure of saturation rather than chroma since it is
79 calculated relative to the achromatic response for the stimulus
80 rather than that of a similarly illuminated white.
81 Br
82 Correlate of *brightness* :math:`Br`.
83 A_1
84 First stage :math:`A_1` response.
85 T_1
86 First stage :math:`T_1` response.
87 D_1
88 First stage :math:`D_1` response.
89 A_2
90 Second stage :math:`A_2` response.
91 T_2
92 Second stage :math:`A_2` response.
93 D_2
94 Second stage :math:`D_2` response.
96 References
97 ----------
98 :cite:`Fairchild2013v`, :cite:`Guth1995a`
99 """
101 H: float | NDArrayFloat | None = field(default_factory=lambda: None)
102 C: float | NDArrayFloat | None = field(default_factory=lambda: None)
103 Br: float | NDArrayFloat | None = field(default_factory=lambda: None)
104 A_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
105 T_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
106 D_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
107 A_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
108 T_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
109 D_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
112@dataclass
113class CAM_Specification_ATD95(MixinDataclassArithmetic):
114 """
115 Define the *ATD (1995)* colour vision model specification.
117 This specification provides a standardized interface for the *ATD (1995)*
118 model with field names consistent across all colour appearance models in
119 :mod:`colour.appearance`. While the field names differ from the original
120 *Fairchild (2013)* reference notation, they map directly to the model's
121 perceptual correlates.
123 Parameters
124 ----------
125 h
126 *Hue* angle :math:`H` in degrees.
127 C
128 Correlate of *saturation* :math:`C`. *Guth (1995)* incorrectly uses
129 the terms saturation and chroma interchangeably. However, :math:`C`
130 represents a measure of saturation rather than chroma since it is
131 measured relative to the achromatic response for the stimulus rather
132 than that of a similarly illuminated white.
133 Q
134 Correlate of *brightness* :math:`Br`.
135 A_1
136 First stage :math:`A_1` response.
137 T_1
138 First stage :math:`T_1` response.
139 D_1
140 First stage :math:`D_1` response.
141 A_2
142 Second stage :math:`A_2` response.
143 T_2
144 Second stage :math:`T_2` response.
145 D_2
146 Second stage :math:`D_2` response.
148 Notes
149 -----
150 - This specification is the one used in the current model
151 implementation.
153 References
154 ----------
155 :cite:`Fairchild2013v`, :cite:`Guth1995a`
156 """
158 h: float | NDArrayFloat | None = field(default_factory=lambda: None)
159 C: float | NDArrayFloat | None = field(default_factory=lambda: None)
160 Q: float | NDArrayFloat | None = field(default_factory=lambda: None)
161 A_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
162 T_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
163 D_1: float | NDArrayFloat | None = field(default_factory=lambda: None)
164 A_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
165 T_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
166 D_2: float | NDArrayFloat | None = field(default_factory=lambda: None)
169def XYZ_to_ATD95(
170 XYZ: Domain100,
171 XYZ_0: Domain100,
172 Y_0: ArrayLike,
173 k_1: ArrayLike,
174 k_2: ArrayLike,
175 sigma: ArrayLike = 300,
176) -> Annotated[CAM_Specification_ATD95, 360]:
177 """
178 Compute the *ATD (1995)* colour vision model correlates from the specified
179 *CIE XYZ* tristimulus values.
181 Parameters
182 ----------
183 XYZ
184 *CIE XYZ* tristimulus values of test sample / stimulus.
185 XYZ_0
186 *CIE XYZ* tristimulus values of reference white.
187 Y_0
188 Absolute adapting field luminance in :math:`cd/m^2`.
189 k_1
190 Application specific weight :math:`k_1`.
191 k_2
192 Application specific weight :math:`k_2`.
193 sigma
194 Constant :math:`\\sigma` varied to predict different types of data.
196 Returns
197 -------
198 :class:`colour.CAM_Specification_ATD95`
199 *ATD (1995)* colour vision model specification.
201 Notes
202 -----
203 +---------------------+-----------------------+---------------+
204 | **Domain** | **Scale - Reference** | **Scale - 1** |
205 +=====================+=======================+===============+
206 | ``XYZ`` | 100 | 1 |
207 +---------------------+-----------------------+---------------+
208 | ``XYZ_0`` | 100 | 1 |
209 +---------------------+-----------------------+---------------+
211 +---------------------+-----------------------+---------------+
212 | **Range** | **Scale - Reference** | **Scale - 1** |
213 +=====================+=======================+===============+
214 | ``specification.h`` | 360 | 1 |
215 +---------------------+-----------------------+---------------+
217 - For unrelated colours, there is only self-adaptation and :math:`k_1`
218 is set to 1.0 while :math:`k_2` is set to 0.0. For related colours
219 such as typical colorimetric applications, :math:`k_1` is set to 0.0
220 and :math:`k_2` is set to a value between 15 and 50 *(Guth, 1995)*.
222 References
223 ----------
224 :cite:`Fairchild2013v`, :cite:`Guth1995a`
226 Examples
227 --------
228 >>> XYZ = np.array([19.01, 20.00, 21.78])
229 >>> XYZ_0 = np.array([95.05, 100.00, 108.88])
230 >>> Y_0 = 318.31
231 >>> k_1 = 0.0
232 >>> k_2 = 50.0
233 >>> XYZ_to_ATD95(XYZ, XYZ_0, Y_0, k_1, k_2) # doctest: +ELLIPSIS
234 CAM_Specification_ATD95(h=1.9089869..., C=1.2064060..., Q=0.1814003..., \
235A_1=0.1787931... T_1=0.0286942..., D_1=0.0107584..., A_2=0.0192182..., \
236T_2=0.0205377..., D_2=0.0107584...)
237 """
239 XYZ = to_domain_100(XYZ)
240 XYZ_0 = to_domain_100(XYZ_0)
241 Y_0 = as_float_array(Y_0)
242 k_1 = as_float_array(k_1)
243 k_2 = as_float_array(k_2)
244 sigma = as_float_array(sigma)
246 XYZ = luminance_to_retinal_illuminance(XYZ, Y_0)
247 XYZ_0 = luminance_to_retinal_illuminance(XYZ_0, Y_0)
249 # Computing adaptation model.
250 LMS = XYZ_to_LMS_ATD95(XYZ)
251 XYZ_a = k_1[..., None] * XYZ + k_2[..., None] * XYZ_0
252 LMS_a = XYZ_to_LMS_ATD95(XYZ_a)
254 LMS_g = LMS * (sigma[..., None] / (sigma[..., None] + LMS_a))
256 # Computing opponent colour dimensions.
257 A_1, T_1, D_1, A_2, T_2, D_2 = tsplit(opponent_colour_dimensions(LMS_g))
259 # Computing the correlate of *brightness* :math:`Br`.
260 Br = spow(A_1**2 + T_1**2 + D_1**2, 0.5)
262 # Computing the correlate of *saturation* :math:`C`.
263 C = spow(T_2**2 + D_2**2, 0.5) / A_2
265 # Computing the *hue* :math:`H`. Note that the reference does not take the
266 # modulus of the :math:`H`, thus :math:`H` can exceed 360 degrees.
267 H = T_2 / D_2
269 return CAM_Specification_ATD95(
270 h=as_float(from_range_degrees(H)),
271 C=C,
272 Q=Br,
273 A_1=A_1,
274 T_1=T_1,
275 D_1=D_1,
276 A_2=A_2,
277 T_2=T_2,
278 D_2=D_2,
279 )
282def luminance_to_retinal_illuminance(XYZ: ArrayLike, Y_c: ArrayLike) -> NDArrayFloat:
283 """
284 Convert luminance in :math:`cd/m^2` to retinal illuminance in trolands.
286 This function converts photometric luminance values to retinal illuminance
287 by applying a power transformation that accounts for pupil area effects
288 under the specified adapting field luminance conditions.
290 Parameters
291 ----------
292 XYZ
293 *CIE XYZ* tristimulus values in photometric units.
294 Y_c
295 Absolute adapting field luminance in :math:`cd/m^2`.
297 Returns
298 -------
299 :class:`numpy.ndarray`
300 Retinal illuminance values in trolands corresponding to the
301 tristimulus values.
303 Examples
304 --------
305 >>> XYZ = np.array([19.01, 20.00, 21.78])
306 >>> Y_0 = 318.31
307 >>> luminance_to_retinal_illuminance(XYZ, Y_0) # doctest: +ELLIPSIS
308 array([ 479.4445924..., 499.3174313..., 534.5631673...])
309 """
311 XYZ = as_float_array(XYZ)
312 Y_c = as_float_array(Y_c)
314 return 18 * spow(Y_c[..., None] * XYZ / 100, 0.8)
317def XYZ_to_LMS_ATD95(XYZ: ArrayLike) -> NDArrayFloat:
318 """
319 Convert *CIE XYZ* tristimulus values to *LMS* cone responses using the
320 *ATD95* colour appearance model.
322 Parameters
323 ----------
324 XYZ
325 *CIE XYZ* tristimulus values.
327 Returns
328 -------
329 :class:`numpy.ndarray`
330 *LMS* cone responses.
332 Examples
333 --------
334 >>> XYZ = np.array([19.01, 20.00, 21.78])
335 >>> XYZ_to_LMS_ATD95(XYZ) # doctest: +ELLIPSIS
336 array([ 6.2283272..., 7.4780666..., 3.8859772...])
337 """
339 LMS = vecmul(
340 [
341 [0.2435, 0.8524, -0.0516],
342 [-0.3954, 1.1642, 0.0837],
343 [0.0000, 0.0400, 0.6225],
344 ],
345 XYZ,
346 )
347 LMS *= np.array([0.66, 1.0, 0.43])
349 LMS_p = spow(LMS, 0.7)
350 LMS_p += np.array([0.024, 0.036, 0.31])
352 return LMS_p
355def opponent_colour_dimensions(LMS_g: ArrayLike) -> NDArrayFloat:
356 """
357 Compute opponent colour dimensions from the specified post-adaptation cone
358 signals.
360 Parameters
361 ----------
362 LMS_g
363 Post-adaptation cone signals.
365 Returns
366 -------
367 :class:`numpy.ndarray`
368 Opponent colour dimensions.
370 Examples
371 --------
372 >>> LMS_g = np.array([6.95457922, 7.08945043, 6.44069316])
373 >>> opponent_colour_dimensions(LMS_g) # doctest: +ELLIPSIS
374 array([ 0.1787931..., 0.0286942..., 0.0107584..., 0.0192182..., ...])
375 """
377 L_g, M_g, S_g = tsplit(LMS_g)
379 A_1i = 3.57 * L_g + 2.64 * M_g
380 T_1i = 7.18 * L_g - 6.21 * M_g
381 D_1i = -0.7 * L_g + 0.085 * M_g + S_g
382 A_2i = 0.09 * A_1i
383 T_2i = 0.43 * T_1i + 0.76 * D_1i
384 D_2i = D_1i
386 A_1 = final_response(A_1i)
387 T_1 = final_response(T_1i)
388 D_1 = final_response(D_1i)
389 A_2 = final_response(A_2i)
390 T_2 = final_response(T_2i)
391 D_2 = final_response(D_2i)
393 return tstack([A_1, T_1, D_1, A_2, T_2, D_2])
396def final_response(value: ArrayLike) -> NDArrayFloat:
397 """
398 Compute the final response of the specified opponent colour dimension.
400 Parameters
401 ----------
402 value
403 Opponent colour dimension.
405 Returns
406 -------
407 :class:`numpy.ndarray`
408 Final response of the opponent colour dimension.
410 Examples
411 --------
412 >>> final_response(43.54399695501678) # doctest: +ELLIPSIS
413 0.1787931...
414 """
416 value = as_float_array(value)
418 return as_float(value / (200 + np.abs(value)))