Coverage for colour/adaptation/cie1994.py: 100%
101 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2CIE 1994 Chromatic Adaptation Model
3===================================
5Define the *CIE 1994* chromatic adaptation model for predicting corresponding
6colours under different viewing conditions.
8- :func:`colour.adaptation.chromatic_adaptation_CIE1994`
10References
11----------
12- :cite:`CIETC1-321994b` : CIE TC 1-32. (1994). CIE 109-1994 A Method of
13 Predicting Corresponding Colours under Different Chromatic and Illuminance
14 Adaptations. Commission Internationale de l'Eclairage.
15 ISBN:978-3-900734-51-0
16"""
18from __future__ import annotations
20import typing
22import numpy as np
24from colour.adaptation import CAT_VON_KRIES
25from colour.algebra import sdiv, sdiv_mode, spow, vecmul
27if typing.TYPE_CHECKING:
28 from colour.hints import DTypeFloat, NDArray
30from colour.hints import ( # noqa: TC001
31 ArrayLike,
32 Domain100,
33 NDArrayFloat,
34 Range100,
35)
36from colour.utilities import (
37 as_float_array,
38 from_range_100,
39 to_domain_100,
40 tsplit,
41 tstack,
42 usage_warning,
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 "MATRIX_XYZ_TO_RGB_CIE1994",
54 "MATRIX_RGB_TO_XYZ_CIE1994",
55 "chromatic_adaptation_CIE1994",
56 "XYZ_to_RGB_CIE1994",
57 "RGB_to_XYZ_CIE1994",
58 "intermediate_values",
59 "effective_adapting_responses",
60 "beta_1",
61 "beta_2",
62 "exponential_factors",
63 "K_coefficient",
64 "corresponding_colour",
65]
67MATRIX_XYZ_TO_RGB_CIE1994: NDArrayFloat = CAT_VON_KRIES
68"""
69*CIE 1994* colour appearance model *CIE XYZ* tristimulus values to cone
70responses matrix.
71"""
73MATRIX_RGB_TO_XYZ_CIE1994: NDArrayFloat = np.linalg.inv(MATRIX_XYZ_TO_RGB_CIE1994)
74"""
75*CIE 1994* colour appearance model cone responses to *CIE XYZ* tristimulus
76values matrix.
77"""
80def chromatic_adaptation_CIE1994(
81 XYZ_1: Domain100,
82 xy_o1: ArrayLike,
83 xy_o2: ArrayLike,
84 Y_o: Domain100,
85 E_o1: ArrayLike,
86 E_o2: ArrayLike,
87 n: ArrayLike = 1,
88) -> Range100:
89 """
90 Adapt the specified stimulus *CIE XYZ* tristimulus values from test
91 viewing conditions to reference viewing conditions using the
92 *CIE 1994* chromatic adaptation model.
94 Parameters
95 ----------
96 XYZ_1
97 *CIE XYZ* tristimulus values of test sample / stimulus.
98 xy_o1
99 Chromaticity coordinates :math:`x_{o1}` and :math:`y_{o1}` of test
100 illuminant and background.
101 xy_o2
102 Chromaticity coordinates :math:`x_{o2}` and :math:`y_{o2}` of
103 reference illuminant and background.
104 Y_o
105 Luminance factor :math:`Y_o` of achromatic background as percentage
106 normalised to domain [18, 100] in **'Reference'** domain-range scale.
107 E_o1
108 Test illuminance :math:`E_{o1}` in lux.
109 E_o2
110 Reference illuminance :math:`E_{o2}` in lux.
111 n
112 Noise component in fundamental primary system.
114 Returns
115 -------
116 :class:`numpy.ndarray`
117 *CIE XYZ* tristimulus values of the stimulus corresponding colour.
119 Notes
120 -----
121 +------------+-----------------------+---------------+
122 | **Domain** | **Scale - Reference** | **Scale - 1** |
123 +============+=======================+===============+
124 | ``XYZ_1`` | 100 | 1 |
125 +------------+-----------------------+---------------+
126 | ``Y_o`` | 100 | 1 |
127 +------------+-----------------------+---------------+
129 +------------+-----------------------+---------------+
130 | **Range** | **Scale - Reference** | **Scale - 1** |
131 +============+=======================+===============+
132 | ``XYZ_2`` | 100 | 1 |
133 +------------+-----------------------+---------------+
135 References
136 ----------
137 :cite:`CIETC1-321994b`
139 Examples
140 --------
141 >>> XYZ_1 = np.array([28.00, 21.26, 5.27])
142 >>> xy_o1 = np.array([0.4476, 0.4074])
143 >>> xy_o2 = np.array([0.3127, 0.3290])
144 >>> Y_o = 20
145 >>> E_o1 = 1000
146 >>> E_o2 = 1000
147 >>> chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2)
148 ... # doctest: +ELLIPSIS
149 array([ 24.0337952..., 21.1562121..., 17.6430119...])
150 """
152 XYZ_1 = to_domain_100(XYZ_1)
153 Y_o = as_float_array(to_domain_100(Y_o))
154 E_o1 = as_float_array(E_o1)
155 E_o2 = as_float_array(E_o2)
157 if np.any(Y_o < 18) or np.any(Y_o > 100):
158 usage_warning(
159 '"Y_o" luminance factor must be in [18, 100] domain, '
160 "unpredictable results may occur!"
161 )
163 RGB_1 = XYZ_to_RGB_CIE1994(XYZ_1)
165 xez_1 = intermediate_values(xy_o1)
166 xez_2 = intermediate_values(xy_o2)
168 RGB_o1 = effective_adapting_responses(xez_1, Y_o, E_o1)
169 RGB_o2 = effective_adapting_responses(xez_2, Y_o, E_o2)
171 bRGB_o1 = exponential_factors(RGB_o1)
172 bRGB_o2 = exponential_factors(RGB_o2)
174 K = K_coefficient(xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, n)
176 RGB_2 = corresponding_colour(RGB_1, xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, K, n)
177 XYZ_2 = RGB_to_XYZ_CIE1994(RGB_2)
179 return from_range_100(XYZ_2)
182def XYZ_to_RGB_CIE1994(XYZ: ArrayLike) -> NDArrayFloat:
183 """
184 Convert from *CIE XYZ* tristimulus values to cone responses using the
185 *CIE 1994* colour appearance model transformation.
187 Parameters
188 ----------
189 XYZ
190 *CIE XYZ* tristimulus values.
192 Returns
193 -------
194 :class:`numpy.ndarray`
195 Cone responses.
197 Examples
198 --------
199 >>> XYZ = np.array([28.00, 21.26, 5.27])
200 >>> XYZ_to_RGB_CIE1994(XYZ) # doctest: +ELLIPSIS
201 array([ 25.8244273..., 18.6791422..., 4.8390194...])
202 """
204 return vecmul(MATRIX_XYZ_TO_RGB_CIE1994, XYZ)
207def RGB_to_XYZ_CIE1994(RGB: ArrayLike) -> NDArrayFloat:
208 """
209 Convert from cone responses to *CIE XYZ* tristimulus values using the
210 *CIE 1994* colour appearance model inverse transformation.
212 Parameters
213 ----------
214 RGB
215 Cone responses.
217 Returns
218 -------
219 :class:`numpy.ndarray`
220 *CIE XYZ* tristimulus values.
222 Examples
223 --------
224 >>> RGB = np.array([25.82442730, 18.67914220, 4.83901940])
225 >>> RGB_to_XYZ_CIE1994(RGB) # doctest: +ELLIPSIS
226 array([ 28. , 21.26, 5.27])
227 """
229 return vecmul(MATRIX_RGB_TO_XYZ_CIE1994, RGB)
232def intermediate_values(xy_o: ArrayLike) -> NDArrayFloat:
233 """
234 Compute the intermediate values :math:`\\xi`, :math:`\\eta`, and
235 :math:`\\zeta`.
237 Parameters
238 ----------
239 xy_o
240 Chromaticity coordinates :math:`x_o` and :math:`y_o` of the
241 whitepoint.
243 Returns
244 -------
245 :class:`numpy.ndarray`
246 Intermediate values :math:`\\xi`, :math:`\\eta`, and
247 :math:`\\zeta`.
249 Examples
250 --------
251 >>> xy_o = np.array([0.4476, 0.4074])
252 >>> intermediate_values(xy_o) # doctest: +ELLIPSIS
253 array([ 1.1185719..., 0.9329553..., 0.3268087...])
254 """
256 x_o, y_o = tsplit(xy_o)
258 # Computing :math:`\\xi` :math:`\\eta`, :math:`\\zeta` values.
259 xi = (0.48105 * x_o + 0.78841 * y_o - 0.08081) / y_o
260 eta = (-0.27200 * x_o + 1.11962 * y_o + 0.04570) / y_o
261 zeta = (0.91822 * (1 - x_o - y_o)) / y_o
263 return tstack([xi, eta, zeta])
266def effective_adapting_responses(
267 xez: ArrayLike, Y_o: ArrayLike, E_o: ArrayLike
268) -> NDArrayFloat:
269 """
270 Compute the effective adapting responses in the fundamental primary system
271 of the test or reference field.
273 Parameters
274 ----------
275 xez
276 Intermediate values :math:`\\xi`, :math:`\\eta`, :math:`\\zeta`.
277 Y_o
278 Luminance factor :math:`Y_o` of achromatic background as percentage
279 normalised to domain [18, 100] in **'Reference'** domain-range scale.
280 E_o
281 Test or reference illuminance :math:`E_o` in lux.
283 Returns
284 -------
285 :class:`numpy.ndarray`
286 Effective adapting responses.
288 Examples
289 --------
290 >>> xez = np.array([1.11857195, 0.93295530, 0.32680879])
291 >>> E_o = 1000
292 >>> Y_o = 20
293 >>> effective_adapting_responses(xez, Y_o, E_o) # doctest: +ELLIPSIS
294 array([ 71.2105020..., 59.3937790..., 20.8052937...])
295 """
297 xez = as_float_array(xez)
298 Y_o = as_float_array(Y_o)
299 E_o = as_float_array(E_o)
301 return ((Y_o[..., None] * E_o[..., None]) / (100 * np.pi)) * xez
304@typing.overload
305def beta_1(x: float | DTypeFloat) -> DTypeFloat: ...
306@typing.overload
307def beta_1(x: NDArray) -> NDArrayFloat: ...
308@typing.overload
309def beta_1(x: ArrayLike) -> DTypeFloat | NDArrayFloat: ...
310def beta_1(x: ArrayLike) -> DTypeFloat | NDArrayFloat:
311 """
312 Compute the exponent :math:`\\beta_1` for the middle and long-wavelength
313 sensitive cones.
315 Parameters
316 ----------
317 x
318 Middle and long-wavelength sensitive cone response.
320 Returns
321 -------
322 :class:`numpy.ndarray`
323 Exponent :math:`\\beta_1`.
325 Examples
326 --------
327 >>> beta_1(318.323316315) # doctest: +ELLIPSIS
328 4.6106222...
329 """
331 x_p = spow(x, 0.4495)
333 return (x_p * 6.362 + 6.469) / (x_p + 6.469)
336@typing.overload
337def beta_2(x: float | DTypeFloat) -> DTypeFloat: ...
338@typing.overload
339def beta_2(x: NDArray) -> NDArrayFloat: ...
340@typing.overload
341def beta_2(x: ArrayLike) -> DTypeFloat | NDArrayFloat: ...
342def beta_2(x: ArrayLike) -> DTypeFloat | NDArrayFloat:
343 """
344 Compute the exponent :math:`\\beta_2` for the short-wavelength sensitive
345 cones.
347 Parameters
348 ----------
349 x
350 Short-wavelength sensitive cone response.
352 Returns
353 -------
354 :class:`numpy.ndarray`
355 Exponent :math:`\\beta_2`.
357 Examples
358 --------
359 >>> beta_2(318.323316315) # doctest: +ELLIPSIS
360 4.6522416...
361 """
363 x_p = spow(x, 0.5128)
365 return (x_p * 8.091 + 8.414) * 0.7844 / (x_p + 8.414)
368def exponential_factors(RGB_o: ArrayLike) -> NDArrayFloat:
369 """
370 Compute the chromatic adaptation exponential factors
371 :math:`\\beta_1(R_o)`, :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)` of
372 the specified cone responses.
374 Parameters
375 ----------
376 RGB_o
377 Cone responses.
379 Returns
380 -------
381 :class:`numpy.ndarray`
382 Chromatic adaptation exponential factors :math:`\\beta_1(R_o)`,
383 :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)`.
385 Examples
386 --------
387 >>> RGB_o = np.array([318.32331631, 318.30352317, 318.23283482])
388 >>> exponential_factors(RGB_o) # doctest: +ELLIPSIS
389 array([ 4.6106222..., 4.6105892..., 4.6520698...])
390 """
392 R_o, G_o, B_o = tsplit(RGB_o)
394 bR_o = beta_1(R_o)
395 bG_o = beta_1(G_o)
396 bB_o = beta_2(B_o)
398 return tstack([bR_o, bG_o, bB_o])
401def K_coefficient(
402 xez_1: ArrayLike,
403 xez_2: ArrayLike,
404 bRGB_o1: ArrayLike,
405 bRGB_o2: ArrayLike,
406 Y_o: ArrayLike,
407 n: ArrayLike = 1,
408) -> NDArrayFloat:
409 """
410 Compute the coefficient :math:`K` for correcting the difference between
411 the test and reference illuminances.
413 Parameters
414 ----------
415 xez_1
416 Intermediate values :math:`\\xi_1`, :math:`\\eta_1`, :math:`\\zeta_1`
417 for the test illuminant and background.
418 xez_2
419 Intermediate values :math:`\\xi_2`, :math:`\\eta_2`, :math:`\\zeta_2`
420 for the reference illuminant and background.
421 bRGB_o1
422 Chromatic adaptation exponential factors :math:`\\beta_1(R_{o1})`,
423 :math:`\\beta_1(G_{o1})` and :math:`\\beta_2(B_{o1})` of test sample.
424 bRGB_o2
425 Chromatic adaptation exponential factors :math:`\\beta_1(R_{o2})`,
426 :math:`\\beta_1(G_{o2})` and :math:`\\beta_2(B_{o2})` of reference
427 sample.
428 Y_o
429 Luminance factor :math:`Y_o` of achromatic background as percentage
430 normalised to domain [18, 100] in **'Reference'** domain-range scale.
431 n
432 Noise component in fundamental primary system.
434 Returns
435 -------
436 :class:`numpy.ndarray`
437 Coefficient :math:`K`.
439 Examples
440 --------
441 >>> xez_1 = np.array([1.11857195, 0.93295530, 0.32680879])
442 >>> xez_2 = np.array([1.00000372, 1.00000176, 0.99999461])
443 >>> bRGB_o1 = np.array([3.74852518, 3.63920879, 2.78924811])
444 >>> bRGB_o2 = np.array([3.68102374, 3.68102256, 3.56557351])
445 >>> Y_o = 20
446 >>> K_coefficient(xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o)
447 1.0
448 """
450 xi_1, eta_1, _zeta_1 = tsplit(xez_1)
451 xi_2, eta_2, _zeta_2 = tsplit(xez_2)
452 bR_o1, bG_o1, _bB_o1 = tsplit(bRGB_o1)
453 bR_o2, bG_o2, _bB_o2 = tsplit(bRGB_o2)
454 Y_o = as_float_array(Y_o)
455 n = as_float_array(n)
457 K = spow((Y_o * xi_1 + n) / (20 * xi_1 + n), (2 / 3) * bR_o1) / spow(
458 (Y_o * xi_2 + n) / (20 * xi_2 + n), (2 / 3) * bR_o2
459 )
461 K *= spow((Y_o * eta_1 + n) / (20 * eta_1 + n), (1 / 3) * bG_o1) / spow(
462 (Y_o * eta_2 + n) / (20 * eta_2 + n), (1 / 3) * bG_o2
463 )
465 return K
468def corresponding_colour(
469 RGB_1: ArrayLike,
470 xez_1: ArrayLike,
471 xez_2: ArrayLike,
472 bRGB_o1: ArrayLike,
473 bRGB_o2: ArrayLike,
474 Y_o: ArrayLike,
475 K: ArrayLike,
476 n: ArrayLike = 1,
477) -> NDArrayFloat:
478 """
479 Compute corresponding colour cone responses of the specified test sample cone
480 responses :math:`RGB_1` under chromatic adaptation.
482 Parameters
483 ----------
484 RGB_1
485 Test sample cone responses :math:`RGB_1`.
486 xez_1
487 Intermediate values :math:`\\xi_1`, :math:`\\eta_1`, :math:`\\zeta_1`
488 for test illuminant and background.
489 xez_2
490 Intermediate values :math:`\\xi_2`, :math:`\\eta_2`, :math:`\\zeta_2`
491 for reference illuminant and background.
492 bRGB_o1
493 Chromatic adaptation exponential factors :math:`\\beta_1(R_{o1})`,
494 :math:`\\beta_1(G_{o1})` and :math:`\\beta_2(B_{o1})` of test
495 sample.
496 bRGB_o2
497 Chromatic adaptation exponential factors :math:`\\beta_1(R_{o2})`,
498 :math:`\\beta_1(G_{o2})` and :math:`\\beta_2(B_{o2})` of reference
499 sample.
500 Y_o
501 Luminance factor :math:`Y_o` of achromatic background as percentage
502 normalised to domain [18, 100] in **'Reference'** domain-range
503 scale.
504 K
505 Coefficient :math:`K`.
506 n
507 Noise component in fundamental primary system.
509 Returns
510 -------
511 :class:`numpy.ndarray`
512 Corresponding colour cone responses of the specified test sample cone
513 responses.
515 Examples
516 --------
517 >>> RGB_1 = np.array([25.82442730, 18.67914220, 4.83901940])
518 >>> xez_1 = np.array([1.11857195, 0.93295530, 0.32680879])
519 >>> xez_2 = np.array([1.00000372, 1.00000176, 0.99999461])
520 >>> bRGB_o1 = np.array([3.74852518, 3.63920879, 2.78924811])
521 >>> bRGB_o2 = np.array([3.68102374, 3.68102256, 3.56557351])
522 >>> Y_o = 20
523 >>> K = 1
524 >>> corresponding_colour(RGB_1, xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, K)
525 ... # doctest: +ELLIPSIS
526 array([ 23.1636901..., 20.0211948..., 16.2001664...])
527 """
529 R_1, G_1, B_1 = tsplit(RGB_1)
530 xi_1, eta_1, zeta_1 = tsplit(xez_1)
531 xi_2, eta_2, zeta_2 = tsplit(xez_2)
532 bR_o1, bG_o1, bB_o1 = tsplit(bRGB_o1)
533 bR_o2, bG_o2, bB_o2 = tsplit(bRGB_o2)
534 Y_o = as_float_array(Y_o)
535 K = as_float_array(K)
536 n = as_float_array(n)
538 def RGB_c(
539 x_1: NDArrayFloat,
540 x_2: NDArrayFloat,
541 y_1: NDArrayFloat,
542 y_2: NDArrayFloat,
543 z: NDArrayFloat,
544 n: NDArrayFloat,
545 ) -> NDArrayFloat:
546 """Compute the corresponding colour cone responses component."""
548 with sdiv_mode():
549 return (Y_o * x_2 + n) * spow(K, sdiv(1, y_2)) * spow(
550 (z + n) / (Y_o * x_1 + n), sdiv(y_1, y_2)
551 ) - n
553 R_2 = RGB_c(xi_1, xi_2, bR_o1, bR_o2, R_1, n)
554 G_2 = RGB_c(eta_1, eta_2, bG_o1, bG_o2, G_1, n)
555 B_2 = RGB_c(zeta_1, zeta_2, bB_o1, bB_o2, B_1, n)
557 return tstack([R_2, G_2, B_2])