Coverage for adaptation/cmccat2000.py: 56%
57 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"""
2CMCCAT2000 Chromatic Adaptation Model
3=====================================
5Define the *CMCCAT2000* chromatic adaptation model for predicting
6corresponding colours under different viewing conditions.
8- :class:`colour.adaptation.InductionFactors_CMCCAT2000`
9- :class:`colour.VIEWING_CONDITIONS_CMCCAT2000`
10- :func:`colour.adaptation.chromatic_adaptation_forward_CMCCAT2000`
11- :func:`colour.adaptation.chromatic_adaptation_inverse_CMCCAT2000`
12- :func:`colour.adaptation.chromatic_adaptation_CMCCAT2000`
14References
15----------
16- :cite:`Li2002a` : Li, C., Luo, M. R., Rigg, B., & Hunt, R. W. G. (2002).
17 CMC 2000 chromatic adaptation transform: CMCCAT2000. Color Research &
18 Application, 27(1), 49-58. doi:10.1002/col.10005
19- :cite:`Westland2012k` : Westland, S., Ripamonti, C., & Cheung, V. (2012).
20 CMCCAT2000. In Computational Colour Science Using MATLAB (2nd ed., pp.
21 83-86). ISBN:978-0-470-66569-5
22"""
24from __future__ import annotations
26import typing
27from dataclasses import dataclass
29import numpy as np
31from colour.adaptation import CAT_CMCCAT2000
32from colour.algebra import vecmul
34if typing.TYPE_CHECKING:
35 from colour.hints import Literal
37from colour.hints import ( # noqa: TC001
38 ArrayLike,
39 Domain100,
40 NDArrayFloat,
41 Range100,
42)
43from colour.utilities import (
44 CanonicalMapping,
45 MixinDataclassIterable,
46 as_float_array,
47 from_range_100,
48 to_domain_100,
49 validate_method,
50)
52__author__ = "Colour Developers"
53__copyright__ = "Copyright 2013 Colour Developers"
54__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
55__maintainer__ = "Colour Developers"
56__email__ = "colour-developers@colour-science.org"
57__status__ = "Production"
59__all__ = [
60 "CAT_INVERSE_CMCCAT2000",
61 "InductionFactors_CMCCAT2000",
62 "VIEWING_CONDITIONS_CMCCAT2000",
63 "chromatic_adaptation_forward_CMCCAT2000",
64 "chromatic_adaptation_inverse_CMCCAT2000",
65 "chromatic_adaptation_CMCCAT2000",
66]
68CAT_INVERSE_CMCCAT2000: NDArrayFloat = np.linalg.inv(CAT_CMCCAT2000)
69"""
70Inverse *CMCCAT2000* chromatic adaptation transform.
72CAT_INVERSE_CMCCAT2000
73"""
76@dataclass(frozen=True)
77class InductionFactors_CMCCAT2000(MixinDataclassIterable):
78 """
79 Define the *CMCCAT2000* chromatic adaptation model induction factors.
81 Parameters
82 ----------
83 F
84 :math:`F` surround condition factor that modulates the degree of
85 adaptation based on the viewing environment.
87 References
88 ----------
89 :cite:`Li2002a`, :cite:`Westland2012k`
90 """
92 F: float
95VIEWING_CONDITIONS_CMCCAT2000: CanonicalMapping = CanonicalMapping(
96 {
97 "Average": InductionFactors_CMCCAT2000(1),
98 "Dim": InductionFactors_CMCCAT2000(0.8),
99 "Dark": InductionFactors_CMCCAT2000(0.8),
100 }
101)
102VIEWING_CONDITIONS_CMCCAT2000.__doc__ = """
103Define the reference *CMCCAT2000* chromatic adaptation model viewing
104conditions.
106The viewing conditions include three standard surround conditions with
107their corresponding induction factors:
109- *Average*: Induction factor of 1.0
110- *Dim*: Induction factor of 0.8
111- *Dark*: Induction factor of 0.8
113These values represent the standard viewing conditions used in the
114*CMCCAT2000* chromatic adaptation transform.
116References
117----------
118:cite:`Li2002a`, :cite:`Westland2012k`
119"""
122def chromatic_adaptation_forward_CMCCAT2000(
123 XYZ: Domain100,
124 XYZ_w: Domain100,
125 XYZ_wr: Domain100,
126 L_A1: ArrayLike,
127 L_A2: ArrayLike,
128 surround: InductionFactors_CMCCAT2000 = VIEWING_CONDITIONS_CMCCAT2000["Average"],
129) -> Range100:
130 """
131 Adapt the specified stimulus *CIE XYZ* tristimulus values from test
132 viewing conditions to reference viewing conditions using the
133 *CMCCAT2000* forward chromatic adaptation model.
135 Parameters
136 ----------
137 XYZ
138 *CIE XYZ* tristimulus values of the stimulus to adapt.
139 XYZ_w
140 Test viewing condition *CIE XYZ* tristimulus values of the
141 whitepoint.
142 XYZ_wr
143 Reference viewing condition *CIE XYZ* tristimulus values of the
144 whitepoint.
145 L_A1
146 Luminance of test adapting field :math:`L_{A1}` in
147 :math:`cd/m^2`.
148 L_A2
149 Luminance of reference adapting field :math:`L_{A2}` in
150 :math:`cd/m^2`.
151 surround
152 Surround viewing conditions induction factors.
154 Returns
155 -------
156 :class:`numpy.ndarray`
157 *CIE XYZ* tristimulus values of the stimulus corresponding colour.
159 Notes
160 -----
161 +------------+-----------------------+---------------+
162 | **Domain** | **Scale - Reference** | **Scale - 1** |
163 +============+=======================+===============+
164 | ``XYZ`` | 100 | 1 |
165 +------------+-----------------------+---------------+
166 | ``XYZ_w`` | 100 | 1 |
167 +------------+-----------------------+---------------+
168 | ``XYZ_wr`` | 100 | 1 |
169 +------------+-----------------------+---------------+
171 +------------+-----------------------+---------------+
172 | **Range** | **Scale - Reference** | **Scale - 1** |
173 +============+=======================+===============+
174 | ``XYZ_c`` | 100 | 1 |
175 +------------+-----------------------+---------------+
177 References
178 ----------
179 :cite:`Li2002a`, :cite:`Westland2012k`
181 Examples
182 --------
183 >>> XYZ = np.array([22.48, 22.74, 8.54])
184 >>> XYZ_w = np.array([111.15, 100.00, 35.20])
185 >>> XYZ_wr = np.array([94.81, 100.00, 107.30])
186 >>> L_A1 = 200
187 >>> L_A2 = 200
188 >>> chromatic_adaptation_forward_CMCCAT2000(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2)
189 ... # doctest: +ELLIPSIS
190 array([ 19.5269832..., 23.0683396..., 24.9717522...])
191 """
193 XYZ = to_domain_100(XYZ)
194 XYZ_w = to_domain_100(XYZ_w)
195 XYZ_wr = to_domain_100(XYZ_wr)
196 L_A1 = as_float_array(L_A1)
197 L_A2 = as_float_array(L_A2)
199 RGB = vecmul(CAT_CMCCAT2000, XYZ)
200 RGB_w = vecmul(CAT_CMCCAT2000, XYZ_w)
201 RGB_wr = vecmul(CAT_CMCCAT2000, XYZ_wr)
203 D = surround.F * (
204 0.08 * np.log10(0.5 * (L_A1 + L_A2))
205 + 0.76
206 - 0.45 * (L_A1 - L_A2) / (L_A1 + L_A2)
207 )
209 D = np.clip(D, 0, 1)
210 a = D * XYZ_w[..., 1] / XYZ_wr[..., 1]
212 RGB_c = RGB * (a[..., None] * (RGB_wr / RGB_w) + 1 - D[..., None])
213 XYZ_c = vecmul(CAT_INVERSE_CMCCAT2000, RGB_c)
215 return from_range_100(XYZ_c)
218def chromatic_adaptation_inverse_CMCCAT2000(
219 XYZ_c: Domain100,
220 XYZ_w: Domain100,
221 XYZ_wr: Domain100,
222 L_A1: ArrayLike,
223 L_A2: ArrayLike,
224 surround: InductionFactors_CMCCAT2000 = VIEWING_CONDITIONS_CMCCAT2000["Average"],
225) -> Range100:
226 """
227 Adapt the specified *CIE XYZ* tristimulus values from reference viewing
228 conditions to test viewing conditions using the inverse *CMCCAT2000*
229 chromatic adaptation model.
231 Parameters
232 ----------
233 XYZ_c
234 *CIE XYZ* tristimulus values of the adapted stimulus in the reference
235 viewing conditions.
236 XYZ_w
237 Test viewing condition *CIE XYZ* tristimulus values of the whitepoint.
238 XYZ_wr
239 Reference viewing condition *CIE XYZ* tristimulus values of the
240 whitepoint.
241 L_A1
242 Luminance of test adapting field :math:`L_{A1}` in :math:`cd/m^2`.
243 L_A2
244 Luminance of reference adapting field :math:`L_{A2}` in :math:`cd/m^2`.
245 surround
246 Surround viewing conditions induction factors.
248 Returns
249 -------
250 :class:`numpy.ndarray`
251 *CIE XYZ* tristimulus values of the stimulus adapted to the test
252 viewing conditions.
254 Notes
255 -----
256 +------------+-----------------------+---------------+
257 | **Domain** | **Scale - Reference** | **Scale - 1** |
258 +============+=======================+===============+
259 | ``XYZ_c`` | 100 | 1 |
260 +------------+-----------------------+---------------+
261 | ``XYZ_w`` | 100 | 1 |
262 +------------+-----------------------+---------------+
263 | ``XYZ_wr`` | 100 | 1 |
264 +------------+-----------------------+---------------+
266 +------------+-----------------------+---------------+
267 | **Range** | **Scale - Reference** | **Scale - 1** |
268 +============+=======================+===============+
269 | ``XYZ`` | 100 | 1 |
270 +------------+-----------------------+---------------+
272 References
273 ----------
274 :cite:`Li2002a`, :cite:`Westland2012k`
276 Examples
277 --------
278 >>> XYZ_c = np.array([19.53, 23.07, 24.97])
279 >>> XYZ_w = np.array([111.15, 100.00, 35.20])
280 >>> XYZ_wr = np.array([94.81, 100.00, 107.30])
281 >>> L_A1 = 200
282 >>> L_A2 = 200
283 >>> chromatic_adaptation_inverse_CMCCAT2000(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2)
284 ... # doctest: +ELLIPSIS
285 array([ 22.4839876..., 22.7419485..., 8.5393392...])
286 """
288 XYZ_c = to_domain_100(XYZ_c)
289 XYZ_w = to_domain_100(XYZ_w)
290 XYZ_wr = to_domain_100(XYZ_wr)
291 L_A1 = as_float_array(L_A1)
292 L_A2 = as_float_array(L_A2)
294 RGB_c = vecmul(CAT_CMCCAT2000, XYZ_c)
295 RGB_w = vecmul(CAT_CMCCAT2000, XYZ_w)
296 RGB_wr = vecmul(CAT_CMCCAT2000, XYZ_wr)
298 D = surround.F * (
299 0.08 * np.log10(0.5 * (L_A1 + L_A2))
300 + 0.76
301 - 0.45 * (L_A1 - L_A2) / (L_A1 + L_A2)
302 )
304 D = np.clip(D, 0, 1)
305 a = D * XYZ_w[..., 1] / XYZ_wr[..., 1]
307 RGB = RGB_c / (a[..., None] * (RGB_wr / RGB_w) + 1 - D[..., None])
308 XYZ = vecmul(CAT_INVERSE_CMCCAT2000, RGB)
310 return from_range_100(XYZ)
313def chromatic_adaptation_CMCCAT2000(
314 XYZ: Domain100,
315 XYZ_w: Domain100,
316 XYZ_wr: Domain100,
317 L_A1: ArrayLike,
318 L_A2: ArrayLike,
319 surround: InductionFactors_CMCCAT2000 = VIEWING_CONDITIONS_CMCCAT2000["Average"],
320 direction: Literal["Forward", "Inverse"] | str = "Forward",
321) -> Range100:
322 """
323 Adapt the specified stimulus *CIE XYZ* tristimulus values from test
324 viewing conditions to reference viewing conditions using the
325 *CMCCAT2000* chromatic adaptation model.
327 This definition provides a convenient wrapper around
328 :func:`colour.adaptation.chromatic_adaptation_forward_CMCCAT2000` and
329 :func:`colour.adaptation.chromatic_adaptation_inverse_CMCCAT2000`.
331 Parameters
332 ----------
333 XYZ
334 *CIE XYZ* tristimulus values of the stimulus to adapt.
335 XYZ_w
336 Source viewing condition *CIE XYZ* tristimulus values of the
337 whitepoint.
338 XYZ_wr
339 Target viewing condition *CIE XYZ* tristimulus values of the
340 whitepoint.
341 L_A1
342 Luminance of test adapting field :math:`L_{A1}` in :math:`cd/m^2`.
343 L_A2
344 Luminance of reference adapting field :math:`L_{A2}` in
345 :math:`cd/m^2`.
346 surround
347 Surround viewing conditions induction factors.
348 direction
349 Chromatic adaptation direction.
351 Returns
352 -------
353 :class:`numpy.ndarray`
354 *CIE XYZ* tristimulus values of the stimulus corresponding colour.
356 Notes
357 -----
358 +------------+-----------------------+---------------+
359 | **Domain** | **Scale - Reference** | **Scale - 1** |
360 +============+=======================+===============+
361 | ``XYZ`` | 100 | 1 |
362 +------------+-----------------------+---------------+
363 | ``XYZ_w`` | 100 | 1 |
364 +------------+-----------------------+---------------+
365 | ``XYZ_wr`` | 100 | 1 |
366 +------------+-----------------------+---------------+
368 +------------+-----------------------+---------------+
369 | **Range** | **Scale - Reference** | **Scale - 1** |
370 +============+=======================+===============+
371 | ``XYZ`` | 100 | 1 |
372 +------------+-----------------------+---------------+
374 References
375 ----------
376 :cite:`Li2002a`, :cite:`Westland2012k`
378 Examples
379 --------
380 >>> XYZ = np.array([22.48, 22.74, 8.54])
381 >>> XYZ_w = np.array([111.15, 100.00, 35.20])
382 >>> XYZ_wr = np.array([94.81, 100.00, 107.30])
383 >>> L_A1 = 200
384 >>> L_A2 = 200
385 >>> chromatic_adaptation_CMCCAT2000(
386 ... XYZ, XYZ_w, XYZ_wr, L_A1, L_A2, direction="Forward"
387 ... )
388 ... # doctest: +ELLIPSIS
389 array([ 19.5269832..., 23.0683396..., 24.9717522...])
391 Using the *CMCCAT2000* inverse model:
393 >>> XYZ = np.array([19.52698326, 23.06833960, 24.97175229])
394 >>> XYZ_w = np.array([111.15, 100.00, 35.20])
395 >>> XYZ_wr = np.array([94.81, 100.00, 107.30])
396 >>> L_A1 = 200
397 >>> L_A2 = 200
398 >>> chromatic_adaptation_CMCCAT2000(
399 ... XYZ, XYZ_w, XYZ_wr, L_A1, L_A2, direction="Inverse"
400 ... )
401 ... # doctest: +ELLIPSIS
402 array([ 22.48, 22.74, 8.54])
403 """
405 direction = validate_method(
406 direction,
407 ("Forward", "Inverse"),
408 '"{0}" direction is invalid, it must be one of {1}!',
409 )
411 if direction == "forward":
412 return chromatic_adaptation_forward_CMCCAT2000(
413 XYZ, XYZ_w, XYZ_wr, L_A1, L_A2, surround
414 )
416 return chromatic_adaptation_inverse_CMCCAT2000(
417 XYZ, XYZ_w, XYZ_wr, L_A1, L_A2, surround
418 )