Coverage for colour/appearance/nayatani95.py: 93%

183 statements  

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

1""" 

2Nayatani (1995) Colour Appearance Model 

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

4 

5Define the *Nayatani (1995)* colour appearance model for predicting 

6perceptual colour attributes under varying viewing conditions. 

7 

8- :class:`colour.CAM_Specification_Nayatani95` 

9- :func:`colour.XYZ_to_Nayatani95` 

10 

11References 

12---------- 

13- :cite:`Fairchild2013ba` : Fairchild, M. D. (2013). The Nayatani et al. 

14 Model. In Color Appearance Models (3rd ed., pp. 4810-5085). Wiley. 

15 ISBN:B00DAYO8E2 

16- :cite:`Nayatani1995a` : Nayatani, Y., Sobagaki, H., & Yano, K. H. T. 

17 (1995). Lightness dependency of chroma scales of a nonlinear 

18 color-appearance model and its latest formulation. Color Research & 

19 Application, 20(3), 156-167. doi:10.1002/col.5080200305 

20""" 

21 

22from __future__ import annotations 

23 

24import typing 

25from dataclasses import dataclass, field 

26 

27import numpy as np 

28 

29from colour.adaptation.cie1994 import ( 

30 MATRIX_XYZ_TO_RGB_CIE1994, 

31 beta_1, 

32 exponential_factors, 

33 intermediate_values, 

34) 

35from colour.algebra import spow, vecmul 

36 

37if typing.TYPE_CHECKING: 

38 from colour.hints import ArrayLike, Domain100 

39 

40from colour.hints import Annotated, NDArrayFloat, cast 

41from colour.models import XYZ_to_xy 

42from colour.utilities import ( 

43 MixinDataclassArithmetic, 

44 as_float, 

45 as_float_array, 

46 from_range_degrees, 

47 to_domain_100, 

48 tsplit, 

49 tstack, 

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

61 "CAM_ReferenceSpecification_Nayatani95", 

62 "CAM_Specification_Nayatani95", 

63 "XYZ_to_Nayatani95", 

64 "illuminance_to_luminance", 

65 "XYZ_to_RGB_Nayatani95", 

66 "scaling_coefficient", 

67 "achromatic_response", 

68 "tritanopic_response", 

69 "protanopic_response", 

70 "brightness_correlate", 

71 "ideal_white_brightness_correlate", 

72 "achromatic_lightness_correlate", 

73 "normalised_achromatic_lightness_correlate", 

74 "hue_angle", 

75 "saturation_components", 

76 "saturation_correlate", 

77 "chroma_components", 

78 "chroma_correlate", 

79 "colourfulness_components", 

80 "colourfulness_correlate", 

81 "chromatic_strength_function", 

82] 

83 

84MATRIX_XYZ_TO_RGB_NAYATANI95: NDArrayFloat = MATRIX_XYZ_TO_RGB_CIE1994 

85""" 

86*Nayatani (1995)* colour appearance model *CIE XYZ* tristimulus values to cone 

87responses matrix. 

88""" 

89 

90 

91@dataclass 

92class CAM_ReferenceSpecification_Nayatani95(MixinDataclassArithmetic): 

93 """ 

94 Define the *Nayatani (1995)* colour appearance model reference 

95 specification. 

96 

97 This specification contains field names consistent with the *Fairchild 

98 (2013)* reference. 

99 

100 Parameters 

101 ---------- 

102 L_star_P 

103 Correlate of *achromatic lightness* :math:`L_p^\\star`. 

104 C 

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

106 theta 

107 *Hue* angle :math:`\\theta` in degrees. 

108 S 

109 Correlate of *saturation* :math:`S`. 

110 B_r 

111 Correlate of *brightness* :math:`B_r`. 

112 M 

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

114 H 

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

116 H_C 

117 *Hue* :math:`h` composition :math:`H_C`. 

118 L_star_N 

119 Correlate of *normalised achromatic lightness* :math:`L_n^\\star`. 

120 

121 References 

122 ---------- 

123 :cite:`Fairchild2013ba`, :cite:`Nayatani1995a` 

124 """ 

125 

126 L_star_P: float | NDArrayFloat | None = field(default_factory=lambda: None) 

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

128 theta: float | NDArrayFloat | None = field(default_factory=lambda: None) 

129 S: float | NDArrayFloat | None = field(default_factory=lambda: None) 

130 B_r: float | NDArrayFloat | None = field(default_factory=lambda: None) 

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

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

133 H_C: float | NDArrayFloat | None = field(default_factory=lambda: None) 

134 L_star_N: float | NDArrayFloat | None = field(default_factory=lambda: None) 

135 

136 

137@dataclass 

138class CAM_Specification_Nayatani95(MixinDataclassArithmetic): 

139 """ 

140 Define the *Nayatani (1995)* colour appearance model specification. 

141 

142 This specification provides a standardized interface for the 

143 *Nayatani (1995)* model with field names consistent across all colour 

144 appearance models in :mod:`colour.appearance`. While the field names differ 

145 from the original *Fairchild (2013)* reference notation, they map directly 

146 to the model's perceptual correlates. 

147 

148 Parameters 

149 ---------- 

150 L_star_P 

151 Correlate of *achromatic lightness* :math:`L_p^\\star`. 

152 C 

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

154 h 

155 *Hue* angle :math:`\\theta` in degrees. 

156 s 

157 Correlate of *saturation* :math:`S`. 

158 Q 

159 Correlate of *brightness* :math:`B_r`. 

160 M 

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

162 H 

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

164 HC 

165 *Hue* :math:`h` composition :math:`H_C`. 

166 L_star_N 

167 Correlate of *normalised achromatic lightness* :math:`L_n^\\star`. 

168 

169 Notes 

170 ----- 

171 - This specification is the one used in the current model 

172 implementation. 

173 

174 References 

175 ---------- 

176 :cite:`Fairchild2013ba`, :cite:`Nayatani1995a` 

177 """ 

178 

179 L_star_P: float | NDArrayFloat | None = field(default_factory=lambda: None) 

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

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

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

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

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

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

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

187 L_star_N: float | NDArrayFloat | None = field(default_factory=lambda: None) 

188 

189 

190def XYZ_to_Nayatani95( 

191 XYZ: Domain100, 

192 XYZ_n: Domain100, 

193 Y_o: ArrayLike, 

194 E_o: ArrayLike, 

195 E_or: ArrayLike, 

196 n: ArrayLike = 1, 

197) -> Annotated[CAM_Specification_Nayatani95, 360]: 

198 """ 

199 Compute the *Nayatani (1995)* colour appearance model correlates from the 

200 specified *CIE XYZ* tristimulus values. 

201 

202 Parameters 

203 ---------- 

204 XYZ 

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

206 XYZ_n 

207 *CIE XYZ* tristimulus values of reference white. 

208 Y_o 

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

210 normalised to domain [0.18, 1.0] in **'Reference'** domain-range 

211 scale. 

212 E_o 

213 Illuminance :math:`E_o` of the viewing field in lux. 

214 E_or 

215 Normalising illuminance :math:`E_{or}` in lux usually normalised to 

216 domain [1000, 3000]. 

217 n 

218 Noise term used in the non-linear chromatic adaptation model. 

219 

220 Returns 

221 ------- 

222 :class:`colour.CAM_Specification_Nayatani95` 

223 *Nayatani (1995)* colour appearance model specification. 

224 

225 Notes 

226 ----- 

227 +---------------------+-----------------------+---------------+ 

228 | **Domain** | **Scale - Reference** | **Scale - 1** | 

229 +=====================+=======================+===============+ 

230 | ``XYZ`` | 100 | 1 | 

231 +---------------------+-----------------------+---------------+ 

232 | ``XYZ_n`` | 100 | 1 | 

233 +---------------------+-----------------------+---------------+ 

234 

235 +---------------------+-----------------------+---------------+ 

236 | **Range** | **Scale - Reference** | **Scale - 1** | 

237 +=====================+=======================+===============+ 

238 | ``specification.h`` | 360 | 1 | 

239 +---------------------+-----------------------+---------------+ 

240 

241 References 

242 ---------- 

243 :cite:`Fairchild2013ba`, :cite:`Nayatani1995a` 

244 

245 Examples 

246 -------- 

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

248 >>> XYZ_n = np.array([95.05, 100.00, 108.88]) 

249 >>> Y_o = 20.0 

250 >>> E_o = 5000.0 

251 >>> E_or = 1000.0 

252 >>> XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or) # doctest: +ELLIPSIS 

253 CAM_Specification_Nayatani95(L_star_P=49.9998829..., C=0.0133550..., \ 

254h=257.5232268..., s=0.0133550..., Q=62.6266734..., M=0.0167262..., \ 

255H=None, HC=None, L_star_N=50.0039154...) 

256 """ 

257 

258 XYZ = to_domain_100(XYZ) 

259 XYZ_n = to_domain_100(XYZ_n) 

260 Y_o = as_float_array(Y_o) 

261 E_o = as_float_array(E_o) 

262 E_or = as_float_array(E_or) 

263 

264 # Computing adapting luminance :math:`L_o` and normalising luminance 

265 # :math:`L_{or}` in in :math:`cd/m^2`. 

266 # L_o = illuminance_to_luminance(E_o, Y_o) 

267 L_or = illuminance_to_luminance(E_or, Y_o) 

268 

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

270 xez = intermediate_values(XYZ_to_xy(XYZ_n / 100)) 

271 xi, eta, _zeta = tsplit(xez) 

272 

273 # Computing adapting field cone responses. 

274 RGB_o = ((Y_o[..., None] * E_o[..., None]) / (100 * np.pi)) * xez 

275 

276 # Computing stimulus cone responses. 

277 RGB = XYZ_to_RGB_Nayatani95(XYZ) 

278 R, G, _B = tsplit(RGB) 

279 

280 # Computing exponential factors of the chromatic adaptation. 

281 bRGB_o = exponential_factors(RGB_o) 

282 bL_or = beta_1(L_or) 

283 

284 # Computing scaling coefficients :math:`e(R)` and :math:`e(G)` 

285 eR = scaling_coefficient(R, xi) 

286 eG = scaling_coefficient(G, eta) 

287 

288 # Computing opponent colour dimensions. 

289 # Computing achromatic response :math:`Q`: 

290 Q_response = achromatic_response(RGB, bRGB_o, xez, bL_or, eR, eG, n) 

291 

292 # Computing tritanopic response :math:`t`: 

293 t_response = tritanopic_response(RGB, bRGB_o, xez, n) 

294 

295 # Computing protanopic response :math:`p`: 

296 p_response = protanopic_response(RGB, bRGB_o, xez, n) 

297 

298 # Computing the correlate of *brightness* :math:`B_r`. 

299 B_r = brightness_correlate(bRGB_o, bL_or, Q_response) 

300 

301 # Computing *brightness* :math:`B_{rw}` of ideal white. 

302 brightness_ideal_white = ideal_white_brightness_correlate(bRGB_o, xez, bL_or, n) 

303 

304 # Computing the correlate of achromatic *Lightness* :math:`L_p^\\star`. 

305 L_star_P = achromatic_lightness_correlate(Q_response) 

306 

307 # Computing the correlate of normalised achromatic *Lightness* 

308 # :math:`L_n^\\star`. 

309 L_star_N = normalised_achromatic_lightness_correlate(B_r, brightness_ideal_white) 

310 

311 # Computing the *hue* angle :math:`\\theta`. 

312 theta = hue_angle(p_response, t_response) 

313 # TODO: Implement hue quadrature & composition computation. 

314 

315 # Computing the correlate of *saturation* :math:`S`. 

316 S_RG, S_YB = tsplit(saturation_components(theta, bL_or, t_response, p_response)) 

317 S = saturation_correlate(S_RG, S_YB) 

318 

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

320 # C_RG, C_YB = tsplit(chroma_components(L_star_P, S_RG, S_YB)) 

321 C = chroma_correlate(L_star_P, S) 

322 

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

324 # TODO: Investigate components usage. 

325 # M_RG, M_YB = tsplit(colourfulness_components(C_RG, C_YB, 

326 # brightness_ideal_white)) 

327 M = colourfulness_correlate(C, brightness_ideal_white) 

328 

329 return CAM_Specification_Nayatani95( 

330 L_star_P=L_star_P, 

331 C=C, 

332 h=as_float(from_range_degrees(theta)), 

333 s=S, 

334 Q=B_r, 

335 M=M, 

336 H=None, 

337 HC=None, 

338 L_star_N=L_star_N, 

339 ) 

340 

341 

342def illuminance_to_luminance(E: ArrayLike, Y_f: ArrayLike) -> NDArrayFloat: 

343 """ 

344 Convert the specified *illuminance* :math:`E` value in lux to *luminance* 

345 :math:`Y` in :math:`cd/m^2`. 

346 

347 Parameters 

348 ---------- 

349 E 

350 *Illuminance* :math:`E` in lux. 

351 Y_f 

352 *Luminance* factor :math:`Y_f` in :math:`cd/m^2`. 

353 

354 Returns 

355 ------- 

356 :class:`numpy.ndarray` 

357 *Luminance* :math:`Y` in :math:`cd/m^2`. 

358 

359 Examples 

360 -------- 

361 >>> illuminance_to_luminance(5000.0, 20.0) # doctest: +ELLIPSIS 

362 318.3098861... 

363 """ 

364 

365 E = as_float_array(E) 

366 Y_f = as_float_array(Y_f) 

367 

368 return Y_f * E / (100 * np.pi) 

369 

370 

371def XYZ_to_RGB_Nayatani95(XYZ: ArrayLike) -> NDArrayFloat: 

372 """ 

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

374 

375 Parameters 

376 ---------- 

377 XYZ 

378 *CIE XYZ* tristimulus values. 

379 

380 Returns 

381 ------- 

382 :class:`numpy.ndarray` 

383 Cone responses. 

384 

385 Examples 

386 -------- 

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

388 >>> XYZ_to_RGB_Nayatani95(XYZ) # doctest: +ELLIPSIS 

389 array([ 20.0005206..., 19.999783 ..., 19.9988316...]) 

390 """ 

391 

392 return vecmul(MATRIX_XYZ_TO_RGB_NAYATANI95, XYZ) 

393 

394 

395def scaling_coefficient(x: ArrayLike, y: ArrayLike) -> NDArrayFloat: 

396 """ 

397 Compute the scaling coefficient :math:`e(R)` or :math:`e(G)`. 

398 

399 Parameters 

400 ---------- 

401 x 

402 Cone response. 

403 y 

404 Intermediate value. 

405 

406 Returns 

407 ------- 

408 :class:`numpy.ndarray` 

409 Scaling coefficient :math:`e(R)` or :math:`e(G)`. 

410 

411 Examples 

412 -------- 

413 >>> x = 20.000520600000002 

414 >>> y = 1.000042192 

415 >>> scaling_coefficient(x, y) 

416 1.0 

417 """ 

418 

419 x = as_float_array(x) 

420 y = as_float_array(y) 

421 

422 return as_float(np.where(x >= (20 * y), 1.758, 1)) 

423 

424 

425def achromatic_response( 

426 RGB: ArrayLike, 

427 bRGB_o: ArrayLike, 

428 xez: ArrayLike, 

429 bL_or: ArrayLike, 

430 eR: ArrayLike, 

431 eG: ArrayLike, 

432 n: ArrayLike = 1, 

433) -> NDArrayFloat: 

434 """ 

435 Compute the achromatic response :math:`Q` from the specified stimulus 

436 cone responses. 

437 

438 Parameters 

439 ---------- 

440 RGB 

441 Stimulus cone responses. 

442 bRGB_o 

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

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

445 xez 

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

447 bL_or 

448 Normalising chromatic adaptation exponential factor 

449 :math:`\\beta_1(B_{or})`. 

450 eR 

451 Scaling coefficient :math:`e(R)`. 

452 eG 

453 Scaling coefficient :math:`e(G)`. 

454 n 

455 Noise term used in the non-linear chromatic adaptation model. 

456 

457 Returns 

458 ------- 

459 :class:`numpy.ndarray` 

460 Achromatic response :math:`Q`. 

461 

462 Examples 

463 -------- 

464 >>> RGB = np.array([20.00052060, 19.99978300, 19.99883160]) 

465 >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) 

466 >>> xez = np.array([1.00004219, 0.99998001, 0.99975794]) 

467 >>> bL_or = 3.681021495604089 

468 >>> eR = 1.0 

469 >>> eG = 1.758 

470 >>> n = 1.0 

471 >>> achromatic_response(RGB, bRGB_o, xez, bL_or, eR, eG, n) 

472 ... # doctest: +ELLIPSIS 

473 -0.0001169... 

474 """ 

475 

476 R, G, _B = tsplit(RGB) 

477 bR_o, bG_o, _bB_o = tsplit(bRGB_o) 

478 xi, eta, _zeta = tsplit(xez) 

479 bL_or = as_float_array(bL_or) 

480 eR = as_float_array(eR) 

481 eG = as_float_array(eG) 

482 

483 Q = (2 / 3) * bR_o * eR * np.log10((R + n) / (20 * xi + n)) 

484 Q += (1 / 3) * bG_o * eG * np.log10((G + n) / (20 * eta + n)) 

485 Q *= 41.69 / bL_or 

486 

487 return as_float(Q) 

488 

489 

490def tritanopic_response( 

491 RGB: ArrayLike, bRGB_o: ArrayLike, xez: ArrayLike, n: ArrayLike 

492) -> NDArrayFloat: 

493 """ 

494 Compute the tritanopic response :math:`t` from the specified stimulus cone 

495 responses. 

496 

497 Parameters 

498 ---------- 

499 RGB 

500 Stimulus cone responses. 

501 bRGB_o 

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

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

504 xez 

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

506 n 

507 Noise term used in the non-linear chromatic adaptation model. 

508 

509 Returns 

510 ------- 

511 :class:`numpy.ndarray` 

512 Tritanopic response :math:`t`. 

513 

514 Examples 

515 -------- 

516 >>> RGB = np.array([20.00052060, 19.99978300, 19.99883160]) 

517 >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) 

518 >>> xez = np.array([1.00004219, 0.99998001, 0.99975794]) 

519 >>> n = 1.0 

520 >>> tritanopic_response(RGB, bRGB_o, xez, n) # doctest: +ELLIPSIS 

521 -1.7703650...e-05 

522 """ 

523 

524 R, G, B = tsplit(RGB) 

525 bR_o, bG_o, bB_o = tsplit(bRGB_o) 

526 xi, eta, zeta = tsplit(xez) 

527 

528 t = bR_o * np.log10((R + n) / (20 * xi + n)) 

529 t += -(12 / 11) * bG_o * np.log10((G + n) / (20 * eta + n)) 

530 t += (1 / 11) * bB_o * np.log10((B + n) / (20 * zeta + n)) 

531 

532 return as_float(t) 

533 

534 

535def protanopic_response( 

536 RGB: ArrayLike, bRGB_o: ArrayLike, xez: ArrayLike, n: ArrayLike 

537) -> NDArrayFloat: 

538 """ 

539 Compute the protanopic response :math:`p` from the specified stimulus cone 

540 responses. 

541 

542 Parameters 

543 ---------- 

544 RGB 

545 Stimulus cone responses. 

546 bRGB_o 

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

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

549 xez 

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

551 n 

552 Noise term used in the non-linear chromatic adaptation model. 

553 

554 Returns 

555 ------- 

556 :class:`numpy.ndarray` 

557 Protanopic response :math:`p`. 

558 

559 Examples 

560 -------- 

561 >>> RGB = np.array([20.00052060, 19.99978300, 19.99883160]) 

562 >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) 

563 >>> xez = np.array([1.00004219, 0.99998001, 0.99975794]) 

564 >>> n = 1.0 

565 >>> protanopic_response(RGB, bRGB_o, xez, n) # doctest: +ELLIPSIS 

566 -8.0021426...e-05 

567 """ 

568 

569 R, G, B = tsplit(RGB) 

570 bR_o, bG_o, bB_o = tsplit(bRGB_o) 

571 xi, eta, zeta = tsplit(xez) 

572 

573 p = (1 / 9) * bR_o * np.log10((R + n) / (20 * xi + n)) 

574 p += (1 / 9) * bG_o * np.log10((G + n) / (20 * eta + n)) 

575 p += -(2 / 9) * bB_o * np.log10((B + n) / (20 * zeta + n)) 

576 

577 return as_float(p) 

578 

579 

580def brightness_correlate( 

581 bRGB_o: ArrayLike, bL_or: ArrayLike, Q: ArrayLike 

582) -> NDArrayFloat: 

583 """ 

584 Compute the *brightness* correlate :math:`B_r`. 

585 

586 Parameters 

587 ---------- 

588 bRGB_o 

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

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

591 bL_or 

592 Normalising chromatic adaptation exponential factor 

593 :math:`\\beta_1(B_{or})`. 

594 Q 

595 Achromatic response :math:`Q`. 

596 

597 Returns 

598 ------- 

599 :class:`numpy.ndarray` 

600 *Brightness* correlate :math:`B_r`. 

601 

602 Examples 

603 -------- 

604 >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) 

605 >>> bL_or = 3.681021495604089 

606 >>> Q = -0.000117024294955 

607 >>> brightness_correlate(bRGB_o, bL_or, Q) # doctest: +ELLIPSIS 

608 62.6266734... 

609 """ 

610 

611 bR_o, bG_o, _bB_o = tsplit(bRGB_o) 

612 bL_or = as_float_array(bL_or) 

613 Q = as_float_array(Q) 

614 

615 B_r = (50 / bL_or) * ((2 / 3) * bR_o + (1 / 3) * bG_o) + Q 

616 

617 return as_float(B_r) 

618 

619 

620def ideal_white_brightness_correlate( 

621 bRGB_o: ArrayLike, 

622 xez: ArrayLike, 

623 bL_or: ArrayLike, 

624 n: ArrayLike, 

625) -> NDArrayFloat: 

626 """ 

627 Compute the ideal white *brightness* correlate :math:`B_{rw}`. 

628 

629 Parameters 

630 ---------- 

631 bRGB_o 

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

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

634 xez 

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

636 bL_or 

637 Normalising chromatic adaptation exponential factor 

638 :math:`\\beta_1(B_{or})`. 

639 n 

640 Noise term used in the non-linear chromatic adaptation model. 

641 

642 Returns 

643 ------- 

644 :class:`numpy.ndarray` 

645 Ideal white *brightness* correlate :math:`B_{rw}`. 

646 

647 Examples 

648 -------- 

649 >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) 

650 >>> xez = np.array([1.00004219, 0.99998001, 0.99975794]) 

651 >>> bL_or = 3.681021495604089 

652 >>> n = 1.0 

653 >>> ideal_white_brightness_correlate(bRGB_o, xez, bL_or, n) 

654 ... # doctest: +ELLIPSIS 

655 125.2435392... 

656 """ 

657 

658 bR_o, bG_o, _bB_o = tsplit(bRGB_o) 

659 xi, eta, _zeta = tsplit(xez) 

660 bL_or = as_float_array(bL_or) 

661 

662 B_rw = (2 / 3) * bR_o * 1.758 * np.log10((100 * xi + n) / (20 * xi + n)) 

663 B_rw += (1 / 3) * bG_o * 1.758 * np.log10((100 * eta + n) / (20 * eta + n)) 

664 B_rw *= 41.69 / bL_or 

665 B_rw += (50 / bL_or) * (2 / 3) * bR_o 

666 B_rw += (50 / bL_or) * (1 / 3) * bG_o 

667 

668 return as_float(B_rw) 

669 

670 

671def achromatic_lightness_correlate( 

672 Q: ArrayLike, 

673) -> NDArrayFloat: 

674 """ 

675 Compute the *achromatic lightness* correlate :math:`L_p^\\star`. 

676 

677 Parameters 

678 ---------- 

679 Q 

680 Achromatic response :math:`Q`. 

681 

682 Returns 

683 ------- 

684 :class:`numpy.ndarray` 

685 *Achromatic lightness* correlate :math:`L_p^\\star`. 

686 

687 Examples 

688 -------- 

689 >>> Q = -0.000117024294955 

690 >>> achromatic_lightness_correlate(Q) # doctest: +ELLIPSIS 

691 49.9998829... 

692 """ 

693 

694 Q = as_float_array(Q) 

695 

696 return as_float(Q + 50) 

697 

698 

699def normalised_achromatic_lightness_correlate( 

700 B_r: ArrayLike, B_rw: ArrayLike 

701) -> NDArrayFloat: 

702 """ 

703 Compute the *normalised achromatic lightness* correlate 

704 :math:`L_n^\\star`. 

705 

706 Parameters 

707 ---------- 

708 B_r 

709 *Brightness* correlate :math:`B_r`. 

710 B_rw 

711 Ideal white *brightness* correlate :math:`B_{rw}`. 

712 

713 Returns 

714 ------- 

715 :class:`numpy.ndarray` 

716 *Normalised achromatic lightness* correlate :math:`L_n^\\star`. 

717 

718 Examples 

719 -------- 

720 >>> B_r = 62.626673467230766 

721 >>> B_rw = 125.24353925846037 

722 >>> normalised_achromatic_lightness_correlate(B_r, B_rw) 

723 ... # doctest: +ELLIPSIS 

724 50.0039154... 

725 """ 

726 

727 B_r = as_float_array(B_r) 

728 B_rw = as_float_array(B_rw) 

729 

730 return as_float(100 * B_r / B_rw) 

731 

732 

733def hue_angle(p: ArrayLike, t: ArrayLike) -> NDArrayFloat: 

734 """ 

735 Compute the *hue* angle :math:`h` in degrees from the specified 

736 protanopic and tritanopic responses. 

737 

738 Parameters 

739 ---------- 

740 p 

741 Protanopic response :math:`p`. 

742 t 

743 Tritanopic response :math:`t`. 

744 

745 Returns 

746 ------- 

747 :class:`numpy.ndarray` 

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

749 

750 Examples 

751 -------- 

752 >>> p = -8.002142682085493e-05 

753 >>> t = -0.000017703650669 

754 >>> hue_angle(p, t) # doctest: +ELLIPSIS 

755 257.5250300... 

756 """ 

757 

758 p = as_float_array(p) 

759 t = as_float_array(t) 

760 

761 h_L = np.degrees(np.arctan2(p, t)) % 360 

762 

763 return as_float(h_L) 

764 

765 

766def chromatic_strength_function( 

767 theta: ArrayLike, 

768) -> NDArrayFloat: 

769 """ 

770 Define the chromatic strength function :math:`E_s(\\theta)` used to 

771 correct saturation scale as a function of hue angle :math:`\\theta` in 

772 degrees. 

773 

774 Parameters 

775 ---------- 

776 theta 

777 Hue angle :math:`\\theta` in degrees. 

778 

779 Returns 

780 ------- 

781 :class:`numpy.ndarray` 

782 Corrected saturation scale. 

783 

784 Examples 

785 -------- 

786 >>> h = 257.52322689806243 

787 >>> chromatic_strength_function(h) # doctest: +ELLIPSIS 

788 1.2267869... 

789 """ 

790 

791 theta = np.radians(theta) 

792 

793 E_s = cast("NDArrayFloat", 0.9394) 

794 E_s += -0.2478 * np.sin(1 * theta) 

795 E_s += -0.0743 * np.sin(2 * theta) 

796 E_s += +0.0666 * np.sin(3 * theta) 

797 E_s += -0.0186 * np.sin(4 * theta) 

798 E_s += -0.0055 * np.cos(1 * theta) 

799 E_s += -0.0521 * np.cos(2 * theta) 

800 E_s += -0.0573 * np.cos(3 * theta) 

801 E_s += -0.0061 * np.cos(4 * theta) 

802 

803 return as_float(E_s) 

804 

805 

806def saturation_components( 

807 h: ArrayLike, 

808 bL_or: ArrayLike, 

809 t: ArrayLike, 

810 p: ArrayLike, 

811) -> NDArrayFloat: 

812 """ 

813 Compute the *saturation* components :math:`S_{RG}` and :math:`S_{YB}`. 

814 

815 Parameters 

816 ---------- 

817 h 

818 Correlate of *hue* :math:`h` in degrees. 

819 bL_or 

820 Normalising chromatic adaptation exponential factor 

821 :math:`\\beta_1(B_or)`. 

822 t 

823 Tritanopic response :math:`t`. 

824 p 

825 Protanopic response :math:`p`. 

826 

827 Returns 

828 ------- 

829 :class:`numpy.ndarray` 

830 *Saturation* components :math:`S_{RG}` and :math:`S_{YB}`. 

831 

832 Examples 

833 -------- 

834 >>> h = 257.52322689806243 

835 >>> bL_or = 3.681021495604089 

836 >>> t = -0.000017706764677 

837 >>> p = -0.000080023561356 

838 >>> saturation_components(h, bL_or, t, p) # doctest: +ELLIPSIS 

839 array([-0.0028852..., -0.0130396...]) 

840 """ 

841 

842 h = as_float_array(h) 

843 bL_or = as_float_array(bL_or) 

844 t = as_float_array(t) 

845 p = as_float_array(p) 

846 

847 E_s = chromatic_strength_function(h) 

848 S_RG = 488.93 / bL_or * E_s * t 

849 S_YB = 488.93 / bL_or * E_s * p 

850 

851 return tstack([S_RG, S_YB]) 

852 

853 

854def saturation_correlate(S_RG: ArrayLike, S_YB: ArrayLike) -> NDArrayFloat: 

855 """ 

856 Compute the correlate of *saturation* :math:`S`. 

857 

858 Parameters 

859 ---------- 

860 S_RG 

861 *Saturation* component :math:`S_{RG}`. 

862 S_YB 

863 *Saturation* component :math:`S_{YB}`. 

864 

865 Returns 

866 ------- 

867 :class:`numpy.ndarray` 

868 Correlate of *saturation* :math:`S`. 

869 

870 Examples 

871 -------- 

872 >>> S_RG = -0.002885271638197 

873 >>> S_YB = -0.013039632941332 

874 >>> saturation_correlate(S_RG, S_YB) # doctest: +ELLIPSIS 

875 0.0133550... 

876 """ 

877 

878 S_RG = as_float_array(S_RG) 

879 S_YB = as_float_array(S_YB) 

880 

881 S = np.hypot(S_RG, S_YB) 

882 

883 return as_float(S) 

884 

885 

886def chroma_components( 

887 L_star_P: ArrayLike, 

888 S_RG: ArrayLike, 

889 S_YB: ArrayLike, 

890) -> NDArrayFloat: 

891 """ 

892 Compute the *chroma* components :math:`C_{RG}` and :math:`C_{YB}`. 

893 

894 Parameters 

895 ---------- 

896 L_star_P 

897 *Achromatic lightness* correlate :math:`L_p^\\star`. 

898 S_RG 

899 *Saturation* component :math:`S_{RG}`. 

900 S_YB 

901 *Saturation* component :math:`S_{YB}`. 

902 

903 Returns 

904 ------- 

905 :class:`numpy.ndarray` 

906 *Chroma* components :math:`C_{RG}` and :math:`C_{YB}`. 

907 

908 Examples 

909 -------- 

910 >>> L_star_P = 49.99988297570504 

911 >>> S_RG = -0.002885271638197 

912 >>> S_YB = -0.013039632941332 

913 >>> chroma_components(L_star_P, S_RG, S_YB) # doctest: +ELLIPSIS 

914 array([-0.00288527, -0.01303961]) 

915 """ 

916 

917 L_star_P = as_float_array(L_star_P) 

918 S_RG = as_float_array(S_RG) 

919 S_YB = as_float_array(S_YB) 

920 

921 C_RG = spow(L_star_P / 50, 0.7) * S_RG 

922 C_YB = spow(L_star_P / 50, 0.7) * S_YB 

923 

924 return tstack([C_RG, C_YB]) 

925 

926 

927def chroma_correlate(L_star_P: ArrayLike, S: ArrayLike) -> NDArrayFloat: 

928 """ 

929 Compute the correlate of *chroma* :math:`C`. 

930 

931 Parameters 

932 ---------- 

933 L_star_P 

934 *Achromatic lightness* correlate :math:`L_p^\\star`. 

935 S 

936 Correlate of *saturation* :math:`S`. 

937 

938 Returns 

939 ------- 

940 :class:`numpy.ndarray` 

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

942 

943 Examples 

944 -------- 

945 >>> L_star_P = 49.99988297570504 

946 >>> S = 0.013355029751778 

947 >>> chroma_correlate(L_star_P, S) # doctest: +ELLIPSIS 

948 0.0133550... 

949 """ 

950 

951 L_star_P = as_float_array(L_star_P) 

952 S = as_float_array(S) 

953 

954 return spow(L_star_P / 50, 0.7) * S 

955 

956 

957def colourfulness_components( 

958 C_RG: ArrayLike, 

959 C_YB: ArrayLike, 

960 B_rw: ArrayLike, 

961) -> NDArrayFloat: 

962 """ 

963 Compute the *colourfulness* components :math:`M_{RG}` and :math:`M_{YB}`. 

964 

965 Parameters 

966 ---------- 

967 C_RG 

968 *Chroma* component :math:`C_{RG}`. 

969 C_YB 

970 *Chroma* component :math:`C_{YB}`. 

971 B_rw 

972 Ideal white *brightness* correlate :math:`B_{rw}`. 

973 

974 Returns 

975 ------- 

976 :class:`numpy.ndarray` 

977 *Colourfulness* components :math:`M_{RG}` and :math:`M_{YB}`. 

978 

979 Examples 

980 -------- 

981 >>> C_RG = -0.002885271638197 

982 >>> C_YB = -0.013039632941332 

983 >>> B_rw = 125.24353925846037 

984 >>> colourfulness_components(C_RG, C_YB, B_rw) # doctest: +ELLIPSIS 

985 array([-0.0036136..., -0.0163313...]) 

986 """ 

987 

988 C_RG = as_float_array(C_RG) 

989 C_YB = as_float_array(C_YB) 

990 B_rw = as_float_array(B_rw) 

991 

992 M_RG = C_RG * B_rw / 100 

993 M_YB = C_YB * B_rw / 100 

994 

995 return tstack([M_RG, M_YB]) 

996 

997 

998def colourfulness_correlate(C: ArrayLike, B_rw: ArrayLike) -> NDArrayFloat: 

999 """ 

1000 Compute the correlate of *colourfulness* :math:`M`. 

1001 

1002 Parameters 

1003 ---------- 

1004 C 

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

1006 B_rw 

1007 Ideal white *brightness* correlate :math:`B_{rw}`. 

1008 

1009 Returns 

1010 ------- 

1011 :class:`numpy.ndarray` 

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

1013 

1014 Examples 

1015 -------- 

1016 >>> C = 0.013355007871689 

1017 >>> B_rw = 125.24353925846037 

1018 >>> colourfulness_correlate(C, B_rw) # doctest: +ELLIPSIS 

1019 0.0167262... 

1020 """ 

1021 

1022 C = as_float_array(C) 

1023 B_rw = as_float_array(B_rw) 

1024 

1025 M = C * B_rw / 100 

1026 

1027 return as_float(M)