Coverage for notation/munsell.py: 90%

1"""

2 Munsell Renotation System

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

4

5 Define objects for *Munsell Renotation System* computations.

6

7- :func:`colour.notation.munsell_value_Priest1920`: Compute *Munsell* value

8 :math:`V` from the specified *luminance* :math:`Y` using

9 *Priest, Gibson and MacNicholas (1920)* method.

10- :func:`colour.notation.munsell_value_Munsell1933`: Compute *Munsell* value

11 :math:`V` from the specified *luminance* :math:`Y` using

12 *Munsell, Sloan and Godlove (1933)* method.

13- :func:`colour.notation.munsell_value_Moon1943`: Compute *Munsell* value

14 :math:`V` from the specified *luminance* :math:`Y` using

15 *Moon and Spencer (1943)* method.

16- :func:`colour.notation.munsell_value_Saunderson1944`: Compute *Munsell*

17 value :math:`V` from the specified *luminance* :math:`Y` using

18 *Saunderson and Milner (1944)* method.

19- :func:`colour.notation.munsell_value_Ladd1955`: Compute *Munsell* value

20 :math:`V` from the specified *luminance* :math:`Y` using

21 *Ladd and Pinney (1955)* method.

22- :func:`colour.notation.munsell_value_McCamy1987`: Compute *Munsell* value

23 :math:`V` from the specified *luminance* :math:`Y` using *McCamy (1987)*

24 method.

25- :func:`colour.notation.munsell_value_ASTMD1535`: Compute *Munsell* value

26 :math:`V` from the specified *luminance* :math:`Y` using *ASTM D1535-08e1*

27 method.

28- :attr:`colour.MUNSELL_VALUE_METHODS`: Supported *Munsell* value

29 computation methods.

30- :func:`colour.munsell_value`: Compute *Munsell* value :math:`V` from

31 specified *luminance* :math:`Y` using the specified method.

32- :func:`colour.munsell_colour_to_xyY`

33- :func:`colour.xyY_to_munsell_colour`

34

35 Notes

36-----

37- The Munsell Renotation data commonly available within the *all.dat*,

38 *experimental.dat* and *real.dat* files features *CIE xyY* colourspace

39 values that are scaled by a :math:`1 / 0.975 \\simeq 1.02568` factor. If

40 you are performing conversions using *Munsell* *Colorlab* specification,

41 e.g., *2.5R 9/2*, according to *ASTM D1535-08e1* method, you should not

42 scale the output :math:`Y` Luminance. However, if you use directly the

43 *CIE xyY* colourspace values from the Munsell Renotation data, you should

44 scale the :math:`Y` Luminance before conversions by a :math:`0.975`

45 factor.

46

47 *ASTM D1535-08e1* states that::

48

49 The coefficients of this equation are obtained from the 1943 equation

50 by multiplying each coefficient by 0.975, the reflectance factor of

51 magnesium oxide with respect to the perfect reflecting diffuser, and

52 rounding to ve digits of precision.

53

54 References

55----------

56- :cite:`ASTMInternational1989a` : ASTM International. (1989). ASTM D1535-89

57 - Standard Practice for Specifying Color by the Munsell System (pp. 1-29).

58 Retrieved September 25, 2014, from

59 http://www.astm.org/DATABASE.CART/HISTORICAL/D1535-89.htm

60- :cite:`Centore2012a` : Centore, P. (2012). An open-source inversion

61 algorithm for the Munsell renotation. Color Research & Application, 37(6),

62 455-464. doi:10.1002/col.20715

63- :cite:`Centore2014k` : Centore, P. (2014).

64 MunsellAndKubelkaMunkToolboxApr2014 -

65 MunsellRenotationRoutines/MunsellHueToASTMHue.m.

66 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

67- :cite:`Centore2014l` : Centore, P. (2014).

68 MunsellAndKubelkaMunkToolboxApr2014 -

69 MunsellSystemRoutines/LinearVsRadialInterpOnRenotationOvoid.m.

70 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

71- :cite:`Centore2014m` : Centore, P. (2014).

72 MunsellAndKubelkaMunkToolboxApr2014 -

73 MunsellRenotationRoutines/MunsellToxyY.m.

74 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

75- :cite:`Centore2014n` : Centore, P. (2014).

76 MunsellAndKubelkaMunkToolboxApr2014 -

77 MunsellRenotationRoutines/FindHueOnRenotationOvoid.m.

78 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

79- :cite:`Centore2014o` : Centore, P. (2014).

80 MunsellAndKubelkaMunkToolboxApr2014 -

81 MunsellSystemRoutines/BoundingRenotationHues.m.

82 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

83- :cite:`Centore2014p` : Centore, P. (2014).

84 MunsellAndKubelkaMunkToolboxApr2014 -

85 MunsellRenotationRoutines/xyYtoMunsell.m.

86 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

87- :cite:`Centore2014q` : Centore, P. (2014).

88 MunsellAndKubelkaMunkToolboxApr2014 -

89 MunsellRenotationRoutines/MunsellToxyForIntegerMunsellValue.m.

90 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

91- :cite:`Centore2014r` : Centore, P. (2014).

92 MunsellAndKubelkaMunkToolboxApr2014 -

93 MunsellRenotationRoutines/MaxChromaForExtrapolatedRenotation.m.

94 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

95- :cite:`Centore2014s` : Centore, P. (2014).

96 MunsellAndKubelkaMunkToolboxApr2014 -

97 MunsellRenotationRoutines/MunsellHueToChromDiagHueAngle.m.

98 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

99- :cite:`Centore2014t` : Centore, P. (2014).

100 MunsellAndKubelkaMunkToolboxApr2014 -

101 MunsellRenotationRoutines/ChromDiagHueAngleToMunsellHue.m.

102 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

103- :cite:`Centore2014u` : Centore, P. (2014).

104 MunsellAndKubelkaMunkToolboxApr2014 -

105 GeneralRoutines/CIELABtoApproxMunsellSpec.m.

106 https://github.com/colour-science/MunsellAndKubelkaMunkToolbox

107- :cite:`Centorea` : Centore, P. (n.d.). The Munsell and Kubelka-Munk

108 Toolbox. Retrieved January 23, 2018, from

109 http://www.munsellcolourscienceforpainters.com/\

110 MunsellAndKubelkaMunkToolbox/MunsellAndKubelkaMunkToolbox.html

111- :cite:`Wikipedia2007c` : Nayatani, Y., Sobagaki, H., & Yano, K. H. T.

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

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

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

115"""

116

117 from __future__ import annotations

118

119 import re

120 import typing

121

122 import numpy as np

123

124 from colour.algebra import (

125 Extrapolator,

126 LinearInterpolator,

127 cartesian_to_cylindrical,

128 euclidean_distance,

129 polar_to_cartesian,

130 sdiv,

131 sdiv_mode,

132 spow,

133)

134 from colour.colorimetry import CCS_ILLUMINANTS, luminance_ASTMD1535

135 from colour.constants import (

136 PATTERN_FLOATING_POINT_NUMBER,

137 THRESHOLD_INTEGER,

138 TOLERANCE_ABSOLUTE_DEFAULT,

139 TOLERANCE_RELATIVE_DEFAULT,

140)

141

142 if typing.TYPE_CHECKING:

143 from colour.hints import (

144 Dict,

145 Domain1,

146 Domain100,

147 Literal,

148 NDArrayFloat,

149 NDArrayStr,

150 Range1,

151 Range10,

152 Tuple,

153 )

154

155 from colour.hints import ArrayLike, NDArrayFloat, cast

156 from colour.models import Lab_to_LCHab # pyright: ignore

157 from colour.models import XYZ_to_Lab, XYZ_to_xy, xyY_to_XYZ

158 from colour.notation import MUNSELL_COLOURS_ALL

159 from colour.utilities import (

160 CACHE_REGISTRY,

161 CanonicalMapping,

162 Lookup,

163 as_float,

164 as_float_array,

165 as_float_scalar,

166 as_int_scalar,

167 attest,

168 domain_range_scale,

169 from_range_1,

170 from_range_10,

171 get_domain_range_scale,

172 is_caching_enabled,

173 is_integer,

174 is_numeric,

175 to_domain_1,

176 to_domain_10,

177 to_domain_100,

178 tsplit,

179 tstack,

180 usage_warning,

181 validate_method,

182)

183 from colour.volume import is_within_macadam_limits

184

185 __author__ = "Colour Developers, Paul Centore"

186 __copyright__ = "Copyright 2013 Colour Developers"

187 __copyright__ += ", "

188 __copyright__ += (

189 "The Munsell and Kubelka-Munk Toolbox: Copyright 2010-2018 Paul Centore "

190 "(Gales Ferry, CT 06335, USA); used by permission."

191)

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

193 __maintainer__ = "Colour Developers"

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

195 __status__ = "Production"

196

197 __all__ = [

198 "MUNSELL_GRAY_PATTERN",

199 "MUNSELL_COLOUR_PATTERN",

200 "MUNSELL_GRAY_FORMAT",

201 "MUNSELL_COLOUR_FORMAT",

202 "MUNSELL_GRAY_EXTENDED_FORMAT",

203 "MUNSELL_COLOUR_EXTENDED_FORMAT",

204 "MUNSELL_HUE_LETTER_CODES",

205 "ILLUMINANT_NAME_MUNSELL",

206 "CCS_ILLUMINANT_MUNSELL",

207 "munsell_value_Priest1920",

208 "munsell_value_Munsell1933",

209 "munsell_value_Moon1943",

210 "munsell_value_Saunderson1944",

211 "munsell_value_Ladd1955",

212 "munsell_value_McCamy1987",

213 "munsell_value_ASTMD1535",

214 "MUNSELL_VALUE_METHODS",

215 "munsell_value",

216 "munsell_specification_to_xyY",

217 "munsell_colour_to_xyY",

218 "xyY_to_munsell_specification",

219 "xyY_to_munsell_colour",

220 "parse_munsell_colour",

221 "is_grey_munsell_colour",

222 "normalise_munsell_specification",

223 "munsell_colour_to_munsell_specification",

224 "munsell_specification_to_munsell_colour",

225 "xyY_from_renotation",

226 "is_specification_in_renotation",

227 "bounding_hues_from_renotation",

228 "hue_to_hue_angle",

229 "hue_angle_to_hue",

230 "hue_to_ASTM_hue",

231 "interpolation_method_from_renotation_ovoid",

232 "xy_from_renotation_ovoid",

233 "LCHab_to_munsell_specification",

234 "maximum_chroma_from_renotation",

235 "munsell_specification_to_xy",

236]

237

238 MUNSELL_GRAY_PATTERN: str = f"N(?P<value>{PATTERN_FLOATING_POINT_NUMBER})"

239 MUNSELL_COLOUR_PATTERN: str = (

240 f"(?P<hue>{PATTERN_FLOATING_POINT_NUMBER})\\s*"

241 f"(?P<letter>BG|GY|YR|RP|PB|B|G|Y|R|P)\\s*"

242 f"(?P<value>{PATTERN_FLOATING_POINT_NUMBER})\\s*\\/\\s*"

243 f"(?P<chroma>[-+]?{PATTERN_FLOATING_POINT_NUMBER})"

244)

245

246 MUNSELL_GRAY_FORMAT: str = "N{0}"

247 MUNSELL_COLOUR_FORMAT: str = "{0} {1}/{2}"

248 MUNSELL_GRAY_EXTENDED_FORMAT: str = "N{0:.{1}f}"

249 MUNSELL_COLOUR_EXTENDED_FORMAT: str = "{0:.{1}f}{2} {3:.{4}f}/{5:.{6}f}"

250

251 MUNSELL_HUE_LETTER_CODES: Lookup = Lookup(

252 {

253 "BG": 2,

254 "GY": 4,

255 "YR": 6,

256 "RP": 8,

257 "PB": 10,

258 "B": 1,

259 "G": 3,

260 "Y": 5,

261 "R": 7,

262 "P": 9,

263 }

264)

265

266 ILLUMINANT_NAME_MUNSELL: str = "C"

267 CCS_ILLUMINANT_MUNSELL: NDArrayFloat = CCS_ILLUMINANTS[

268 "CIE 1931 2 Degree Standard Observer"

269][ILLUMINANT_NAME_MUNSELL]

270

271 _CACHE_MUNSELL_SPECIFICATIONS: dict = CACHE_REGISTRY.register_cache(

272 f"{__name__}._CACHE_MUNSELL_SPECIFICATIONS"

273)

274 _CACHE_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR: dict = CACHE_REGISTRY.register_cache(

275 f"{__name__}._CACHE_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR"

276)

277 _CACHE_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION: dict = CACHE_REGISTRY.register_cache(

278 f"{__name__}._CACHE_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION"

279)

280

281

282 def _munsell_specifications() -> NDArrayFloat:

283 """

284 Return the *Munsell Renotation System* specifications and cache them if

285 not already existing.

286

287 The *Munsell Renotation System* data is stored in

288 :attr:`colour.notation.MUNSELL_COLOURS` attribute in a 2 columns form::

289

290 (

291 (("2.5GY", 0.2, 2.0), (0.713, 1.414, 0.237)),

292 (("5GY", 0.2, 2.0), (0.449, 1.145, 0.237)),

293 ...,

294 (("7.5GY", 0.2, 2.0), (0.262, 0.837, 0.237)),

295 )

296

297 The first column is converted from *Munsell* colour to specification

298 using

299 :func:`colour.notation.munsell.munsell_colour_to_munsell_specification`

300 definition:

301

302 ('2.5GY', 0.2, 2.0) --> (2.5, 0.2, 2.0, 4)

303

304 Returns

305 -------

306 :class:`numpy.NDArrayFloat`

307 *Munsell Renotation System* specifications.

308 """

309

310 global _CACHE_MUNSELL_SPECIFICATIONS # noqa: PLW0602

311

312 if is_caching_enabled() and "All" in _CACHE_MUNSELL_SPECIFICATIONS:

313 return _CACHE_MUNSELL_SPECIFICATIONS["All"]

314

315 munsell_specifications = np.array(

316 [

317 munsell_colour_to_munsell_specification(

318 MUNSELL_COLOUR_FORMAT.format(*colour[0])

319 )

320 for colour in MUNSELL_COLOURS_ALL

321 ]

322 )

323

324 _CACHE_MUNSELL_SPECIFICATIONS["All"] = munsell_specifications

325

326 return munsell_specifications

327

328

329 def _munsell_value_ASTMD1535_interpolator() -> Extrapolator:

330 """

331 Return the *Munsell* value interpolator for the *ASTM D1535-08e1*

332 method, caching it if not existing.

333

334 Returns

335 -------

336 :class:`colour.Extrapolator`

337 *Munsell* value interpolator for the *ASTM D1535-08e1* method.

338 """

339

340 global _CACHE_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR # noqa: PLW0602

341

342 if "ASTM D1535-08 Interpolator" in (

343 _CACHE_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR

344 ):

345 return _CACHE_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR[

346 "ASTM D1535-08 Interpolator"

347 ]

348

349 munsell_values = np.arange(0, 10, 0.001)

350 interpolator = LinearInterpolator(

351 luminance_ASTMD1535(munsell_values), munsell_values

352 )

353 extrapolator = Extrapolator(interpolator)

354

355 _CACHE_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR["ASTM D1535-08 Interpolator"] = (

356 extrapolator

357 )

358

359 return extrapolator

360

361

362 def _munsell_maximum_chromas_from_renotation() -> Tuple[

363 Tuple[Tuple[float, float], float], ...

364]:

365 """

366 Return the maximum *Munsell* chromas from *Munsell Renotation System*

367 data.

368

369 Returns

370 -------

371 :class:`tuple`

372 Maximum *Munsell* chromas.

373 """

374

375 global _CACHE_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION # noqa: PLW0602

376

377 if "Maximum Chromas From Renotation" in (

378 _CACHE_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION

379 ):

380 return _CACHE_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION[

381 "Maximum Chromas From Renotation"

382 ]

383

384 chromas = {}

385 for munsell_colour in MUNSELL_COLOURS_ALL:

386 hue, value, chroma, code = tsplit(

387 munsell_colour_to_munsell_specification(

388 MUNSELL_COLOUR_FORMAT.format(*munsell_colour[0])

389 )

390 )

391 index = (hue, value, code)

392 if index in chromas:

393 chroma = max([chromas[index], chroma])

394

395 chromas[index] = cast("float", chroma)

396

397 maximum_chromas_from_renotation = tuple(chromas.items())

398

399 _CACHE_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION[

400 "Maximum Chromas From Renotation"

401 ] = maximum_chromas_from_renotation

402

403 return maximum_chromas_from_renotation

404

405

406 def munsell_value_Priest1920(

407 Y: Domain100,

408) -> Range10:

409 """

410 Compute the *Munsell* value :math:`V` from the specified *luminance*

411 :math:`Y` using *Priest et al. (1920)* method.

412

413 Parameters

414 ----------

415 Y

416 *Luminance* :math:`Y`.

417

418 Returns

419 -------

420 :class:`np.float` or :class:`numpy.NDArrayFloat`

421 *Munsell* value :math:`V`.

422

423 Notes

424 -----

425 +------------+-----------------------+---------------+

426 | **Domain** | **Scale - Reference** | **Scale - 1** |

427 +============+=======================+===============+

428 | ``Y`` | 100 | 1 |

429 +------------+-----------------------+---------------+

430

431 +------------+-----------------------+---------------+

432 | **Range** | **Scale - Reference** | **Scale - 1** |

433 +============+=======================+===============+

434 | ``V`` | 10 | 1 |

435 +------------+-----------------------+---------------+

436

437 References

438 ----------

439 :cite:`Wikipedia2007c`

440

441 Examples

442 --------

443 >>> munsell_value_Priest1920(12.23634268) # doctest: +ELLIPSIS

444 3.4980484...

445 """

446

447 Y = to_domain_100(Y)

448

449 V = 10 * np.sqrt(Y / 100)

450

451 return as_float(from_range_10(V))

452

453

454 def munsell_value_Munsell1933(

455 Y: Domain100,

456) -> Range10:

457 """

458 Compute *Munsell* value :math:`V` from the specified *luminance* :math:`Y`

459 using *Munsell et al. (1933)* method.

460

461 Parameters

462 ----------

463 Y

464 *Luminance* :math:`Y`.

465

466 Returns

467 -------

468 :class:`np.float` or :class:`numpy.NDArrayFloat`

469 *Munsell* value :math:`V`.

470

471 Notes

472 -----

473 +------------+-----------------------+---------------+

474 | **Domain** | **Scale - Reference** | **Scale - 1** |

475 +============+=======================+===============+

476 | ``Y`` | 100 | 1 |

477 +------------+-----------------------+---------------+

478

479 +------------+-----------------------+---------------+

480 | **Range** | **Scale - Reference** | **Scale - 1** |

481 +============+=======================+===============+

482 | ``V`` | 10 | 1 |

483 +------------+-----------------------+---------------+

484

485 References

486 ----------

487 :cite:`Wikipedia2007c`

488

489 Examples

490 --------

491 >>> munsell_value_Munsell1933(12.23634268) # doctest: +ELLIPSIS

492 4.1627702...

493 """

494

495 Y = to_domain_100(Y)

496

497 V = np.sqrt(1.4742 * Y - 0.004743 * (Y * Y))

498

499 return as_float(from_range_10(V))

500

501

502 def munsell_value_Moon1943(Y: Domain100) -> Range10:

503 """

504 Compute *Munsell* value :math:`V` from the specified *luminance* :math:`Y`

505 using *Moon and Spencer (1943)* method.

506

507 Parameters

508 ----------

509 Y

510 *Luminance* :math:`Y`.

511

512 Returns

513 -------

514 :class:`np.float` or :class:`numpy.NDArrayFloat`

515 *Munsell* value :math:`V`.

516

517 Notes

518 -----

519 +------------+-----------------------+---------------+

520 | **Domain** | **Scale - Reference** | **Scale - 1** |

521 +============+=======================+===============+

522 | ``Y`` | 100 | 1 |

523 +------------+-----------------------+---------------+

524

525 +------------+-----------------------+---------------+

526 | **Range** | **Scale - Reference** | **Scale - 1** |

527 +============+=======================+===============+

528 | ``V`` | 10 | 1 |

529 +------------+-----------------------+---------------+

530

531 References

532 ----------

533 :cite:`Wikipedia2007c`

534

535 Examples

536 --------

537 >>> munsell_value_Moon1943(12.23634268) # doctest: +ELLIPSIS

538 4.0688120...

539 """

540

541 Y = to_domain_100(Y)

542

543 V = 1.4 * spow(Y, 0.426)

544

545 return as_float(from_range_10(V))

546

547

548 def munsell_value_Saunderson1944(

549 Y: Domain100,

550) -> Range10:

551 """

552 Compute the *Munsell* value :math:`V` from the specified *luminance* :math:`Y`

553 using *Saunderson and Milner (1944)* method.

554

555 Parameters

556 ----------

557 Y

558 *Luminance* :math:`Y`.

559

560 Returns

561 -------

562 :class:`np.float` or :class:`numpy.NDArrayFloat`

563 *Munsell* value :math:`V`.

564

565 Notes

566 -----

567 +------------+-----------------------+---------------+

568 | **Domain** | **Scale - Reference** | **Scale - 1** |

569 +============+=======================+===============+

570 | ``Y`` | 100 | 1 |

571 +------------+-----------------------+---------------+

572

573 +------------+-----------------------+---------------+

574 | **Range** | **Scale - Reference** | **Scale - 1** |

575 +============+=======================+===============+

576 | ``V`` | 10 | 1 |

577 +------------+-----------------------+---------------+

578

579 References

580 ----------

581 :cite:`Wikipedia2007c`

582

583 Examples

584 --------

585 >>> munsell_value_Saunderson1944(12.23634268) # doctest: +ELLIPSIS

586 4.0444736...

587 """

588

589 Y = to_domain_100(Y)

590

591 V = 2.357 * spow(Y, 0.343) - 1.52

592

593 return as_float(from_range_10(V))

594

595

596 def munsell_value_Ladd1955(Y: Domain100) -> Range10:

597 """

598 Compute *Munsell* value :math:`V` from the specified *luminance* :math:`Y`

599 using *Ladd and Pinney (1955)* method.

600

601 Parameters

602 ----------

603 Y

604 *Luminance* :math:`Y`.

605

606 Returns

607 -------

608 :class:`np.float` or :class:`numpy.NDArrayFloat`

609 *Munsell* value :math:`V`.

610

611 Notes

612 -----

613 +------------+-----------------------+---------------+

614 | **Domain** | **Scale - Reference** | **Scale - 1** |

615 +============+=======================+===============+

616 | ``Y`` | 100 | 1 |

617 +------------+-----------------------+---------------+

618

619 +------------+-----------------------+---------------+

620 | **Range** | **Scale - Reference** | **Scale - 1** |

621 +============+=======================+===============+

622 | ``V`` | 10 | 1 |

623 +------------+-----------------------+---------------+

624

625 References

626 ----------

627 :cite:`Wikipedia2007c`

628

629 Examples

630 --------

631 >>> munsell_value_Ladd1955(12.23634268) # doctest: +ELLIPSIS

632 4.0511633...

633 """

634

635 Y = to_domain_100(Y)

636

637 V = 2.468 * spow(Y, 1 / 3) - 1.636

638

639 return as_float(from_range_10(V))

640

641

642 def munsell_value_McCamy1987(

643 Y: Domain100,

644) -> Range10:

645 """

646 Compute *Munsell* value :math:`V` from the specified *luminance* :math:`Y`

647 using *McCamy (1987)* method.

648

649 Parameters

650 ----------

651 Y

652 *Luminance* :math:`Y`.

653

654 Returns

655 -------

656 :class:`np.float` or :class:`numpy.NDArrayFloat`

657 *Munsell* value :math:`V`.

658

659 Notes

660 -----

661 +------------+-----------------------+---------------+

662 | **Domain** | **Scale - Reference** | **Scale - 1** |

663 +============+=======================+===============+

664 | ``Y`` | 100 | 1 |

665 +------------+-----------------------+---------------+

666

667 +------------+-----------------------+---------------+

668 | **Range** | **Scale - Reference** | **Scale - 1** |

669 +============+=======================+===============+

670 | ``V`` | 10 | 1 |

671 +------------+-----------------------+---------------+

672

673 References

674 ----------

675 :cite:`ASTMInternational1989a`

676

677 Examples

678 --------

679 >>> munsell_value_McCamy1987(12.23634268) # doctest: +ELLIPSIS

680 4.0814348...

681 """

682

683 Y = to_domain_100(Y)

684

685 with sdiv_mode():

686 V = np.where(

687 Y <= 0.9,

688 0.87445 * spow(Y, 0.9967),

689 2.49268 * spow(Y, 1 / 3)

690 - 1.5614

691 - (0.985 / (((0.1073 * Y - 3.084) ** 2) + 7.54))

692 + sdiv(0.0133, spow(Y, 2.3))

693 + 0.0084 * np.sin(4.1 * spow(Y, 1 / 3) + 1)

694 + sdiv(0.0221, Y) * np.sin(0.39 * (Y - 2))

695 - (sdiv(0.0037, 0.44 * Y)) * np.sin(1.28 * (Y - 0.53)),

696 )

697

698 return as_float(from_range_10(V))

699

700

701 def munsell_value_ASTMD1535(

702 Y: Domain100,

703) -> Range10:

704 """

705 Compute the *Munsell* value :math:`V` from the specified *luminance*

706 :math:`Y` using an inverse lookup table from *ASTM D1535-08e1* method.

707

708 Parameters

709 ----------

710 Y

711 *Luminance* :math:`Y`.

712

713 Returns

714 -------

715 :class:`np.float` or :class:`numpy.NDArrayFloat`

716 *Munsell* value :math:`V`.

717

718 Notes

719 -----

720 +------------+-----------------------+---------------+

721 | **Domain** | **Scale - Reference** | **Scale - 1** |

722 +============+=======================+===============+

723 | ``Y`` | 100 | 1 |

724 +------------+-----------------------+---------------+

725

726 +------------+-----------------------+---------------+

727 | **Range** | **Scale - Reference** | **Scale - 1** |

728 +============+=======================+===============+

729 | ``V`` | 10 | 1 |

730 +------------+-----------------------+---------------+

731

732 - The *Munsell* value computation with *ASTM D1535-08e1* method is

733 only defined for domain [0, 100].

734

735 References

736 ----------

737 :cite:`ASTMInternational1989a`

738

739 Examples

740 --------

741 >>> munsell_value_ASTMD1535(12.23634268) # doctest: +ELLIPSIS

742 4.0824437...

743 """

744

745 Y = to_domain_100(Y)

746

747 V = _munsell_value_ASTMD1535_interpolator()(Y)

748

749 return as_float(from_range_10(V))

750

751

752 MUNSELL_VALUE_METHODS: CanonicalMapping = CanonicalMapping(

753 {

754 "Priest 1920": munsell_value_Priest1920,

755 "Munsell 1933": munsell_value_Munsell1933,

756 "Moon 1943": munsell_value_Moon1943,

757 "Saunderson 1944": munsell_value_Saunderson1944,

758 "Ladd 1955": munsell_value_Ladd1955,

759 "McCamy 1987": munsell_value_McCamy1987,

760 "ASTM D1535": munsell_value_ASTMD1535,

761 }

762)

763 MUNSELL_VALUE_METHODS.__doc__ = """

764 Supported *Munsell* value computation methods.

765

766 References

767----------

768:cite:`ASTMInternational1989a`, :cite:`Wikipedia2007c`

769

770 Aliases:

771

772- 'astm2008': 'ASTM D1535'

773"""

774 MUNSELL_VALUE_METHODS["astm2008"] = MUNSELL_VALUE_METHODS["ASTM D1535"]

775

776

777 def munsell_value(

778 Y: Domain100,

779 method: (

780 Literal[

781 "ASTM D1535",

782 "Ladd 1955",

783 "McCamy 1987",

784 "Moon 1943",

785 "Munsell 1933",

786 "Priest 1920",

787 "Saunderson 1944",

788 ]

789 | str

790 ) = "ASTM D1535",

791) -> Range10:

792 """

793 Compute the *Munsell* value :math:`V` from the specified *luminance*

794 :math:`Y` using the specified computational method.

795

796 Parameters

797 ----------

798 Y

799 *Luminance* :math:`Y`.

800 method

801 Computation method.

802

803 Returns

804 -------

805 :class:`np.float` or :class:`numpy.NDArrayFloat`

806 *Munsell* value :math:`V`.

807

808 Notes

809 -----

810 +------------+-----------------------+---------------+

811 | **Domain** | **Scale - Reference** | **Scale - 1** |

812 +============+=======================+===============+

813 | ``Y`` | 100 | 1 |

814 +------------+-----------------------+---------------+

815

816 +------------+-----------------------+---------------+

817 | **Range** | **Scale - Reference** | **Scale - 1** |

818 +============+=======================+===============+

819 | ``V`` | 10 | 1 |

820 +------------+-----------------------+---------------+

821

822 References

823 ----------

824 :cite:`ASTMInternational1989a`, :cite:`Wikipedia2007c`

825

826 Examples

827 --------

828 >>> munsell_value(12.23634268) # doctest: +ELLIPSIS

829 4.0824437...

830 >>> munsell_value(12.23634268, method="Priest 1920") # doctest: +ELLIPSIS

831 3.4980484...

832 >>> munsell_value(12.23634268, method="Munsell 1933") # doctest: +ELLIPSIS

833 4.1627702...

834 >>> munsell_value(12.23634268, method="Moon 1943") # doctest: +ELLIPSIS

835 4.0688120...

836 >>> munsell_value(12.23634268, method="Saunderson 1944")

837 ... # doctest: +ELLIPSIS

838 4.0444736...

839 >>> munsell_value(12.23634268, method="Ladd 1955") # doctest: +ELLIPSIS

840 4.0511633...

841 >>> munsell_value(12.23634268, method="McCamy 1987") # doctest: +ELLIPSIS

842 4.0814348...

843 """

844

845 method = validate_method(method, tuple(MUNSELL_VALUE_METHODS))

846

847 return MUNSELL_VALUE_METHODS[method](Y)

848

849

850 def _munsell_scale_factor() -> NDArrayFloat:

851 """

852 Return the domain-range scale factor for the *Munsell Renotation System*.

853

854 Returns

855 -------

856 :class:`numpy.NDArrayFloat`

857 Domain-range scale factor for the *Munsell Renotation System*.

858 """

859

860 return np.array([10, 10, 50 if get_domain_range_scale() == "1" else 2, 10])

861

862

863 def _munsell_specification_to_xyY(specification: ArrayLike) -> NDArrayFloat:

864 """

865 Convert from *Munsell* *Colorlab* specification to *CIE xyY* colourspace.

866

867 Parameters

868 ----------

869 specification

870 *Munsell* *Colorlab* specification.

871

872 Returns

873 -------

874 :class:`numpy.NDArrayFloat`

875 *CIE xyY* colourspace array.

876 """

877

878 specification = normalise_munsell_specification(specification)

879

880 if is_grey_munsell_colour(specification):

881 specification = to_domain_10(specification)

882 hue, value, chroma, code = specification

883 else:

884 specification = to_domain_10(specification, _munsell_scale_factor())

885 hue, value, chroma, code = specification

886 code = as_int_scalar(code)

887

888 attest(

889 0 <= hue <= 10,

890 f'"{specification}" specification hue must be normalised to '

891 f"domain [0, 10]!",

892 )

893

894 attest(

895 0 <= value <= 10,

896 f'"{specification}" specification value must be normalised to '

897 f"domain [0, 10]!",

898 )

899

900 with domain_range_scale("ignore"):

901 Y = luminance_ASTMD1535(value)

902

903 if is_integer(value):

904 value_minus = value_plus = round(value)

905 else:

906 value_minus = np.floor(value)

907 value_plus = value_minus + 1

908

909 specification_minus = as_float_array(

910 value_minus

911 if is_grey_munsell_colour(specification)

912 else [hue, value_minus, chroma, code]

913 )

914 x_minus, y_minus = tsplit(munsell_specification_to_xy(specification_minus))

915

916 specification_plus = as_float_array(

917 value_plus

918 if (is_grey_munsell_colour(specification) or value_plus == 10)

919 else [hue, value_plus, chroma, code]

920 )

921 x_plus, y_plus = tsplit(munsell_specification_to_xy(specification_plus))

922

923 if value_minus == value_plus:

924 x = as_float(x_minus)

925 y = as_float(y_minus)

926 else:

927 with domain_range_scale("ignore"):

928 Y_minus = luminance_ASTMD1535(value_minus)

929 Y_plus = luminance_ASTMD1535(value_plus)

930

931 Y_minus_plus = np.squeeze([Y_minus, Y_plus])

932 x_minus_plus = np.squeeze([x_minus, x_plus])

933 y_minus_plus = np.squeeze([y_minus, y_plus])

934

935 x = as_float(LinearInterpolator(Y_minus_plus, x_minus_plus)(Y))

936 y = as_float(LinearInterpolator(Y_minus_plus, y_minus_plus)(Y))

937

938 Y = from_range_1(Y / 100)

939

940 return tstack([x, y, Y])

941

942

943 def munsell_specification_to_xyY(specification: ArrayLike) -> NDArrayFloat:

944 """

945 Convert specified *Munsell* *Colorlab* specification to *CIE xyY*

946 colourspace.

947

948 Parameters

949 ----------

950 specification

951 *Munsell* *Colorlab* specification.

952

953 Returns

954 -------

955 :class:`numpy.NDArrayFloat`

956 *CIE xyY* colourspace array.

957

958 Notes

959 -----

960 +-------------------+-----------------------+---------------+

961 | **Domain** | **Scale - Reference** | **Scale - 1** |

962 +===================+=======================+===============+

963 | ``specification`` | ``hue`` : 10 | 1 |

964 | | | |

965 | | ``value`` : 10 | 1 |

966 | | | |

967 | | ``chroma`` : 50 | 1 |

968 | | | |

969 | | ``code`` : 10 | 1 |

970 +-------------------+-----------------------+---------------+

971

972 +-------------------+-----------------------+---------------+

973 | **Range** | **Scale - Reference** | **Scale - 1** |

974 +===================+=======================+===============+

975 | ``xyY`` | 1 | 1 |

976 +-------------------+-----------------------+---------------+

977

978 References

979 ----------

980 :cite:`Centore2014m`

981

982 Examples

983 --------

984 >>> munsell_specification_to_xyY(np.array([2.1, 8.0, 17.9, 4]))

985 ... # doctest: +ELLIPSIS

986 array([ 0.4400632..., 0.5522428..., 0.5761962...])

987 >>> munsell_specification_to_xyY(np.array([np.nan, 8.9, np.nan, np.nan]))

988 ... # doctest: +ELLIPSIS

989 array([ 0.31006 , 0.31616 , 0.7461345...])

990 """

991

992 specification = as_float_array(specification)

993 shape = list(specification.shape)

994

995 xyY = [_munsell_specification_to_xyY(a) for a in np.reshape(specification, (-1, 4))]

996

997 shape[-1] = 3

998

999 return np.reshape(as_float_array(xyY), shape)

1000

1001

1002 def munsell_colour_to_xyY(munsell_colour: ArrayLike) -> Range1:

1003 """

1004 Convert the specified *Munsell* colour to *CIE xyY* colourspace.

1005

1006 Parameters

1007 ----------

1008 munsell_colour

1009 *Munsell* colour notation formatted as "H V/C" where H is hue,

1010 V is value, and C is chroma.

1011

1012 Returns

1013 -------

1014 :class:`numpy.NDArrayFloat`

1015 *CIE xyY* colourspace array.

1016

1017 Notes

1018 -----

1019 +-----------+-----------------------+---------------+

1020 | **Range** | **Scale - Reference** | **Scale - 1** |

1021 +===========+=======================+===============+

1022 | ``xyY`` | 1 | 1 |

1023 +-----------+-----------------------+---------------+

1024

1025 References

1026 ----------

1027 :cite:`Centorea`, :cite:`Centore2012a`

1028

1029 Examples

1030 --------

1031 >>> munsell_colour_to_xyY("4.2YR 8.1/5.3") # doctest: +ELLIPSIS

1032 array([ 0.3873694..., 0.3575165..., 0.59362 ])

1033 >>> munsell_colour_to_xyY("N8.9") # doctest: +ELLIPSIS

1034 array([ 0.31006 , 0.31616 , 0.7461345...])

1035 """

1036

1037 munsell_colour = np.array(munsell_colour)

1038 shape = list(munsell_colour.shape)

1039

1040 specification = np.array(

1041 [munsell_colour_to_munsell_specification(a) for a in np.ravel(munsell_colour)]

1042 )

1043

1044 return munsell_specification_to_xyY(

1045 from_range_10(np.reshape(specification, (*shape, 4)), _munsell_scale_factor())

1046 )

1047

1048

1049 def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat:

1050 """

1051 Convert from *CIE xyY* colourspace to *Munsell* *Colorlab*

1052 specification.

1053

1054 Parameters

1055 ----------

1056 xyY

1057 *CIE xyY* colourspace array.

1058

1059 Returns

1060 -------

1061 :class:`numpy.NDArrayFloat`

1062 *Munsell* *Colorlab* specification.

1063

1064 Raises

1065 ------

1066 ValueError

1067 If the specified *CIE xyY* colourspace array is not within

1068 MacAdam limits.

1069 RuntimeError

1070 If the maximum iterations count has been reached without

1071 converging to a result.

1072 """

1073

1074 xyY = as_float_array(xyY)

1075

1076 x, y, Y = tsplit(xyY)

1077 Y = to_domain_1(Y)

1078

1079 if not is_within_macadam_limits(xyY, ILLUMINANT_NAME_MUNSELL):

1080 usage_warning(

1081 f'"{xyY! r}" is not within "MacAdam" limits for illuminant '

1082 f'"{ILLUMINANT_NAME_MUNSELL}"!'

1083 )

1084

1085 with domain_range_scale("ignore"):

1086 value = munsell_value_ASTMD1535(Y * 100)

1087

1088 if is_integer(value):

1089 value = np.around(value)

1090

1091 with domain_range_scale("ignore"):

1092 x_center, y_center, Y_center = tsplit(_munsell_specification_to_xyY(value))

1093

1094 rho_input, phi_input, _z_input = tsplit(

1095 cartesian_to_cylindrical([x - x_center, y - y_center, Y_center])

1096 )

1097 phi_input = np.degrees(phi_input)

1098

1099 grey_threshold = THRESHOLD_INTEGER

1100

1101 if rho_input < grey_threshold:

1102 return from_range_10(normalise_munsell_specification(value))

1103

1104 XYZ = xyY_to_XYZ(xyY)

1105

1106 _X, Y, _Z = tsplit(XYZ)

1107 x_i, y_i = CCS_ILLUMINANT_MUNSELL

1108 X_r, Y_r, Z_r = xyY_to_XYZ([x_i, y_i, Y])

1109

1110 with sdiv_mode():

1111 XYZ_r = np.array([(1 / Y_r) * X_r, 1, (1 / Y_r) * Z_r])

1112

1113 Lab = XYZ_to_Lab(XYZ, XYZ_to_xy(XYZ_r))

1114 LCHab = Lab_to_LCHab(Lab)

1115 hue_initial, _value_initial, chroma_initial, code_initial = tsplit(

1116 LCHab_to_munsell_specification(LCHab)

1117 )

1118 specification_current = [

1119 hue_initial,

1120 value,

1121 (5 / 5.5) * chroma_initial,

1122 code_initial,

1123 ]

1124

1125 convergence_threshold = THRESHOLD_INTEGER / 1e4

1126 iterations_maximum = 64

1127 iterations = 0

1128

1129 while iterations <= iterations_maximum:

1130 iterations += 1

1131

1132 (

1133 hue_current,

1134 _value_current,

1135 chroma_current,

1136 code_current,

1137 ) = specification_current

1138 hue_angle_current = hue_to_hue_angle([hue_current, code_current])

1139

1140 chroma_maximum = maximum_chroma_from_renotation(

1141 [hue_current, value, code_current]

1142 )

1143 if chroma_current > chroma_maximum:

1144 chroma_current = specification_current[2] = chroma_maximum

1145

1146 with domain_range_scale("ignore"):

1147 x_current, y_current, _Y_current = tsplit(

1148 _munsell_specification_to_xyY(specification_current)

1149 )

1150

1151 rho_current, phi_current, _z_current = tsplit(

1152 cartesian_to_cylindrical(

1153 [x_current - x_center, y_current - y_center, Y_center]

1154 )

1155 )

1156 phi_current = np.degrees(phi_current)

1157 phi_current_difference = (360 - phi_input + phi_current) % 360

1158 if phi_current_difference > 180:

1159 phi_current_difference -= 360

1160

1161 phi_differences_data = [phi_current_difference]

1162 hue_angles_differences_data = [0]

1163 hue_angles = [hue_angle_current]

1164

1165 iterations_maximum_inner = 16

1166 iterations_inner = 0

1167 extrapolate = False

1168

1169 while (

1170 np.sign(np.min(phi_differences_data))

1171 == np.sign(np.max(phi_differences_data))

1172 and extrapolate is False

1173 ):

1174 iterations_inner += 1

1175

1176 if iterations_inner > iterations_maximum_inner:

1177 # NOTE: This exception is likely never raised in practice:

1178 # 300K iterations with random numbers never reached this code

1179 # path, it is kept for consistency with the reference

1180 # implementation.

1181 error = (

1182 "Maximum inner iterations count reached"

1183 " without convergence!"

1184 ) # pragma: no cover

1185

1186 raise RuntimeError( # pragma: no cover

1187 error

1188 )

1189

1190 hue_angle_inner = (

1191 hue_angle_current + iterations_inner * (phi_input - phi_current)

1192 ) % 360

1193 hue_angle_difference_inner = (

1194 iterations_inner * (phi_input - phi_current) % 360

1195 )

1196 if hue_angle_difference_inner > 180:

1197 hue_angle_difference_inner -= 360

1198

1199 hue_inner, code_inner = hue_angle_to_hue(hue_angle_inner)

1200

1201 with domain_range_scale("ignore"):

1202 x_inner, y_inner, _Y_inner = _munsell_specification_to_xyY(

1203 [

1204 hue_inner,

1205 value,

1206 chroma_current,

1207 code_inner,

1208 ]

1209 )

1210

1211 if len(phi_differences_data) >= 2:

1212 extrapolate = True

1213

1214 if extrapolate is False:

1215 rho_inner, phi_inner, _z_inner = cartesian_to_cylindrical(

1216 [x_inner - x_center, y_inner - y_center, Y_center]

1217 )

1218 phi_inner = np.degrees(phi_inner)

1219 phi_inner_difference = (360 - phi_input + phi_inner) % 360

1220 if phi_inner_difference > 180:

1221 phi_inner_difference -= 360

1222

1223 phi_differences_data.append(phi_inner_difference)

1224 hue_angles.append(hue_angle_inner)

1225 hue_angles_differences_data.append(hue_angle_difference_inner)

1226

1227 phi_differences = np.array(phi_differences_data)

1228 hue_angles_differences = np.array(hue_angles_differences_data)

1229

1230 phi_differences_indexes = phi_differences.argsort()

1231

1232 phi_differences = phi_differences[phi_differences_indexes]

1233 hue_angles_differences = hue_angles_differences[phi_differences_indexes]

1234

1235 hue_angle_difference_new = (

1236 Extrapolator(LinearInterpolator(phi_differences, hue_angles_differences))(0)

1237 % 360

1238 )

1239 hue_angle_new = cast(

1240 "float", (hue_angle_current + hue_angle_difference_new) % 360

1241 )

1242

1243 hue_new, code_new = hue_angle_to_hue(hue_angle_new)

1244 specification_current = [hue_new, value, chroma_current, code_new]

1245

1246 with domain_range_scale("ignore"):

1247 x_current, y_current, _Y_current = _munsell_specification_to_xyY(

1248 specification_current

1249 )

1250

1251 chroma_scale = 50 if get_domain_range_scale() == "1" else 2

1252

1253 difference = euclidean_distance([x, y], [x_current, y_current])

1254 if difference < convergence_threshold:

1255 return from_range_10(

1256 np.array(specification_current),

1257 np.array([10, 10, chroma_scale, 10]),

1258 )

1259

1260 # TODO: Consider refactoring implementation.

1261 (

1262 hue_current,

1263 _value_current,

1264 chroma_current,

1265 code_current,

1266 ) = specification_current

1267 chroma_maximum = maximum_chroma_from_renotation(

1268 [hue_current, value, code_current]

1269 )

1270

1271 # NOTE: This condition is likely never "True" while producing a valid

1272 # "Munsell Specification" in practice: 100K iterations with random

1273 # numbers never reached this code path while producing a valid

1274 # "Munsell Specification".

1275 if chroma_current > chroma_maximum:

1276 chroma_current = specification_current[2] = chroma_maximum

1277

1278 with domain_range_scale("ignore"):

1279 x_current, y_current, _Y_current = _munsell_specification_to_xyY(

1280 specification_current

1281 )

1282

1283 rho_current, phi_current, _z_current = cartesian_to_cylindrical(

1284 [x_current - x_center, y_current - y_center, Y_center]

1285 )

1286

1287 rho_bounds_data = [rho_current]

1288 chroma_bounds_data = [chroma_current]

1289

1290 iterations_maximum_inner = 16

1291 iterations_inner = 0

1292 while not (np.min(rho_bounds_data) < rho_input < np.max(rho_bounds_data)):

1293 iterations_inner += 1

1294

1295 if iterations_inner > iterations_maximum_inner:

1296 error = "Maximum inner iterations count reached without convergence!"

1297

1298 raise RuntimeError(error)

1299

1300 with sdiv_mode():

1301 chroma_inner = (

1302 (rho_input / rho_current) ** iterations_inner

1303 ) * chroma_current

1304

1305 if chroma_inner > chroma_maximum:

1306 chroma_inner = specification_current[2] = chroma_maximum

1307

1308 specification_inner = [

1309 hue_current,

1310 value,

1311 chroma_inner,

1312 code_current,

1313 ]

1314

1315 with domain_range_scale("ignore"):

1316 x_inner, y_inner, _Y_inner = _munsell_specification_to_xyY(

1317 specification_inner

1318 )

1319

1320 rho_inner, phi_inner, _z_inner = cartesian_to_cylindrical(

1321 [x_inner - x_center, y_inner - y_center, Y_center]

1322 )

1323

1324 rho_bounds_data.append(rho_inner)

1325 chroma_bounds_data.append(chroma_inner)

1326

1327 rho_bounds = np.array(rho_bounds_data)

1328 chroma_bounds = np.array(chroma_bounds_data)

1329

1330 rhos_bounds_indexes = rho_bounds.argsort()

1331

1332 rho_bounds = rho_bounds[rhos_bounds_indexes]

1333 chroma_bounds = chroma_bounds[rhos_bounds_indexes]

1334 chroma_new = LinearInterpolator(rho_bounds, chroma_bounds)(rho_input)

1335

1336 specification_current = [hue_current, value, chroma_new, code_current]

1337

1338 with domain_range_scale("ignore"):

1339 x_current, y_current, _Y_current = _munsell_specification_to_xyY(

1340 specification_current

1341 )

1342

1343 difference = euclidean_distance([x, y], [x_current, y_current])

1344 if difference < convergence_threshold:

1345 return from_range_10(

1346 np.array(specification_current),

1347 np.array([10, 10, chroma_scale, 10]),

1348 )

1349

1350 # NOTE: This exception is likely never reached in practice: 300K iterations

1351 # with random numbers never reached this code path, it is kept for

1352 # consistency with the reference # implementation

1353 error = (

1354 "Maximum outside iterations count reached "

1355 "without convergence!"

1356 ) # pragma: no cover

1357

1358 raise RuntimeError( # pragma: no cover

1359 error

1360 )

1361

1362

1363 def xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat:

1364 """

1365 Convert from *CIE xyY* colourspace to *Munsell* *Colorlab*

1366 specification.

1367

1368 Parameters

1369 ----------

1370 xyY

1371 *CIE xyY* colourspace array.

1372

1373 Returns

1374 -------

1375 :class:`numpy.NDArrayFloat`

1376 *Munsell* *Colorlab* specification.

1377

1378 Raises

1379 ------

1380 ValueError

1381 If the specified *CIE xyY* colourspace array is not within

1382 MacAdam limits.

1383 RuntimeError

1384 If the maximum iterations count has been reached without

1385 converging to a result.

1386

1387 Notes

1388 -----

1389 +-------------------+-----------------------+---------------+

1390 | **Domain** | **Scale - Reference** | **Scale - 1** |

1391 +===================+=======================+===============+

1392 | ``xyY`` | 1 | 1 |

1393 +-------------------+-----------------------+---------------+

1394

1395 +-------------------+-----------------------+---------------+

1396 | **Range** | **Scale - Reference** | **Scale - 1** |

1397 +===================+=======================+===============+

1398 | ``specification`` | ``hue`` : 10 | 1 |

1399 | | | |

1400 | | ``value`` : 10 | 1 |

1401 | | | |

1402 | | ``chroma`` : 50 | 1 |

1403 | | | |

1404 | | ``code`` : 10 | 1 |

1405 +-------------------+-----------------------+---------------+

1406

1407 References

1408 ----------

1409 :cite:`Centore2014p`

1410

1411 Examples

1412 --------

1413 >>> xyY = np.array([0.38736945, 0.35751656, 0.59362000])

1414 >>> xyY_to_munsell_specification(xyY) # doctest: +ELLIPSIS

1415 array([ 4.2000019..., 8.0999999..., 5.2999996..., 6. ])

1416 """

1417

1418 xyY = as_float_array(xyY)

1419 shape = list(xyY.shape)

1420

1421 specification = [_xyY_to_munsell_specification(a) for a in np.reshape(xyY, (-1, 3))]

1422

1423 shape[-1] = 4

1424

1425 return np.reshape(as_float_array(specification), shape)

1426

1427

1428 def xyY_to_munsell_colour(

1429 xyY: Domain1,

1430 hue_decimals: int = 1,

1431 value_decimals: int = 1,

1432 chroma_decimals: int = 1,

1433) -> str | NDArrayStr:

1434 """

1435 Convert from *CIE xyY* colourspace to *Munsell* colour notation.

1436

1437 Parameters

1438 ----------

1439 xyY

1440 *CIE xyY* colourspace array representing chromaticity coordinates

1441 and luminance.

1442 hue_decimals

1443 Number of decimal places for formatting the hue component.

1444 value_decimals

1445 Number of decimal places for formatting the value component.

1446 chroma_decimals

1447 Number of decimal places for formatting the chroma component.

1448

1449 Returns

1450 -------

1451 :class:`str` or :class:`numpy.NDArrayFloat`

1452 *Munsell* colour notation formatted as "H V/C" where H is hue,

1453 V is value, and C is chroma.

1454

1455 Notes

1456 -----

1457 +------------+-----------------------+---------------+

1458 | **Domain** | **Scale - Reference** | **Scale - 1** |

1459 +============+=======================+===============+

1460 | ``xyY`` | 1 | 1 |

1461 +------------+-----------------------+---------------+

1462

1463 References

1464 ----------

1465 :cite:`Centorea`, :cite:`Centore2012a`

1466

1467 Examples

1468 --------

1469 >>> xyY = np.array([0.38736945, 0.35751656, 0.59362000])

1470 >>> xyY_to_munsell_colour(xyY)

1471 '4.2YR 8.1/5.3'

1472 """

1473

1474 specification = to_domain_10(

1475 xyY_to_munsell_specification(xyY), _munsell_scale_factor()

1476 )

1477 shape = list(specification.shape)

1478 decimals = (hue_decimals, value_decimals, chroma_decimals)

1479

1480 munsell_colour = np.reshape(

1481 np.array(

1482 [

1483 munsell_specification_to_munsell_colour(a, *decimals)

1484 for a in np.reshape(specification, (-1, 4))

1485 ]

1486 ),

1487 shape[:-1],

1488 )

1489

1490 return str(munsell_colour) if shape == [4] else munsell_colour

1491

1492

1493 def parse_munsell_colour(munsell_colour: str) -> NDArrayFloat:

1494 """

1495 Parse specified *Munsell* colour and return an intermediate *Munsell*

1496 *Colorlab* specification.

1497

1498 Parameters

1499 ----------

1500 munsell_colour

1501 *Munsell* colour.

1502

1503 Returns

1504 -------

1505 :class:`numpy.NDArrayFloat`

1506 Intermediate *Munsell* *Colorlab* specification.

1507

1508 Raises

1509 ------

1510 ValueError

1511 If the specified specification is not a valid *Munsell Renotation

1512 System* colour specification.

1513

1514 Examples

1515 --------

1516 >>> parse_munsell_colour("N5.2")

1517 array([ nan, 5.2, nan, nan])

1518 >>> parse_munsell_colour("0YR 2.0/4.0")

1519 array([ 0., 2., 4., 6.])

1520 """

1521

1522 match = re.match(MUNSELL_GRAY_PATTERN, munsell_colour, flags=re.IGNORECASE)

1523 if match:

1524 return tstack(

1525 [

1526 np.nan,

1527 match.group("value"),

1528 np.nan,

1529 np.nan,

1530 ]

1531 )

1532

1533 match = re.match(MUNSELL_COLOUR_PATTERN, munsell_colour, flags=re.IGNORECASE)

1534 if match:

1535 return tstack(

1536 [

1537 match.group("hue"),

1538 match.group("value"),

1539 match.group("chroma"),

1540 MUNSELL_HUE_LETTER_CODES[match.group("letter").upper()],

1541 ]

1542 )

1543

1544 error = (

1545 f'"{munsell_colour}" is not a valid "Munsell Renotation System" '

1546 f"colour specification!"

1547 )

1548

1549 raise ValueError(error)

1550

1551

1552 def is_grey_munsell_colour(specification: ArrayLike) -> bool:

1553 """

1554 Determine whether the specified *Munsell* *Colorlab* specification

1555 represents a grey colour.

1556

1557 Parameters

1558 ----------

1559 specification

1560 *Munsell* *Colorlab* specification.

1561

1562 Returns

1563 -------

1564 :class:`bool`

1565 Whether the specification represents a grey colour.

1566

1567 Examples

1568 --------

1569 >>> is_grey_munsell_colour(np.array([0.0, 2.0, 4.0, 6]))

1570 False

1571 >>> is_grey_munsell_colour(np.array([np.nan, 0.5, np.nan, np.nan]))

1572 True

1573 """

1574

1575 specification = as_float_array(specification)

1576

1577 specification = np.squeeze(specification[~np.isnan(specification)])

1578

1579 return is_numeric(as_float(specification))

1580

1581

1582 def normalise_munsell_specification(specification: ArrayLike) -> NDArrayFloat:

1583 """

1584 Normalise the specified *Munsell* *Colorlab* specification.

1585

1586 Parameters

1587 ----------

1588 specification

1589 *Munsell* *Colorlab* specification to be normalised.

1590

1591 Returns

1592 -------

1593 :class:`numpy.NDArrayFloat`

1594 Normalised *Munsell* *Colorlab* specification.

1595

1596 Examples

1597 --------

1598 >>> normalise_munsell_specification(np.array([0.0, 2.0, 4.0, 6]))

1599 array([ 10., 2., 4., 7.])

1600 >>> normalise_munsell_specification(np.array([np.nan, 0.5, np.nan, np.nan]))

1601 array([ nan, 0.5, nan, nan])

1602 """

1603

1604 specification = as_float_array(specification)

1605

1606 if is_grey_munsell_colour(specification):

1607 return specification * np.array([np.nan, 1, np.nan, np.nan])

1608

1609 hue, value, chroma, code = specification

1610

1611 if hue == 0:

1612 # 0YR is equivalent to 10R.

1613 hue, code = 10, (code + 1) % 10

1614

1615 if chroma == 0:

1616 return tstack([np.nan, value, np.nan, np.nan])

1617

1618 return tstack([hue, value, chroma, code])

1619

1620

1621 def munsell_colour_to_munsell_specification(

1622 munsell_colour: str,

1623) -> NDArrayFloat:

1624 """

1625 Convert from *Munsell* colour notation to *Munsell* *Colorlab*

1626 specification.

1627

1628 Parameters

1629 ----------

1630 munsell_colour

1631 *Munsell* colour notation.

1632

1633 Returns

1634 -------

1635 :class:`numpy.NDArrayFloat`

1636 *Munsell* *Colorlab* specification as a 4-element array containing hue,

1637 value, chroma, and code values.

1638

1639 Examples

1640 --------

1641 >>> munsell_colour_to_munsell_specification("N5.2")

1642 array([ nan, 5.2, nan, nan])

1643 >>> munsell_colour_to_munsell_specification("0YR 2.0/4.0")

1644 array([ 10., 2., 4., 7.])

1645 """

1646

1647 return normalise_munsell_specification(parse_munsell_colour(munsell_colour))

1648

1649

1650 def munsell_specification_to_munsell_colour(

1651 specification: ArrayLike,

1652 hue_decimals: int = 1,

1653 value_decimals: int = 1,

1654 chroma_decimals: int = 1,

1655) -> str:

1656 """

1657 Convert from *Munsell* *Colorlab* specification to *Munsell* colour

1658 notation.

1659

1660 Parameters

1661 ----------

1662 specification

1663 *Munsell* *Colorlab* specification as a 4-element array containing hue,

1664 value, chroma, and code values.

1665 hue_decimals

1666 Number of decimal places for hue formatting.

1667 value_decimals

1668 Number of decimal places for value formatting.

1669 chroma_decimals

1670 Number of decimal places for chroma formatting.

1671

1672 Returns

1673 -------

1674 :class:`str`

1675 *Munsell* colour notation.

1676

1677 Examples

1678 --------

1679 >>> munsell_specification_to_munsell_colour(np.array([np.nan, 5.2, np.nan, np.nan]))

1680 'N5.2'

1681 >>> munsell_specification_to_munsell_colour(np.array([10, 2.0, 4.0, 7]))

1682 '10.0R 2.0/4.0'

1683 """

1684

1685 hue, value, chroma, code = tsplit(normalise_munsell_specification(specification))

1686

1687 if is_grey_munsell_colour(specification):

1688 return MUNSELL_GRAY_EXTENDED_FORMAT.format(value, value_decimals)

1689

1690 hue = round(hue, hue_decimals)

1691 attest(

1692 0 <= hue <= 10,

1693 f'"{specification! r}" specification hue must be normalised to domain [0, 10]!',

1694 )

1695

1696 value = round(value, value_decimals)

1697 attest(

1698 0 <= value <= 10,

1699 f'"{specification! r}" specification value must be normalised to '

1700 f"domain [0, 10]!",

1701 )

1702

1703 chroma = round(chroma, chroma_decimals)

1704 attest(

1705 0 <= chroma <= 50,

1706 f'"{specification! r}" specification chroma must be normalised to '

1707 f"domain [0, 50]!",

1708 )

1709

1710 code_values = MUNSELL_HUE_LETTER_CODES.values()

1711 code = round(code, 1)

1712 attest(

1713 code in code_values,

1714 f'"{specification! r}" specification code must one of "{code_values}"!',

1715 )

1716

1717 if value == 0:

1718 return MUNSELL_GRAY_EXTENDED_FORMAT.format(value, value_decimals)

1719

1720 hue_letter = MUNSELL_HUE_LETTER_CODES.first_key_from_value(code)

1721

1722 return MUNSELL_COLOUR_EXTENDED_FORMAT.format(

1723 hue,

1724 hue_decimals,

1725 hue_letter,

1726 value,

1727 value_decimals,

1728 chroma,

1729 chroma_decimals,

1730 )

1731

1732

1733 def xyY_from_renotation(

1734 specification: ArrayLike,

1735 absolute_tolerance: float = TOLERANCE_ABSOLUTE_DEFAULT,

1736 relative_tolerance: float = TOLERANCE_RELATIVE_DEFAULT,

1737) -> NDArrayFloat:

1738 """

1739 Compute the *CIE xyY* colourspace vector for the specified *Munsell*

1740 *Colorlab* specification from *Munsell Renotation System* data.

1741

1742 Parameters

1743 ----------

1744 specification

1745 *Munsell* *Colorlab* specification.

1746 absolute_tolerance

1747 Absolute tolerance for finding the corresponding *Munsell

1748 Renotation System* data.

1749 relative_tolerance

1750 Relative tolerance for finding the corresponding *Munsell

1751 Renotation System* data.

1752

1753 Returns

1754 -------

1755 :class:`numpy.NDArrayFloat`

1756 *CIE xyY* colourspace vector.

1757

1758 Raises

1759 ------

1760 ValueError

1761 If the specified specification does not exist in the *Munsell

1762 Renotation System* data.

1763

1764 Examples

1765 --------

1766 >>> xyY_from_renotation(np.array([2.5, 0.2, 2.0, 4])) # doctest: +ELLIPSIS

1767 array([ 0.71..., 1.41..., 0.23...])

1768 """

1769

1770 specification = normalise_munsell_specification(specification)

1771

1772 try:

1773 index = np.argwhere(

1774 np.all(

1775 np.isclose(

1776 specification,

1777 _munsell_specifications(),

1778 atol=absolute_tolerance,

1779 rtol=relative_tolerance,

1780 ),

1781 axis=-1,

1782 )

1783 )

1784

1785 return MUNSELL_COLOURS_ALL[as_int_scalar(index[0])][1]

1786

1787 except Exception as exception:

1788 error = (

1789 f'"{specification}" specification does not exists in '

1790 '"Munsell Renotation System" data!'

1791 )

1792

1793 raise ValueError(error) from exception

1794

1795

1796 def is_specification_in_renotation(specification: ArrayLike) -> bool:

1797 """

1798 Determine whether the specified *Munsell* *Colorlab* specification exists

1799 in the *Munsell Renotation System* data.

1800

1801 Parameters

1802 ----------

1803 specification

1804 *Munsell* *Colorlab* specification.

1805

1806 Returns

1807 -------

1808 :class:`bool`

1809 Whether specification is in *Munsell Renotation System* data.

1810

1811 Examples

1812 --------

1813 >>> is_specification_in_renotation(np.array([2.5, 0.2, 2.0, 4]))

1814 True

1815 >>> is_specification_in_renotation(np.array([64, 0.2, 2.0, 4]))

1816 False

1817 """

1818

1819 try:

1820 xyY_from_renotation(specification)

1821 except ValueError:

1822 return False

1823 else:

1824 return True

1825

1826

1827 def bounding_hues_from_renotation(hue_and_code: ArrayLike) -> NDArrayFloat:

1828 """

1829 Return the two bounding hues from *Munsell Renotation System* data for

1830 a specified *Munsell* *Colorlab* specification hue and code.

1831

1832 Parameters

1833 ----------

1834 hue_and_code

1835 *Munsell* *Colorlab* specification hue and *Munsell* *Colorlab*

1836 specification code.

1837

1838 Returns

1839 -------

1840 :class:`numpy.NDArrayFloat`

1841 Bounding hues.

1842

1843 References

1844 ----------

1845 :cite:`Centore2014o`

1846

1847 Examples

1848 --------

1849 >>> bounding_hues_from_renotation([3.2, 4])

1850 array([[ 2.5, 4. ],

1851 [ 5. , 4. ]])

1852

1853 # Coverage Doctests

1854

1855 >>> bounding_hues_from_renotation([0.0, 1])

1856 array([[ 10., 2.],

1857 [ 10., 2.]])

1858 """

1859

1860 hue, code = as_float_array(hue_and_code)

1861

1862 hue_cw: float

1863 code_cw: float

1864 hue_ccw: float

1865 code_ccw: float

1866

1867 if hue % 2.5 == 0:

1868 if hue == 0:

1869 hue_cw = 10

1870 code_cw = (code + 1) % 10

1871 else:

1872 hue_cw = hue

1873 code_cw = code

1874 hue_ccw = hue_cw

1875 code_ccw = code_cw

1876 else:

1877 hue_cw = 2.5 * np.floor(hue / 2.5)

1878 hue_ccw = (hue_cw + 2.5) % 10

1879 if hue_ccw == 0:

1880 hue_ccw = 10

1881

1882 if hue_cw == 0:

1883 hue_cw = 10

1884 code_cw = (code + 1) % 10

1885 if code_cw == 0:

1886 code_cw = 10

1887 else:

1888 code_cw = code

1889 code_ccw = code

1890

1891 return as_float_array([(hue_cw, code_cw), (hue_ccw, code_ccw)])

1892

1893

1894 def hue_to_hue_angle(hue_and_code: ArrayLike) -> float:

1895 """

1896 Convert from *Munsell* *Colorlab* specification hue and code to hue angle

1897 in degrees.

1898

1899 Parameters

1900 ----------

1901 hue_and_code

1902 *Munsell* *Colorlab* specification hue and *Munsell* *Colorlab*

1903 specification code.

1904

1905 Returns

1906 -------

1907 :class:`float`

1908 Hue angle in degrees.

1909

1910 References

1911 ----------

1912 :cite:`Centore2014s`

1913

1914 Examples

1915 --------

1916 >>> hue_to_hue_angle([3.2, 4])

1917 65.5

1918 """

1919

1920 hue, code = as_float_array(hue_and_code)

1921

1922 single_hue = ((17 - code) % 10 + (hue / 10) - 0.5) % 10

1923

1924 hue_angle = LinearInterpolator(

1925 [0, 2, 3, 4, 5, 6, 8, 9, 10], [0, 45, 70, 135, 160, 225, 255, 315, 360]

1926 )(single_hue)

1927

1928 return as_float_scalar(hue_angle)

1929

1930

1931 def hue_angle_to_hue(hue_angle: float) -> NDArrayFloat:

1932 """

1933 Convert from hue angle in degrees to *Munsell* *Colorlab* specification hue

1934 and code.

1935

1936 Parameters

1937 ----------

1938 hue_angle

1939 Hue angle in degrees.

1940

1941 Returns

1942 -------

1943 :class:`numpy.NDArrayFloat`

1944 *Munsell* *Colorlab* specification hue and *Munsell* *Colorlab*

1945 specification code.

1946

1947 References

1948 ----------

1949 :cite:`Centore2014t`

1950

1951 Examples

1952 --------

1953 >>> hue_angle_to_hue(65.54) # doctest: +ELLIPSIS

1954 array([ 3.216, 4. ])

1955 """

1956

1957 single_hue = LinearInterpolator(

1958 [0, 45, 70, 135, 160, 225, 255, 315, 360], [0, 2, 3, 4, 5, 6, 8, 9, 10]

1959 )(hue_angle)

1960

1961 if single_hue <= 0.5:

1962 code = 7

1963 elif single_hue <= 1.5:

1964 code = 6

1965 elif single_hue <= 2.5:

1966 code = 5

1967 elif single_hue <= 3.5:

1968 code = 4

1969 elif single_hue <= 4.5:

1970 code = 3

1971 elif single_hue <= 5.5:

1972 code = 2

1973 elif single_hue <= 6.5:

1974 code = 1

1975 elif single_hue <= 7.5:

1976 code = 10

1977 elif single_hue <= 8.5:

1978 code = 9

1979 elif single_hue <= 9.5:

1980 code = 8

1981 else:

1982 code = 7

1983

1984 hue = (10 * (single_hue % 1) + 5) % 10

1985 if hue == 0:

1986 hue = 10

1987

1988 return tstack(cast("ArrayLike", [hue, code]))

1989

1990

1991 def hue_to_ASTM_hue(hue_and_code: ArrayLike) -> float:

1992 """

1993 Convert the *Munsell* *Colorlab* specification hue and code to *ASTM* hue

1994 number.

1995

1996 Parameters

1997 ----------

1998 hue_and_code

1999 *Munsell* *Colorlab* specification hue and *Munsell* *Colorlab*

2000 specification code.

2001

2002 Returns

2003 -------

2004 :class:`float`

2005 *ASTM* hue number.

2006

2007 References

2008 ----------

2009 :cite:`Centore2014k`

2010

2011 Examples

2012 --------

2013 >>> hue_to_ASTM_hue([3.2, 4]) # doctest: +ELLIPSIS

2014 33.2...

2015 """

2016

2017 hue, code = as_float_array(hue_and_code)

2018

2019 ASTM_hue = 10 * ((7 - code) % 10) + hue

2020

2021 return 100 if ASTM_hue == 0 else ASTM_hue

2022

2023

2024 def interpolation_method_from_renotation_ovoid(

2025 specification: ArrayLike,

2026) -> Literal["Linear", "Radial"] | None:

2027 """

2028 Determine the interpolation method for drawing ovoids in the *Munsell

2029 Renotation System*.

2030

2031 Determine whether to use linear or radial interpolation when drawing

2032 ovoids through data points in the *Munsell Renotation System* data from

2033 the specified *Munsell* *Colorlab* specification.

2034

2035 Parameters

2036 ----------

2037 specification

2038 *Munsell* *Colorlab* specification.

2039

2040 Returns

2041 -------

2042 :class:`str` or :py:data:`None`

2043 Interpolation method.

2044

2045 References

2046 ----------

2047 :cite:`Centore2014l`

2048

2049 Examples

2050 --------

2051 >>> interpolation_method_from_renotation_ovoid([2.5, 5.0, 12.0, 4])

2052 'Radial'

2053 """

2054

2055 specification = normalise_munsell_specification(specification)

2056

2057 interpolation_methods: Dict[int, Literal["Linear", "Radial"] | None] = {

2058 0: None,

2059 1: "Linear",

2060 2: "Radial",

2061 }

2062

2063 if is_grey_munsell_colour(specification):

2064 # No interpolation needed for grey colours.

2065 interpolation_method = 0

2066 else:

2067 hue, value, chroma, code = specification

2068

2069 attest(

2070 0 <= value <= 10,

2071 f'"{specification}" specification value must be normalised to '

2072 f"domain [0, 10]!",

2073 )

2074

2075 attest(

2076 is_integer(value),

2077 f'"{specification}" specification value must be an int!',

2078 )

2079

2080 value = round(value)

2081

2082 attest(

2083 2 <= chroma <= 50,

2084 f'"{specification}" specification chroma must be normalised to '

2085 f"domain [2, 50]!",

2086 )

2087

2088 attest(

2089 abs(2 * (chroma / 2 - round(chroma / 2))) <= THRESHOLD_INTEGER,

2090 f'"{specification}" specification chroma must be an int and multiple of 2!',

2091 )

2092

2093 chroma = 2 * round(chroma / 2)

2094

2095 interpolation_method = 0

2096

2097 # Standard Munsell Renotation System hue, no interpolation needed.

2098 if hue % 2.5 == 0:

2099 interpolation_method = 0

2100

2101 ASTM_hue = hue_to_ASTM_hue([hue, code])

2102

2103 if value == 1:

2104 if chroma == 2:

2105 if 15 < ASTM_hue < 30 or 60 < ASTM_hue < 85:

2106 interpolation_method = 2

2107 else:

2108 interpolation_method = 1

2109 elif chroma == 4:

2110 if 12.5 < ASTM_hue < 27.5 or 57.5 < ASTM_hue < 80:

2111 interpolation_method = 2

2112 else:

2113 interpolation_method = 1

2114 elif chroma == 6:

2115 interpolation_method = 2 if 55 < ASTM_hue < 80 else 1

2116 elif chroma == 8:

2117 interpolation_method = 2 if 67.5 < ASTM_hue < 77.5 else 1

2118 elif chroma >= 10:

2119 # NOTE: This condition is likely never "True" while producing a

2120 # valid "Munsell Specification" in practice: 1M iterations with

2121 # random numbers never reached this code path while producing a

2122 # valid "Munsell Specification".

2123 if 72.5 < ASTM_hue < 77.5: # pragma: no cover # noqa: SIM108

2124 interpolation_method = 2

2125 else:

2126 interpolation_method = 1

2127 else: # pragma: no cover

2128 interpolation_method = 1

2129 elif value == 2:

2130 if chroma == 2:

2131 if 15 < ASTM_hue < 27.5 or 77.5 < ASTM_hue < 80:

2132 interpolation_method = 2

2133 else:

2134 interpolation_method = 1

2135 elif chroma == 4:

2136 if 12.5 < ASTM_hue < 30 or 62.5 < ASTM_hue < 80:

2137 interpolation_method = 2

2138 else:

2139 interpolation_method = 1

2140 elif chroma == 6:

2141 if 7.5 < ASTM_hue < 22.5 or 62.5 < ASTM_hue < 80:

2142 interpolation_method = 2

2143 else:

2144 interpolation_method = 1

2145 elif chroma == 8:

2146 if 7.5 < ASTM_hue < 15 or 60 < ASTM_hue < 80:

2147 interpolation_method = 2

2148 else:

2149 interpolation_method = 1

2150 elif chroma >= 10:

2151 interpolation_method = 2 if 65 < ASTM_hue < 77.5 else 1

2152 else: # pragma: no cover

2153 interpolation_method = 1

2154 elif value == 3:

2155 if chroma == 2:

2156 if 10 < ASTM_hue < 37.5 or 65 < ASTM_hue < 85:

2157 interpolation_method = 2

2158 else:

2159 interpolation_method = 1

2160 elif chroma == 4:

2161 if 5 < ASTM_hue < 37.5 or 55 < ASTM_hue < 72.5:

2162 interpolation_method = 2

2163 else:

2164 interpolation_method = 1

2165 elif chroma in (6, 8, 10):

2166 if 7.5 < ASTM_hue < 37.5 or 57.5 < ASTM_hue < 82.5:

2167 interpolation_method = 2

2168 else:

2169 interpolation_method = 1

2170 elif chroma >= 12:

2171 if 7.5 < ASTM_hue < 42.5 or 57.5 < ASTM_hue < 80:

2172 interpolation_method = 2

2173 else:

2174 interpolation_method = 1

2175 else: # pragma: no cover

2176 interpolation_method = 1

2177 elif value == 4:

2178 if chroma in (2, 4):

2179 if 7.5 < ASTM_hue < 42.5 or 57.5 < ASTM_hue < 85:

2180 interpolation_method = 2

2181 else:

2182 interpolation_method = 1

2183 elif chroma in (6, 8):

2184 if 7.5 < ASTM_hue < 40 or 57.5 < ASTM_hue < 82.5:

2185 interpolation_method = 2

2186 else:

2187 interpolation_method = 1

2188 elif chroma >= 10:

2189 if 7.5 < ASTM_hue < 40 or 57.5 < ASTM_hue < 80:

2190 interpolation_method = 2

2191 else:

2192 interpolation_method = 1

2193 else: # pragma: no cover

2194 interpolation_method = 1

2195 elif value == 5:

2196 if chroma == 2:

2197 if 5 < ASTM_hue < 37.5 or 55 < ASTM_hue < 85:

2198 interpolation_method = 2

2199 else:

2200 interpolation_method = 1

2201 elif chroma in (4, 6, 8):

2202 if 2.5 < ASTM_hue < 42.5 or 55 < ASTM_hue < 85:

2203 interpolation_method = 2

2204 else:

2205 interpolation_method = 1

2206 elif chroma >= 10:

2207 if 2.5 < ASTM_hue < 42.5 or 55 < ASTM_hue < 82.5:

2208 interpolation_method = 2

2209 else:

2210 interpolation_method = 1

2211 else: # pragma: no cover

2212 interpolation_method = 1

2213 elif value == 6:

2214 if chroma in (2, 4):

2215 if 5 < ASTM_hue < 37.5 or 55 < ASTM_hue < 87.5:

2216 interpolation_method = 2

2217 else:

2218 interpolation_method = 1

2219 elif chroma == 6:

2220 if 5 < ASTM_hue < 42.5 or 57.5 < ASTM_hue < 87.5:

2221 interpolation_method = 2

2222 else:

2223 interpolation_method = 1

2224 elif chroma in (8, 10):

2225 if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 85:

2226 interpolation_method = 2

2227 else:

2228 interpolation_method = 1

2229 elif chroma in (12, 14):

2230 if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 82.5:

2231 interpolation_method = 2

2232 else:

2233 interpolation_method = 1

2234 elif chroma >= 16:

2235 if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 80:

2236 interpolation_method = 2

2237 else:

2238 interpolation_method = 1

2239 else: # pragma: no cover

2240 interpolation_method = 1

2241 elif value == 7:

2242 if chroma in (2, 4, 6):

2243 if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 85:

2244 interpolation_method = 2

2245 else:

2246 interpolation_method = 1

2247 elif chroma == 8:

2248 if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 82.5:

2249 interpolation_method = 2

2250 else:

2251 interpolation_method = 1

2252 elif chroma == 10:

2253 if 30 < ASTM_hue < 42.5 or 5 < ASTM_hue < 25 or 60 < ASTM_hue < 82.5:

2254 interpolation_method = 2

2255 else:

2256 interpolation_method = 1

2257 elif chroma == 12:

2258 if (

2259 30 < ASTM_hue < 42.5

2260 or 7.5 < ASTM_hue < 27.5

2261 or 80 < ASTM_hue < 82.5

2262 ):

2263 interpolation_method = 2

2264 else:

2265 interpolation_method = 1

2266 elif chroma >= 14:

2267 if 32.5 < ASTM_hue < 40 or 7.5 < ASTM_hue < 15 or 80 < ASTM_hue < 82.5:

2268 interpolation_method = 2

2269 else:

2270 interpolation_method = 1

2271 else: # pragma: no cover

2272 interpolation_method = 1

2273 elif value == 8:

2274 if chroma in (2, 4, 6, 8, 10, 12):

2275 if 5 < ASTM_hue < 40 or 60 < ASTM_hue < 85:

2276 interpolation_method = 2

2277 else:

2278 interpolation_method = 1

2279 elif chroma >= 14:

2280 if 32.5 < ASTM_hue < 40 or 5 < ASTM_hue < 15 or 60 < ASTM_hue < 85:

2281 interpolation_method = 2

2282 else:

2283 interpolation_method = 1

2284 else: # pragma: no cover

2285 interpolation_method = 1

2286 elif value == 9:

2287 if chroma in (2, 4):

2288 if 5 < ASTM_hue < 40 or 55 < ASTM_hue < 80:

2289 interpolation_method = 2

2290 else:

2291 interpolation_method = 1

2292 elif chroma in (6, 8, 10, 12, 14):

2293 interpolation_method = 2 if 5 < ASTM_hue < 42.5 else 1

2294 elif chroma >= 16:

2295 interpolation_method = 2 if 35 < ASTM_hue < 42.5 else 1

2296 else: # pragma: no cover

2297 interpolation_method = 1

2298 elif value == 10:

2299 # Ideal white, no interpolation needed.

2300 interpolation_method = 0

2301

2302 return interpolation_methods[interpolation_method]

2303

2304

2305 def xy_from_renotation_ovoid(specification: ArrayLike) -> NDArrayFloat:

2306 """

2307 Convert specified *Munsell* *Colorlab* specification to *CIE xy*

2308 chromaticity coordinates on *Munsell Renotation System* ovoid.

2309

2310 The *CIE xy* point will be on the ovoid about the achromatic point,

2311 corresponding to the *Munsell* *Colorlab* specification value and

2312 chroma.

2313

2314 Parameters

2315 ----------

2316 specification

2317 *Munsell* *Colorlab* specification.

2318

2319 Returns

2320 -------

2321 :class:`numpy.NDArrayFloat`

2322 *CIE xy* chromaticity coordinates.

2323

2324 Raises

2325 ------

2326 ValueError

2327 If an invalid interpolation method is retrieved from internal

2328 computations.

2329

2330 References

2331 ----------

2332 :cite:`Centore2014n`

2333

2334 Examples

2335 --------

2336 >>> xy_from_renotation_ovoid([2.5, 5.0, 12.0, 4])

2337 ... # doctest: +ELLIPSIS

2338 array([ 0.4333..., 0.5602...])

2339 >>> xy_from_renotation_ovoid([np.nan, 8, np.nan, np.nan])

2340 ... # doctest: +ELLIPSIS

2341 array([ 0.31006..., 0.31616...])

2342 """

2343

2344 specification = normalise_munsell_specification(specification)

2345

2346 if is_grey_munsell_colour(specification):

2347 return CCS_ILLUMINANT_MUNSELL

2348

2349 hue, value, chroma, code = specification

2350

2351 attest(

2352 1 <= value <= 9,

2353 f'"{specification}" specification value must be normalised to domain [1, 9]!',

2354 )

2355

2356 attest(

2357 is_integer(value),

2358 f'"{specification}" specification value must be an int!',

2359 )

2360

2361 value = round(value)

2362

2363 attest(

2364 2 <= chroma <= 50,

2365 f'"{specification}" specification chroma must be normalised to domain [2, 50]!',

2366 )

2367

2368 attest(

2369 abs(2 * (chroma / 2 - round(chroma / 2))) <= THRESHOLD_INTEGER,

2370 f'"{specification}" specification chroma must be an int and multiple of 2!',

2371 )

2372

2373 chroma = 2 * round(chroma / 2)

2374

2375 # Checking if renotation data is available without interpolation using

2376 # specified threshold.

2377 if (

2378 abs(hue) < THRESHOLD_INTEGER

2379 or abs(hue - 2.5) < THRESHOLD_INTEGER

2380 or abs(hue - 5) < THRESHOLD_INTEGER

2381 or abs(hue - 7.5) < THRESHOLD_INTEGER

2382 or abs(hue - 10) < THRESHOLD_INTEGER

2383 ):

2384 hue = 2.5 * round(hue / 2.5)

2385

2386 x, y, _Y = xyY_from_renotation([hue, value, chroma, code])

2387

2388 return tstack([x, y])

2389

2390 hue_code_cw, hue_code_ccw = bounding_hues_from_renotation([hue, code])

2391 hue_minus, code_minus = hue_code_cw

2392 hue_plus, code_plus = hue_code_ccw

2393

2394 x_grey, y_grey = CCS_ILLUMINANT_MUNSELL

2395

2396 specification_minus = (hue_minus, value, chroma, code_minus)

2397 x_minus, y_minus, Y_minus = xyY_from_renotation(specification_minus)

2398 rho_minus, phi_minus, _z_minus = cartesian_to_cylindrical(

2399 [x_minus - x_grey, y_minus - y_grey, Y_minus]

2400 )

2401 phi_minus = np.degrees(phi_minus)

2402

2403 specification_plus = (hue_plus, value, chroma, code_plus)

2404 x_plus, y_plus, Y_plus = xyY_from_renotation(specification_plus)

2405 rho_plus, phi_plus, _z_plus = cartesian_to_cylindrical(

2406 [x_plus - x_grey, y_plus - y_grey, Y_plus]

2407 )

2408 phi_plus = np.degrees(phi_plus)

2409

2410 hue_angle_lower = hue_to_hue_angle([hue_minus, code_minus])

2411 hue_angle = hue_to_hue_angle([hue, code])

2412 hue_angle_upper = hue_to_hue_angle([hue_plus, code_plus])

2413

2414 if phi_minus - phi_plus > 180:

2415 phi_plus += 360

2416

2417 if hue_angle_lower == 0:

2418 hue_angle_lower = 360

2419

2420 if hue_angle_lower > hue_angle_upper:

2421 if hue_angle_lower > hue_angle:

2422 hue_angle_lower -= 360

2423 else:

2424 hue_angle_lower -= 360

2425 hue_angle -= 360

2426

2427 interpolation_method = interpolation_method_from_renotation_ovoid(specification)

2428

2429 attest(

2430 interpolation_method is not None,

2431 f'Interpolation method must be one of: "{"Linear, Radial"}"',

2432 )

2433

2434 hue_angle_lower_upper = np.squeeze([hue_angle_lower, hue_angle_upper])

2435

2436 if interpolation_method == "Linear":

2437 x_minus_plus = np.squeeze([x_minus, x_plus])

2438 y_minus_plus = np.squeeze([y_minus, y_plus])

2439

2440 x = LinearInterpolator(hue_angle_lower_upper, x_minus_plus)(hue_angle)

2441 y = LinearInterpolator(hue_angle_lower_upper, y_minus_plus)(hue_angle)

2442 elif interpolation_method == "Radial":

2443 rho_minus_plus = np.squeeze([rho_minus, rho_plus])

2444 phi_minus_plus = np.squeeze([phi_minus, phi_plus])

2445

2446 rho = as_float_array(

2447 LinearInterpolator(hue_angle_lower_upper, rho_minus_plus)(hue_angle)

2448 )

2449 phi = as_float_array(

2450 LinearInterpolator(hue_angle_lower_upper, phi_minus_plus)(hue_angle)

2451 )

2452

2453 rho_phi = np.squeeze([rho, np.radians(phi)])

2454 x, y = tsplit(polar_to_cartesian(rho_phi) + tstack([x_grey, y_grey]))

2455

2456 return tstack([x, y])

2457

2458

2459 def LCHab_to_munsell_specification(LCHab: ArrayLike) -> NDArrayFloat:

2460 """

2461 Convert from *CIE L\\*C\\*Hab* colourspace to approximate *Munsell*

2462 *Colorlab* specification.

2463

2464 Parameters

2465 ----------

2466 LCHab

2467 *CIE L\\*C\\*Hab* colourspace array.

2468

2469 Returns

2470 -------

2471 :class:`numpy.NDArrayFloat`

2472 *Munsell* *Colorlab* specification.

2473

2474 References

2475 ----------

2476 :cite:`Centore2014u`

2477

2478 Examples

2479 --------

2480 >>> LCHab = np.array([100, 17.50664796, 244.93046842])

2481 >>> LCHab_to_munsell_specification(LCHab) # doctest: +ELLIPSIS

2482 array([ 8.0362412..., 10. , 3.5013295..., 1. ])

2483 """

2484

2485 L, C, Hab = tsplit(LCHab)

2486

2487 if Hab == 0:

2488 code = 8

2489 elif Hab <= 36:

2490 code = 7

2491 elif Hab <= 72:

2492 code = 6

2493 elif Hab <= 108:

2494 code = 5

2495 elif Hab <= 144:

2496 code = 4

2497 elif Hab <= 180:

2498 code = 3

2499 elif Hab <= 216:

2500 code = 2

2501 elif Hab <= 252:

2502 code = 1

2503 elif Hab <= 288:

2504 code = 10

2505 elif Hab <= 324:

2506 code = 9

2507 else:

2508 code = 8

2509

2510 hue = LinearInterpolator([0, 36], [0, 10])(Hab % 36)

2511 if hue == 0:

2512 hue = 10

2513

2514 value = L / 10

2515 chroma = C / 5

2516

2517 return tstack(cast("ArrayLike", [hue, value, chroma, code]))

2518

2519

2520 def maximum_chroma_from_renotation(hue_and_value_and_code: ArrayLike) -> float:

2521 """

2522 Return the maximum *Munsell* chroma from *Munsell Renotation System*

2523 data using the specified *Munsell* *Colorlab* specification hue, value, and

2524 code.

2525

2526 Parameters

2527 ----------

2528 hue_and_value_and_code

2529 *Munsell* *Colorlab* specification hue, value, and code.

2530

2531 Returns

2532 -------

2533 :class:`float`

2534 Maximum chroma.

2535

2536 References

2537 ----------

2538 :cite:`Centore2014r`

2539

2540 Examples

2541 --------

2542 >>> maximum_chroma_from_renotation([2.5, 5, 5])

2543 14.0

2544 """

2545

2546 hue, value, code = as_float_array(hue_and_value_and_code)

2547

2548 # Ideal white, no chroma.

2549 if value >= 9.99:

2550 return 0

2551

2552 attest(

2553 1 <= value <= 10,

2554 f'"{value}" value must be normalised to domain [1, 10]!',

2555 )

2556

2557 if value % 1 == 0:

2558 value_minus = value

2559 value_plus = value

2560 else:

2561 value_minus = np.floor(value)

2562 value_plus = value_minus + 1

2563

2564 hue_code_cw, hue_code_ccw = bounding_hues_from_renotation([hue, code])

2565 hue_cw, code_cw = hue_code_cw

2566 hue_ccw, code_ccw = hue_code_ccw

2567

2568 maximum_chromas = _munsell_maximum_chromas_from_renotation()

2569 specification_for_indexes = [chroma[0] for chroma in maximum_chromas]

2570

2571 ma_limit_mcw = maximum_chromas[

2572 specification_for_indexes.index(

2573 (hue_cw, value_minus, code_cw) # pyright: ignore

2574 )

2575 ][1]

2576 ma_limit_mccw = maximum_chromas[

2577 specification_for_indexes.index(

2578 (hue_ccw, value_minus, code_ccw) # pyright: ignore

2579 )

2580 ][1]

2581

2582 if value_plus <= 9:

2583 ma_limit_pcw = maximum_chromas[

2584 specification_for_indexes.index(

2585 (hue_cw, value_plus, code_cw) # pyright: ignore

2586 )

2587 ][1]

2588 ma_limit_pccw = maximum_chromas[

2589 specification_for_indexes.index(

2590 (hue_ccw, value_plus, code_ccw) # pyright: ignore

2591 )

2592 ][1]

2593 max_chroma = np.min([ma_limit_mcw, ma_limit_mccw, ma_limit_pcw, ma_limit_pccw])

2594 else:

2595 L = as_float_scalar(luminance_ASTMD1535(value))

2596 L9 = as_float_scalar(luminance_ASTMD1535(9))

2597 L10 = as_float_scalar(luminance_ASTMD1535(10))

2598

2599 max_chroma = np.min(

2600 [

2601 LinearInterpolator([L9, L10], [ma_limit_mcw, 0])(L),

2602 LinearInterpolator([L9, L10], [ma_limit_mccw, 0])(L),

2603 ]

2604 )

2605

2606 return as_float_scalar(max_chroma)

2607

2608

2609 def munsell_specification_to_xy(specification: ArrayLike) -> NDArrayFloat:

2610 """

2611 Convert the specified *Munsell* *Colorlab* specification to *CIE xy*

2612 chromaticity coordinates by interpolating over *Munsell Renotation

2613 System* data.

2614

2615 Parameters

2616 ----------

2617 specification

2618 *Munsell* *Colorlab* specification.

2619

2620 Returns

2621 -------

2622 :class:`numpy.NDArrayFloat`

2623 *CIE xy* chromaticity coordinates.

2624

2625 References

2626 ----------

2627 :cite:`Centore2014q`

2628

2629 Examples

2630 --------

2631 >>> munsell_specification_to_xy([2.1, 8.0, 17.9, 4])

2632 ... # doctest: +ELLIPSIS

2633 array([ 0.4400632..., 0.5522428...])

2634 >>> munsell_specification_to_xy([np.nan, 8, np.nan, np.nan])

2635 ... # doctest: +ELLIPSIS

2636 array([ 0.31006..., 0.31616...])

2637 """

2638

2639 specification = normalise_munsell_specification(specification)

2640

2641 if is_grey_munsell_colour(specification):

2642 return CCS_ILLUMINANT_MUNSELL

2643

2644 hue, value, chroma, code = specification

2645

2646 attest(

2647 0 <= value <= 10,

2648 f'"{specification}" specification value must be normalised to domain [0, 10]!',

2649 )

2650

2651 attest(

2652 is_integer(value),

2653 f'"{specification}" specification value must be an int!',

2654 )

2655

2656 value = round(value)

2657

2658 if chroma % 2 == 0:

2659 chroma_minus = chroma_plus = chroma

2660 else:

2661 chroma_minus = 2 * np.floor(chroma / 2)

2662 chroma_plus = chroma_minus + 2

2663

2664 if chroma_minus == 0:

2665 # Smallest chroma ovoid collapses to illuminant chromaticity

2666 # coordinates.

2667 x_minus, y_minus = CCS_ILLUMINANT_MUNSELL

2668 else:

2669 x_minus, y_minus = xy_from_renotation_ovoid([hue, value, chroma_minus, code])

2670

2671 x_plus, y_plus = xy_from_renotation_ovoid([hue, value, chroma_plus, code])

2672

2673 if chroma_minus == chroma_plus:

2674 x = x_minus

2675 y = y_minus

2676 else:

2677 chroma_minus_plus = np.squeeze([chroma_minus, chroma_plus])

2678 x_minus_plus = np.squeeze([x_minus, x_plus])

2679 y_minus_plus = np.squeeze([y_minus, y_plus])

2680

2681 x = LinearInterpolator(chroma_minus_plus, x_minus_plus)(chroma)

2682 y = LinearInterpolator(chroma_minus_plus, y_minus_plus)(chroma)

2683

2684 return tstack([x, y])