Coverage for colour/notation/munsell.py: 100%

683 statements  

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

1""" 

2Munsell Renotation System 

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

4 

5Define 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 

35Notes 

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 

54References 

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/\ 

110MunsellAndKubelkaMunkToolbox/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 

117from __future__ import annotations 

118 

119import re 

120import typing 

121 

122import numpy as np 

123 

124from 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) 

134from colour.colorimetry import CCS_ILLUMINANTS, luminance_ASTMD1535 

135from colour.constants import ( 

136 PATTERN_FLOATING_POINT_NUMBER, 

137 THRESHOLD_INTEGER, 

138 TOLERANCE_ABSOLUTE_DEFAULT, 

139 TOLERANCE_RELATIVE_DEFAULT, 

140) 

141 

142if 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 

155from colour.hints import ArrayLike, NDArrayFloat, cast 

156from colour.models import Lab_to_LCHab # pyright: ignore 

157from colour.models import XYZ_to_Lab, XYZ_to_xy, xyY_to_XYZ 

158from colour.notation import MUNSELL_COLOURS_ALL 

159from 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) 

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

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

239MUNSELL_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 

246MUNSELL_GRAY_FORMAT: str = "N{0}" 

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

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

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

250 

251MUNSELL_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 

266ILLUMINANT_NAME_MUNSELL: str = "C" 

267CCS_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 

282def _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 

329def _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 

362def _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 

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

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

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

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

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

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

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

752MUNSELL_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) 

763MUNSELL_VALUE_METHODS.__doc__ = """ 

764Supported *Munsell* value computation methods. 

765 

766References 

767---------- 

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

769 

770Aliases: 

771 

772- 'astm2008': 'ASTM D1535' 

773""" 

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

775 

776 

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

850def _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 

863def _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 

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

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

1049def _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 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2609def 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])