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
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
1"""
2Munsell Renotation System
3=========================
5Define objects for *Munsell Renotation System* computations.
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`
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.
47 *ASTM D1535-08e1* states that::
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.
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"""
117from __future__ import annotations
119import re
120import typing
122import numpy as np
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)
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 )
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
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"
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]
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)
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}"
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)
266ILLUMINANT_NAME_MUNSELL: str = "C"
267CCS_ILLUMINANT_MUNSELL: NDArrayFloat = CCS_ILLUMINANTS[
268 "CIE 1931 2 Degree Standard Observer"
269][ILLUMINANT_NAME_MUNSELL]
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)
282def _munsell_specifications() -> NDArrayFloat:
283 """
284 Return the *Munsell Renotation System* specifications and cache them if
285 not already existing.
287 The *Munsell Renotation System* data is stored in
288 :attr:`colour.notation.MUNSELL_COLOURS` attribute in a 2 columns form::
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 )
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:
302 ('2.5GY', 0.2, 2.0) --> (2.5, 0.2, 2.0, 4)
304 Returns
305 -------
306 :class:`numpy.NDArrayFloat`
307 *Munsell Renotation System* specifications.
308 """
310 global _CACHE_MUNSELL_SPECIFICATIONS # noqa: PLW0602
312 if is_caching_enabled() and "All" in _CACHE_MUNSELL_SPECIFICATIONS:
313 return _CACHE_MUNSELL_SPECIFICATIONS["All"]
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 )
324 _CACHE_MUNSELL_SPECIFICATIONS["All"] = munsell_specifications
326 return munsell_specifications
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.
334 Returns
335 -------
336 :class:`colour.Extrapolator`
337 *Munsell* value interpolator for the *ASTM D1535-08e1* method.
338 """
340 global _CACHE_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR # noqa: PLW0602
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 ]
349 munsell_values = np.arange(0, 10, 0.001)
350 interpolator = LinearInterpolator(
351 luminance_ASTMD1535(munsell_values), munsell_values
352 )
353 extrapolator = Extrapolator(interpolator)
355 _CACHE_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR["ASTM D1535-08 Interpolator"] = (
356 extrapolator
357 )
359 return extrapolator
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.
369 Returns
370 -------
371 :class:`tuple`
372 Maximum *Munsell* chromas.
373 """
375 global _CACHE_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION # noqa: PLW0602
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 ]
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])
395 chromas[index] = cast("float", chroma)
397 maximum_chromas_from_renotation = tuple(chromas.items())
399 _CACHE_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION[
400 "Maximum Chromas From Renotation"
401 ] = maximum_chromas_from_renotation
403 return maximum_chromas_from_renotation
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.
413 Parameters
414 ----------
415 Y
416 *Luminance* :math:`Y`.
418 Returns
419 -------
420 :class:`np.float` or :class:`numpy.NDArrayFloat`
421 *Munsell* value :math:`V`.
423 Notes
424 -----
425 +------------+-----------------------+---------------+
426 | **Domain** | **Scale - Reference** | **Scale - 1** |
427 +============+=======================+===============+
428 | ``Y`` | 100 | 1 |
429 +------------+-----------------------+---------------+
431 +------------+-----------------------+---------------+
432 | **Range** | **Scale - Reference** | **Scale - 1** |
433 +============+=======================+===============+
434 | ``V`` | 10 | 1 |
435 +------------+-----------------------+---------------+
437 References
438 ----------
439 :cite:`Wikipedia2007c`
441 Examples
442 --------
443 >>> munsell_value_Priest1920(12.23634268) # doctest: +ELLIPSIS
444 3.4980484...
445 """
447 Y = to_domain_100(Y)
449 V = 10 * np.sqrt(Y / 100)
451 return as_float(from_range_10(V))
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.
461 Parameters
462 ----------
463 Y
464 *Luminance* :math:`Y`.
466 Returns
467 -------
468 :class:`np.float` or :class:`numpy.NDArrayFloat`
469 *Munsell* value :math:`V`.
471 Notes
472 -----
473 +------------+-----------------------+---------------+
474 | **Domain** | **Scale - Reference** | **Scale - 1** |
475 +============+=======================+===============+
476 | ``Y`` | 100 | 1 |
477 +------------+-----------------------+---------------+
479 +------------+-----------------------+---------------+
480 | **Range** | **Scale - Reference** | **Scale - 1** |
481 +============+=======================+===============+
482 | ``V`` | 10 | 1 |
483 +------------+-----------------------+---------------+
485 References
486 ----------
487 :cite:`Wikipedia2007c`
489 Examples
490 --------
491 >>> munsell_value_Munsell1933(12.23634268) # doctest: +ELLIPSIS
492 4.1627702...
493 """
495 Y = to_domain_100(Y)
497 V = np.sqrt(1.4742 * Y - 0.004743 * (Y * Y))
499 return as_float(from_range_10(V))
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.
507 Parameters
508 ----------
509 Y
510 *Luminance* :math:`Y`.
512 Returns
513 -------
514 :class:`np.float` or :class:`numpy.NDArrayFloat`
515 *Munsell* value :math:`V`.
517 Notes
518 -----
519 +------------+-----------------------+---------------+
520 | **Domain** | **Scale - Reference** | **Scale - 1** |
521 +============+=======================+===============+
522 | ``Y`` | 100 | 1 |
523 +------------+-----------------------+---------------+
525 +------------+-----------------------+---------------+
526 | **Range** | **Scale - Reference** | **Scale - 1** |
527 +============+=======================+===============+
528 | ``V`` | 10 | 1 |
529 +------------+-----------------------+---------------+
531 References
532 ----------
533 :cite:`Wikipedia2007c`
535 Examples
536 --------
537 >>> munsell_value_Moon1943(12.23634268) # doctest: +ELLIPSIS
538 4.0688120...
539 """
541 Y = to_domain_100(Y)
543 V = 1.4 * spow(Y, 0.426)
545 return as_float(from_range_10(V))
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.
555 Parameters
556 ----------
557 Y
558 *Luminance* :math:`Y`.
560 Returns
561 -------
562 :class:`np.float` or :class:`numpy.NDArrayFloat`
563 *Munsell* value :math:`V`.
565 Notes
566 -----
567 +------------+-----------------------+---------------+
568 | **Domain** | **Scale - Reference** | **Scale - 1** |
569 +============+=======================+===============+
570 | ``Y`` | 100 | 1 |
571 +------------+-----------------------+---------------+
573 +------------+-----------------------+---------------+
574 | **Range** | **Scale - Reference** | **Scale - 1** |
575 +============+=======================+===============+
576 | ``V`` | 10 | 1 |
577 +------------+-----------------------+---------------+
579 References
580 ----------
581 :cite:`Wikipedia2007c`
583 Examples
584 --------
585 >>> munsell_value_Saunderson1944(12.23634268) # doctest: +ELLIPSIS
586 4.0444736...
587 """
589 Y = to_domain_100(Y)
591 V = 2.357 * spow(Y, 0.343) - 1.52
593 return as_float(from_range_10(V))
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.
601 Parameters
602 ----------
603 Y
604 *Luminance* :math:`Y`.
606 Returns
607 -------
608 :class:`np.float` or :class:`numpy.NDArrayFloat`
609 *Munsell* value :math:`V`.
611 Notes
612 -----
613 +------------+-----------------------+---------------+
614 | **Domain** | **Scale - Reference** | **Scale - 1** |
615 +============+=======================+===============+
616 | ``Y`` | 100 | 1 |
617 +------------+-----------------------+---------------+
619 +------------+-----------------------+---------------+
620 | **Range** | **Scale - Reference** | **Scale - 1** |
621 +============+=======================+===============+
622 | ``V`` | 10 | 1 |
623 +------------+-----------------------+---------------+
625 References
626 ----------
627 :cite:`Wikipedia2007c`
629 Examples
630 --------
631 >>> munsell_value_Ladd1955(12.23634268) # doctest: +ELLIPSIS
632 4.0511633...
633 """
635 Y = to_domain_100(Y)
637 V = 2.468 * spow(Y, 1 / 3) - 1.636
639 return as_float(from_range_10(V))
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.
649 Parameters
650 ----------
651 Y
652 *Luminance* :math:`Y`.
654 Returns
655 -------
656 :class:`np.float` or :class:`numpy.NDArrayFloat`
657 *Munsell* value :math:`V`.
659 Notes
660 -----
661 +------------+-----------------------+---------------+
662 | **Domain** | **Scale - Reference** | **Scale - 1** |
663 +============+=======================+===============+
664 | ``Y`` | 100 | 1 |
665 +------------+-----------------------+---------------+
667 +------------+-----------------------+---------------+
668 | **Range** | **Scale - Reference** | **Scale - 1** |
669 +============+=======================+===============+
670 | ``V`` | 10 | 1 |
671 +------------+-----------------------+---------------+
673 References
674 ----------
675 :cite:`ASTMInternational1989a`
677 Examples
678 --------
679 >>> munsell_value_McCamy1987(12.23634268) # doctest: +ELLIPSIS
680 4.0814348...
681 """
683 Y = to_domain_100(Y)
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 )
698 return as_float(from_range_10(V))
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.
708 Parameters
709 ----------
710 Y
711 *Luminance* :math:`Y`.
713 Returns
714 -------
715 :class:`np.float` or :class:`numpy.NDArrayFloat`
716 *Munsell* value :math:`V`.
718 Notes
719 -----
720 +------------+-----------------------+---------------+
721 | **Domain** | **Scale - Reference** | **Scale - 1** |
722 +============+=======================+===============+
723 | ``Y`` | 100 | 1 |
724 +------------+-----------------------+---------------+
726 +------------+-----------------------+---------------+
727 | **Range** | **Scale - Reference** | **Scale - 1** |
728 +============+=======================+===============+
729 | ``V`` | 10 | 1 |
730 +------------+-----------------------+---------------+
732 - The *Munsell* value computation with *ASTM D1535-08e1* method is
733 only defined for domain [0, 100].
735 References
736 ----------
737 :cite:`ASTMInternational1989a`
739 Examples
740 --------
741 >>> munsell_value_ASTMD1535(12.23634268) # doctest: +ELLIPSIS
742 4.0824437...
743 """
745 Y = to_domain_100(Y)
747 V = _munsell_value_ASTMD1535_interpolator()(Y)
749 return as_float(from_range_10(V))
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.
766References
767----------
768:cite:`ASTMInternational1989a`, :cite:`Wikipedia2007c`
770Aliases:
772- 'astm2008': 'ASTM D1535'
773"""
774MUNSELL_VALUE_METHODS["astm2008"] = MUNSELL_VALUE_METHODS["ASTM D1535"]
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.
796 Parameters
797 ----------
798 Y
799 *Luminance* :math:`Y`.
800 method
801 Computation method.
803 Returns
804 -------
805 :class:`np.float` or :class:`numpy.NDArrayFloat`
806 *Munsell* value :math:`V`.
808 Notes
809 -----
810 +------------+-----------------------+---------------+
811 | **Domain** | **Scale - Reference** | **Scale - 1** |
812 +============+=======================+===============+
813 | ``Y`` | 100 | 1 |
814 +------------+-----------------------+---------------+
816 +------------+-----------------------+---------------+
817 | **Range** | **Scale - Reference** | **Scale - 1** |
818 +============+=======================+===============+
819 | ``V`` | 10 | 1 |
820 +------------+-----------------------+---------------+
822 References
823 ----------
824 :cite:`ASTMInternational1989a`, :cite:`Wikipedia2007c`
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 """
845 method = validate_method(method, tuple(MUNSELL_VALUE_METHODS))
847 return MUNSELL_VALUE_METHODS[method](Y)
850def _munsell_scale_factor() -> NDArrayFloat:
851 """
852 Return the domain-range scale factor for the *Munsell Renotation System*.
854 Returns
855 -------
856 :class:`numpy.NDArrayFloat`
857 Domain-range scale factor for the *Munsell Renotation System*.
858 """
860 return np.array([10, 10, 50 if get_domain_range_scale() == "1" else 2, 10])
863def _munsell_specification_to_xyY(specification: ArrayLike) -> NDArrayFloat:
864 """
865 Convert from *Munsell* *Colorlab* specification to *CIE xyY* colourspace.
867 Parameters
868 ----------
869 specification
870 *Munsell* *Colorlab* specification.
872 Returns
873 -------
874 :class:`numpy.NDArrayFloat`
875 *CIE xyY* colourspace array.
876 """
878 specification = normalise_munsell_specification(specification)
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)
888 attest(
889 0 <= hue <= 10,
890 f'"{specification}" specification hue must be normalised to '
891 f"domain [0, 10]!",
892 )
894 attest(
895 0 <= value <= 10,
896 f'"{specification}" specification value must be normalised to '
897 f"domain [0, 10]!",
898 )
900 with domain_range_scale("ignore"):
901 Y = luminance_ASTMD1535(value)
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
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))
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))
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)
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])
935 x = as_float(LinearInterpolator(Y_minus_plus, x_minus_plus)(Y))
936 y = as_float(LinearInterpolator(Y_minus_plus, y_minus_plus)(Y))
938 Y = from_range_1(Y / 100)
940 return tstack([x, y, Y])
943def munsell_specification_to_xyY(specification: ArrayLike) -> NDArrayFloat:
944 """
945 Convert specified *Munsell* *Colorlab* specification to *CIE xyY*
946 colourspace.
948 Parameters
949 ----------
950 specification
951 *Munsell* *Colorlab* specification.
953 Returns
954 -------
955 :class:`numpy.NDArrayFloat`
956 *CIE xyY* colourspace array.
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 +-------------------+-----------------------+---------------+
972 +-------------------+-----------------------+---------------+
973 | **Range** | **Scale - Reference** | **Scale - 1** |
974 +===================+=======================+===============+
975 | ``xyY`` | 1 | 1 |
976 +-------------------+-----------------------+---------------+
978 References
979 ----------
980 :cite:`Centore2014m`
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 """
992 specification = as_float_array(specification)
993 shape = list(specification.shape)
995 xyY = [_munsell_specification_to_xyY(a) for a in np.reshape(specification, (-1, 4))]
997 shape[-1] = 3
999 return np.reshape(as_float_array(xyY), shape)
1002def munsell_colour_to_xyY(munsell_colour: ArrayLike) -> Range1:
1003 """
1004 Convert the specified *Munsell* colour to *CIE xyY* colourspace.
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.
1012 Returns
1013 -------
1014 :class:`numpy.NDArrayFloat`
1015 *CIE xyY* colourspace array.
1017 Notes
1018 -----
1019 +-----------+-----------------------+---------------+
1020 | **Range** | **Scale - Reference** | **Scale - 1** |
1021 +===========+=======================+===============+
1022 | ``xyY`` | 1 | 1 |
1023 +-----------+-----------------------+---------------+
1025 References
1026 ----------
1027 :cite:`Centorea`, :cite:`Centore2012a`
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 """
1037 munsell_colour = np.array(munsell_colour)
1038 shape = list(munsell_colour.shape)
1040 specification = np.array(
1041 [munsell_colour_to_munsell_specification(a) for a in np.ravel(munsell_colour)]
1042 )
1044 return munsell_specification_to_xyY(
1045 from_range_10(np.reshape(specification, (*shape, 4)), _munsell_scale_factor())
1046 )
1049def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat:
1050 """
1051 Convert from *CIE xyY* colourspace to *Munsell* *Colorlab*
1052 specification.
1054 Parameters
1055 ----------
1056 xyY
1057 *CIE xyY* colourspace array.
1059 Returns
1060 -------
1061 :class:`numpy.NDArrayFloat`
1062 *Munsell* *Colorlab* specification.
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 """
1074 xyY = as_float_array(xyY)
1076 x, y, Y = tsplit(xyY)
1077 Y = to_domain_1(Y)
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 )
1085 with domain_range_scale("ignore"):
1086 value = munsell_value_ASTMD1535(Y * 100)
1088 if is_integer(value):
1089 value = np.around(value)
1091 with domain_range_scale("ignore"):
1092 x_center, y_center, Y_center = tsplit(_munsell_specification_to_xyY(value))
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)
1099 grey_threshold = THRESHOLD_INTEGER
1101 if rho_input < grey_threshold:
1102 return from_range_10(normalise_munsell_specification(value))
1104 XYZ = xyY_to_XYZ(xyY)
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])
1110 with sdiv_mode():
1111 XYZ_r = np.array([(1 / Y_r) * X_r, 1, (1 / Y_r) * Z_r])
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 ]
1125 convergence_threshold = THRESHOLD_INTEGER / 1e4
1126 iterations_maximum = 64
1127 iterations = 0
1129 while iterations <= iterations_maximum:
1130 iterations += 1
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])
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
1146 with domain_range_scale("ignore"):
1147 x_current, y_current, _Y_current = tsplit(
1148 _munsell_specification_to_xyY(specification_current)
1149 )
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
1161 phi_differences_data = [phi_current_difference]
1162 hue_angles_differences_data = [0]
1163 hue_angles = [hue_angle_current]
1165 iterations_maximum_inner = 16
1166 iterations_inner = 0
1167 extrapolate = False
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
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
1186 raise RuntimeError( # pragma: no cover
1187 error
1188 )
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
1199 hue_inner, code_inner = hue_angle_to_hue(hue_angle_inner)
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 )
1211 if len(phi_differences_data) >= 2:
1212 extrapolate = True
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
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)
1227 phi_differences = np.array(phi_differences_data)
1228 hue_angles_differences = np.array(hue_angles_differences_data)
1230 phi_differences_indexes = phi_differences.argsort()
1232 phi_differences = phi_differences[phi_differences_indexes]
1233 hue_angles_differences = hue_angles_differences[phi_differences_indexes]
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 )
1243 hue_new, code_new = hue_angle_to_hue(hue_angle_new)
1244 specification_current = [hue_new, value, chroma_current, code_new]
1246 with domain_range_scale("ignore"):
1247 x_current, y_current, _Y_current = _munsell_specification_to_xyY(
1248 specification_current
1249 )
1251 chroma_scale = 50 if get_domain_range_scale() == "1" else 2
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 )
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 )
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
1278 with domain_range_scale("ignore"):
1279 x_current, y_current, _Y_current = _munsell_specification_to_xyY(
1280 specification_current
1281 )
1283 rho_current, phi_current, _z_current = cartesian_to_cylindrical(
1284 [x_current - x_center, y_current - y_center, Y_center]
1285 )
1287 rho_bounds_data = [rho_current]
1288 chroma_bounds_data = [chroma_current]
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
1295 if iterations_inner > iterations_maximum_inner:
1296 error = "Maximum inner iterations count reached without convergence!"
1298 raise RuntimeError(error)
1300 with sdiv_mode():
1301 chroma_inner = (
1302 (rho_input / rho_current) ** iterations_inner
1303 ) * chroma_current
1305 if chroma_inner > chroma_maximum:
1306 chroma_inner = specification_current[2] = chroma_maximum
1308 specification_inner = [
1309 hue_current,
1310 value,
1311 chroma_inner,
1312 code_current,
1313 ]
1315 with domain_range_scale("ignore"):
1316 x_inner, y_inner, _Y_inner = _munsell_specification_to_xyY(
1317 specification_inner
1318 )
1320 rho_inner, phi_inner, _z_inner = cartesian_to_cylindrical(
1321 [x_inner - x_center, y_inner - y_center, Y_center]
1322 )
1324 rho_bounds_data.append(rho_inner)
1325 chroma_bounds_data.append(chroma_inner)
1327 rho_bounds = np.array(rho_bounds_data)
1328 chroma_bounds = np.array(chroma_bounds_data)
1330 rhos_bounds_indexes = rho_bounds.argsort()
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)
1336 specification_current = [hue_current, value, chroma_new, code_current]
1338 with domain_range_scale("ignore"):
1339 x_current, y_current, _Y_current = _munsell_specification_to_xyY(
1340 specification_current
1341 )
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 )
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
1358 raise RuntimeError( # pragma: no cover
1359 error
1360 )
1363def xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat:
1364 """
1365 Convert from *CIE xyY* colourspace to *Munsell* *Colorlab*
1366 specification.
1368 Parameters
1369 ----------
1370 xyY
1371 *CIE xyY* colourspace array.
1373 Returns
1374 -------
1375 :class:`numpy.NDArrayFloat`
1376 *Munsell* *Colorlab* specification.
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.
1387 Notes
1388 -----
1389 +-------------------+-----------------------+---------------+
1390 | **Domain** | **Scale - Reference** | **Scale - 1** |
1391 +===================+=======================+===============+
1392 | ``xyY`` | 1 | 1 |
1393 +-------------------+-----------------------+---------------+
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 +-------------------+-----------------------+---------------+
1407 References
1408 ----------
1409 :cite:`Centore2014p`
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 """
1418 xyY = as_float_array(xyY)
1419 shape = list(xyY.shape)
1421 specification = [_xyY_to_munsell_specification(a) for a in np.reshape(xyY, (-1, 3))]
1423 shape[-1] = 4
1425 return np.reshape(as_float_array(specification), shape)
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.
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.
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.
1455 Notes
1456 -----
1457 +------------+-----------------------+---------------+
1458 | **Domain** | **Scale - Reference** | **Scale - 1** |
1459 +============+=======================+===============+
1460 | ``xyY`` | 1 | 1 |
1461 +------------+-----------------------+---------------+
1463 References
1464 ----------
1465 :cite:`Centorea`, :cite:`Centore2012a`
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 """
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)
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 )
1490 return str(munsell_colour) if shape == [4] else munsell_colour
1493def parse_munsell_colour(munsell_colour: str) -> NDArrayFloat:
1494 """
1495 Parse specified *Munsell* colour and return an intermediate *Munsell*
1496 *Colorlab* specification.
1498 Parameters
1499 ----------
1500 munsell_colour
1501 *Munsell* colour.
1503 Returns
1504 -------
1505 :class:`numpy.NDArrayFloat`
1506 Intermediate *Munsell* *Colorlab* specification.
1508 Raises
1509 ------
1510 ValueError
1511 If the specified specification is not a valid *Munsell Renotation
1512 System* colour specification.
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 """
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 )
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 )
1544 error = (
1545 f'"{munsell_colour}" is not a valid "Munsell Renotation System" '
1546 f"colour specification!"
1547 )
1549 raise ValueError(error)
1552def is_grey_munsell_colour(specification: ArrayLike) -> bool:
1553 """
1554 Determine whether the specified *Munsell* *Colorlab* specification
1555 represents a grey colour.
1557 Parameters
1558 ----------
1559 specification
1560 *Munsell* *Colorlab* specification.
1562 Returns
1563 -------
1564 :class:`bool`
1565 Whether the specification represents a grey colour.
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 """
1575 specification = as_float_array(specification)
1577 specification = np.squeeze(specification[~np.isnan(specification)])
1579 return is_numeric(as_float(specification))
1582def normalise_munsell_specification(specification: ArrayLike) -> NDArrayFloat:
1583 """
1584 Normalise the specified *Munsell* *Colorlab* specification.
1586 Parameters
1587 ----------
1588 specification
1589 *Munsell* *Colorlab* specification to be normalised.
1591 Returns
1592 -------
1593 :class:`numpy.NDArrayFloat`
1594 Normalised *Munsell* *Colorlab* specification.
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 """
1604 specification = as_float_array(specification)
1606 if is_grey_munsell_colour(specification):
1607 return specification * np.array([np.nan, 1, np.nan, np.nan])
1609 hue, value, chroma, code = specification
1611 if hue == 0:
1612 # 0YR is equivalent to 10R.
1613 hue, code = 10, (code + 1) % 10
1615 if chroma == 0:
1616 return tstack([np.nan, value, np.nan, np.nan])
1618 return tstack([hue, value, chroma, code])
1621def munsell_colour_to_munsell_specification(
1622 munsell_colour: str,
1623) -> NDArrayFloat:
1624 """
1625 Convert from *Munsell* colour notation to *Munsell* *Colorlab*
1626 specification.
1628 Parameters
1629 ----------
1630 munsell_colour
1631 *Munsell* colour notation.
1633 Returns
1634 -------
1635 :class:`numpy.NDArrayFloat`
1636 *Munsell* *Colorlab* specification as a 4-element array containing hue,
1637 value, chroma, and code values.
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 """
1647 return normalise_munsell_specification(parse_munsell_colour(munsell_colour))
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.
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.
1672 Returns
1673 -------
1674 :class:`str`
1675 *Munsell* colour notation.
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 """
1685 hue, value, chroma, code = tsplit(normalise_munsell_specification(specification))
1687 if is_grey_munsell_colour(specification):
1688 return MUNSELL_GRAY_EXTENDED_FORMAT.format(value, value_decimals)
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 )
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 )
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 )
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 )
1717 if value == 0:
1718 return MUNSELL_GRAY_EXTENDED_FORMAT.format(value, value_decimals)
1720 hue_letter = MUNSELL_HUE_LETTER_CODES.first_key_from_value(code)
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 )
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.
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.
1753 Returns
1754 -------
1755 :class:`numpy.NDArrayFloat`
1756 *CIE xyY* colourspace vector.
1758 Raises
1759 ------
1760 ValueError
1761 If the specified specification does not exist in the *Munsell
1762 Renotation System* data.
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 """
1770 specification = normalise_munsell_specification(specification)
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 )
1785 return MUNSELL_COLOURS_ALL[as_int_scalar(index[0])][1]
1787 except Exception as exception:
1788 error = (
1789 f'"{specification}" specification does not exists in '
1790 '"Munsell Renotation System" data!'
1791 )
1793 raise ValueError(error) from exception
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.
1801 Parameters
1802 ----------
1803 specification
1804 *Munsell* *Colorlab* specification.
1806 Returns
1807 -------
1808 :class:`bool`
1809 Whether specification is in *Munsell Renotation System* data.
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 """
1819 try:
1820 xyY_from_renotation(specification)
1821 except ValueError:
1822 return False
1823 else:
1824 return True
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.
1832 Parameters
1833 ----------
1834 hue_and_code
1835 *Munsell* *Colorlab* specification hue and *Munsell* *Colorlab*
1836 specification code.
1838 Returns
1839 -------
1840 :class:`numpy.NDArrayFloat`
1841 Bounding hues.
1843 References
1844 ----------
1845 :cite:`Centore2014o`
1847 Examples
1848 --------
1849 >>> bounding_hues_from_renotation([3.2, 4])
1850 array([[ 2.5, 4. ],
1851 [ 5. , 4. ]])
1853 # Coverage Doctests
1855 >>> bounding_hues_from_renotation([0.0, 1])
1856 array([[ 10., 2.],
1857 [ 10., 2.]])
1858 """
1860 hue, code = as_float_array(hue_and_code)
1862 hue_cw: float
1863 code_cw: float
1864 hue_ccw: float
1865 code_ccw: float
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
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
1891 return as_float_array([(hue_cw, code_cw), (hue_ccw, code_ccw)])
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.
1899 Parameters
1900 ----------
1901 hue_and_code
1902 *Munsell* *Colorlab* specification hue and *Munsell* *Colorlab*
1903 specification code.
1905 Returns
1906 -------
1907 :class:`float`
1908 Hue angle in degrees.
1910 References
1911 ----------
1912 :cite:`Centore2014s`
1914 Examples
1915 --------
1916 >>> hue_to_hue_angle([3.2, 4])
1917 65.5
1918 """
1920 hue, code = as_float_array(hue_and_code)
1922 single_hue = ((17 - code) % 10 + (hue / 10) - 0.5) % 10
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)
1928 return as_float_scalar(hue_angle)
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.
1936 Parameters
1937 ----------
1938 hue_angle
1939 Hue angle in degrees.
1941 Returns
1942 -------
1943 :class:`numpy.NDArrayFloat`
1944 *Munsell* *Colorlab* specification hue and *Munsell* *Colorlab*
1945 specification code.
1947 References
1948 ----------
1949 :cite:`Centore2014t`
1951 Examples
1952 --------
1953 >>> hue_angle_to_hue(65.54) # doctest: +ELLIPSIS
1954 array([ 3.216, 4. ])
1955 """
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)
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
1984 hue = (10 * (single_hue % 1) + 5) % 10
1985 if hue == 0:
1986 hue = 10
1988 return tstack(cast("ArrayLike", [hue, code]))
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.
1996 Parameters
1997 ----------
1998 hue_and_code
1999 *Munsell* *Colorlab* specification hue and *Munsell* *Colorlab*
2000 specification code.
2002 Returns
2003 -------
2004 :class:`float`
2005 *ASTM* hue number.
2007 References
2008 ----------
2009 :cite:`Centore2014k`
2011 Examples
2012 --------
2013 >>> hue_to_ASTM_hue([3.2, 4]) # doctest: +ELLIPSIS
2014 33.2...
2015 """
2017 hue, code = as_float_array(hue_and_code)
2019 ASTM_hue = 10 * ((7 - code) % 10) + hue
2021 return 100 if ASTM_hue == 0 else ASTM_hue
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*.
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.
2035 Parameters
2036 ----------
2037 specification
2038 *Munsell* *Colorlab* specification.
2040 Returns
2041 -------
2042 :class:`str` or :py:data:`None`
2043 Interpolation method.
2045 References
2046 ----------
2047 :cite:`Centore2014l`
2049 Examples
2050 --------
2051 >>> interpolation_method_from_renotation_ovoid([2.5, 5.0, 12.0, 4])
2052 'Radial'
2053 """
2055 specification = normalise_munsell_specification(specification)
2057 interpolation_methods: Dict[int, Literal["Linear", "Radial"] | None] = {
2058 0: None,
2059 1: "Linear",
2060 2: "Radial",
2061 }
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
2069 attest(
2070 0 <= value <= 10,
2071 f'"{specification}" specification value must be normalised to '
2072 f"domain [0, 10]!",
2073 )
2075 attest(
2076 is_integer(value),
2077 f'"{specification}" specification value must be an int!',
2078 )
2080 value = round(value)
2082 attest(
2083 2 <= chroma <= 50,
2084 f'"{specification}" specification chroma must be normalised to '
2085 f"domain [2, 50]!",
2086 )
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 )
2093 chroma = 2 * round(chroma / 2)
2095 interpolation_method = 0
2097 # Standard Munsell Renotation System hue, no interpolation needed.
2098 if hue % 2.5 == 0:
2099 interpolation_method = 0
2101 ASTM_hue = hue_to_ASTM_hue([hue, code])
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
2302 return interpolation_methods[interpolation_method]
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.
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.
2314 Parameters
2315 ----------
2316 specification
2317 *Munsell* *Colorlab* specification.
2319 Returns
2320 -------
2321 :class:`numpy.NDArrayFloat`
2322 *CIE xy* chromaticity coordinates.
2324 Raises
2325 ------
2326 ValueError
2327 If an invalid interpolation method is retrieved from internal
2328 computations.
2330 References
2331 ----------
2332 :cite:`Centore2014n`
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 """
2344 specification = normalise_munsell_specification(specification)
2346 if is_grey_munsell_colour(specification):
2347 return CCS_ILLUMINANT_MUNSELL
2349 hue, value, chroma, code = specification
2351 attest(
2352 1 <= value <= 9,
2353 f'"{specification}" specification value must be normalised to domain [1, 9]!',
2354 )
2356 attest(
2357 is_integer(value),
2358 f'"{specification}" specification value must be an int!',
2359 )
2361 value = round(value)
2363 attest(
2364 2 <= chroma <= 50,
2365 f'"{specification}" specification chroma must be normalised to domain [2, 50]!',
2366 )
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 )
2373 chroma = 2 * round(chroma / 2)
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)
2386 x, y, _Y = xyY_from_renotation([hue, value, chroma, code])
2388 return tstack([x, y])
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
2394 x_grey, y_grey = CCS_ILLUMINANT_MUNSELL
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)
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)
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])
2414 if phi_minus - phi_plus > 180:
2415 phi_plus += 360
2417 if hue_angle_lower == 0:
2418 hue_angle_lower = 360
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
2427 interpolation_method = interpolation_method_from_renotation_ovoid(specification)
2429 attest(
2430 interpolation_method is not None,
2431 f'Interpolation method must be one of: "{"Linear, Radial"}"',
2432 )
2434 hue_angle_lower_upper = np.squeeze([hue_angle_lower, hue_angle_upper])
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])
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])
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 )
2453 rho_phi = np.squeeze([rho, np.radians(phi)])
2454 x, y = tsplit(polar_to_cartesian(rho_phi) + tstack([x_grey, y_grey]))
2456 return tstack([x, y])
2459def LCHab_to_munsell_specification(LCHab: ArrayLike) -> NDArrayFloat:
2460 """
2461 Convert from *CIE L\\*C\\*Hab* colourspace to approximate *Munsell*
2462 *Colorlab* specification.
2464 Parameters
2465 ----------
2466 LCHab
2467 *CIE L\\*C\\*Hab* colourspace array.
2469 Returns
2470 -------
2471 :class:`numpy.NDArrayFloat`
2472 *Munsell* *Colorlab* specification.
2474 References
2475 ----------
2476 :cite:`Centore2014u`
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 """
2485 L, C, Hab = tsplit(LCHab)
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
2510 hue = LinearInterpolator([0, 36], [0, 10])(Hab % 36)
2511 if hue == 0:
2512 hue = 10
2514 value = L / 10
2515 chroma = C / 5
2517 return tstack(cast("ArrayLike", [hue, value, chroma, code]))
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.
2526 Parameters
2527 ----------
2528 hue_and_value_and_code
2529 *Munsell* *Colorlab* specification hue, value, and code.
2531 Returns
2532 -------
2533 :class:`float`
2534 Maximum chroma.
2536 References
2537 ----------
2538 :cite:`Centore2014r`
2540 Examples
2541 --------
2542 >>> maximum_chroma_from_renotation([2.5, 5, 5])
2543 14.0
2544 """
2546 hue, value, code = as_float_array(hue_and_value_and_code)
2548 # Ideal white, no chroma.
2549 if value >= 9.99:
2550 return 0
2552 attest(
2553 1 <= value <= 10,
2554 f'"{value}" value must be normalised to domain [1, 10]!',
2555 )
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
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
2568 maximum_chromas = _munsell_maximum_chromas_from_renotation()
2569 specification_for_indexes = [chroma[0] for chroma in maximum_chromas]
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]
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))
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 )
2606 return as_float_scalar(max_chroma)
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.
2615 Parameters
2616 ----------
2617 specification
2618 *Munsell* *Colorlab* specification.
2620 Returns
2621 -------
2622 :class:`numpy.NDArrayFloat`
2623 *CIE xy* chromaticity coordinates.
2625 References
2626 ----------
2627 :cite:`Centore2014q`
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 """
2639 specification = normalise_munsell_specification(specification)
2641 if is_grey_munsell_colour(specification):
2642 return CCS_ILLUMINANT_MUNSELL
2644 hue, value, chroma, code = specification
2646 attest(
2647 0 <= value <= 10,
2648 f'"{specification}" specification value must be normalised to domain [0, 10]!',
2649 )
2651 attest(
2652 is_integer(value),
2653 f'"{specification}" specification value must be an int!',
2654 )
2656 value = round(value)
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
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])
2671 x_plus, y_plus = xy_from_renotation_ovoid([hue, value, chroma_plus, code])
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])
2681 x = LinearInterpolator(chroma_minus_plus, x_minus_plus)(chroma)
2682 y = LinearInterpolator(chroma_minus_plus, y_minus_plus)(chroma)
2684 return tstack([x, y])