Coverage for colour/appearance/ciecam16.py: 100%

119 statements  

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

1""" 

2CIECAM16 Colour Appearance Model 

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

4 

5Define the *CIECAM16* colour appearance model for predicting perceptual colour 

6attributes under varying viewing conditions. 

7 

8- :class:`colour.appearance.InductionFactors_CIECAM16` 

9- :attr:`colour.VIEWING_CONDITIONS_CIECAM16` 

10- :class:`colour.CAM_Specification_CIECAM16` 

11- :func:`colour.XYZ_to_CIECAM16` 

12- :func:`colour.CIECAM16_to_XYZ` 

13 

14References 

15---------- 

16- :cite:`CIEDivision12022` : CIE Division 1 & CIE Division 8. (2022). 

17 CIE 248:2022 The CIE 2016 Colour Appearance Model for Colour Management 

18 Systems: CIECAM16. Commission Internationale de l'Eclairage. 

19 ISBN:978-3-902842-94-7 

20""" 

21 

22from __future__ import annotations 

23 

24from dataclasses import astuple, dataclass, field 

25 

26import numpy as np 

27 

28from colour.algebra import spow, vecmul 

29from colour.appearance.cam16 import MATRIX_16, MATRIX_INVERSE_16 

30from colour.appearance.ciecam02 import ( 

31 VIEWING_CONDITIONS_CIECAM02, 

32 InductionFactors_CIECAM02, 

33 P, 

34 achromatic_response_forward, 

35 achromatic_response_inverse, 

36 brightness_correlate, 

37 chroma_correlate, 

38 colourfulness_correlate, 

39 degree_of_adaptation, 

40 eccentricity_factor, 

41 hue_angle, 

42 hue_quadrature, 

43 lightness_correlate, 

44 matrix_post_adaptation_non_linear_response_compression, 

45 opponent_colour_dimensions_forward, 

46 opponent_colour_dimensions_inverse, 

47 post_adaptation_non_linear_response_compression_forward, 

48 saturation_correlate, 

49 temporary_magnitude_quantity_inverse, 

50 viewing_conditions_dependent_parameters, 

51) 

52from colour.hints import ( # noqa: TC001 

53 Annotated, 

54 ArrayLike, 

55 Domain100, 

56 NDArrayFloat, 

57 Range100, 

58) 

59from colour.utilities import ( 

60 CanonicalMapping, 

61 MixinDataclassArithmetic, 

62 MixinDataclassIterable, 

63 as_float, 

64 as_float_array, 

65 from_range_100, 

66 from_range_degrees, 

67 has_only_nan, 

68 ones, 

69 to_domain_100, 

70 to_domain_degrees, 

71 tsplit, 

72) 

73 

74__author__ = "Colour Developers" 

75__copyright__ = "Copyright 2013 Colour Developers" 

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

77__maintainer__ = "Colour Developers" 

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

79__status__ = "Production" 

80 

81__all__ = [ 

82 "InductionFactors_CIECAM16", 

83 "VIEWING_CONDITIONS_CIECAM16", 

84 "CAM_Specification_CIECAM16", 

85 "XYZ_to_CIECAM16", 

86 "CIECAM16_to_XYZ", 

87 "f_e_forward", 

88 "f_e_inverse", 

89 "f_q", 

90 "d_f_q", 

91] 

92 

93 

94@dataclass(frozen=True) 

95class InductionFactors_CIECAM16(MixinDataclassIterable): 

96 """ 

97 Define the *CIECAM16* colour appearance model induction factors. 

98 

99 Parameters 

100 ---------- 

101 F 

102 Maximum degree of adaptation :math:`F`. 

103 c 

104 Exponential non-linearity :math:`c`. 

105 N_c 

106 Chromatic induction factor :math:`N_c`. 

107 

108 Notes 

109 ----- 

110 - The *CIECAM16* colour appearance model induction factors are the same 

111 as *CIECAM02* colour appearance model. 

112 

113 References 

114 ---------- 

115 :cite:`CIEDivision12022` 

116 """ 

117 

118 F: float 

119 c: float 

120 N_c: float 

121 

122 

123VIEWING_CONDITIONS_CIECAM16: CanonicalMapping = CanonicalMapping( 

124 VIEWING_CONDITIONS_CIECAM02 

125) 

126VIEWING_CONDITIONS_CIECAM16.__doc__ = """ 

127Define the reference *CIECAM16* colour appearance model viewing conditions. 

128 

129References 

130---------- 

131:cite:`CIEDivision12022` 

132""" 

133 

134 

135@dataclass 

136class CAM_Specification_CIECAM16(MixinDataclassArithmetic): 

137 """ 

138 Define the *CIECAM16* colour appearance model specification. 

139 

140 Parameters 

141 ---------- 

142 J 

143 Correlate of *lightness* :math:`J`. 

144 C 

145 Correlate of *chroma* :math:`C`. 

146 h 

147 *Hue* angle :math:`h` in degrees. 

148 s 

149 Correlate of *saturation* :math:`s`. 

150 Q 

151 Correlate of *brightness* :math:`Q`. 

152 M 

153 Correlate of *colourfulness* :math:`M`. 

154 H 

155 *Hue* :math:`h` quadrature :math:`H`. 

156 HC 

157 *Hue* :math:`h` composition :math:`H^C`. 

158 

159 References 

160 ---------- 

161 :cite:`CIEDivision12022` 

162 """ 

163 

164 J: float | NDArrayFloat | None = field(default_factory=lambda: None) 

165 C: float | NDArrayFloat | None = field(default_factory=lambda: None) 

166 h: float | NDArrayFloat | None = field(default_factory=lambda: None) 

167 s: float | NDArrayFloat | None = field(default_factory=lambda: None) 

168 Q: float | NDArrayFloat | None = field(default_factory=lambda: None) 

169 M: float | NDArrayFloat | None = field(default_factory=lambda: None) 

170 H: float | NDArrayFloat | None = field(default_factory=lambda: None) 

171 HC: float | NDArrayFloat | None = field(default_factory=lambda: None) 

172 

173 

174def XYZ_to_CIECAM16( 

175 XYZ: Domain100, 

176 XYZ_w: Domain100, 

177 L_A: ArrayLike, 

178 Y_b: ArrayLike, 

179 surround: ( 

180 InductionFactors_CIECAM02 | InductionFactors_CIECAM16 

181 ) = VIEWING_CONDITIONS_CIECAM16["Average"], 

182 discount_illuminant: bool = False, 

183 compute_H: bool = True, 

184) -> Annotated[CAM_Specification_CIECAM16, (100, 100, 360, 100, 100, 100, 400)]: 

185 """ 

186 Compute the *CIECAM16* colour appearance model correlates from the 

187 specified *CIE XYZ* tristimulus values. 

188 

189 Parameters 

190 ---------- 

191 XYZ 

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

193 XYZ_w 

194 *CIE XYZ* tristimulus values of reference white. 

195 L_A 

196 Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`, (often 

197 taken to be 20% of the luminance of a white object in the scene). 

198 Y_b 

199 Luminous factor of background :math:`Y_b` such as 

200 :math:`Y_b = 100 \\times L_b / L_w` where :math:`L_w` is the 

201 luminance of the light source and :math:`L_b` is the luminance of 

202 the background. For viewing images, :math:`Y_b` can be the average 

203 :math:`Y` value for the pixels in the entire image, or frequently, 

204 a :math:`Y` value of 20, approximate an :math:`L^*` of 50 is used. 

205 surround 

206 Surround viewing conditions induction factors. 

207 discount_illuminant 

208 Truth value indicating if the illuminant should be discounted. 

209 compute_H 

210 Whether to compute *Hue* :math:`h` quadrature :math:`H`. 

211 :math:`H` is rarely used, and expensive to compute. 

212 

213 Returns 

214 ------- 

215 :class:`colour.CAM_Specification_CIECAM16` 

216 *CIECAM16* colour appearance model specification. 

217 

218 Notes 

219 ----- 

220 +---------------------+-----------------------+---------------+ 

221 | **Domain** | **Scale - Reference** | **Scale - 1** | 

222 +=====================+=======================+===============+ 

223 | ``XYZ`` | 100 | 1 | 

224 +---------------------+-----------------------+---------------+ 

225 | ``XYZ_w`` | 100 | 1 | 

226 +---------------------+-----------------------+---------------+ 

227 

228 +---------------------+-----------------------+---------------+ 

229 | **Range** | **Scale - Reference** | **Scale - 1** | 

230 +=====================+=======================+===============+ 

231 | ``specification.J`` | 100 | 1 | 

232 +---------------------+-----------------------+---------------+ 

233 | ``specification.C`` | 100 | 1 | 

234 +---------------------+-----------------------+---------------+ 

235 | ``specification.h`` | 360 | 1 | 

236 +---------------------+-----------------------+---------------+ 

237 | ``specification.s`` | 100 | 1 | 

238 +---------------------+-----------------------+---------------+ 

239 | ``specification.Q`` | 100 | 1 | 

240 +---------------------+-----------------------+---------------+ 

241 | ``specification.M`` | 100 | 1 | 

242 +---------------------+-----------------------+---------------+ 

243 | ``specification.H`` | 400 | 1 | 

244 +---------------------+-----------------------+---------------+ 

245 

246 References 

247 ---------- 

248 :cite:`CIEDivision12022` 

249 

250 Examples 

251 -------- 

252 >>> XYZ = np.array([19.01, 20.00, 21.78]) 

253 >>> XYZ_w = np.array([95.05, 100.00, 108.88]) 

254 >>> L_A = 318.31 

255 >>> Y_b = 20.0 

256 >>> surround = VIEWING_CONDITIONS_CIECAM16["Average"] 

257 >>> XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround) # doctest: +ELLIPSIS 

258 CAM_Specification_CIECAM16(J=41.7312079..., C=0.1033557..., \ 

259h=217.0679597..., s=2.3450150..., Q=195.3717089..., M=0.1074367..., \ 

260H=275.5949861..., HC=None) 

261 """ 

262 

263 XYZ = to_domain_100(XYZ) 

264 XYZ_w = to_domain_100(XYZ_w) 

265 _X_w, Y_w, _Z_w = tsplit(XYZ_w) 

266 L_A = as_float_array(L_A) 

267 Y_b = as_float_array(Y_b) 

268 

269 # Step 0 

270 # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. 

271 RGB_w = vecmul(MATRIX_16, XYZ_w) 

272 

273 # Computing degree of adaptation :math:`D`. 

274 D = ( 

275 np.clip(degree_of_adaptation(surround.F, L_A), 0, 1) 

276 if not discount_illuminant 

277 else ones(L_A.shape) 

278 ) 

279 

280 n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) 

281 

282 D_RGB = D[..., None] * 100 / RGB_w + 1 - D[..., None] 

283 RGB_wc = D_RGB * RGB_w 

284 

285 # Applying forward post-adaptation non-linear response compression. 

286 RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L) 

287 

288 # Computing achromatic responses for the whitepoint. 

289 A_w = achromatic_response_forward(RGB_aw, N_bb) 

290 

291 # Step 1 

292 # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. 

293 RGB = vecmul(MATRIX_16, XYZ) 

294 

295 # Step 2 

296 RGB_c = D_RGB * RGB 

297 

298 # Step 3 

299 # Applying forward post-adaptation non-linear response compression. 

300 RGB_a = f_e_forward(RGB_c, F_L) + 0.1 

301 

302 # Step 4 

303 # Converting to preliminary cartesian coordinates. 

304 a, b = tsplit(opponent_colour_dimensions_forward(RGB_a)) 

305 

306 # Computing the *hue* angle :math:`h`. 

307 h = hue_angle(a, b) 

308 

309 # Step 5 

310 # Computing eccentricity factor *e_t*. 

311 e_t = eccentricity_factor(h) 

312 

313 # Computing hue :math:`h` quadrature :math:`H`. 

314 H = hue_quadrature(h) if compute_H else np.full(h.shape, np.nan) 

315 # TODO: Compute hue composition. 

316 

317 # Step 6 

318 # Computing achromatic responses for the stimulus. 

319 A = achromatic_response_forward(RGB_a, N_bb) 

320 

321 # Step 7 

322 # Computing the correlate of *Lightness* :math:`J`. 

323 J = lightness_correlate(A, A_w, surround.c, z) 

324 

325 # Step 8 

326 # Computing the correlate of *brightness* :math:`Q`. 

327 Q = brightness_correlate(surround.c, J, A_w, F_L) 

328 

329 # Step 9 

330 # Computing the correlate of *chroma* :math:`C`. 

331 C = chroma_correlate(J, n, surround.N_c, N_cb, e_t, a, b, RGB_a) 

332 

333 # Computing the correlate of *colourfulness* :math:`M`. 

334 M = colourfulness_correlate(C, F_L) 

335 

336 # Computing the correlate of *saturation* :math:`s`. 

337 s = saturation_correlate(M, Q) 

338 

339 return CAM_Specification_CIECAM16( 

340 J=as_float(from_range_100(J)), 

341 C=as_float(from_range_100(C)), 

342 h=as_float(from_range_degrees(h)), 

343 s=as_float(from_range_100(s)), 

344 Q=as_float(from_range_100(Q)), 

345 M=as_float(from_range_100(M)), 

346 H=as_float(from_range_degrees(H, 400)), 

347 HC=None, 

348 ) 

349 

350 

351def CIECAM16_to_XYZ( 

352 specification: Annotated[ 

353 CAM_Specification_CIECAM16, (100, 100, 360, 100, 100, 100, 400) 

354 ], 

355 XYZ_w: Domain100, 

356 L_A: ArrayLike, 

357 Y_b: ArrayLike, 

358 surround: ( 

359 InductionFactors_CIECAM02 | InductionFactors_CIECAM16 

360 ) = VIEWING_CONDITIONS_CIECAM16["Average"], 

361 discount_illuminant: bool = False, 

362) -> Range100: 

363 """ 

364 Convert the *CIECAM16* colour appearance model specification to *CIE XYZ* 

365 tristimulus values. 

366 

367 Parameters 

368 ---------- 

369 specification 

370 *CIECAM16* colour appearance model specification. Correlate of 

371 *lightness* :math:`J`, correlate of *chroma* :math:`C` or correlate of 

372 *colourfulness* :math:`M` and *hue* angle :math:`h` in degrees must be 

373 specified, e.g., :math:`JCh` or :math:`JMh`. 

374 XYZ_w 

375 *CIE XYZ* tristimulus values of reference white. 

376 L_A 

377 Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`, (often taken 

378 to be 20% of the luminance of a white object in the scene). 

379 Y_b 

380 Luminous factor of background :math:`Y_b` such as 

381 :math:`Y_b = 100 \\times L_b / L_w` where :math:`L_w` is the luminance 

382 of the light source and :math:`L_b` is the luminance of the background. 

383 For viewing images, :math:`Y_b` can be the average :math:`Y` value for 

384 the pixels in the entire image, or frequently, a :math:`Y` value of 20, 

385 approximating an :math:`L^*` of 50 is used. 

386 surround 

387 Surround viewing conditions. 

388 discount_illuminant 

389 Discount the illuminant. 

390 

391 Returns 

392 ------- 

393 :class:`numpy.ndarray` 

394 *CIE XYZ* tristimulus values. 

395 

396 Raises 

397 ------ 

398 ValueError 

399 If neither :math:`C` or :math:`M` correlates have been defined in the 

400 ``specification`` argument. 

401 

402 Notes 

403 ----- 

404 +---------------------+-----------------------+---------------+ 

405 | **Domain** | **Scale - Reference** | **Scale - 1** | 

406 +=====================+=======================+===============+ 

407 | ``specification.J`` | 100 | 1 | 

408 +---------------------+-----------------------+---------------+ 

409 | ``specification.C`` | 100 | 1 | 

410 +---------------------+-----------------------+---------------+ 

411 | ``specification.h`` | 360 | 1 | 

412 +---------------------+-----------------------+---------------+ 

413 | ``specification.s`` | 100 | 1 | 

414 +---------------------+-----------------------+---------------+ 

415 | ``specification.Q`` | 100 | 1 | 

416 +---------------------+-----------------------+---------------+ 

417 | ``specification.M`` | 100 | 1 | 

418 +---------------------+-----------------------+---------------+ 

419 | ``specification.H`` | 360 | 1 | 

420 +---------------------+-----------------------+---------------+ 

421 | ``XYZ_w`` | 100 | 1 | 

422 +---------------------+-----------------------+---------------+ 

423 

424 +---------------------+-----------------------+---------------+ 

425 | **Range** | **Scale - Reference** | **Scale - 1** | 

426 +=====================+=======================+===============+ 

427 | ``XYZ`` | 100 | 1 | 

428 +---------------------+-----------------------+---------------+ 

429 

430 References 

431 ---------- 

432 :cite:`CIEDivision12022` 

433 

434 Examples 

435 -------- 

436 >>> specification = CAM_Specification_CIECAM16( 

437 ... J=41.731207905126638, C=0.103355738709070, h=217.067959767393010 

438 ... ) 

439 >>> XYZ_w = np.array([95.05, 100.00, 108.88]) 

440 >>> L_A = 318.31 

441 >>> Y_b = 20.0 

442 >>> CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b) # doctest: +ELLIPSIS 

443 array([ 19.01..., 20... , 21.78...]) 

444 """ 

445 

446 J, C, h, _s, _Q, M, _H, _HC = astuple(specification) 

447 

448 J = to_domain_100(J) 

449 C = to_domain_100(C) 

450 h = to_domain_degrees(h) 

451 M = to_domain_100(M) 

452 L_A = as_float_array(L_A) 

453 XYZ_w = to_domain_100(XYZ_w) 

454 _X_w, Y_w, _Z_w = tsplit(XYZ_w) 

455 

456 # Step 0 

457 # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. 

458 RGB_w = vecmul(MATRIX_16, XYZ_w) 

459 

460 # Computing degree of adaptation :math:`D`. 

461 D = ( 

462 np.clip(degree_of_adaptation(surround.F, L_A), 0, 1) 

463 if not discount_illuminant 

464 else ones(L_A.shape) 

465 ) 

466 

467 n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) 

468 

469 D_RGB = D[..., None] * 100 / RGB_w + 1 - D[..., None] 

470 RGB_wc = D_RGB * RGB_w 

471 

472 # Applying forward post-adaptation non-linear response compression. 

473 RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L) 

474 

475 # Computing achromatic responses for the whitepoint. 

476 A_w = achromatic_response_forward(RGB_aw, N_bb) 

477 

478 # Step 1 

479 if has_only_nan(C) and not has_only_nan(M): 

480 C = M / spow(F_L, 0.25) 

481 elif has_only_nan(C): 

482 error = ( 

483 'Either "C" or "M" correlate must be defined in ' 

484 'the "CAM_Specification_CIECAM16" argument!' 

485 ) 

486 

487 raise ValueError(error) 

488 

489 # Step 2 

490 # Computing temporary magnitude quantity :math:`t`. 

491 t = temporary_magnitude_quantity_inverse(C, J, n) 

492 

493 # Computing eccentricity factor *e_t*. 

494 e_t = eccentricity_factor(h) 

495 

496 # Computing achromatic response :math:`A` for the stimulus. 

497 A = achromatic_response_inverse(A_w, J, surround.c, z) 

498 

499 # Computing *P_1* to *P_3*. 

500 P_n = P(surround.N_c, N_cb, e_t, t, A, N_bb) 

501 _P_1, P_2, _P_3 = tsplit(P_n) 

502 

503 # Step 3 

504 # Computing opponent colour dimensions :math:`a` and :math:`b`. 

505 ab = opponent_colour_dimensions_inverse(P_n, h) 

506 a, b = tsplit(ab) * np.where(t == 0, 0, 1) 

507 

508 # Step 4 

509 # Applying post-adaptation non-linear response compression matrix. 

510 RGB_a = matrix_post_adaptation_non_linear_response_compression(P_2, a, b) 

511 

512 # Step 5 

513 # Applying inverse post-adaptation non-linear response compression. 

514 RGB_c = f_e_inverse(RGB_a - 0.1, F_L) 

515 

516 # Step 6 

517 RGB = RGB_c / D_RGB 

518 

519 # Step 7 

520 XYZ = vecmul(MATRIX_INVERSE_16, RGB) 

521 

522 return from_range_100(XYZ) 

523 

524 

525def f_e_forward(RGB_c: ArrayLike, F_L: ArrayLike) -> NDArrayFloat: 

526 """ 

527 Compute the post-adaptation cone responses. 

528 

529 Parameters 

530 ---------- 

531 RGB_c 

532 *CMCCAT2000* transform sharpened :math:`RGB_c` array. 

533 F_L 

534 *Luminance* level adaptation factor :math:`F_L`. 

535 

536 Returns 

537 ------- 

538 :class:`numpy.ndarray` 

539 Compressed *CMCCAT2000* transform sharpened :math:`RGB_a` array. 

540 

541 Notes 

542 ----- 

543 - This definition is different from :cite:`Li2017` and provides linear 

544 extensions under 0.26 and above 150. It also omits the 0.1 offset 

545 that is now part of the general model. 

546 

547 Examples 

548 -------- 

549 >>> RGB_c = np.array([19.99693975, 20.00186123, 20.01350530]) 

550 >>> F_L = 1.16754446415 

551 >>> f_e_forward(RGB_c, F_L) 

552 ... # doctest: +ELLIPSIS 

553 array([ 7.8463202..., 7.8471152..., 7.8489959...]) 

554 """ 

555 

556 RGB_c = as_float_array(RGB_c) 

557 F_L = as_float_array(F_L) 

558 q_L, q_U = 0.26, 150 

559 

560 f_q_F_L_q_U = f_q(F_L, q_U)[..., None] 

561 f_q_F_L_q_L = f_q(F_L, q_L)[..., None] 

562 f_q_F_L_RGB_c = f_q(F_L[..., None], RGB_c) 

563 d_f_q_F_L_q_U = d_f_q(F_L, q_U)[..., None] 

564 

565 return np.select( 

566 [ 

567 RGB_c > q_U, 

568 np.logical_and(q_L <= RGB_c, RGB_c <= q_U), 

569 RGB_c < q_L, 

570 ], 

571 [ 

572 f_q_F_L_q_U + d_f_q_F_L_q_U * (RGB_c - q_U), 

573 f_q_F_L_RGB_c, 

574 f_q_F_L_q_L * RGB_c / q_L, 

575 ], 

576 ) 

577 

578 

579def f_e_inverse(RGB_a: ArrayLike, F_L: ArrayLike) -> NDArrayFloat: 

580 """ 

581 Compute the inverse of the forward eccentricity factor modified cone-like 

582 responses. 

583 

584 Parameters 

585 ---------- 

586 RGB_a 

587 *CMCCAT2000* transform sharpened :math:`RGB_a` array. 

588 F_L 

589 *Luminance* level adaptation factor :math:`F_L`. 

590 

591 Returns 

592 ------- 

593 :class:`numpy.ndarray` 

594 Compressed *CMCCAT2000* transform sharpened :math:`RGB_c` array. 

595 

596 Notes 

597 ----- 

598 - This definition is different from :cite:`Li2017` and provides linear 

599 extensions under 0.26 and above 150. It also omits the 0.1 offset 

600 that is now part of the general model. 

601 

602 Examples 

603 -------- 

604 >>> RGB_a = np.array([7.8463202, 7.84711528, 7.84899595]) 

605 >>> F_L = 1.16754446415 

606 >>> f_e_inverse(RGB_a, F_L) 

607 ... # doctest: +ELLIPSIS 

608 array([ 19.9969397..., 20.0018612..., 20.0135052...]) 

609 """ 

610 

611 RGB_a = as_float_array(RGB_a) 

612 F_L = as_float_array(F_L) 

613 q_L, q_U = 0.26, 150 

614 

615 f_q_F_L_q_U = f_q(F_L, q_U)[..., None] 

616 f_q_F_L_q_L = f_q(F_L, q_L)[..., None] 

617 d_f_q_F_L_q_U = d_f_q(F_L, q_U)[..., None] 

618 

619 return np.select( 

620 [ 

621 RGB_a > f_q_F_L_q_U, 

622 np.logical_and(f_q_F_L_q_L <= RGB_a, RGB_a <= f_q_F_L_q_U), 

623 RGB_a < f_q_F_L_q_L, 

624 ], 

625 [ 

626 q_U + (RGB_a - f_q_F_L_q_U) / d_f_q_F_L_q_U, 

627 100 / F_L[..., None] * spow((27.13 * RGB_a) / (400 - RGB_a), 1 / 0.42), 

628 q_L * RGB_a / f_q_F_L_q_L, 

629 ], 

630 ) 

631 

632 

633def f_q(F_L: ArrayLike, q: ArrayLike) -> NDArrayFloat: 

634 """ 

635 Evaluate the :math:`f(q)` function for chromatic adaptation. 

636 

637 Parameters 

638 ---------- 

639 F_L 

640 *Luminance* level adaptation factor :math:`F_L`. 

641 q 

642 :math:`q` parameter. 

643 

644 Returns 

645 ------- 

646 :class:`numpy.ndarray` 

647 Evaluated :math:`f(q)` function result. 

648 

649 Examples 

650 -------- 

651 >>> f_q(1.17, 0.26) # doctest: +ELLIPSIS 

652 1.2886520... 

653 """ 

654 

655 F_L = as_float_array(F_L) 

656 q = as_float_array(q) 

657 

658 F_L_q_100 = spow((F_L * q) / 100, 0.42) 

659 

660 return (400 * F_L_q_100) / (27.13 + F_L_q_100) 

661 

662 

663def d_f_q(F_L: ArrayLike, q: ArrayLike) -> NDArrayFloat: 

664 """ 

665 Compute the :math:`f'(q)` function derivative. 

666 

667 Parameters 

668 ---------- 

669 F_L 

670 *Luminance* level adaptation factor :math:`F_L`. 

671 q 

672 :math:`q` parameter. 

673 

674 Returns 

675 ------- 

676 :class:`numpy.ndarray` 

677 Evaluated :math:`f'(q)` function derivative. 

678 

679 Examples 

680 -------- 

681 >>> d_f_q(1.17, 0.26) # doctest: +ELLIPSIS 

682 2.0749623... 

683 """ 

684 

685 F_L = as_float_array(F_L) 

686 q = as_float_array(q) 

687 

688 F_L_q_100 = (F_L * q) / 100 

689 

690 return (1.68 * 27.13 * F_L * spow(F_L_q_100, -0.58)) / ( 

691 27.13 + spow(F_L_q_100, 0.42) 

692 ) ** 2