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

101 statements  

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

1""" 

2CIE 1994 Chromatic Adaptation Model 

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

4 

5Define the *CIE 1994* chromatic adaptation model for predicting corresponding 

6colours under different viewing conditions. 

7 

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

9 

10References 

11---------- 

12- :cite:`CIETC1-321994b` : CIE TC 1-32. (1994). CIE 109-1994 A Method of 

13 Predicting Corresponding Colours under Different Chromatic and Illuminance 

14 Adaptations. Commission Internationale de l'Eclairage. 

15 ISBN:978-3-900734-51-0 

16""" 

17 

18from __future__ import annotations 

19 

20import typing 

21 

22import numpy as np 

23 

24from colour.adaptation import CAT_VON_KRIES 

25from colour.algebra import sdiv, sdiv_mode, spow, vecmul 

26 

27if typing.TYPE_CHECKING: 

28 from colour.hints import DTypeFloat, NDArray 

29 

30from colour.hints import ( # noqa: TC001 

31 ArrayLike, 

32 Domain100, 

33 NDArrayFloat, 

34 Range100, 

35) 

36from colour.utilities import ( 

37 as_float_array, 

38 from_range_100, 

39 to_domain_100, 

40 tsplit, 

41 tstack, 

42 usage_warning, 

43) 

44 

45__author__ = "Colour Developers" 

46__copyright__ = "Copyright 2013 Colour Developers" 

47__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" 

48__maintainer__ = "Colour Developers" 

49__email__ = "colour-developers@colour-science.org" 

50__status__ = "Production" 

51 

52__all__ = [ 

53 "MATRIX_XYZ_TO_RGB_CIE1994", 

54 "MATRIX_RGB_TO_XYZ_CIE1994", 

55 "chromatic_adaptation_CIE1994", 

56 "XYZ_to_RGB_CIE1994", 

57 "RGB_to_XYZ_CIE1994", 

58 "intermediate_values", 

59 "effective_adapting_responses", 

60 "beta_1", 

61 "beta_2", 

62 "exponential_factors", 

63 "K_coefficient", 

64 "corresponding_colour", 

65] 

66 

67MATRIX_XYZ_TO_RGB_CIE1994: NDArrayFloat = CAT_VON_KRIES 

68""" 

69*CIE 1994* colour appearance model *CIE XYZ* tristimulus values to cone 

70responses matrix. 

71""" 

72 

73MATRIX_RGB_TO_XYZ_CIE1994: NDArrayFloat = np.linalg.inv(MATRIX_XYZ_TO_RGB_CIE1994) 

74""" 

75*CIE 1994* colour appearance model cone responses to *CIE XYZ* tristimulus 

76values matrix. 

77""" 

78 

79 

80def chromatic_adaptation_CIE1994( 

81 XYZ_1: Domain100, 

82 xy_o1: ArrayLike, 

83 xy_o2: ArrayLike, 

84 Y_o: Domain100, 

85 E_o1: ArrayLike, 

86 E_o2: ArrayLike, 

87 n: ArrayLike = 1, 

88) -> Range100: 

89 """ 

90 Adapt the specified stimulus *CIE XYZ* tristimulus values from test 

91 viewing conditions to reference viewing conditions using the 

92 *CIE 1994* chromatic adaptation model. 

93 

94 Parameters 

95 ---------- 

96 XYZ_1 

97 *CIE XYZ* tristimulus values of test sample / stimulus. 

98 xy_o1 

99 Chromaticity coordinates :math:`x_{o1}` and :math:`y_{o1}` of test 

100 illuminant and background. 

101 xy_o2 

102 Chromaticity coordinates :math:`x_{o2}` and :math:`y_{o2}` of 

103 reference illuminant and background. 

104 Y_o 

105 Luminance factor :math:`Y_o` of achromatic background as percentage 

106 normalised to domain [18, 100] in **'Reference'** domain-range scale. 

107 E_o1 

108 Test illuminance :math:`E_{o1}` in lux. 

109 E_o2 

110 Reference illuminance :math:`E_{o2}` in lux. 

111 n 

112 Noise component in fundamental primary system. 

113 

114 Returns 

115 ------- 

116 :class:`numpy.ndarray` 

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

118 

119 Notes 

120 ----- 

121 +------------+-----------------------+---------------+ 

122 | **Domain** | **Scale - Reference** | **Scale - 1** | 

123 +============+=======================+===============+ 

124 | ``XYZ_1`` | 100 | 1 | 

125 +------------+-----------------------+---------------+ 

126 | ``Y_o`` | 100 | 1 | 

127 +------------+-----------------------+---------------+ 

128 

129 +------------+-----------------------+---------------+ 

130 | **Range** | **Scale - Reference** | **Scale - 1** | 

131 +============+=======================+===============+ 

132 | ``XYZ_2`` | 100 | 1 | 

133 +------------+-----------------------+---------------+ 

134 

135 References 

136 ---------- 

137 :cite:`CIETC1-321994b` 

138 

139 Examples 

140 -------- 

141 >>> XYZ_1 = np.array([28.00, 21.26, 5.27]) 

142 >>> xy_o1 = np.array([0.4476, 0.4074]) 

143 >>> xy_o2 = np.array([0.3127, 0.3290]) 

144 >>> Y_o = 20 

145 >>> E_o1 = 1000 

146 >>> E_o2 = 1000 

147 >>> chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) 

148 ... # doctest: +ELLIPSIS 

149 array([ 24.0337952..., 21.1562121..., 17.6430119...]) 

150 """ 

151 

152 XYZ_1 = to_domain_100(XYZ_1) 

153 Y_o = as_float_array(to_domain_100(Y_o)) 

154 E_o1 = as_float_array(E_o1) 

155 E_o2 = as_float_array(E_o2) 

156 

157 if np.any(Y_o < 18) or np.any(Y_o > 100): 

158 usage_warning( 

159 '"Y_o" luminance factor must be in [18, 100] domain, ' 

160 "unpredictable results may occur!" 

161 ) 

162 

163 RGB_1 = XYZ_to_RGB_CIE1994(XYZ_1) 

164 

165 xez_1 = intermediate_values(xy_o1) 

166 xez_2 = intermediate_values(xy_o2) 

167 

168 RGB_o1 = effective_adapting_responses(xez_1, Y_o, E_o1) 

169 RGB_o2 = effective_adapting_responses(xez_2, Y_o, E_o2) 

170 

171 bRGB_o1 = exponential_factors(RGB_o1) 

172 bRGB_o2 = exponential_factors(RGB_o2) 

173 

174 K = K_coefficient(xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, n) 

175 

176 RGB_2 = corresponding_colour(RGB_1, xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, K, n) 

177 XYZ_2 = RGB_to_XYZ_CIE1994(RGB_2) 

178 

179 return from_range_100(XYZ_2) 

180 

181 

182def XYZ_to_RGB_CIE1994(XYZ: ArrayLike) -> NDArrayFloat: 

183 """ 

184 Convert from *CIE XYZ* tristimulus values to cone responses using the 

185 *CIE 1994* colour appearance model transformation. 

186 

187 Parameters 

188 ---------- 

189 XYZ 

190 *CIE XYZ* tristimulus values. 

191 

192 Returns 

193 ------- 

194 :class:`numpy.ndarray` 

195 Cone responses. 

196 

197 Examples 

198 -------- 

199 >>> XYZ = np.array([28.00, 21.26, 5.27]) 

200 >>> XYZ_to_RGB_CIE1994(XYZ) # doctest: +ELLIPSIS 

201 array([ 25.8244273..., 18.6791422..., 4.8390194...]) 

202 """ 

203 

204 return vecmul(MATRIX_XYZ_TO_RGB_CIE1994, XYZ) 

205 

206 

207def RGB_to_XYZ_CIE1994(RGB: ArrayLike) -> NDArrayFloat: 

208 """ 

209 Convert from cone responses to *CIE XYZ* tristimulus values using the 

210 *CIE 1994* colour appearance model inverse transformation. 

211 

212 Parameters 

213 ---------- 

214 RGB 

215 Cone responses. 

216 

217 Returns 

218 ------- 

219 :class:`numpy.ndarray` 

220 *CIE XYZ* tristimulus values. 

221 

222 Examples 

223 -------- 

224 >>> RGB = np.array([25.82442730, 18.67914220, 4.83901940]) 

225 >>> RGB_to_XYZ_CIE1994(RGB) # doctest: +ELLIPSIS 

226 array([ 28. , 21.26, 5.27]) 

227 """ 

228 

229 return vecmul(MATRIX_RGB_TO_XYZ_CIE1994, RGB) 

230 

231 

232def intermediate_values(xy_o: ArrayLike) -> NDArrayFloat: 

233 """ 

234 Compute the intermediate values :math:`\\xi`, :math:`\\eta`, and 

235 :math:`\\zeta`. 

236 

237 Parameters 

238 ---------- 

239 xy_o 

240 Chromaticity coordinates :math:`x_o` and :math:`y_o` of the 

241 whitepoint. 

242 

243 Returns 

244 ------- 

245 :class:`numpy.ndarray` 

246 Intermediate values :math:`\\xi`, :math:`\\eta`, and 

247 :math:`\\zeta`. 

248 

249 Examples 

250 -------- 

251 >>> xy_o = np.array([0.4476, 0.4074]) 

252 >>> intermediate_values(xy_o) # doctest: +ELLIPSIS 

253 array([ 1.1185719..., 0.9329553..., 0.3268087...]) 

254 """ 

255 

256 x_o, y_o = tsplit(xy_o) 

257 

258 # Computing :math:`\\xi` :math:`\\eta`, :math:`\\zeta` values. 

259 xi = (0.48105 * x_o + 0.78841 * y_o - 0.08081) / y_o 

260 eta = (-0.27200 * x_o + 1.11962 * y_o + 0.04570) / y_o 

261 zeta = (0.91822 * (1 - x_o - y_o)) / y_o 

262 

263 return tstack([xi, eta, zeta]) 

264 

265 

266def effective_adapting_responses( 

267 xez: ArrayLike, Y_o: ArrayLike, E_o: ArrayLike 

268) -> NDArrayFloat: 

269 """ 

270 Compute the effective adapting responses in the fundamental primary system 

271 of the test or reference field. 

272 

273 Parameters 

274 ---------- 

275 xez 

276 Intermediate values :math:`\\xi`, :math:`\\eta`, :math:`\\zeta`. 

277 Y_o 

278 Luminance factor :math:`Y_o` of achromatic background as percentage 

279 normalised to domain [18, 100] in **'Reference'** domain-range scale. 

280 E_o 

281 Test or reference illuminance :math:`E_o` in lux. 

282 

283 Returns 

284 ------- 

285 :class:`numpy.ndarray` 

286 Effective adapting responses. 

287 

288 Examples 

289 -------- 

290 >>> xez = np.array([1.11857195, 0.93295530, 0.32680879]) 

291 >>> E_o = 1000 

292 >>> Y_o = 20 

293 >>> effective_adapting_responses(xez, Y_o, E_o) # doctest: +ELLIPSIS 

294 array([ 71.2105020..., 59.3937790..., 20.8052937...]) 

295 """ 

296 

297 xez = as_float_array(xez) 

298 Y_o = as_float_array(Y_o) 

299 E_o = as_float_array(E_o) 

300 

301 return ((Y_o[..., None] * E_o[..., None]) / (100 * np.pi)) * xez 

302 

303 

304@typing.overload 

305def beta_1(x: float | DTypeFloat) -> DTypeFloat: ... 

306@typing.overload 

307def beta_1(x: NDArray) -> NDArrayFloat: ... 

308@typing.overload 

309def beta_1(x: ArrayLike) -> DTypeFloat | NDArrayFloat: ... 

310def beta_1(x: ArrayLike) -> DTypeFloat | NDArrayFloat: 

311 """ 

312 Compute the exponent :math:`\\beta_1` for the middle and long-wavelength 

313 sensitive cones. 

314 

315 Parameters 

316 ---------- 

317 x 

318 Middle and long-wavelength sensitive cone response. 

319 

320 Returns 

321 ------- 

322 :class:`numpy.ndarray` 

323 Exponent :math:`\\beta_1`. 

324 

325 Examples 

326 -------- 

327 >>> beta_1(318.323316315) # doctest: +ELLIPSIS 

328 4.6106222... 

329 """ 

330 

331 x_p = spow(x, 0.4495) 

332 

333 return (x_p * 6.362 + 6.469) / (x_p + 6.469) 

334 

335 

336@typing.overload 

337def beta_2(x: float | DTypeFloat) -> DTypeFloat: ... 

338@typing.overload 

339def beta_2(x: NDArray) -> NDArrayFloat: ... 

340@typing.overload 

341def beta_2(x: ArrayLike) -> DTypeFloat | NDArrayFloat: ... 

342def beta_2(x: ArrayLike) -> DTypeFloat | NDArrayFloat: 

343 """ 

344 Compute the exponent :math:`\\beta_2` for the short-wavelength sensitive 

345 cones. 

346 

347 Parameters 

348 ---------- 

349 x 

350 Short-wavelength sensitive cone response. 

351 

352 Returns 

353 ------- 

354 :class:`numpy.ndarray` 

355 Exponent :math:`\\beta_2`. 

356 

357 Examples 

358 -------- 

359 >>> beta_2(318.323316315) # doctest: +ELLIPSIS 

360 4.6522416... 

361 """ 

362 

363 x_p = spow(x, 0.5128) 

364 

365 return (x_p * 8.091 + 8.414) * 0.7844 / (x_p + 8.414) 

366 

367 

368def exponential_factors(RGB_o: ArrayLike) -> NDArrayFloat: 

369 """ 

370 Compute the chromatic adaptation exponential factors 

371 :math:`\\beta_1(R_o)`, :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)` of 

372 the specified cone responses. 

373 

374 Parameters 

375 ---------- 

376 RGB_o 

377 Cone responses. 

378 

379 Returns 

380 ------- 

381 :class:`numpy.ndarray` 

382 Chromatic adaptation exponential factors :math:`\\beta_1(R_o)`, 

383 :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)`. 

384 

385 Examples 

386 -------- 

387 >>> RGB_o = np.array([318.32331631, 318.30352317, 318.23283482]) 

388 >>> exponential_factors(RGB_o) # doctest: +ELLIPSIS 

389 array([ 4.6106222..., 4.6105892..., 4.6520698...]) 

390 """ 

391 

392 R_o, G_o, B_o = tsplit(RGB_o) 

393 

394 bR_o = beta_1(R_o) 

395 bG_o = beta_1(G_o) 

396 bB_o = beta_2(B_o) 

397 

398 return tstack([bR_o, bG_o, bB_o]) 

399 

400 

401def K_coefficient( 

402 xez_1: ArrayLike, 

403 xez_2: ArrayLike, 

404 bRGB_o1: ArrayLike, 

405 bRGB_o2: ArrayLike, 

406 Y_o: ArrayLike, 

407 n: ArrayLike = 1, 

408) -> NDArrayFloat: 

409 """ 

410 Compute the coefficient :math:`K` for correcting the difference between 

411 the test and reference illuminances. 

412 

413 Parameters 

414 ---------- 

415 xez_1 

416 Intermediate values :math:`\\xi_1`, :math:`\\eta_1`, :math:`\\zeta_1` 

417 for the test illuminant and background. 

418 xez_2 

419 Intermediate values :math:`\\xi_2`, :math:`\\eta_2`, :math:`\\zeta_2` 

420 for the reference illuminant and background. 

421 bRGB_o1 

422 Chromatic adaptation exponential factors :math:`\\beta_1(R_{o1})`, 

423 :math:`\\beta_1(G_{o1})` and :math:`\\beta_2(B_{o1})` of test sample. 

424 bRGB_o2 

425 Chromatic adaptation exponential factors :math:`\\beta_1(R_{o2})`, 

426 :math:`\\beta_1(G_{o2})` and :math:`\\beta_2(B_{o2})` of reference 

427 sample. 

428 Y_o 

429 Luminance factor :math:`Y_o` of achromatic background as percentage 

430 normalised to domain [18, 100] in **'Reference'** domain-range scale. 

431 n 

432 Noise component in fundamental primary system. 

433 

434 Returns 

435 ------- 

436 :class:`numpy.ndarray` 

437 Coefficient :math:`K`. 

438 

439 Examples 

440 -------- 

441 >>> xez_1 = np.array([1.11857195, 0.93295530, 0.32680879]) 

442 >>> xez_2 = np.array([1.00000372, 1.00000176, 0.99999461]) 

443 >>> bRGB_o1 = np.array([3.74852518, 3.63920879, 2.78924811]) 

444 >>> bRGB_o2 = np.array([3.68102374, 3.68102256, 3.56557351]) 

445 >>> Y_o = 20 

446 >>> K_coefficient(xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o) 

447 1.0 

448 """ 

449 

450 xi_1, eta_1, _zeta_1 = tsplit(xez_1) 

451 xi_2, eta_2, _zeta_2 = tsplit(xez_2) 

452 bR_o1, bG_o1, _bB_o1 = tsplit(bRGB_o1) 

453 bR_o2, bG_o2, _bB_o2 = tsplit(bRGB_o2) 

454 Y_o = as_float_array(Y_o) 

455 n = as_float_array(n) 

456 

457 K = spow((Y_o * xi_1 + n) / (20 * xi_1 + n), (2 / 3) * bR_o1) / spow( 

458 (Y_o * xi_2 + n) / (20 * xi_2 + n), (2 / 3) * bR_o2 

459 ) 

460 

461 K *= spow((Y_o * eta_1 + n) / (20 * eta_1 + n), (1 / 3) * bG_o1) / spow( 

462 (Y_o * eta_2 + n) / (20 * eta_2 + n), (1 / 3) * bG_o2 

463 ) 

464 

465 return K 

466 

467 

468def corresponding_colour( 

469 RGB_1: ArrayLike, 

470 xez_1: ArrayLike, 

471 xez_2: ArrayLike, 

472 bRGB_o1: ArrayLike, 

473 bRGB_o2: ArrayLike, 

474 Y_o: ArrayLike, 

475 K: ArrayLike, 

476 n: ArrayLike = 1, 

477) -> NDArrayFloat: 

478 """ 

479 Compute corresponding colour cone responses of the specified test sample cone 

480 responses :math:`RGB_1` under chromatic adaptation. 

481 

482 Parameters 

483 ---------- 

484 RGB_1 

485 Test sample cone responses :math:`RGB_1`. 

486 xez_1 

487 Intermediate values :math:`\\xi_1`, :math:`\\eta_1`, :math:`\\zeta_1` 

488 for test illuminant and background. 

489 xez_2 

490 Intermediate values :math:`\\xi_2`, :math:`\\eta_2`, :math:`\\zeta_2` 

491 for reference illuminant and background. 

492 bRGB_o1 

493 Chromatic adaptation exponential factors :math:`\\beta_1(R_{o1})`, 

494 :math:`\\beta_1(G_{o1})` and :math:`\\beta_2(B_{o1})` of test 

495 sample. 

496 bRGB_o2 

497 Chromatic adaptation exponential factors :math:`\\beta_1(R_{o2})`, 

498 :math:`\\beta_1(G_{o2})` and :math:`\\beta_2(B_{o2})` of reference 

499 sample. 

500 Y_o 

501 Luminance factor :math:`Y_o` of achromatic background as percentage 

502 normalised to domain [18, 100] in **'Reference'** domain-range 

503 scale. 

504 K 

505 Coefficient :math:`K`. 

506 n 

507 Noise component in fundamental primary system. 

508 

509 Returns 

510 ------- 

511 :class:`numpy.ndarray` 

512 Corresponding colour cone responses of the specified test sample cone 

513 responses. 

514 

515 Examples 

516 -------- 

517 >>> RGB_1 = np.array([25.82442730, 18.67914220, 4.83901940]) 

518 >>> xez_1 = np.array([1.11857195, 0.93295530, 0.32680879]) 

519 >>> xez_2 = np.array([1.00000372, 1.00000176, 0.99999461]) 

520 >>> bRGB_o1 = np.array([3.74852518, 3.63920879, 2.78924811]) 

521 >>> bRGB_o2 = np.array([3.68102374, 3.68102256, 3.56557351]) 

522 >>> Y_o = 20 

523 >>> K = 1 

524 >>> corresponding_colour(RGB_1, xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, K) 

525 ... # doctest: +ELLIPSIS 

526 array([ 23.1636901..., 20.0211948..., 16.2001664...]) 

527 """ 

528 

529 R_1, G_1, B_1 = tsplit(RGB_1) 

530 xi_1, eta_1, zeta_1 = tsplit(xez_1) 

531 xi_2, eta_2, zeta_2 = tsplit(xez_2) 

532 bR_o1, bG_o1, bB_o1 = tsplit(bRGB_o1) 

533 bR_o2, bG_o2, bB_o2 = tsplit(bRGB_o2) 

534 Y_o = as_float_array(Y_o) 

535 K = as_float_array(K) 

536 n = as_float_array(n) 

537 

538 def RGB_c( 

539 x_1: NDArrayFloat, 

540 x_2: NDArrayFloat, 

541 y_1: NDArrayFloat, 

542 y_2: NDArrayFloat, 

543 z: NDArrayFloat, 

544 n: NDArrayFloat, 

545 ) -> NDArrayFloat: 

546 """Compute the corresponding colour cone responses component.""" 

547 

548 with sdiv_mode(): 

549 return (Y_o * x_2 + n) * spow(K, sdiv(1, y_2)) * spow( 

550 (z + n) / (Y_o * x_1 + n), sdiv(y_1, y_2) 

551 ) - n 

552 

553 R_2 = RGB_c(xi_1, xi_2, bR_o1, bR_o2, R_1, n) 

554 G_2 = RGB_c(eta_1, eta_2, bG_o1, bG_o2, G_1, n) 

555 B_2 = RGB_c(zeta_1, zeta_2, bB_o1, bB_o2, B_1, n) 

556 

557 return tstack([R_2, G_2, B_2])