Coverage for appearance/nayatani95.py: 61%

1"""

2 Nayatani (1995) Colour Appearance Model

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

4

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

6 perceptual colour attributes under varying viewing conditions.

7

8- :class:`colour.CAM_Specification_Nayatani95`

9- :func:`colour.XYZ_to_Nayatani95`

10

11 References

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

22 from __future__ import annotations

23

24 import typing

25 from dataclasses import dataclass, field

26

27 import numpy as np

28

29 from colour.adaptation.cie1994 import (

30 MATRIX_XYZ_TO_RGB_CIE1994,

31 beta_1,

32 exponential_factors,

33 intermediate_values,

34)

35 from colour.algebra import spow, vecmul

36

37 if typing.TYPE_CHECKING:

38 from colour.hints import ArrayLike, Domain100

39

40 from colour.hints import Annotated, NDArrayFloat, cast

41 from colour.models import XYZ_to_xy

42 from 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

84 MATRIX_XYZ_TO_RGB_NAYATANI95: NDArrayFloat = MATRIX_XYZ_TO_RGB_CIE1994

85"""

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

87 responses matrix.

88"""

89

90

91@dataclass

92 class 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

138 class 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

190 def 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..., \

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

255 H=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

342 def 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

371 def 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

395 def 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

425 def 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

490 def 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

535 def 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

580 def 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

620 def 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

671 def 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

699 def 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

733 def 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

766 def 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

806 def 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

854 def 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

886 def 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

927 def 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

957 def 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

998 def 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)