Coverage for appearance/kim2009.py: 68%
116 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"""
2Kim, Weyrich and Kautz (2009) Colour Appearance Model
3=====================================================
5Define the *Kim, Weyrich and Kautz (2009)* colour appearance model for
6predicting perceptual colour attributes under varying viewing conditions.
8This model extends *CIECAM02* to handle high dynamic range viewing conditions
9by introducing media-specific parameters that modulate lightness prediction.
11- :class:`colour.appearance.InductionFactors_Kim2009`
12- :attr:`colour.VIEWING_CONDITIONS_KIM2009`
13- :class:`colour.appearance.MediaParameters_Kim2009`
14- :attr:`colour.MEDIA_PARAMETERS_KIM2009`
15- :class:`colour.CAM_Specification_Kim2009`
16- :func:`colour.XYZ_to_Kim2009`
17- :func:`colour.Kim2009_to_XYZ`
19References
20----------
21- :cite:`Kim2009` : Kim, M., Weyrich, T., & Kautz, J. (2009). Modeling Human
22 Color Perception under Extended Luminance Levels. ACM Transactions on
23 Graphics, 28(3), 27:1--27:9. doi:10.1145/1531326.1531333
24"""
26from __future__ import annotations
28from dataclasses import astuple, dataclass, field
30import numpy as np
32from colour.adaptation import CAT_CAT02
33from colour.algebra import spow, vecmul
34from colour.appearance.ciecam02 import (
35 CAT_INVERSE_CAT02,
36 VIEWING_CONDITIONS_CIECAM02,
37 RGB_to_rgb,
38 degree_of_adaptation,
39 full_chromatic_adaptation_forward,
40 full_chromatic_adaptation_inverse,
41 hue_quadrature,
42 rgb_to_RGB,
43)
44from colour.hints import ( # noqa: TC001
45 Annotated,
46 ArrayLike,
47 Domain100,
48 NDArrayFloat,
49 Range100,
50)
51from colour.utilities import (
52 CanonicalMapping,
53 MixinDataclassArithmetic,
54 MixinDataclassIterable,
55 as_float,
56 as_float_array,
57 from_range_100,
58 from_range_degrees,
59 has_only_nan,
60 ones,
61 to_domain_100,
62 to_domain_degrees,
63 tsplit,
64 tstack,
65)
67__author__ = "Colour Developers"
68__copyright__ = "Copyright 2013 Colour Developers"
69__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
70__maintainer__ = "Colour Developers"
71__email__ = "colour-developers@colour-science.org"
72__status__ = "Production"
74__all__ = [
75 "InductionFactors_Kim2009",
76 "VIEWING_CONDITIONS_KIM2009",
77 "MediaParameters_Kim2009",
78 "MEDIA_PARAMETERS_KIM2009",
79 "CAM_Specification_Kim2009",
80 "XYZ_to_Kim2009",
81 "Kim2009_to_XYZ",
82]
85@dataclass(frozen=True)
86class InductionFactors_Kim2009(MixinDataclassIterable):
87 """
88 Define the *Kim, Weyrich and Kautz (2009)* colour appearance model
89 surround induction factors.
91 Parameters
92 ----------
93 F
94 Maximum degree of adaptation :math:`F`.
95 c
96 Exponential non-linearity :math:`c`.
97 N_c
98 Chromatic induction factor :math:`N_c`.
100 Notes
101 -----
102 - The *Kim, Weyrich and Kautz (2009)* colour appearance model induction
103 factors are the same as the *CIECAM02* colour appearance model.
104 - The *Kim, Weyrich and Kautz (2009)* colour appearance model separates
105 the surround modelled by the
106 :class:`colour.appearance.InductionFactors_Kim2009` class instance
107 from the media, modelled with the
108 :class:`colour.appearance.MediaParameters_Kim2009` class instance.
110 References
111 ----------
112 :cite:`Kim2009`
113 """
115 F: float
116 c: float
117 N_c: float
120VIEWING_CONDITIONS_KIM2009: CanonicalMapping = CanonicalMapping(
121 VIEWING_CONDITIONS_CIECAM02
122)
123VIEWING_CONDITIONS_KIM2009.__doc__ = """
124Define the reference *Kim, Weyrich and Kautz (2009)* colour appearance model
125viewing conditions inherited from *CIECAM02*.
127References
128----------
129:cite:`Kim2009`
130"""
133@dataclass(frozen=True)
134class MediaParameters_Kim2009:
135 """
136 Define the media parameters for the *Kim, Weyrich and Kautz (2009)* colour
137 appearance model.
139 Parameters
140 ----------
141 E
142 Lightness prediction modulating parameter :math:`E`.
144 References
145 ----------
146 :cite:`Kim2009`
147 """
149 E: float
152MEDIA_PARAMETERS_KIM2009: CanonicalMapping = CanonicalMapping(
153 {
154 "High-luminance LCD Display": MediaParameters_Kim2009(1),
155 "Transparent Advertising Media": MediaParameters_Kim2009(1.2175),
156 "CRT Displays": MediaParameters_Kim2009(1.4572),
157 "Reflective Paper": MediaParameters_Kim2009(1.7526),
158 }
159)
160MEDIA_PARAMETERS_KIM2009.__doc__ = """
161Define the reference *Kim, Weyrich and Kautz (2009)* colour appearance model
162media parameters.
164References
165----------
166:cite:`Kim2009`
168Aliases:
170- 'bright_lcd_display': 'High-luminance LCD Display'
171- 'advertising_transparencies': 'Transparent Advertising Media'
172- 'crt': 'CRT Displays'
173- 'paper': 'Reflective Paper'
174"""
175MEDIA_PARAMETERS_KIM2009["bright_lcd_display"] = MEDIA_PARAMETERS_KIM2009[
176 "High-luminance LCD Display"
177]
178MEDIA_PARAMETERS_KIM2009["advertising_transparencies"] = MEDIA_PARAMETERS_KIM2009[
179 "Transparent Advertising Media"
180]
181MEDIA_PARAMETERS_KIM2009["crt"] = MEDIA_PARAMETERS_KIM2009["CRT Displays"]
182MEDIA_PARAMETERS_KIM2009["paper"] = MEDIA_PARAMETERS_KIM2009["Reflective Paper"]
185@dataclass
186class CAM_Specification_Kim2009(MixinDataclassArithmetic):
187 """
188 Represent the *Kim, Weyrich and Kautz (2009)* colour appearance model
189 output specification.
191 Parameters
192 ----------
193 J
194 Correlate of *Lightness* :math:`J`.
195 C
196 Correlate of *chroma* :math:`C`.
197 h
198 *Hue* angle :math:`h` in degrees.
199 s
200 Correlate of *saturation* :math:`s`.
201 Q
202 Correlate of *brightness* :math:`Q`.
203 M
204 Correlate of *colourfulness* :math:`M`.
205 H
206 *Hue* :math:`h` quadrature :math:`H`.
207 HC
208 *Hue* :math:`h` composition :math:`H^C`.
210 References
211 ----------
212 :cite:`Kim2009`
213 """
215 J: float | NDArrayFloat | None = field(default_factory=lambda: None)
216 C: float | NDArrayFloat | None = field(default_factory=lambda: None)
217 h: float | NDArrayFloat | None = field(default_factory=lambda: None)
218 s: float | NDArrayFloat | None = field(default_factory=lambda: None)
219 Q: float | NDArrayFloat | None = field(default_factory=lambda: None)
220 M: float | NDArrayFloat | None = field(default_factory=lambda: None)
221 H: float | NDArrayFloat | None = field(default_factory=lambda: None)
222 HC: float | NDArrayFloat | None = field(default_factory=lambda: None)
225def XYZ_to_Kim2009(
226 XYZ: Domain100,
227 XYZ_w: Domain100,
228 L_A: ArrayLike,
229 media: MediaParameters_Kim2009 = MEDIA_PARAMETERS_KIM2009["CRT Displays"],
230 surround: InductionFactors_Kim2009 = VIEWING_CONDITIONS_KIM2009["Average"],
231 n_c: float = 0.57,
232 discount_illuminant: bool = False,
233 compute_H: bool = True,
234) -> Annotated[CAM_Specification_Kim2009, (100, 100, 360, 100, 100, 100, 400)]:
235 """
236 Compute the *Kim, Weyrich and Kautz (2009)* colour appearance model
237 correlates from the specified *CIE XYZ* tristimulus values.
239 Parameters
240 ----------
241 XYZ
242 *CIE XYZ* tristimulus values of test sample / stimulus.
243 XYZ_w
244 *CIE XYZ* tristimulus values of reference white.
245 L_A
246 Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`, (often
247 taken to be 20% of the luminance of a white object in the scene).
248 media
249 Media parameters.
250 surround
251 Surround viewing conditions induction factors.
252 discount_illuminant
253 Truth value indicating if the illuminant should be discounted.
254 compute_H
255 Whether to compute *Hue* :math:`h` quadrature :math:`H`.
256 :math:`H` is rarely used, and expensive to compute.
257 n_c
258 Cone response sigmoidal curve modulating factor :math:`n_c`.
260 Returns
261 -------
262 :class:`colour.CAM_Specification_Kim2009`
263 *Kim, Weyrich and Kautz (2009)* colour appearance model
264 specification.
266 Notes
267 -----
268 +---------------------+-----------------------+---------------+
269 | **Domain** | **Scale - Reference** | **Scale - 1** |
270 +=====================+=======================+===============+
271 | ``XYZ`` | 100 | 1 |
272 +---------------------+-----------------------+---------------+
273 | ``XYZ_w`` | 100 | 1 |
274 +---------------------+-----------------------+---------------+
276 +---------------------+-----------------------+---------------+
277 | **Range** | **Scale - Reference** | **Scale - 1** |
278 +=====================+=======================+===============+
279 | ``specification.J`` | 100 | 1 |
280 +---------------------+-----------------------+---------------+
281 | ``specification.C`` | 100 | 1 |
282 +---------------------+-----------------------+---------------+
283 | ``specification.h`` | 360 | 1 |
284 +---------------------+-----------------------+---------------+
285 | ``specification.s`` | 100 | 1 |
286 +---------------------+-----------------------+---------------+
287 | ``specification.Q`` | 100 | 1 |
288 +---------------------+-----------------------+---------------+
289 | ``specification.M`` | 100 | 1 |
290 +---------------------+-----------------------+---------------+
291 | ``specification.H`` | 400 | 1 |
292 +---------------------+-----------------------+---------------+
294 References
295 ----------
296 :cite:`Kim2009`
298 Examples
299 --------
300 >>> XYZ = np.array([19.01, 20.00, 21.78])
301 >>> XYZ_w = np.array([95.05, 100.00, 108.88])
302 >>> L_A = 318.31
303 >>> media = MEDIA_PARAMETERS_KIM2009["CRT Displays"]
304 >>> surround = VIEWING_CONDITIONS_KIM2009["Average"]
305 >>> XYZ_to_Kim2009(XYZ, XYZ_w, L_A, media, surround)
306 ... # doctest: +ELLIPSIS
307 CAM_Specification_Kim2009(J=28.8619089..., C=0.5592455..., \
308h=219.0480667..., s=9.3837797..., Q=52.7138883..., M=0.4641738..., \
309H=278.0602824..., HC=None)
310 """
312 XYZ = to_domain_100(XYZ)
313 XYZ_w = to_domain_100(XYZ_w)
314 _X_w, Y_w, _Z_w = tsplit(XYZ_w)
315 L_A = as_float_array(L_A)
317 # Converting *CIE XYZ* tristimulus values to *CMCCAT2000* transform
318 # sharpened *RGB* values.
319 RGB = vecmul(CAT_CAT02, XYZ)
320 RGB_w = vecmul(CAT_CAT02, XYZ_w)
322 # Computing degree of adaptation :math:`D`.
323 D = (
324 degree_of_adaptation(surround.F, L_A)
325 if not discount_illuminant
326 else ones(L_A.shape)
327 )
329 # Computing full chromatic adaptation.
330 XYZ_c = full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D)
331 XYZ_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D)
333 # Converting to *Hunt-Pointer-Estevez* colourspace.
334 LMS = RGB_to_rgb(XYZ_c)
335 LMS_w = RGB_to_rgb(XYZ_wc)
337 # Cones absolute response.
338 LMS_n_c = spow(LMS, n_c)
339 LMS_w_n_c = spow(LMS_w, n_c)
340 L_A_n_c = spow(L_A, n_c)
341 LMS_p = LMS_n_c / (LMS_n_c + L_A_n_c)
342 LMS_wp = LMS_w_n_c / (LMS_w_n_c + L_A_n_c)
344 # Achromatic signal :math:`A` and :math:`A_w`.
345 v_A = np.array([40, 20, 1])
346 A = np.sum(v_A * LMS_p, axis=-1) / 61
347 A_w = np.sum(v_A * LMS_wp, axis=-1) / 61
349 # Perceived *Lightness* :math:`J_p`.
350 a_j, b_j, o_j, n_j = 0.89, 0.24, 0.65, 3.65
351 A_A_w = A / A_w
352 J_p = spow((-(A_A_w - b_j) * spow(o_j, n_j)) / (A_A_w - b_j - a_j), 1 / n_j)
354 # Computing the media dependent *Lightness* :math:`J`.
355 J = 100 * (media.E * (J_p - 1) + 1)
357 # Computing the correlate of *brightness* :math:`Q`.
358 n_q = 0.1308
359 Q = J * spow(Y_w, n_q)
361 # Opponent signals :math:`a` and :math:`b`.
362 a = (1 / 11) * np.sum(np.array([11, -12, 1]) * LMS_p, axis=-1)
363 b = (1 / 9) * np.sum(np.array([1, 1, -2]) * LMS_p, axis=-1)
365 # Computing the correlate of *chroma* :math:`C`.
366 a_k, n_k = 456.5, 0.62
367 C = a_k * spow(np.hypot(a, b), n_k)
369 # Computing the correlate of *colourfulness* :math:`M`.
370 a_m, b_m = 0.11, 0.61
371 M = C * (a_m * np.log10(Y_w) + b_m)
373 # Computing the correlate of *saturation* :math:`s`.
374 s = 100 * np.sqrt(M / Q)
376 # Computing the *hue* angle :math:`h`.
377 h = np.degrees(np.arctan2(b, a)) % 360
379 # Computing hue :math:`h` quadrature :math:`H`.
380 H = hue_quadrature(h) if compute_H else np.full(h.shape, np.nan)
382 return CAM_Specification_Kim2009(
383 J=as_float(from_range_100(J)),
384 C=as_float(from_range_100(C)),
385 h=as_float(from_range_degrees(h)),
386 s=as_float(from_range_100(s)),
387 Q=as_float(from_range_100(Q)),
388 M=as_float(from_range_100(M)),
389 H=as_float(from_range_degrees(H, 400)),
390 HC=None,
391 )
394def Kim2009_to_XYZ(
395 specification: Annotated[
396 CAM_Specification_Kim2009, (100, 100, 360, 100, 100, 100, 400)
397 ],
398 XYZ_w: Domain100,
399 L_A: ArrayLike,
400 media: MediaParameters_Kim2009 = MEDIA_PARAMETERS_KIM2009["CRT Displays"],
401 surround: InductionFactors_Kim2009 = VIEWING_CONDITIONS_KIM2009["Average"],
402 n_c: float = 0.57,
403 discount_illuminant: bool = False,
404) -> Range100:
405 """
406 Convert the *Kim, Weyrich and Kautz (2009)* colour appearance model
407 specification to *CIE XYZ* tristimulus values.
409 Parameters
410 ----------
411 specification
412 *Kim, Weyrich and Kautz (2009)* colour appearance model specification.
413 Correlate of *Lightness* :math:`J`, correlate of *chroma* :math:`C` or
414 correlate of *colourfulness* :math:`M` and *hue* angle :math:`h` in
415 degrees must be specified, e.g., :math:`JCh` or :math:`JMh`.
416 XYZ_w
417 *CIE XYZ* tristimulus values of reference white.
418 L_A
419 Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`, (often taken
420 to be 20% of the luminance of a white object in the scene).
421 media
422 Media parameters.
423 surround
424 Surround viewing conditions induction factors.
425 n_c
426 Cone response sigmoidal curve modulating factor :math:`n_c`.
427 discount_illuminant
428 Truth value indicating if the illuminant should be discounted.
430 Returns
431 -------
432 :class:`numpy.ndarray`
433 *CIE XYZ* tristimulus values.
435 Raises
436 ------
437 ValueError
438 If neither :math:`C` nor :math:`M` correlates have been defined in the
439 ``specification`` argument.
441 Notes
442 -----
443 +---------------------+-----------------------+---------------+
444 | **Domain** | **Scale - Reference** | **Scale - 1** |
445 +=====================+=======================+===============+
446 | ``specification.J`` | 100 | 1 |
447 +---------------------+-----------------------+---------------+
448 | ``specification.C`` | 100 | 1 |
449 +---------------------+-----------------------+---------------+
450 | ``specification.h`` | 360 | 1 |
451 +---------------------+-----------------------+---------------+
452 | ``specification.s`` | 100 | 1 |
453 +---------------------+-----------------------+---------------+
454 | ``specification.Q`` | 100 | 1 |
455 +---------------------+-----------------------+---------------+
456 | ``specification.M`` | 100 | 1 |
457 +---------------------+-----------------------+---------------+
458 | ``specification.H`` | 360 | 1 |
459 +---------------------+-----------------------+---------------+
460 | ``XYZ_w`` | 100 | 1 |
461 +---------------------+-----------------------+---------------+
463 +---------------------+-----------------------+---------------+
464 | **Range** | **Scale - Reference** | **Scale - 1** |
465 +=====================+=======================+===============+
466 | ``XYZ`` | 100 | 1 |
467 +---------------------+-----------------------+---------------+
469 References
470 ----------
471 :cite:`Kim2009`
473 Examples
474 --------
475 >>> specification = CAM_Specification_Kim2009(
476 ... J=28.861908975839647, C=0.5592455924373706, h=219.04806677662953
477 ... )
478 >>> XYZ_w = np.array([95.05, 100.00, 108.88])
479 >>> L_A = 318.31
480 >>> media = MEDIA_PARAMETERS_KIM2009["CRT Displays"]
481 >>> surround = VIEWING_CONDITIONS_KIM2009["Average"]
482 >>> Kim2009_to_XYZ(specification, XYZ_w, L_A, media, surround)
483 ... # doctest: +ELLIPSIS
484 array([ 19.0099995..., 19.9999999..., 21.7800000...])
485 """
487 J, C, h, _s, _Q, M, _H, _HC = astuple(specification)
489 J = to_domain_100(J)
490 C = to_domain_100(C)
491 h = to_domain_degrees(h)
492 M = to_domain_100(M)
493 L_A = as_float_array(L_A)
494 XYZ_w = to_domain_100(XYZ_w)
495 _X_w, Y_w, _Z_w = tsplit(XYZ_w)
497 # Converting *CIE XYZ* tristimulus values to *CMCCAT2000* transform
498 # sharpened *RGB* values.
499 RGB_w = vecmul(CAT_CAT02, XYZ_w)
501 # Computing degree of adaptation :math:`D`.
502 D = (
503 degree_of_adaptation(surround.F, L_A)
504 if not discount_illuminant
505 else ones(L_A.shape)
506 )
508 # Computing full chromatic adaptation.
509 XYZ_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D)
511 # Converting to *Hunt-Pointer-Estevez* colourspace.
512 LMS_w = RGB_to_rgb(XYZ_wc)
514 # n_q = 0.1308
515 # J = Q / spow(Y_w, n_q)
516 if has_only_nan(C) and not has_only_nan(M):
517 a_m, b_m = 0.11, 0.61
518 C = M / (a_m * np.log10(Y_w) + b_m)
519 elif has_only_nan(C):
520 error = (
521 'Either "C" or "M" correlate must be defined in '
522 'the "CAM_Specification_Kim2009" argument!'
523 )
525 raise ValueError(error)
527 # Cones absolute response.
528 LMS_w_n_c = spow(LMS_w, n_c)
529 L_A_n_c = spow(L_A, n_c)
530 LMS_wp = LMS_w_n_c / (LMS_w_n_c + L_A_n_c)
532 # Achromatic signal :math:`A_w`
533 v_A = np.array([40, 20, 1])
534 A_w = np.sum(v_A * LMS_wp, axis=-1) / 61
536 # Perceived *Lightness* :math:`J_p`.
537 J_p = (J / 100 - 1) / media.E + 1
539 # Achromatic signal :math:`A`.
540 a_j, b_j, n_j, o_j = 0.89, 0.24, 3.65, 0.65
541 J_p_n_j = spow(J_p, n_j)
542 A = A_w * ((a_j * J_p_n_j) / (J_p_n_j + spow(o_j, n_j)) + b_j)
544 # Opponent signals :math:`a` and :math:`b`.
545 a_k, n_k = 456.5, 0.62
546 C_a_k_n_k = spow(C / a_k, 1 / n_k)
547 hr = np.radians(h)
548 a, b = np.cos(hr) * C_a_k_n_k, np.sin(hr) * C_a_k_n_k
550 # Cones absolute response.
551 M = np.array(
552 [
553 [1.0000, 0.3215, 0.2053],
554 [1.0000, -0.6351, -0.1860],
555 [1.0000, -0.1568, -4.4904],
556 ]
557 )
558 LMS_p = vecmul(M, tstack([A, a, b]))
559 LMS = spow((-spow(L_A, n_c) * LMS_p) / (LMS_p - 1), 1 / n_c)
561 # Converting to *Hunt-Pointer-Estevez* colourspace.
562 RGB_c = rgb_to_RGB(LMS)
564 # Applying inverse full chromatic adaptation.
565 RGB = full_chromatic_adaptation_inverse(RGB_c, RGB_w, Y_w, D)
567 XYZ = vecmul(CAT_INVERSE_CAT02, RGB)
569 return from_range_100(XYZ)