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

57 statements  

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

1""" 

2CMCCAT2000 Chromatic Adaptation Model 

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

4 

5Define the *CMCCAT2000* chromatic adaptation model for predicting 

6corresponding colours under different viewing conditions. 

7 

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` 

13 

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

23 

24from __future__ import annotations 

25 

26import typing 

27from dataclasses import dataclass 

28 

29import numpy as np 

30 

31from colour.adaptation import CAT_CMCCAT2000 

32from colour.algebra import vecmul 

33 

34if typing.TYPE_CHECKING: 

35 from colour.hints import Literal 

36 

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) 

51 

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" 

58 

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] 

67 

68CAT_INVERSE_CMCCAT2000: NDArrayFloat = np.linalg.inv(CAT_CMCCAT2000) 

69""" 

70Inverse *CMCCAT2000* chromatic adaptation transform. 

71 

72CAT_INVERSE_CMCCAT2000 

73""" 

74 

75 

76@dataclass(frozen=True) 

77class InductionFactors_CMCCAT2000(MixinDataclassIterable): 

78 """ 

79 Define the *CMCCAT2000* chromatic adaptation model induction factors. 

80 

81 Parameters 

82 ---------- 

83 F 

84 :math:`F` surround condition factor that modulates the degree of 

85 adaptation based on the viewing environment. 

86 

87 References 

88 ---------- 

89 :cite:`Li2002a`, :cite:`Westland2012k` 

90 """ 

91 

92 F: float 

93 

94 

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. 

105 

106The viewing conditions include three standard surround conditions with 

107their corresponding induction factors: 

108 

109- *Average*: Induction factor of 1.0 

110- *Dim*: Induction factor of 0.8 

111- *Dark*: Induction factor of 0.8 

112 

113These values represent the standard viewing conditions used in the 

114*CMCCAT2000* chromatic adaptation transform. 

115 

116References 

117---------- 

118:cite:`Li2002a`, :cite:`Westland2012k` 

119""" 

120 

121 

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. 

134 

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. 

153 

154 Returns 

155 ------- 

156 :class:`numpy.ndarray` 

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

158 

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

170 

171 +------------+-----------------------+---------------+ 

172 | **Range** | **Scale - Reference** | **Scale - 1** | 

173 +============+=======================+===============+ 

174 | ``XYZ_c`` | 100 | 1 | 

175 +------------+-----------------------+---------------+ 

176 

177 References 

178 ---------- 

179 :cite:`Li2002a`, :cite:`Westland2012k` 

180 

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

192 

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) 

198 

199 RGB = vecmul(CAT_CMCCAT2000, XYZ) 

200 RGB_w = vecmul(CAT_CMCCAT2000, XYZ_w) 

201 RGB_wr = vecmul(CAT_CMCCAT2000, XYZ_wr) 

202 

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 ) 

208 

209 D = np.clip(D, 0, 1) 

210 a = D * XYZ_w[..., 1] / XYZ_wr[..., 1] 

211 

212 RGB_c = RGB * (a[..., None] * (RGB_wr / RGB_w) + 1 - D[..., None]) 

213 XYZ_c = vecmul(CAT_INVERSE_CMCCAT2000, RGB_c) 

214 

215 return from_range_100(XYZ_c) 

216 

217 

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. 

230 

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. 

247 

248 Returns 

249 ------- 

250 :class:`numpy.ndarray` 

251 *CIE XYZ* tristimulus values of the stimulus adapted to the test 

252 viewing conditions. 

253 

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

265 

266 +------------+-----------------------+---------------+ 

267 | **Range** | **Scale - Reference** | **Scale - 1** | 

268 +============+=======================+===============+ 

269 | ``XYZ`` | 100 | 1 | 

270 +------------+-----------------------+---------------+ 

271 

272 References 

273 ---------- 

274 :cite:`Li2002a`, :cite:`Westland2012k` 

275 

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

287 

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) 

293 

294 RGB_c = vecmul(CAT_CMCCAT2000, XYZ_c) 

295 RGB_w = vecmul(CAT_CMCCAT2000, XYZ_w) 

296 RGB_wr = vecmul(CAT_CMCCAT2000, XYZ_wr) 

297 

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 ) 

303 

304 D = np.clip(D, 0, 1) 

305 a = D * XYZ_w[..., 1] / XYZ_wr[..., 1] 

306 

307 RGB = RGB_c / (a[..., None] * (RGB_wr / RGB_w) + 1 - D[..., None]) 

308 XYZ = vecmul(CAT_INVERSE_CMCCAT2000, RGB) 

309 

310 return from_range_100(XYZ) 

311 

312 

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. 

326 

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

330 

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. 

350 

351 Returns 

352 ------- 

353 :class:`numpy.ndarray` 

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

355 

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

367 

368 +------------+-----------------------+---------------+ 

369 | **Range** | **Scale - Reference** | **Scale - 1** | 

370 +============+=======================+===============+ 

371 | ``XYZ`` | 100 | 1 | 

372 +------------+-----------------------+---------------+ 

373 

374 References 

375 ---------- 

376 :cite:`Li2002a`, :cite:`Westland2012k` 

377 

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

390 

391 Using the *CMCCAT2000* inverse model: 

392 

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

404 

405 direction = validate_method( 

406 direction, 

407 ("Forward", "Inverse"), 

408 '"{0}" direction is invalid, it must be one of {1}!', 

409 ) 

410 

411 if direction == "forward": 

412 return chromatic_adaptation_forward_CMCCAT2000( 

413 XYZ, XYZ_w, XYZ_wr, L_A1, L_A2, surround 

414 ) 

415 

416 return chromatic_adaptation_inverse_CMCCAT2000( 

417 XYZ, XYZ_w, XYZ_wr, L_A1, L_A2, surround 

418 )