Coverage for colour/adaptation/fairchild1990.py: 98%
56 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
1"""
2Fairchild (1990) Chromatic Adaptation Model
3===========================================
5Define the *Fairchild (1990)* chromatic adaptation model for predicting
6corresponding colours under different viewing conditions.
8- :func:`colour.adaptation.chromatic_adaptation_Fairchild1990`
10References
11----------
12- :cite:`Fairchild1991a` : Fairchild, M. D. (1991). Formulation and testing
13 of an incomplete-chromatic-adaptation model. Color Research & Application,
14 16(4), 243-250. doi:10.1002/col.5080160406
15- :cite:`Fairchild2013s` : Fairchild, M. D. (2013). FAIRCHILD'S 1990 MODEL.
16 In Color Appearance Models (3rd ed., pp. 4418-4495). Wiley. ISBN:B00DAYO8E2
17"""
19from __future__ import annotations
21import numpy as np
23from colour.adaptation import CAT_VON_KRIES
24from colour.algebra import sdiv, sdiv_mode, spow, vecmul
25from colour.hints import ( # noqa: TC001
26 ArrayLike,
27 Domain100,
28 NDArrayFloat,
29 Range100,
30)
31from colour.utilities import (
32 as_float_array,
33 from_range_100,
34 ones,
35 row_as_diagonal,
36 to_domain_100,
37 tstack,
38)
40__author__ = "Colour Developers"
41__copyright__ = "Copyright 2013 Colour Developers"
42__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
43__maintainer__ = "Colour Developers"
44__email__ = "colour-developers@colour-science.org"
45__status__ = "Production"
47__all__ = [
48 "MATRIX_XYZ_TO_RGB_FAIRCHILD1990",
49 "MATRIX_RGB_TO_XYZ_FAIRCHILD1990",
50 "chromatic_adaptation_Fairchild1990",
51 "XYZ_to_RGB_Fairchild1990",
52 "RGB_to_XYZ_Fairchild1990",
53 "degrees_of_adaptation",
54]
56MATRIX_XYZ_TO_RGB_FAIRCHILD1990: NDArrayFloat = CAT_VON_KRIES
57"""
58*Fairchild (1990)* colour appearance model *CIE XYZ* tristimulus values to cone
59responses matrix.
60"""
62MATRIX_RGB_TO_XYZ_FAIRCHILD1990: NDArrayFloat = np.linalg.inv(CAT_VON_KRIES)
63"""
64*Fairchild (1990)* colour appearance model cone responses to *CIE XYZ*
65tristimulus values matrix.
66"""
69def chromatic_adaptation_Fairchild1990(
70 XYZ_1: Domain100,
71 XYZ_n: Domain100,
72 XYZ_r: Domain100,
73 Y_n: ArrayLike,
74 discount_illuminant: bool = False,
75) -> Range100:
76 """
77 Adapt the specified stimulus *CIE XYZ* tristimulus values from test
78 viewing conditions to reference viewing conditions using the
79 *Fairchild (1990)* chromatic adaptation model.
81 Parameters
82 ----------
83 XYZ_1
84 *CIE XYZ_1* tristimulus values of test sample / stimulus.
85 XYZ_n
86 Test viewing condition *CIE XYZ_n* tristimulus values of the
87 whitepoint.
88 XYZ_r
89 Reference viewing condition *CIE XYZ_r* tristimulus values of the
90 whitepoint.
91 Y_n
92 Luminance :math:`Y_n` of test adapting stimulus in :math:`cd/m^2`.
93 discount_illuminant
94 Truth value indicating if the illuminant should be discounted.
96 Returns
97 -------
98 :class:`numpy.ndarray`
99 *CIE XYZ* tristimulus values of the stimulus corresponding colour.
101 Notes
102 -----
103 +------------+-----------------------+---------------+
104 | **Domain** | **Scale - Reference** | **Scale - 1** |
105 +============+=======================+===============+
106 | ``XYZ_1`` | 100 | 1 |
107 +------------+-----------------------+---------------+
108 | ``XYZ_n`` | 100 | 1 |
109 +------------+-----------------------+---------------+
110 | ``XYZ_r`` | 100 | 1 |
111 +------------+-----------------------+---------------+
113 +------------+-----------------------+---------------+
114 | **Range** | **Scale - Reference** | **Scale - 1** |
115 +============+=======================+===============+
116 | ``XYZ_2`` | 100 | 1 |
117 +------------+-----------------------+---------------+
119 References
120 ----------
121 :cite:`Fairchild1991a`, :cite:`Fairchild2013s`
123 Examples
124 --------
125 >>> XYZ_1 = np.array([19.53, 23.07, 24.97])
126 >>> XYZ_n = np.array([111.15, 100.00, 35.20])
127 >>> XYZ_r = np.array([94.81, 100.00, 107.30])
128 >>> Y_n = 200
129 >>> chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r, Y_n)
130 ... # doctest: +ELLIPSIS
131 array([ 23.3252634..., 23.3245581..., 76.1159375...])
132 """
134 XYZ_1 = to_domain_100(XYZ_1)
135 XYZ_n = to_domain_100(XYZ_n)
136 XYZ_r = to_domain_100(XYZ_r)
137 Y_n = as_float_array(Y_n)
139 LMS_1 = XYZ_to_RGB_Fairchild1990(XYZ_1)
140 LMS_n = XYZ_to_RGB_Fairchild1990(XYZ_n)
141 LMS_r = XYZ_to_RGB_Fairchild1990(XYZ_r)
143 p_LMS = degrees_of_adaptation(LMS_1, Y_n, discount_illuminant=discount_illuminant)
145 a_LMS_1 = p_LMS / LMS_n
146 a_LMS_2 = p_LMS / LMS_r
148 A_1 = row_as_diagonal(a_LMS_1)
149 A_2 = row_as_diagonal(a_LMS_2)
151 LMSp_1 = vecmul(A_1, LMS_1)
153 c = 0.219 - 0.0784 * np.log10(Y_n)
154 C = row_as_diagonal(tstack([c, c, c]))
156 LMS_a = vecmul(C, LMSp_1)
157 LMSp_2 = vecmul(np.linalg.inv(C), LMS_a)
159 LMS_c = vecmul(np.linalg.inv(A_2), LMSp_2)
160 XYZ_c = RGB_to_XYZ_Fairchild1990(LMS_c)
162 return from_range_100(XYZ_c)
165def XYZ_to_RGB_Fairchild1990(XYZ: ArrayLike) -> NDArrayFloat:
166 """
167 Convert from *CIE XYZ* tristimulus values to cone responses using the
168 *Fairchild (1990)* chromatic adaptation model.
170 Parameters
171 ----------
172 XYZ
173 *CIE XYZ* tristimulus values.
175 Returns
176 -------
177 :class:`numpy.ndarray`
178 Cone responses.
180 Examples
181 --------
182 >>> XYZ = np.array([19.53, 23.07, 24.97])
183 >>> XYZ_to_RGB_Fairchild1990(XYZ) # doctest: +ELLIPSIS
184 array([ 22.1231935..., 23.6054224..., 22.9279534...])
185 """
187 return vecmul(MATRIX_XYZ_TO_RGB_FAIRCHILD1990, XYZ)
190def RGB_to_XYZ_Fairchild1990(RGB: ArrayLike) -> NDArrayFloat:
191 """
192 Convert cone responses to *CIE XYZ* tristimulus values.
194 Parameters
195 ----------
196 RGB
197 Cone responses.
199 Returns
200 -------
201 :class:`numpy.ndarray`
202 *CIE XYZ* tristimulus values.
204 Examples
205 --------
206 >>> RGB = np.array([22.12319350, 23.60542240, 22.92795340])
207 >>> RGB_to_XYZ_Fairchild1990(RGB) # doctest: +ELLIPSIS
208 array([ 19.53, 23.07, 24.97])
209 """
211 return vecmul(MATRIX_RGB_TO_XYZ_FAIRCHILD1990, RGB)
214def degrees_of_adaptation(
215 LMS: ArrayLike,
216 Y_n: ArrayLike,
217 v: ArrayLike = 1 / 3,
218 discount_illuminant: bool = False,
219) -> NDArrayFloat:
220 """
221 Compute the degrees of adaptation :math:`p_L`, :math:`p_M` and
222 :math:`p_S`.
224 Parameters
225 ----------
226 LMS
227 Cone responses.
228 Y_n
229 Luminance :math:`Y_n` of test adapting stimulus in :math:`cd/m^2`.
230 v
231 Exponent :math:`v`.
232 discount_illuminant
233 Truth value indicating if the illuminant should be discounted.
235 Returns
236 -------
237 :class:`numpy.ndarray`
238 Degrees of adaptation :math:`p_L`, :math:`p_M` and :math:`p_S`.
240 Examples
241 --------
242 >>> LMS = np.array([20.00052060, 19.99978300, 19.99883160])
243 >>> Y_n = 31.83
244 >>> degrees_of_adaptation(LMS, Y_n) # doctest: +ELLIPSIS
245 array([ 0.9799324..., 0.9960035..., 1.0233041...])
246 >>> degrees_of_adaptation(LMS, Y_n, 1 / 3, True)
247 array([ 1., 1., 1.])
248 """
250 LMS = as_float_array(LMS)
252 if discount_illuminant:
253 return ones(LMS.shape)
255 Y_n = as_float_array(Y_n)
256 v = as_float_array(v)
258 # E illuminant.
259 LMS_E = vecmul(CAT_VON_KRIES, ones(LMS.shape))
261 Ye_n = spow(Y_n, v)
263 def m_E(x: NDArrayFloat, y: NDArrayFloat) -> NDArrayFloat:
264 """Compute the :math:`m_E` term."""
266 return (3 * x / y) / np.sum(x / y, axis=-1)[..., None]
268 def P_c(x: NDArrayFloat) -> NDArrayFloat:
269 """Compute the :math:`P_L`, :math:`P_M` or :math:`P_S` terms."""
271 return sdiv(
272 1 + Ye_n[..., None] + x,
273 1 + Ye_n[..., None] + sdiv(1, x),
274 )
276 with sdiv_mode():
277 return P_c(m_E(LMS, LMS_E))