Coverage for adaptation/li2025.py: 56%
36 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"""
2Li (2025) Chromatic Adaptation Model
3====================================
5Define the *Li (2025)* chromatic adaptation model for predicting corresponding
6colours under different viewing conditions.
8- :func:`colour.adaptation.chromatic_adaptation_Li2025`
10References
11----------
12- :cite:`Li2025` : Li, M. (2025). One Step CAT16 Chromatic Adaptation
13 Transform. https://github.com/colour-science/colour/pull/1349\
14#issuecomment-3058339414
15"""
17from __future__ import annotations
19import typing
21import numpy as np
23from colour.adaptation import CAT_CAT16
24from colour.algebra import sdiv, sdiv_mode, vecmul
26if typing.TYPE_CHECKING:
27 from colour.hints import ArrayLike, Domain100, NDArrayFloat, Range100
29from colour.utilities import (
30 as_float_array,
31 ones,
32)
34__author__ = "Colour Developers"
35__copyright__ = "Copyright 2013 Colour Developers"
36__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
37__maintainer__ = "Colour Developers"
38__email__ = "colour-developers@colour-science.org"
39__status__ = "Production"
41__all__ = [
42 "CAT_CAT16_INVERSE",
43 "chromatic_adaptation_Li2025",
44]
46CAT_CAT16_INVERSE: NDArrayFloat = np.linalg.inv(CAT_CAT16)
47"""Inverse adaptation matrix :math:`M^{-1}_{CAT16}` for *Li (2025)* method."""
50def chromatic_adaptation_Li2025(
51 XYZ_s: Domain100,
52 XYZ_ws: Domain100,
53 XYZ_wd: Domain100,
54 L_A: ArrayLike,
55 F_surround: ArrayLike,
56 discount_illuminant: bool = False,
57) -> Range100:
58 """
59 Adapt the specified stimulus *CIE XYZ* tristimulus values from test
60 viewing conditions to reference viewing conditions using the
61 *Li (2025)* chromatic adaptation model.
63 This one-step chromatic adaptation transform is based on *CAT16* and
64 includes the degree of adaptation calculation from the viewing conditions
65 as specified by *CIECAM02* colour appearance model.
67 Parameters
68 ----------
69 XYZ_s
70 *CIE XYZ* tristimulus values of stimulus under source illuminant.
71 XYZ_ws
72 *CIE XYZ* tristimulus values of source whitepoint.
73 XYZ_wd
74 *CIE XYZ* tristimulus values of destination whitepoint.
75 L_A
76 Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.
77 F_surround
78 Maximum degree of adaptation :math:`F` from surround viewing
79 conditions.
80 discount_illuminant
81 Truth value indicating if the illuminant should be discounted.
83 Returns
84 -------
85 :class:`numpy.ndarray`
86 *CIE XYZ* tristimulus values of the stimulus corresponding colour.
88 Notes
89 -----
90 +------------+-----------------------+---------------+
91 | **Domain** | **Scale - Reference** | **Scale - 1** |
92 +============+=======================+===============+
93 | ``XYZ_s`` | 100 | 1 |
94 +------------+-----------------------+---------------+
95 | ``XYZ_ws`` | 100 | 1 |
96 +------------+-----------------------+---------------+
97 | ``XYZ_wd`` | 100 | 1 |
98 +------------+-----------------------+---------------+
100 +------------+-----------------------+---------------+
101 | **Range** | **Scale - Reference** | **Scale - 1** |
102 +============+=======================+===============+
103 | ``XYZ_a`` | 100 | 1 |
104 +------------+-----------------------+---------------+
106 References
107 ----------
108 :cite:`Li2025`
110 Examples
111 --------
112 >>> XYZ_s = np.array([48.900, 43.620, 6.250])
113 >>> XYZ_ws = np.array([109.850, 100, 35.585])
114 >>> XYZ_wd = np.array([95.047, 100, 108.883])
115 >>> L_A = 318.31
116 >>> F_surround = 1.0
117 >>> chromatic_adaptation_Li2025( # doctest: +ELLIPSIS
118 ... XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround
119 ... )
120 array([ 40.0072581..., 43.7014895..., 21.3290293...])
121 """
123 XYZ_s = as_float_array(XYZ_s)
124 XYZ_ws = as_float_array(XYZ_ws)
125 XYZ_wd = as_float_array(XYZ_wd)
126 L_A = as_float_array(L_A)
127 F_surround = as_float_array(F_surround)
129 LMS_s = vecmul(CAT_CAT16, XYZ_s)
130 LMS_w_s = vecmul(CAT_CAT16, XYZ_ws)
131 LMS_w_d = vecmul(CAT_CAT16, XYZ_wd)
133 Y_w_s = XYZ_ws[..., 1] if XYZ_ws.ndim > 1 else XYZ_ws[1]
134 Y_w_d = XYZ_wd[..., 1] if XYZ_wd.ndim > 1 else XYZ_wd[1]
136 if discount_illuminant:
137 D = ones(L_A.shape)
138 else:
139 D = F_surround * (1 - (1 / 3.6) * np.exp((-L_A - 42) / 92))
140 D = np.clip(D, 0, 1)
142 D = np.atleast_1d(D)[..., None] if LMS_s.ndim > 1 else D
144 with sdiv_mode():
145 Y_ratio = sdiv(Y_w_s, Y_w_d)
146 Y_ratio = Y_ratio[..., None] if LMS_s.ndim > 1 else Y_ratio
147 LMS_a = LMS_s * (D * Y_ratio * sdiv(LMS_w_d, LMS_w_s) + (1 - D))
149 return vecmul(CAT_CAT16_INVERSE, LMS_a)