diff --git a/colour/volume/spectrum.py b/colour/volume/spectrum.py index 2e4ac4cd0..a96906757 100644 --- a/colour/volume/spectrum.py +++ b/colour/volume/spectrum.py @@ -38,6 +38,7 @@ Literal, NDArrayFloat, ) +from colour.models.cie_xyy import XYZ_to_xy from colour.utilities import ( CACHE_REGISTRY, is_caching_enabled, @@ -393,14 +394,20 @@ def is_within_visible_spectrum( **kwargs: Any, ) -> NDArrayFloat: """ - Return whether given *CIE XYZ* tristimulus values are within the visible - spectrum volume, i.e. *Rösch-MacAdam* colour solid, for given colour - matching functions and illuminant. + Return whether given *CIE XYZ* tristimulus values or *CIE xy* chromaticity + values are within the visible spectrum volume, i.e. *Rösch-MacAdam* colour + solid, for given colour matching functions and illuminant. + + For the brightness invariant *CIE xy* check, the limit is determined by the + spectral locus and "line of purples" depending on the precision of the + spectral arguments. Parameters ---------- XYZ - *CIE XYZ* tristimulus values. + *CIE XYZ* or xy tristimulus values. xy chromaticity mode will be + selected if the size of the last dimension is 2. i.e. if XYZ.shape[-1] + == 2 cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. @@ -439,6 +446,16 @@ def is_within_visible_spectrum( array([ True, False], dtype=bool) """ + XYZ = np.asarray(XYZ) + if XYZ.shape[-1] == 2: + return _is_within_visible_xy( + XYZ, + cmfs=cmfs, + illuminant=illuminant, + tolerance=tolerance, + **kwargs, + ) + cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, @@ -456,3 +473,76 @@ def is_within_visible_spectrum( ) return is_within_mesh_volume(XYZ, vertices, tolerance) + + +def _is_within_visible_xy( + xy: ArrayLike, + cmfs: MultiSpectralDistributions | None = None, + illuminant: SpectralDistribution | None = None, + tolerance: float = 100 * EPSILON, + **kwargs: Any, +) -> NDArrayFloat: + """ + Return whether given *CIE XYZ* tristimulus values are within the visible + spectrum volume, i.e. *Rösch-MacAdam* colour solid, for given colour + matching functions and illuminant. + + Parameters + ---------- + XYZ + *CIE XYZ* tristimulus values. + cmfs + Standard observer colour matching functions, default to the + *CIE 1931 2 Degree Standard Observer*. + illuminant + Illuminant spectral distribution, default to *CIE Illuminant E*. + tolerance + Tolerance allowed in the inside-triangle check. + + Other Parameters + ---------------- + kwargs + {:func:`colour.msds_to_XYZ`}, + See the documentation of the previously listed definition. + + Returns + ------- + :class:`numpy.ndarray` + Are *CIE XYZ* tristimulus values within the visible spectrum volume, + i.e. *Rösch-MacAdam* colour solid. + + Notes + ----- + +------------+-----------------------+---------------+ + | **Domain** | **Scale - Reference** | **Scale - 1** | + +============+=======================+===============+ + | ``XYZ`` | [0, 1] | [0, 1] | + +------------+-----------------------+---------------+ + + Examples + -------- + >>> import numpy as np + >>> is_within_visible_spectrum(np.array([0.33, 0.33])) + array(True, dtype=bool) + >>> a = np.array([[0.33, 0.33], [0.1, 0.1]]) + >>> is_within_visible_spectrum(a) + array([ True, False], dtype=bool) + """ + + cmfs, illuminant = handle_spectral_arguments( + cmfs, + illuminant, + "CIE 1931 2 Degree Standard Observer", + "E", + SPECTRAL_SHAPE_OUTER_SURFACE_XYZ, + ) + + key = (hash(cmfs), hash(illuminant), str(kwargs)) + vertices = _CACHE_OUTER_SURFACE_XYZ_POINTS.get(key) + + if vertices is None: + _CACHE_OUTER_SURFACE_XYZ_POINTS[key] = vertices = XYZ_to_xy( + cmfs.values + ) + + return is_within_mesh_volume(xy, vertices, tolerance) diff --git a/colour/volume/tests/test_spectrum.py b/colour/volume/tests/test_spectrum.py index fa6db6529..4bc388672 100644 --- a/colour/volume/tests/test_spectrum.py +++ b/colour/volume/tests/test_spectrum.py @@ -197,6 +197,16 @@ class TestIsWithinVisibleSpectrum(unittest.TestCase): definition unit tests methods. """ + def test_is_within_visible_spectrum_xy(self): + """Test :func:`colour.volume.spectrum.is_within_visible_spectrum` + detection and use of xy chromaticity. + """ + + samples = [[0.1, 0.1], [0.3, 0.3]] + results = is_within_visible_spectrum(samples) + + np.testing.assert_array_equal(results, (False, True)) + def test_is_within_visible_spectrum(self): """ Test :func:`colour.volume.spectrum.is_within_visible_spectrum`