Coverage for colour/adaptation/li2025.py: 100%

36 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

2Li (2025) Chromatic Adaptation Model 

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

4 

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

6colours under different viewing conditions. 

7 

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

9 

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

16 

17from __future__ import annotations 

18 

19import typing 

20 

21import numpy as np 

22 

23from colour.adaptation import CAT_CAT16 

24from colour.algebra import sdiv, sdiv_mode, vecmul 

25 

26if typing.TYPE_CHECKING: 

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

28 

29from 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 

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

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

48 

49 

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. 

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)