Coverage for adaptation/li2025.py: 56%

1"""

2 Li (2025) Chromatic Adaptation Model

3====================================

4

5 Define the *Li (2025)* chromatic adaptation model for predicting corresponding

6 colours under different viewing conditions.

7

8- :func:`colour.adaptation.chromatic_adaptation_Li2025`

9

10 References

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"""

16

17 from __future__ import annotations

18

19 import typing

20

21 import numpy as np

22

23 from colour.adaptation import CAT_CAT16

24 from colour.algebra import sdiv, sdiv_mode, vecmul

25

26 if typing.TYPE_CHECKING:

27 from colour.hints import ArrayLike, Domain100, NDArrayFloat, Range100

28

29 from colour.utilities import (

30 as_float_array,

31 ones,

32)

33

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"

40

41 __all__ = [

42 "CAT_CAT16_INVERSE",

43 "chromatic_adaptation_Li2025",

44]

45

46 CAT_CAT16_INVERSE: NDArrayFloat = np.linalg.inv(CAT_CAT16)

47"""Inverse adaptation matrix :math:`M^{-1}_{CAT16}` for *Li (2025)* method."""

48

49

50 def 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.

62

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.

66

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.

82

83 Returns

84 -------

85 :class:`numpy.ndarray`

86 *CIE XYZ* tristimulus values of the stimulus corresponding colour.

87

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 +------------+-----------------------+---------------+

99

100 +------------+-----------------------+---------------+

101 | **Range** | **Scale - Reference** | **Scale - 1** |

102 +============+=======================+===============+

103 | ``XYZ_a`` | 100 | 1 |

104 +------------+-----------------------+---------------+

105

106 References

107 ----------

108 :cite:`Li2025`

109

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 """

122

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)

128

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)

132

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]

135

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)

141

142 D = np.atleast_1d(D)[..., None] if LMS_s.ndim > 1 else D

143

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))

148

149 return vecmul(CAT_CAT16_INVERSE, LMS_a)