Source code for colour.volume.spectrum

# -*- coding: utf-8 -*-
"""
Visible Spectrum Volume Computations
====================================

Defines objects related to visible spectrum volume computations.

See Also
--------
`Spectrum Volume Computations Jupyter Notebook
<http://nbviewer.jupyter.org/github/colour-science/colour-notebooks/\
blob/master/notebooks/volume/spectrum.ipynb>`_

References
----------
-   :cite:`Lindbloom2015` :Lindbloom, B. (2015). About the Lab Gamut.
    Retrieved August 20, 2018, from
    http://www.brucelindbloom.com/LabGamutDisplayHelp.html
-   :cite:`Mansencal2018` :Mansencal, T. (2018). How is the visible gamut
    bounded? Retrieved August 19, 2018, from https://stackoverflow.com/a/\
48396021/931625
"""

from __future__ import division, unicode_literals

import numpy as np

from colour.algebra import NearestNeighbourInterpolator
from colour.colorimetry import (
    DEFAULT_SPECTRAL_SHAPE, STANDARD_OBSERVERS_CMFS,
    multi_sds_to_XYZ_integration, SpectralShape, sd_ones)
from colour.volume import is_within_mesh_volume

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2019 - Colour Developers'
__license__ = 'New BSD License - https://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'

__all__ = [
    'generate_pulse_waves', 'XYZ_outer_surface', 'is_within_visible_spectrum'
]

_XYZ_OUTER_SURFACE_CACHE = {}
_XYZ_OUTER_SURFACE_POINTS_CACHE = {}


[docs]def generate_pulse_waves(bins): """ Generates the pulse waves of given number of bins necessary to totally stimulate the colour matching functions. Assuming 5 bins, a first set of SPDs would be as follows:: 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 The second one:: 1 1 0 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 1 1 0 0 0 1 The third: 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1 1 0 0 1 1 1 1 0 0 1 Etc... Parameters ---------- bins : int Number of bins of the pulse waves. Returns ------- ndarray Pulse waves. References ---------- :cite:`Lindbloom2015`, :cite:`Mansencal2018` Examples -------- >>> generate_pulse_waves(5) array([[ 0., 0., 0., 0., 0.], [ 1., 0., 0., 0., 0.], [ 0., 1., 0., 0., 0.], [ 0., 0., 1., 0., 0.], [ 0., 0., 0., 1., 0.], [ 0., 0., 0., 0., 1.], [ 1., 1., 0., 0., 0.], [ 0., 1., 1., 0., 0.], [ 0., 0., 1., 1., 0.], [ 0., 0., 0., 1., 1.], [ 1., 0., 0., 0., 1.], [ 1., 1., 1., 0., 0.], [ 0., 1., 1., 1., 0.], [ 0., 0., 1., 1., 1.], [ 1., 0., 0., 1., 1.], [ 1., 1., 0., 0., 1.], [ 1., 1., 1., 1., 0.], [ 0., 1., 1., 1., 1.], [ 1., 0., 1., 1., 1.], [ 1., 1., 0., 1., 1.], [ 1., 1., 1., 0., 1.], [ 1., 1., 1., 1., 1.]]) """ square_waves = [] square_waves_basis = np.tril(np.ones((bins, bins)))[0:-1, :] for square_wave_basis in square_waves_basis: for i in range(bins): square_waves.append(np.roll(square_wave_basis, i)) return np.vstack([np.zeros(bins), np.vstack(square_waves), np.ones(bins)])
[docs]def XYZ_outer_surface( interval=10, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(STANDARD_OBSERVERS_CMFS[ 'CIE 1931 2 Degree Standard Observer'].shape)): """ Generates the *CIE XYZ* colourspace outer surface for given colour matching functions using multi-spectral conversion of pulse waves to *CIE XYZ* tristimulus values. Parameters ---------- interval : int, optional Wavelength :math:`\\lambda_{i}` range interval used to compute the pulse waves. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. Returns ------- ndarray Outer surface *CIE XYZ* tristimulus values. References ---------- :cite:`Lindbloom2015`, :cite:`Mansencal2018` Examples -------- >>> XYZ_outer_surface(84) # doctest: +ELLIPSIS array([[ 0.0000000...e+00, 0.0000000...e+00, 0.0000000...e+00], [ 1.4766924...e-03, 4.1530347...e-05, 6.9884362...e-03], [ 1.6281275...e-01, 3.7114387...e-02, 9.0151471...e-01], [ 1.8650894...e-01, 5.6617464...e-01, 9.1355179...e-02], [ 6.1555347...e-01, 3.8427775...e-01, 4.7422070...e-04], [ 3.3622045...e-02, 1.2354556...e-02, 0.0000000...e+00], [ 1.0279500...e-04, 3.7121158...e-05, 0.0000000...e+00], [ 1.6428945...e-01, 3.7155917...e-02, 9.0850314...e-01], [ 3.4932169...e-01, 6.0328903...e-01, 9.9286989...e-01], [ 8.0206241...e-01, 9.5045240...e-01, 9.1829399...e-02], [ 6.4917552...e-01, 3.9663231...e-01, 4.7422070...e-04], [ 3.3724840...e-02, 1.2391678...e-02, 0.0000000...e+00], [ 1.5794874...e-03, 7.8651505...e-05, 6.9884362...e-03], [ 3.5079839...e-01, 6.0333056...e-01, 9.9985832...e-01], [ 9.6487517...e-01, 9.8756679...e-01, 9.9334411...e-01], [ 8.3568446...e-01, 9.6280696...e-01, 9.1829399...e-02], [ 6.4927831...e-01, 3.9666943...e-01, 4.7422070...e-04], [ 3.5201532...e-02, 1.2433208...e-02, 6.9884362...e-03], [ 1.6439224...e-01, 3.7193038...e-02, 9.0850314...e-01], [ 9.6635186...e-01, 9.8760832...e-01, 1.0003325...e+00], [ 9.9849722...e-01, 9.9992134...e-01, 9.9334411...e-01], [ 8.3578726...e-01, 9.6284408...e-01, 9.1829399...e-02], [ 6.5075501...e-01, 3.9671096...e-01, 7.4626569...e-03], [ 1.9801429...e-01, 4.9547595...e-02, 9.0850314...e-01], [ 3.5090118...e-01, 6.0336768...e-01, 9.9985832...e-01], [ 9.9997391...e-01, 9.9996287...e-01, 1.0003325...e+00], [ 9.9860001...e-01, 9.9995847...e-01, 9.9334411...e-01], [ 8.3726395...e-01, 9.6288561...e-01, 9.8817836...e-02], [ 8.1356776...e-01, 4.3382535...e-01, 9.0897737...e-01], [ 3.8452323...e-01, 6.1572224...e-01, 9.9985832...e-01], [ 9.6645466...e-01, 9.8764544...e-01, 1.0003325...e+00], [ 1.0000767...e+00, 1.0000000...e+00, 1.0003325...e+00]]) """ key = (interval, hash(cmfs), hash(illuminant)) XYZ = _XYZ_OUTER_SURFACE_CACHE.get(key) if XYZ is None: wavelengths = SpectralShape(DEFAULT_SPECTRAL_SHAPE.start, DEFAULT_SPECTRAL_SHAPE.end, interval).range() values = [] domain = DEFAULT_SPECTRAL_SHAPE.range() for wave in generate_pulse_waves(len(wavelengths)): values.append( NearestNeighbourInterpolator(wavelengths, wave)(domain)) XYZ = multi_sds_to_XYZ_integration(values, DEFAULT_SPECTRAL_SHAPE, cmfs, illuminant) XYZ = XYZ / np.max(XYZ[-1, 1]) _XYZ_OUTER_SURFACE_CACHE[key] = XYZ return XYZ
[docs]def is_within_visible_spectrum( XYZ, interval=10, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(STANDARD_OBSERVERS_CMFS[ 'CIE 1931 2 Degree Standard Observer'].shape), tolerance=None): """ Returns if given *CIE XYZ* tristimulus values are within visible spectrum volume / given colour matching functions volume. Parameters ---------- XYZ : array_like *CIE XYZ* tristimulus values. interval : int, optional Wavelength :math:`\\lambda_{i}` range interval used to compute the pulse waves for the *CIE XYZ* colourspace outer surface. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. tolerance : numeric, optional Tolerance allowed in the inside-triangle check. Returns ------- bool Is within visible spectrum. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ Examples -------- >>> import numpy as np >>> is_within_visible_spectrum(np.array([0.3205, 0.4131, 0.51])) array(True, dtype=bool) >>> a = np.array([[0.3205, 0.4131, 0.51], ... [-0.0005, 0.0031, 0.001]]) >>> is_within_visible_spectrum(a) array([ True, False], dtype=bool) """ key = (interval, hash(cmfs), hash(illuminant)) vertices = _XYZ_OUTER_SURFACE_POINTS_CACHE.get(key) if vertices is None: _XYZ_OUTER_SURFACE_POINTS_CACHE[key] = vertices = (XYZ_outer_surface( interval, STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant)) return is_within_mesh_volume(XYZ, vertices, tolerance)