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

1""" 

2Fairchild (1990) Chromatic Adaptation Model 

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

4 

5Define the *Fairchild (1990)* chromatic adaptation model for predicting 

6corresponding colours under different viewing conditions. 

7 

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

9 

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

18 

19from __future__ import annotations 

20 

21import numpy as np 

22 

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) 

39 

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" 

46 

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] 

55 

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

61 

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

67 

68 

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. 

80 

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. 

95 

96 Returns 

97 ------- 

98 :class:`numpy.ndarray` 

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

100 

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

112 

113 +------------+-----------------------+---------------+ 

114 | **Range** | **Scale - Reference** | **Scale - 1** | 

115 +============+=======================+===============+ 

116 | ``XYZ_2`` | 100 | 1 | 

117 +------------+-----------------------+---------------+ 

118 

119 References 

120 ---------- 

121 :cite:`Fairchild1991a`, :cite:`Fairchild2013s` 

122 

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

133 

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) 

138 

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) 

142 

143 p_LMS = degrees_of_adaptation(LMS_1, Y_n, discount_illuminant=discount_illuminant) 

144 

145 a_LMS_1 = p_LMS / LMS_n 

146 a_LMS_2 = p_LMS / LMS_r 

147 

148 A_1 = row_as_diagonal(a_LMS_1) 

149 A_2 = row_as_diagonal(a_LMS_2) 

150 

151 LMSp_1 = vecmul(A_1, LMS_1) 

152 

153 c = 0.219 - 0.0784 * np.log10(Y_n) 

154 C = row_as_diagonal(tstack([c, c, c])) 

155 

156 LMS_a = vecmul(C, LMSp_1) 

157 LMSp_2 = vecmul(np.linalg.inv(C), LMS_a) 

158 

159 LMS_c = vecmul(np.linalg.inv(A_2), LMSp_2) 

160 XYZ_c = RGB_to_XYZ_Fairchild1990(LMS_c) 

161 

162 return from_range_100(XYZ_c) 

163 

164 

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. 

169 

170 Parameters 

171 ---------- 

172 XYZ 

173 *CIE XYZ* tristimulus values. 

174 

175 Returns 

176 ------- 

177 :class:`numpy.ndarray` 

178 Cone responses. 

179 

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

186 

187 return vecmul(MATRIX_XYZ_TO_RGB_FAIRCHILD1990, XYZ) 

188 

189 

190def RGB_to_XYZ_Fairchild1990(RGB: ArrayLike) -> NDArrayFloat: 

191 """ 

192 Convert cone responses to *CIE XYZ* tristimulus values. 

193 

194 Parameters 

195 ---------- 

196 RGB 

197 Cone responses. 

198 

199 Returns 

200 ------- 

201 :class:`numpy.ndarray` 

202 *CIE XYZ* tristimulus values. 

203 

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

210 

211 return vecmul(MATRIX_RGB_TO_XYZ_FAIRCHILD1990, RGB) 

212 

213 

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`. 

223 

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. 

234 

235 Returns 

236 ------- 

237 :class:`numpy.ndarray` 

238 Degrees of adaptation :math:`p_L`, :math:`p_M` and :math:`p_S`. 

239 

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

249 

250 LMS = as_float_array(LMS) 

251 

252 if discount_illuminant: 

253 return ones(LMS.shape) 

254 

255 Y_n = as_float_array(Y_n) 

256 v = as_float_array(v) 

257 

258 # E illuminant. 

259 LMS_E = vecmul(CAT_VON_KRIES, ones(LMS.shape)) 

260 

261 Ye_n = spow(Y_n, v) 

262 

263 def m_E(x: NDArrayFloat, y: NDArrayFloat) -> NDArrayFloat: 

264 """Compute the :math:`m_E` term.""" 

265 

266 return (3 * x / y) / np.sum(x / y, axis=-1)[..., None] 

267 

268 def P_c(x: NDArrayFloat) -> NDArrayFloat: 

269 """Compute the :math:`P_L`, :math:`P_M` or :math:`P_S` terms.""" 

270 

271 return sdiv( 

272 1 + Ye_n[..., None] + x, 

273 1 + Ye_n[..., None] + sdiv(1, x), 

274 ) 

275 

276 with sdiv_mode(): 

277 return P_c(m_E(LMS, LMS_E))