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)
def test_handle_spectral_arguments(self): """ Test :func:`colour.colorimetry.tristimulus_values.\ handle_spectral_arguments` definition. """ cmfs, illuminant = handle_spectral_arguments() # pylint: disable=E1102 self.assertEqual( cmfs, reshape_msds(MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]), ) self.assertEqual(illuminant, reshape_sd(SDS_ILLUMINANTS["D65"])) shape = SpectralShape(400, 700, 20) cmfs, illuminant = handle_spectral_arguments(shape_default=shape) self.assertEqual(cmfs.shape, shape) self.assertEqual(illuminant.shape, shape) cmfs, illuminant = handle_spectral_arguments( cmfs_default="CIE 2012 2 Degree Standard Observer", illuminant_default="E", shape_default=shape, ) self.assertEqual( cmfs, reshape_msds(MSDS_CMFS["CIE 2012 2 Degree Standard Observer"], shape=shape), ) self.assertEqual(illuminant, sd_ones(shape, interpolator=LinearInterpolator) * 100)
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)
def test_spectral_to_aces_relative_exposure_values(self): """ Test :func:`colour.characterisation.aces_it. sd_to_aces_relative_exposure_values` definition. """ shape = MSDS_ACES_RICD.shape grey_reflector = sd_constant(0.18, shape) np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values(grey_reflector), np.array([0.18, 0.18, 0.18]), decimal=7, ) perfect_reflector = sd_ones(shape) np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values(perfect_reflector), np.array([0.97783784, 0.97783784, 0.97783784]), decimal=7, ) dark_skin = SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"] np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values(dark_skin), np.array([0.11718149, 0.08663609, 0.05897268]), decimal=7, ) dark_skin = SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"] np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values(dark_skin, SDS_ILLUMINANTS["A"]), np.array([0.13583991, 0.09431845, 0.05928214]), decimal=7, ) dark_skin = SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"] np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values( dark_skin, apply_chromatic_adaptation=True), np.array([0.11807796, 0.08690312, 0.05891252]), decimal=7, ) dark_skin = SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"] np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values( dark_skin, apply_chromatic_adaptation=True, chromatic_adaptation_transform="Bradford", ), np.array([0.11805993, 0.08689013, 0.05900396]), decimal=7, )
def tcs_colorimetry_data( sd_irradiance: SpectralDistribution, sds_tcs: MultiSpectralDistributions, cmfs: MultiSpectralDistributions, ) -> Tuple[TCS_ColorimetryData_CIE2017, ...]: """ Return the *test colour samples* colorimetry data under given test light source or reference illuminant spectral distribution for the *CIE 2017 Colour Fidelity Index* (CFI) computations. Parameters ---------- sd_irradiance Test light source or reference illuminant spectral distribution, i.e. the irradiance emitter. sds_tcs *Test colour samples* spectral distributions. cmfs Standard observer colour matching functions. Returns ------- :class:`tuple` *Test colour samples* colorimetry data under the given test light source or reference illuminant spectral distribution. Examples -------- >>> delta_E_to_R_f(4.4410383190) # doctest: +ELLIPSIS 70.1208254... """ XYZ_w = sd_to_XYZ(sd_ones(), cmfs, sd_irradiance) Y_b = 20 L_A = 100 surround = VIEWING_CONDITIONS_CIECAM02["Average"] tcs_data = [] for sd_tcs in sds_tcs.to_sds(): XYZ = sd_to_XYZ(sd_tcs, cmfs, sd_irradiance) CAM = XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, True) JMh = tstack([CAM.J, CAM.M, CAM.h]) Jpapbp = JMh_CIECAM02_to_CAM02UCS(JMh) tcs_data.append( TCS_ColorimetryData_CIE2017(sd_tcs.name, XYZ, CAM, JMh, Jpapbp)) return tuple(tcs_data)
def test_spectral_to_aces_relative_exposure_values(self): """ Tests :func:`colour.models.rgb.aces_it. sd_to_aces_relative_exposure_values` definition. """ shape = ACES_RICD.shape grey_reflector = sd_constant(0.18, shape) np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values(grey_reflector), np.array([0.18, 0.18, 0.18]), decimal=7) perfect_reflector = sd_ones(shape) np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values(perfect_reflector), np.array([0.97783784, 0.97783784, 0.97783784]), decimal=7) dark_skin = COLOURCHECKERS_SDS['ColorChecker N Ohta']['dark skin'] np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values(dark_skin), np.array([0.11717855, 0.08663479, 0.05897071]), decimal=7) dark_skin = COLOURCHECKERS_SDS['ColorChecker N Ohta']['dark skin'] np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values(dark_skin, ILLUMINANTS_SDS['A']), np.array([0.13584109, 0.09431910, 0.05928216]), decimal=7) dark_skin = COLOURCHECKERS_SDS['ColorChecker N Ohta']['dark skin'] np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values( dark_skin, apply_chromatic_adaptation=True), np.array([0.11807662, 0.0869023, 0.05891045]), decimal=7) dark_skin = COLOURCHECKERS_SDS['ColorChecker N Ohta']['dark skin'] np.testing.assert_almost_equal( sd_to_aces_relative_exposure_values( dark_skin, apply_chromatic_adaptation=True, chromatic_adaptation_transform='Bradford'), np.array([0.11805856, 0.08688928, 0.05900204]), decimal=7)
def spectrum_to_XYZ( wavelength, spectrum, # cmfs=None, illuminant=None, ): """ Calculate the rgb color given a wavelength and spectrum Parameters ---------- wavelength spectrum cmfs illuminant Returns ------- """ # if cmfs is None: # cmfs = colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'] if illuminant is None: # illuminant = ILLUMINANTS_SDS['CIE 1931 2 Degree Standard Observer']['D65'] illuminant = sd_ones() elif type(illuminant) == type(''): illuminant = SDS_ILLUMINANTS[illuminant] # # Get illuminant # if type(illuminant) == type(''): # illuminant = colour.ILLUMINANTS_SDS[illuminant] # Build spectral distribution object sd = SpectralDistribution(pd.Series(spectrum, index=wavelength), interpolator=CubicSplineInterpolator, extrapolator=Extrapolator) # Calculate xyz color coordinates. xyz = sd_to_XYZ(sd=sd, illuminant=illuminant) return xyz
def multi_sd_to_XYZ_integration( msd, shape, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones( STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'].shape)): """ Converts given multi-spectral distribution array :math:`msd` with given spectral shape to *CIE XYZ* tristimulus values using given colour matching functions and illuminant. Parameters ---------- msa : array_like Multi-spectral distribution array :math:`msd`, the wavelengths are expected to be in the last axis, e.g. for a 512x384 multi-spectral image with 77 bins, ``msd`` shape should be (384, 512, 77). shape : SpectralShape, optional Spectral shape of the multi-spectral distribution array :math:`msd`, ``cmfs`` and ``illuminant`` will be aligned with it. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. Returns ------- array_like *CIE XYZ* tristimulus values, for a 512x384 multi-spectral image with 77 bins, the output shape will be (384, 512, 3). Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`Wyszecki2000bf` Examples -------- >>> from colour import ILLUMINANTS_SDS >>> msd = np.array([ ... [ ... [0.0137, 0.0913, 0.0152, 0.0281, 0.1918, 0.0430], ... [0.0159, 0.3145, 0.0842, 0.0907, 0.7103, 0.0437], ... [0.0096, 0.2582, 0.4139, 0.2228, 0.0041, 0.3744], ... [0.0111, 0.0709, 0.0220, 0.1249, 0.1817, 0.0020], ... [0.0179, 0.2971, 0.5630, 0.2375, 0.0024, 0.5819], ... [0.1057, 0.4620, 0.1918, 0.5625, 0.4209, 0.0027], ... ], ... [ ... [0.0433, 0.2683, 0.2373, 0.0518, 0.0118, 0.0823], ... [0.0258, 0.0831, 0.0430, 0.3230, 0.2302, 0.0081], ... [0.0248, 0.1203, 0.0054, 0.0065, 0.1860, 0.3625], ... [0.0186, 0.1292, 0.0079, 0.4006, 0.9404, 0.3213], ... [0.0310, 0.1682, 0.3719, 0.0861, 0.0041, 0.7849], ... [0.0473, 0.3221, 0.2268, 0.3161, 0.1124, 0.0024], ... ], ... ]) >>> D65 = ILLUMINANTS_SDS['D65'] >>> multi_sd_to_XYZ( ... msd, SpectralShape(400, 700, 60), illuminant=D65) ... # doctest: +ELLIPSIS array([[[ 7.1958378..., 3.8605390..., 10.1016398...], [ 25.5738615..., 14.7200581..., 34.8440007...], [ 17.5854414..., 28.5668344..., 30.1806687...], [ 11.3271912..., 8.4598177..., 7.9015758...], [ 19.6581831..., 35.5918480..., 35.1430220...], [ 45.8212491..., 39.2600939..., 51.7907710...]], <BLANKLINE> [[ 8.8287837..., 13.3870357..., 30.5702050...], [ 22.3324362..., 18.9560919..., 9.3952305...], [ 6.6887212..., 2.5728891..., 13.2618778...], [ 41.8166227..., 27.1191979..., 14.2627944...], [ 9.2414098..., 20.2056200..., 20.1992502...], [ 24.7830551..., 26.2221584..., 36.4430633...]]]) """ msd = as_float_array(msd) if cmfs.shape != shape: runtime_warning('Aligning "{0}" cmfs shape to "{1}".'.format( cmfs.name, shape)) cmfs = cmfs.copy().align(shape) if illuminant.shape != shape: runtime_warning('Aligning "{0}" illuminant shape to "{1}".'.format( illuminant.name, shape)) illuminant = illuminant.copy().align(shape) S = illuminant.values x_bar, y_bar, z_bar = tsplit(cmfs.values) dw = cmfs.shape.interval k = 100 / (np.sum(y_bar * S) * dw) X_p = msd * x_bar * S * dw Y_p = msd * y_bar * S * dw Z_p = msd * z_bar * S * dw XYZ = k * np.sum(np.array([X_p, Y_p, Z_p]), axis=-1) return from_range_100(np.rollaxis(XYZ, 0, msd.ndim))
def sd_to_XYZ( sd, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(ASTME30815_PRACTISE_SHAPE), method='ASTM E308-15', **kwargs): """ Converts given spectral distribution to *CIE XYZ* tristimulus values using given colour matching functions, illuminant and method. Parameters ---------- sd : SpectralDistribution Spectral distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. method : unicode, optional **{'ASTM E308-15', 'Integration'}**, Computation method. Other Parameters ---------------- mi_5nm_omission_method : bool, optional {:func:`colour.colorimetry.sd_to_XYZ_ASTME30815`}, 5 nm measurement intervals spectral distribution conversion to tristimulus values will use a 5 nm version of the colour matching functions instead of a table of tristimulus weighting factors. mi_20nm_interpolation_method : bool, optional {:func:`colour.colorimetry.sd_to_XYZ_ASTME30815`}, 20 nm measurement intervals spectral distribution conversion to tristimulus values will use a dedicated interpolation method instead of a table of tristimulus weighting factors. use_practice_range : bool, optional {:func:`colour.colorimetry.sd_to_XYZ_ASTME30815`}, Practise *ASTM E308-15* working wavelengths range is [360, 780], if *True* this argument will trim the colour matching functions appropriately. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`, :cite:`Wyszecki2000bf` Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_SDS, SpectralDistribution) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852 ... } >>> sd = SpectralDistribution(data) >>> illuminant = ILLUMINANTS_SDS['D65'] >>> sd_to_XYZ(sd, cmfs, illuminant) ... # doctest: +ELLIPSIS array([ 10.8399031..., 9.6840375..., 6.2164159...]) >>> sd_to_XYZ(sd, cmfs, illuminant, use_practice_range=False) ... # doctest: +ELLIPSIS array([ 10.8399852..., 9.6840602..., 6.2164085...]) >>> sd_to_XYZ(sd, cmfs, illuminant, method='Integration') ... # doctest: +ELLIPSIS array([ 10.8401846..., 9.6837311..., 6.2120912...]) """ function = SD_TO_XYZ_METHODS[method] return function(sd, cmfs, illuminant, **filter_kwargs(function, **kwargs))
def sd_to_XYZ_ASTME30815( sd, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(ASTME30815_PRACTISE_SHAPE), use_practice_range=True, mi_5nm_omission_method=True, mi_20nm_interpolation_method=True): """ Converts given spectral distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant according to practise *ASTM E308-15* method. Parameters ---------- sd : SpectralDistribution Spectral distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. use_practice_range : bool, optional Practise *ASTM E308-15* working wavelengths range is [360, 780], if *True* this argument will trim the colour matching functions appropriately. mi_5nm_omission_method : bool, optional 5 nm measurement intervals spectral distribution conversion to tristimulus values will use a 5 nm version of the colour matching functions instead of a table of tristimulus weighting factors. mi_20nm_interpolation_method : bool, optional 20 nm measurement intervals spectral distribution conversion to tristimulus values will use a dedicated interpolation method instead of a table of tristimulus weighting factors. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Warning ------- - The tables of tristimulus weighting factors are cached in :attr:`colour.colorimetry.tristimulus.\ _TRISTIMULUS_WEIGHTING_FACTORS_CACHE` attribute. Their identifier key is defined by the colour matching functions and illuminant names along the current shape such as: `CIE 1964 10 Degree Standard Observer, A, (360.0, 830.0, 10.0)` Considering the above, one should be mindful that using similar colour matching functions and illuminant names but with different spectral data will lead to unexpected behaviour. Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`ASTMInternational2015b` Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_SDS, SpectralDistribution) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852 ... } >>> sd = SpectralDistribution(data) >>> illuminant = ILLUMINANTS_SDS['D65'] >>> sd_to_XYZ_ASTME30815(sd, cmfs, illuminant) ... # doctest: +ELLIPSIS array([ 10.8399031..., 9.6840375..., 6.2164159...]) """ if sd.shape.interval not in (1, 5, 10, 20): raise ValueError( 'Tristimulus values conversion from spectral data according to ' 'practise "ASTM E308-15" should be performed on spectral data ' 'with measurement interval of 1, 5, 10 or 20nm!') if use_practice_range: cmfs = cmfs.copy().trim(ASTME30815_PRACTISE_SHAPE) method = sd_to_XYZ_tristimulus_weighting_factors_ASTME30815 if sd.shape.interval == 1: method = sd_to_XYZ_integration elif sd.shape.interval == 5 and mi_5nm_omission_method: if cmfs.shape.interval != 5: cmfs = cmfs.copy().interpolate(SpectralShape(interval=5)) method = sd_to_XYZ_integration elif sd.shape.interval == 20 and mi_20nm_interpolation_method: sd = sd.copy() if sd.shape.boundaries != cmfs.shape.boundaries: runtime_warning( 'Trimming "{0}" spectral distribution shape to "{1}" ' 'colour matching functions shape.'.format( illuminant.name, cmfs.name)) sd.trim(cmfs.shape) # Extrapolation of additional 20nm padding intervals. sd.align(SpectralShape(sd.shape.start - 20, sd.shape.end + 20, 10)) for i in range(2): sd[sd.wavelengths[i]] = ( 3 * sd.values[i + 2] - 3 * sd.values[i + 4] + sd.values[i + 6]) # yapf: disable i_e = len(sd.domain) - 1 - i sd[sd.wavelengths[i_e]] = (sd.values[i_e - 6] - 3 * sd.values[i_e - 4] + 3 * sd.values[i_e - 2]) # Interpolating every odd numbered values. # TODO: Investigate code vectorisation. for i in range(3, len(sd.domain) - 3, 2): sd[sd.wavelengths[i]] = (-0.0625 * sd.values[i - 3] + 0.5625 * sd.values[i - 1] + 0.5625 * sd.values[i + 1] - 0.0625 * sd.values[i + 3]) # Discarding the additional 20nm padding intervals. sd.trim(SpectralShape(sd.shape.start + 20, sd.shape.end - 20, 10)) XYZ = method(sd, cmfs, illuminant) return XYZ
def sd_to_XYZ_tristimulus_weighting_factors_ASTME30815( sd, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(ASTME30815_PRACTISE_SHAPE)): """ Converts given spectral distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant using a table of tristimulus weighting factors according to practise *ASTM E308-15* method. Parameters ---------- sd : SpectralDistribution Spectral distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`ASTMInternational2015b` Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_SDS, SpectralDistribution) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852 ... } >>> sd = SpectralDistribution(data) >>> illuminant = ILLUMINANTS_SDS['D65'] >>> sd_to_XYZ_tristimulus_weighting_factors_ASTME30815( ... sd, cmfs, illuminant) # doctest: +ELLIPSIS array([ 10.8402899..., 9.6843539..., 6.2160858...]) """ if illuminant.shape != cmfs.shape: runtime_warning( 'Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant.name, cmfs.name)) illuminant = illuminant.copy().align(cmfs.shape) if sd.shape.boundaries != cmfs.shape.boundaries: runtime_warning('Trimming "{0}" spectral distribution shape to "{1}" ' 'colour matching functions shape.'.format( illuminant.name, cmfs.name)) sd = sd.copy().trim(cmfs.shape) W = tristimulus_weighting_factors_ASTME202211( cmfs, illuminant, SpectralShape(cmfs.shape.start, cmfs.shape.end, sd.shape.interval)) start_w = cmfs.shape.start end_w = cmfs.shape.start + sd.shape.interval * (W.shape[0] - 1) W = adjust_tristimulus_weighting_factors_ASTME30815( W, SpectralShape(start_w, end_w, sd.shape.interval), sd.shape) R = sd.values XYZ = np.sum(W * R[..., np.newaxis], axis=0) return from_range_100(XYZ)
def sd_to_XYZ_integration( sd, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones( STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'].shape)): """ Converts given spectral distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant according to classical integration method. Parameters ---------- sd : SpectralDistribution Spectral distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`Wyszecki2000bf` Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_SDS, SpectralDistribution) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852 ... } >>> sd = SpectralDistribution(data) >>> illuminant = ILLUMINANTS_SDS['D65'] >>> sd_to_XYZ_integration(sd, cmfs, illuminant) ... # doctest: +ELLIPSIS array([ 10.8401846..., 9.6837311..., 6.2120912...]) """ if illuminant.shape != cmfs.shape: runtime_warning( 'Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant.name, cmfs.name)) illuminant = illuminant.copy().align(cmfs.shape) if sd.shape != cmfs.shape: runtime_warning('Aligning "{0}" spectral distribution shape to "{1}" ' 'colour matching functions shape.'.format( sd.name, cmfs.name)) sd = sd.copy().align(cmfs.shape) S = illuminant.values x_bar, y_bar, z_bar = tsplit(cmfs.values) R = sd.values dw = cmfs.shape.interval k = 100 / (np.sum(y_bar * S) * dw) X_p = R * x_bar * S * dw Y_p = R * y_bar * S * dw Z_p = R * z_bar * S * dw XYZ = k * np.sum(np.array([X_p, Y_p, Z_p]), axis=-1) return from_range_100(XYZ)
def XYZ_to_sd_Meng2015( XYZ: ArrayLike, cmfs: Optional[MultiSpectralDistributions] = None, illuminant: Optional[SpectralDistribution] = None, optimisation_kwargs: Optional[Dict] = None, ) -> SpectralDistribution: """ Recover the spectral distribution of given *CIE XYZ* tristimulus values using *Meng et al. (2015)* method. Parameters ---------- XYZ *CIE XYZ* tristimulus values to recover the spectral distribution from. cmfs Standard observer colour matching functions. The wavelength :math:`\\lambda_{i}` range interval of the colour matching functions affects directly the time the computations take. The current default interval of 5 is a good compromise between precision and time spent, default to the *CIE 1931 2 Degree Standard Observer*. illuminant Illuminant spectral distribution, default to *CIE Standard Illuminant D65*. optimisation_kwargs Parameters for :func:`scipy.optimize.minimize` definition. Returns ------- :class:`colour.SpectralDistribution` Recovered spectral distribution. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ - The definition used to convert spectrum to *CIE XYZ* tristimulus values is :func:`colour.colorimetry.spectral_to_XYZ_integration` definition because it processes any measurement interval opposed to :func:`colour.colorimetry.sd_to_XYZ_ASTME308` definition that handles only measurement interval of 1, 5, 10 or 20nm. References ---------- :cite:`Meng2015c` Examples -------- >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> from colour.utilities import numpy_print_options >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> cmfs = ( ... MSDS_CMFS['CIE 1931 2 Degree Standard Observer']. ... copy().align(SpectralShape(360, 780, 10)) ... ) >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape) >>> sd = XYZ_to_sd_Meng2015(XYZ, cmfs, illuminant) >>> with numpy_print_options(suppress=True): ... sd # doctest: +SKIP SpectralDistribution([[ 360. , 0.0762005...], [ 370. , 0.0761792...], [ 380. , 0.0761363...], [ 390. , 0.0761194...], [ 400. , 0.0762539...], [ 410. , 0.0761671...], [ 420. , 0.0754649...], [ 430. , 0.0731519...], [ 440. , 0.0676701...], [ 450. , 0.0577800...], [ 460. , 0.0441993...], [ 470. , 0.0285064...], [ 480. , 0.0138728...], [ 490. , 0.0033585...], [ 500. , 0. ...], [ 510. , 0. ...], [ 520. , 0. ...], [ 530. , 0. ...], [ 540. , 0.0055767...], [ 550. , 0.0317581...], [ 560. , 0.0754491...], [ 570. , 0.1314115...], [ 580. , 0.1937649...], [ 590. , 0.2559311...], [ 600. , 0.3123173...], [ 610. , 0.3584966...], [ 620. , 0.3927335...], [ 630. , 0.4159458...], [ 640. , 0.4306660...], [ 650. , 0.4391040...], [ 660. , 0.4439497...], [ 670. , 0.4463618...], [ 680. , 0.4474625...], [ 690. , 0.4479868...], [ 700. , 0.4482116...], [ 710. , 0.4482800...], [ 720. , 0.4483472...], [ 730. , 0.4484251...], [ 740. , 0.4484633...], [ 750. , 0.4485071...], [ 760. , 0.4484969...], [ 770. , 0.4484853...], [ 780. , 0.4485134...]], interpolator=SpragueInterpolator, interpolator_kwargs={}, extrapolator=Extrapolator, extrapolator_kwargs={...}) >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100 # doctest: +ELLIPSIS array([ 0.2065400..., 0.1219722..., 0.0513695...]) """ XYZ = to_domain_1(XYZ) cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, shape_default=SPECTRAL_SHAPE_MENG2015 ) sd = sd_ones(cmfs.shape) def objective_function(a: ArrayLike) -> FloatingOrNDArray: """Define the objective function.""" return np.sum(np.diff(a) ** 2) def constraint_function(a: ArrayLike) -> NDArray: """Define the constraint function.""" sd[:] = a return ( sd_to_XYZ_integration(sd, cmfs=cmfs, illuminant=illuminant) - XYZ ) wavelengths = sd.wavelengths bins = wavelengths.size optimisation_settings = { "method": "SLSQP", "constraints": {"type": "eq", "fun": constraint_function}, "bounds": np.tile(np.array([0, 1000]), (bins, 1)), "options": { "ftol": 1e-10, }, } if optimisation_kwargs is not None: optimisation_settings.update(optimisation_kwargs) result = minimize(objective_function, sd.values, **optimisation_settings) if not result.success: raise RuntimeError( f"Optimization failed for {XYZ} after {result.nit} iterations: " f'"{result.message}".' ) return SpectralDistribution( from_range_100(result.x * 100), wavelengths, name=f"{XYZ} (XYZ) - Meng (2015)", )
def sd_to_XYZ_integration( sd, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones( STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']. shape), k=None): """ Converts given spectral distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant according to classical integration method. Parameters ---------- sd : SpectralDistribution Spectral distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. k : numeric, optional Normalisation constant :math:`k`. For reflecting or transmitting object colours, :math:`k` is chosen so that :math:`Y = 100` for objects for which the spectral reflectance factor :math:`R(\\lambda)` of the object colour or the spectral transmittance factor :math:`\\tau(\\lambda)` of the object is equal to unity for all wavelengths. For self-luminous objects and illuminants, the constants :math:`k` is usually chosen on the grounds of convenience. If, however, in the CIE 1931 standard colorimetric system, the :math:`Y` value is required to be numerically equal to the absolute value of a photometric quantity, the constant, :math:`k`, must be put equal to the numerical value of :math:`K_m`, the maximum spectral luminous efficacy (which is equal to 683 :math:`lm\\cdot W^{-1}`) and :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration of the radiometric quantity corresponding to the photometric quantity required. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`Wyszecki2000bf` Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_SDS, SpectralDistribution) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852 ... } >>> sd = SpectralDistribution(data) >>> illuminant = ILLUMINANTS_SDS['D65'] >>> sd_to_XYZ_integration(sd, cmfs, illuminant) ... # doctest: +ELLIPSIS array([ 10.8401846..., 9.6837311..., 6.2120912...]) """ if illuminant.shape != cmfs.shape: runtime_warning( 'Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant.name, cmfs.name)) illuminant = illuminant.copy().align(cmfs.shape) if sd.shape != cmfs.shape: runtime_warning('Aligning "{0}" spectral distribution shape to "{1}" ' 'colour matching functions shape.'.format( sd.name, cmfs.name)) sd = sd.copy().align(cmfs.shape) S = illuminant.values x_bar, y_bar, z_bar = tsplit(cmfs.values) R = sd.values dw = cmfs.shape.interval k = 100 / (np.sum(y_bar * S) * dw) if k is None else k X_p = R * x_bar * S * dw Y_p = R * y_bar * S * dw Z_p = R * z_bar * S * dw XYZ = k * np.sum(np.array([X_p, Y_p, Z_p]), axis=-1) return from_range_100(XYZ)
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_sd_to_XYZ_integration(values, DEFAULT_SPECTRAL_SHAPE, cmfs, illuminant) XYZ = XYZ / np.max(XYZ[-1, 1]) _XYZ_OUTER_SURFACE_CACHE[key] = XYZ return XYZ
def plot_visible_spectrum(cmfs='CIE 1931 2 Degree Standard Observer', out_of_gamut_clipping=True, **kwargs): """ Plots the visible colours spectrum using given standard observer *CIE XYZ* colour matching functions. Parameters ---------- cmfs : unicode, optional Standard observer colour matching functions used for spectrum creation. out_of_gamut_clipping : bool, optional Whether to clip out of gamut colours otherwise, the colours will be offset by the absolute minimal colour leading to a rendering on gray background, less saturated and smoother. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Returns ------- tuple Current figure and axes. References ---------- :cite:`Spiker2015a` Examples -------- >>> plot_visible_spectrum() # doctest: +SKIP .. image:: ../_static/Plotting_Plot_Visible_Spectrum.png :align: center :alt: plot_visible_spectrum """ cmfs = first_item(filter_cmfs(cmfs).values()) bounding_box = (min(cmfs.wavelengths), max(cmfs.wavelengths), 0, 1) settings = {'bounding_box': bounding_box, 'y_label': None} settings.update(kwargs) settings['standalone'] = False _figure, axes = plot_single_sd( sd_ones(cmfs.shape), cmfs=cmfs, out_of_gamut_clipping=out_of_gamut_clipping, **settings) # Removing wavelength line as it doubles with the axes spine. axes.lines.pop(0) settings = { 'axes': axes, 'standalone': True, 'title': 'The Visible Spectrum - {0}'.format(cmfs.strict_name), 'x_label': 'Wavelength $\\lambda$ (nm)', } settings.update(kwargs) return render(**settings)
def XYZ_to_sd_Meng2015( XYZ, cmfs=MSDS_CMFS_STANDARD_OBSERVER['CIE 1931 2 Degree Standard Observer'] .copy().align(SPECTRAL_SHAPE_MENG2015), illuminant=SDS_ILLUMINANTS['D65'].copy().align( SPECTRAL_SHAPE_MENG2015), optimisation_kwargs=None, **kwargs): """ Recovers the spectral distribution of given *CIE XYZ* tristimulus values using *Meng et al. (2015)* method. Parameters ---------- XYZ : array_like, (3,) *CIE XYZ* tristimulus values to recover the spectral distribution from. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. The wavelength :math:`\\lambda_{i}` range interval of the colour matching functions affects directly the time the computations take. The current default interval of 5 is a good compromise between precision and time spent. illuminant : SpectralDistribution, optional Illuminant spectral distribution. optimisation_kwargs : dict_like, optional Parameters for :func:`scipy.optimize.minimize` definition. Other Parameters ---------------- \\**kwargs : dict, optional Keywords arguments for deprecation management. Returns ------- SpectralDistribution Recovered spectral distribution. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ - The definition used to convert spectrum to *CIE XYZ* tristimulus values is :func:`colour.colorimetry.spectral_to_XYZ_integration` definition because it processes any measurement interval opposed to :func:`colour.colorimetry.sd_to_XYZ_ASTME308` definition that handles only measurement interval of 1, 5, 10 or 20nm. References ---------- :cite:`Meng2015c` Examples -------- >>> from colour.utilities import numpy_print_options >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> cmfs = ( ... MSDS_CMFS_STANDARD_OBSERVER['CIE 1931 2 Degree Standard Observer']. ... copy().align(SpectralShape(360, 780, 10)) ... ) >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape) >>> sd = XYZ_to_sd_Meng2015(XYZ, cmfs, illuminant) >>> with numpy_print_options(suppress=True): ... # Doctests skip for Python 2.x compatibility. ... sd # doctest: +SKIP SpectralDistribution([[ 360. , 0.0765153...], [ 370. , 0.0764771...], [ 380. , 0.0764286...], [ 390. , 0.0764329...], [ 400. , 0.0765863...], [ 410. , 0.0764339...], [ 420. , 0.0757213...], [ 430. , 0.0733091...], [ 440. , 0.0676493...], [ 450. , 0.0577616...], [ 460. , 0.0440805...], [ 470. , 0.0284802...], [ 480. , 0.0138019...], [ 490. , 0.0033557...], [ 500. , 0. ...], [ 510. , 0. ...], [ 520. , 0. ...], [ 530. , 0. ...], [ 540. , 0.0055360...], [ 550. , 0.0317335...], [ 560. , 0.075457 ...], [ 570. , 0.1314930...], [ 580. , 0.1938219...], [ 590. , 0.2559747...], [ 600. , 0.3122869...], [ 610. , 0.3584363...], [ 620. , 0.3927112...], [ 630. , 0.4158866...], [ 640. , 0.4305832...], [ 650. , 0.4391142...], [ 660. , 0.4439484...], [ 670. , 0.4464121...], [ 680. , 0.4475718...], [ 690. , 0.4481182...], [ 700. , 0.4483734...], [ 710. , 0.4484743...], [ 720. , 0.4485753...], [ 730. , 0.4486474...], [ 740. , 0.4486629...], [ 750. , 0.4486995...], [ 760. , 0.4486925...], [ 770. , 0.4486794...], [ 780. , 0.4486982...]], interpolator=SpragueInterpolator, interpolator_kwargs={}, extrapolator=Extrapolator, extrapolator_kwargs={...}) >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100 # doctest: +ELLIPSIS array([ 0.2065400..., 0.1219722..., 0.0513695...]) """ optimisation_kwargs = handle_arguments_deprecation( { 'ArgumentRenamed': [['optimisation_parameters', 'optimisation_kwargs']], }, **kwargs).get('optimisation_kwargs', optimisation_kwargs) XYZ = to_domain_1(XYZ) if illuminant.shape != cmfs.shape: runtime_warning( 'Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant.name, cmfs.name)) illuminant = illuminant.copy().align(cmfs.shape) sd = sd_ones(cmfs.shape) def objective_function(a): """ Objective function. """ return np.sum(np.diff(a)**2) def constraint_function(a): """ Function defining the constraint. """ sd[:] = a return sd_to_XYZ_integration(sd, cmfs=cmfs, illuminant=illuminant) - XYZ wavelengths = sd.wavelengths bins = wavelengths.size optimisation_settings = { 'method': 'SLSQP', 'constraints': { 'type': 'eq', 'fun': constraint_function }, 'bounds': np.tile(np.array([0, 1000]), (bins, 1)), 'options': { 'ftol': 1e-10, }, } if optimisation_kwargs is not None: optimisation_settings.update(optimisation_kwargs) result = minimize(objective_function, sd.values, **optimisation_settings) if not result.success: raise RuntimeError( 'Optimization failed for {0} after {1} iterations: "{2}".'.format( XYZ, result.nit, result.message)) return SpectralDistribution(from_range_100(result.x * 100), wavelengths, name='{0} (XYZ) - Meng (2015)'.format(XYZ))
def XYZ_outer_surface( cmfs=MSDS_CMFS['CIE 1931 2 Degree Standard Observer'].copy().align( SPECTRAL_SHAPE_OUTER_SURFACE_XYZ), illuminant=sd_ones(SPECTRAL_SHAPE_OUTER_SURFACE_XYZ), **kwargs): """ 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 ---------- cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.msds_to_XYZ`}, Please refer to the documentation of the previously listed definition. Returns ------- ndarray Outer surface *CIE XYZ* tristimulus values. References ---------- :cite:`Lindbloom2015`, :cite:`Mansencal2018` Examples -------- >>> from colour.colorimetry import SPECTRAL_SHAPE_DEFAULT >>> shape = SpectralShape( ... SPECTRAL_SHAPE_DEFAULT.start, SPECTRAL_SHAPE_DEFAULT.end, 84) >>> cmfs = MSDS_CMFS['CIE 1931 2 Degree Standard Observer'] >>> XYZ_outer_surface(cmfs.copy().align(shape)) # doctest: +ELLIPSIS array([[ 0.0000000...e+00, 0.0000000...e+00, 0.0000000...e+00], [ 9.6361381...e-05, 2.9056776...e-06, 4.4961226...e-04], [ 2.5910529...e-01, 2.1031298...e-02, 1.3207468...e+00], [ 1.0561021...e-01, 6.2038243...e-01, 3.5423571...e-02], [ 7.2647980...e-01, 3.5460869...e-01, 2.1005149...e-04], [ 1.0971874...e-02, 3.9635453...e-03, 0.0000000...e+00], [ 3.0792572...e-05, 1.1119762...e-05, 0.0000000...e+00], [ 2.5920165...e-01, 2.1034203...e-02, 1.3211965...e+00], [ 3.6471551...e-01, 6.4141373...e-01, 1.3561704...e+00], [ 8.3209002...e-01, 9.7499113...e-01, 3.5633622...e-02], [ 7.3745167...e-01, 3.5857224...e-01, 2.1005149...e-04], [ 1.1002667...e-02, 3.9746651...e-03, 0.0000000...e+00], [ 1.2715395...e-04, 1.4025439...e-05, 4.4961226...e-04], [ 3.6481187...e-01, 6.4141663...e-01, 1.3566200...e+00], [ 1.0911953...e+00, 9.9602242...e-01, 1.3563805...e+00], [ 8.4306189...e-01, 9.7895467...e-01, 3.5633622...e-02], [ 7.3748247...e-01, 3.5858336...e-01, 2.1005149...e-04], [ 1.1099028...e-02, 3.9775708...e-03, 4.4961226...e-04], [ 2.5923244...e-01, 2.1045323...e-02, 1.3211965...e+00], [ 1.0912916...e+00, 9.9602533...e-01, 1.3568301...e+00], [ 1.1021671...e+00, 9.9998597...e-01, 1.3563805...e+00], [ 8.4309268...e-01, 9.7896579...e-01, 3.5633622...e-02], [ 7.3757883...e-01, 3.5858626...e-01, 6.5966375...e-04], [ 2.7020432...e-01, 2.5008868...e-02, 1.3211965...e+00], [ 3.6484266...e-01, 6.4142775...e-01, 1.3566200...e+00], [ 1.1022635...e+00, 9.9998888...e-01, 1.3568301...e+00], [ 1.1021979...e+00, 9.9999709...e-01, 1.3563805...e+00], [ 8.4318905...e-01, 9.7896870...e-01, 3.6083235...e-02], [ 9.9668412...e-01, 3.7961756...e-01, 1.3214065...e+00], [ 3.7581454...e-01, 6.4539130...e-01, 1.3566200...e+00], [ 1.0913224...e+00, 9.9603645...e-01, 1.3568301...e+00], [ 1.1022943...e+00, 1.0000000...e+00, 1.3568301...e+00]]) """ settings = {'method': 'Integration', 'shape': cmfs.shape} settings.update(kwargs) key = (hash(cmfs), hash(illuminant), six.text_type(settings)) XYZ = _CACHE_OUTER_SURFACE_XYZ.get(key) if XYZ is None: pulse_waves = generate_pulse_waves(len(cmfs.wavelengths)) XYZ = msds_to_XYZ(pulse_waves, cmfs, illuminant, **settings) / 100 _CACHE_OUTER_SURFACE_XYZ[key] = XYZ return XYZ
def multi_sds_to_XYZ_integration( msd, shape, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones( STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']. shape), k=None): """ Converts given multi-spectral distribution array :math:`msd` with given spectral shape to *CIE XYZ* tristimulus values using given colour matching functions and illuminant. Parameters ---------- msa : array_like Multi-spectral distribution array :math:`msd`, the wavelengths are expected to be in the last axis, e.g. for a 512x384 multi-spectral image with 77 bins, ``msd`` shape should be (384, 512, 77). shape : SpectralShape, optional Spectral shape of the multi-spectral distribution array :math:`msd`, ``cmfs`` and ``illuminant`` will be aligned with it. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. k : numeric, optional Normalisation constant :math:`k`. For reflecting or transmitting object colours, :math:`k` is chosen so that :math:`Y = 100` for objects for which the spectral reflectance factor :math:`R(\\lambda)` of the object colour or the spectral transmittance factor :math:`\\tau(\\lambda)` of the object is equal to unity for all wavelengths. For self-luminous objects and illuminants, the constants :math:`k` is usually chosen on the grounds of convenience. If, however, in the CIE 1931 standard colorimetric system, the :math:`Y` value is required to be numerically equal to the absolute value of a photometric quantity, the constant, :math:`k`, must be put equal to the numerical value of :math:`K_m`, the maximum spectral luminous efficacy (which is equal to 683 :math:`lm\\cdot W^{-1}`) and :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration of the radiometric quantity corresponding to the photometric quantity required. Returns ------- array_like *CIE XYZ* tristimulus values, for a 512x384 multi-spectral image with 77 bins, the output shape will be (384, 512, 3). Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`Wyszecki2000bf` Examples -------- >>> from colour import ILLUMINANTS_SDS >>> msd = np.array([ ... [ ... [0.0137, 0.0913, 0.0152, 0.0281, 0.1918, 0.0430], ... [0.0159, 0.3145, 0.0842, 0.0907, 0.7103, 0.0437], ... [0.0096, 0.2582, 0.4139, 0.2228, 0.0041, 0.3744], ... [0.0111, 0.0709, 0.0220, 0.1249, 0.1817, 0.0020], ... [0.0179, 0.2971, 0.5630, 0.2375, 0.0024, 0.5819], ... [0.1057, 0.4620, 0.1918, 0.5625, 0.4209, 0.0027], ... ], ... [ ... [0.0433, 0.2683, 0.2373, 0.0518, 0.0118, 0.0823], ... [0.0258, 0.0831, 0.0430, 0.3230, 0.2302, 0.0081], ... [0.0248, 0.1203, 0.0054, 0.0065, 0.1860, 0.3625], ... [0.0186, 0.1292, 0.0079, 0.4006, 0.9404, 0.3213], ... [0.0310, 0.1682, 0.3719, 0.0861, 0.0041, 0.7849], ... [0.0473, 0.3221, 0.2268, 0.3161, 0.1124, 0.0024], ... ], ... ]) >>> D65 = ILLUMINANTS_SDS['D65'] >>> multi_sds_to_XYZ( ... msd, SpectralShape(400, 700, 60), illuminant=D65) ... # doctest: +ELLIPSIS array([[[ 7.1958378..., 3.8605390..., 10.1016398...], [ 25.5738615..., 14.7200581..., 34.8440007...], [ 17.5854414..., 28.5668344..., 30.1806687...], [ 11.3271912..., 8.4598177..., 7.9015758...], [ 19.6581831..., 35.5918480..., 35.1430220...], [ 45.8212491..., 39.2600939..., 51.7907710...]], <BLANKLINE> [[ 8.8287837..., 13.3870357..., 30.5702050...], [ 22.3324362..., 18.9560919..., 9.3952305...], [ 6.6887212..., 2.5728891..., 13.2618778...], [ 41.8166227..., 27.1191979..., 14.2627944...], [ 9.2414098..., 20.2056200..., 20.1992502...], [ 24.7830551..., 26.2221584..., 36.4430633...]]]) """ msd = as_float_array(msd) if cmfs.shape != shape: runtime_warning('Aligning "{0}" cmfs shape to "{1}".'.format( cmfs.name, shape)) cmfs = cmfs.copy().align(shape) if illuminant.shape != shape: runtime_warning('Aligning "{0}" illuminant shape to "{1}".'.format( illuminant.name, shape)) illuminant = illuminant.copy().align(shape) S = illuminant.values x_bar, y_bar, z_bar = tsplit(cmfs.values) dw = cmfs.shape.interval k = 100 / (np.sum(y_bar * S) * dw) if k is None else k X_p = msd * x_bar * S * dw Y_p = msd * y_bar * S * dw Z_p = msd * z_bar * S * dw XYZ = k * np.sum(np.array([X_p, Y_p, Z_p]), axis=-1) return from_range_100(np.rollaxis(XYZ, 0, msd.ndim))
def sd_to_XYZ( sd, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(ASTME30815_PRACTISE_SHAPE), k=None, method='ASTM E308-15', **kwargs): """ Converts given spectral distribution to *CIE XYZ* tristimulus values using given colour matching functions, illuminant and method. Parameters ---------- sd : SpectralDistribution Spectral distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. k : numeric, optional Normalisation constant :math:`k`. For reflecting or transmitting object colours, :math:`k` is chosen so that :math:`Y = 100` for objects for which the spectral reflectance factor :math:`R(\\lambda)` of the object colour or the spectral transmittance factor :math:`\\tau(\\lambda)` of the object is equal to unity for all wavelengths. For self-luminous objects and illuminants, the constants :math:`k` is usually chosen on the grounds of convenience. If, however, in the CIE 1931 standard colorimetric system, the :math:`Y` value is required to be numerically equal to the absolute value of a photometric quantity, the constant, :math:`k`, must be put equal to the numerical value of :math:`K_m`, the maximum spectral luminous efficacy (which is equal to 683 :math:`lm\\cdot W^{-1}`) and :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration of the radiometric quantity corresponding to the photometric quantity required. method : unicode, optional **{'ASTM E308-15', 'Integration'}**, Computation method. Other Parameters ---------------- mi_5nm_omission_method : bool, optional {:func:`colour.colorimetry.sd_to_XYZ_ASTME30815`}, 5 nm measurement intervals spectral distribution conversion to tristimulus values will use a 5 nm version of the colour matching functions instead of a table of tristimulus weighting factors. mi_20nm_interpolation_method : bool, optional {:func:`colour.colorimetry.sd_to_XYZ_ASTME30815`}, 20 nm measurement intervals spectral distribution conversion to tristimulus values will use a dedicated interpolation method instead of a table of tristimulus weighting factors. use_practice_range : bool, optional {:func:`colour.colorimetry.sd_to_XYZ_ASTME30815`}, Practise *ASTM E308-15* working wavelengths range is [360, 780], if *True* this argument will trim the colour matching functions appropriately. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`, :cite:`Wyszecki2000bf` Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_SDS, SpectralDistribution) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852 ... } >>> sd = SpectralDistribution(data) >>> illuminant = ILLUMINANTS_SDS['D65'] >>> sd_to_XYZ(sd, cmfs, illuminant) ... # doctest: +ELLIPSIS array([ 10.8399031..., 9.6840375..., 6.2164159...]) >>> sd_to_XYZ(sd, cmfs, illuminant, use_practice_range=False) ... # doctest: +ELLIPSIS array([ 10.8399852..., 9.6840602..., 6.2164085...]) >>> sd_to_XYZ(sd, cmfs, illuminant, method='Integration') ... # doctest: +ELLIPSIS array([ 10.8401846..., 9.6837311..., 6.2120912...]) """ function = SD_TO_XYZ_METHODS[method] return function(sd, cmfs, illuminant, k=k, **filter_kwargs(function, **kwargs))
def sd_to_XYZ_tristimulus_weighting_factors_ASTME30815( sd, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(ASTME30815_PRACTISE_SHAPE), k=None): """ Converts given spectral distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant using a table of tristimulus weighting factors according to practise *ASTM E308-15* method. Parameters ---------- sd : SpectralDistribution Spectral distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. k : numeric, optional Normalisation constant :math:`k`. For reflecting or transmitting object colours, :math:`k` is chosen so that :math:`Y = 100` for objects for which the spectral reflectance factor :math:`R(\\lambda)` of the object colour or the spectral transmittance factor :math:`\\tau(\\lambda)` of the object is equal to unity for all wavelengths. For self-luminous objects and illuminants, the constants :math:`k` is usually chosen on the grounds of convenience. If, however, in the CIE 1931 standard colorimetric system, the :math:`Y` value is required to be numerically equal to the absolute value of a photometric quantity, the constant, :math:`k`, must be put equal to the numerical value of :math:`K_m`, the maximum spectral luminous efficacy (which is equal to 683 :math:`lm\\cdot W^{-1}`) and :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration of the radiometric quantity corresponding to the photometric quantity required. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`ASTMInternational2015b` Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_SDS, SpectralDistribution) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852 ... } >>> sd = SpectralDistribution(data) >>> illuminant = ILLUMINANTS_SDS['D65'] >>> sd_to_XYZ_tristimulus_weighting_factors_ASTME30815( ... sd, cmfs, illuminant) # doctest: +ELLIPSIS array([ 10.8402899..., 9.6843539..., 6.2160858...]) """ if illuminant.shape != cmfs.shape: runtime_warning( 'Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant.name, cmfs.name)) illuminant = illuminant.copy().align(cmfs.shape) if sd.shape.boundaries != cmfs.shape.boundaries: runtime_warning('Trimming "{0}" spectral distribution shape to "{1}" ' 'colour matching functions shape.'.format( illuminant.name, cmfs.name)) sd = sd.copy().trim(cmfs.shape) W = tristimulus_weighting_factors_ASTME202211( cmfs, illuminant, SpectralShape(cmfs.shape.start, cmfs.shape.end, sd.shape.interval), k) start_w = cmfs.shape.start end_w = cmfs.shape.start + sd.shape.interval * (W.shape[0] - 1) W = adjust_tristimulus_weighting_factors_ASTME30815( W, SpectralShape(start_w, end_w, sd.shape.interval), sd.shape) R = sd.values XYZ = np.sum(W * R[..., np.newaxis], axis=0) return from_range_100(XYZ)
def multi_sd_to_XYZ( msd, shape=DEFAULT_SPECTRAL_SHAPE, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(ASTME30815_PRACTISE_SHAPE), method='Integration'): """ Converts given multi-spectral distribution array :math:`msd` with given spectral shape to *CIE XYZ* tristimulus values using given colour matching functions and illuminant. Parameters ---------- msa : array_like Multi-spectral distribution array :math:`msd`, the wavelengths are expected to be in the last axis, e.g. for a 512x384 multi-spectral image with 77 bins, ``msd`` shape should be (384, 512, 77). shape : SpectralShape, optional Spectral shape of the multi-spectral distribution array :math:`msd`, ``cmfs`` and ``illuminant`` will be aligned with it. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. method : unicode, optional **{'Integration'}**, Computation method. Returns ------- array_like *CIE XYZ* tristimulus values, for a 512x384 multi-spectral image with 77 bins, the output shape will be (384, 512, 3). Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`Wyszecki2000bf` Examples -------- >>> msd = np.array([ ... [ ... [0.0137, 0.0913, 0.0152, 0.0281, 0.1918, 0.0430], ... [0.0159, 0.3145, 0.0842, 0.0907, 0.7103, 0.0437], ... [0.0096, 0.2582, 0.4139, 0.2228, 0.0041, 0.3744], ... [0.0111, 0.0709, 0.0220, 0.1249, 0.1817, 0.0020], ... [0.0179, 0.2971, 0.5630, 0.2375, 0.0024, 0.5819], ... [0.1057, 0.4620, 0.1918, 0.5625, 0.4209, 0.0027], ... ], ... [ ... [0.0433, 0.2683, 0.2373, 0.0518, 0.0118, 0.0823], ... [0.0258, 0.0831, 0.0430, 0.3230, 0.2302, 0.0081], ... [0.0248, 0.1203, 0.0054, 0.0065, 0.1860, 0.3625], ... [0.0186, 0.1292, 0.0079, 0.4006, 0.9404, 0.3213], ... [0.0310, 0.1682, 0.3719, 0.0861, 0.0041, 0.7849], ... [0.0473, 0.3221, 0.2268, 0.3161, 0.1124, 0.0024], ... ], ... ]) >>> multi_sd_to_XYZ(msd, SpectralShape(400, 700, 60)) ... # doctest: +ELLIPSIS array([[[ 7.6862675..., 4.0925470..., 8.4950412...], [ 27.4119366..., 15.5014764..., 29.2825122...], [ 17.1283666..., 27.7798651..., 25.5232032...], [ 11.9824544..., 8.8127109..., 6.6518695...], [ 19.1030682..., 34.4597818..., 29.7653804...], [ 46.8243374..., 39.9551652..., 43.6541858...]], <BLANKLINE> [[ 8.0978189..., 12.7544378..., 25.8004512...], [ 23.4360673..., 19.6127966..., 7.9342408...], [ 7.0933208..., 2.7894394..., 11.1527704...], [ 45.6313772..., 29.0068105..., 11.9934522...], [ 8.9327884..., 19.4008147..., 17.1534186...], [ 24.6610235..., 26.1093760..., 30.7298791...]]]) """ function = MULTI_SD_TO_XYZ_METHODS[method] return function(msd, shape, cmfs, illuminant)
def XYZ_to_sd_Meng2015( XYZ, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']. copy().align(DEFAULT_SPECTRAL_SHAPE_MENG_2015), illuminant=sd_ones(DEFAULT_SPECTRAL_SHAPE_MENG_2015), optimisation_parameters=None): """ Recovers the spectral distribution of given *CIE XYZ* tristimulus values using *Meng et al. (2015)* method. Parameters ---------- XYZ : array_like, (3,) *CIE XYZ* tristimulus values to recover the spectral distribution from. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. The wavelength :math:`\\lambda_{i}` range interval of the colour matching functions affects directly the time the computations take. The current default interval of 5 is a good compromise between precision and time spent. illuminant : SpectralDistribution, optional Illuminant spectral distribution. optimisation_parameters : dict_like, optional Parameters for :func:`scipy.optimize.minimize` definition. Returns ------- SpectralDistribution Recovered spectral distribution. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ - The definition used to convert spectrum to *CIE XYZ* tristimulus values is :func:`colour.colorimetry.spectral_to_XYZ_integration` definition because it processes any measurement interval opposed to :func:`colour.colorimetry.sd_to_XYZ_ASTME308` definition that handles only measurement interval of 1, 5, 10 or 20nm. References ---------- :cite:`Meng2015c` Examples -------- >>> from colour.utilities import numpy_print_options >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> cmfs = ( ... STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']. ... copy().align(SpectralShape(360, 780, 10)) ... ) >>> sd = XYZ_to_sd_Meng2015(XYZ, cmfs) >>> with numpy_print_options(suppress=True): ... # Doctests skip for Python 2.x compatibility. ... sd # doctest: +SKIP SpectralDistribution([[ 360. , 0.0780114...], [ 370. , 0.0780316...], [ 380. , 0.0780471...], [ 390. , 0.0780351...], [ 400. , 0.0779702...], [ 410. , 0.0778033...], [ 420. , 0.0770958...], [ 430. , 0.0748008...], [ 440. , 0.0693230...], [ 450. , 0.0601136...], [ 460. , 0.0477407...], [ 470. , 0.0334964...], [ 480. , 0.0193352...], [ 490. , 0.0074858...], [ 500. , 0.0001225...], [ 510. , 0. ...], [ 520. , 0. ...], [ 530. , 0. ...], [ 540. , 0.0124896...], [ 550. , 0.0389831...], [ 560. , 0.0775105...], [ 570. , 0.1247947...], [ 580. , 0.1765339...], [ 590. , 0.2281918...], [ 600. , 0.2751347...], [ 610. , 0.3140115...], [ 620. , 0.3433561...], [ 630. , 0.3635777...], [ 640. , 0.3765428...], [ 650. , 0.3841726...], [ 660. , 0.3883633...], [ 670. , 0.3905415...], [ 680. , 0.3916742...], [ 690. , 0.3922554...], [ 700. , 0.3925427...], [ 710. , 0.3926783...], [ 720. , 0.3927330...], [ 730. , 0.3927586...], [ 740. , 0.3927548...], [ 750. , 0.3927681...], [ 760. , 0.3927813...], [ 770. , 0.3927840...], [ 780. , 0.3927536...]], interpolator=SpragueInterpolator, interpolator_args={}, extrapolator=Extrapolator, extrapolator_args={...}) >>> sd_to_XYZ_integration(sd) / 100 # doctest: +ELLIPSIS array([ 0.2065812..., 0.1219752..., 0.0514132...]) """ XYZ = to_domain_1(XYZ) if illuminant.shape != cmfs.shape: runtime_warning( 'Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant.name, cmfs.name)) illuminant = illuminant.copy().align(cmfs.shape) sd = sd_ones(cmfs.shape) def objective_function(a): """ Objective function. """ return np.sum(np.diff(a)**2) def constraint_function(a): """ Function defining the constraint. """ sd[:] = a return sd_to_XYZ_integration(sd, cmfs=cmfs, illuminant=illuminant) - XYZ wavelengths = sd.wavelengths bins = wavelengths.size optimisation_settings = { 'method': 'SLSQP', 'constraints': { 'type': 'eq', 'fun': constraint_function }, 'bounds': np.tile(np.array([0, 1000]), (bins, 1)), 'options': { 'ftol': 1e-10, }, } if optimisation_parameters is not None: optimisation_settings.update(optimisation_parameters) result = minimize(objective_function, sd.values, **optimisation_settings) if not result.success: raise RuntimeError( 'Optimization failed for {0} after {1} iterations: "{2}".'.format( XYZ, result.nit, result.message)) return SpectralDistribution(from_range_100(result.x * 100), wavelengths, name='Meng (2015) - {0}'.format(XYZ))
def multi_sds_to_XYZ( msd, shape=DEFAULT_SPECTRAL_SHAPE, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], illuminant=sd_ones(ASTME30815_PRACTISE_SHAPE), method='Integration', **kwargs): """ Converts given multi-spectral distribution array :math:`msd` with given spectral shape to *CIE XYZ* tristimulus values using given colour matching functions and illuminant. Parameters ---------- msa : array_like Multi-spectral distribution array :math:`msd`, the wavelengths are expected to be in the last axis, e.g. for a 512x384 multi-spectral image with 77 bins, ``msd`` shape should be (384, 512, 77). shape : SpectralShape, optional Spectral shape of the multi-spectral distribution array :math:`msd`, ``cmfs`` and ``illuminant`` will be aligned with it. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. method : unicode, optional **{'Integration'}**, Computation method. Other Parameters ---------------- k : numeric, optional {:func:`colour.colorimetry.multi_sds_to_XYZ_integration`}, Normalisation constant :math:`k`. For reflecting or transmitting object colours, :math:`k` is chosen so that :math:`Y = 100` for objects for which the spectral reflectance factor :math:`R(\\lambda)` of the object colour or the spectral transmittance factor :math:`\\tau(\\lambda)` of the object is equal to unity for all wavelengths. For self-luminous objects and illuminants, the constants :math:`k` is usually chosen on the grounds of convenience. If, however, in the CIE 1931 standard colorimetric system, the :math:`Y` value is required to be numerically equal to the absolute value of a photometric quantity, the constant, :math:`k`, must be put equal to the numerical value of :math:`K_m`, the maximum spectral luminous efficacy (which is equal to 683 :math:`lm\\cdot W^{-1}`) and :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration of the radiometric quantity corresponding to the photometric quantity required. Returns ------- array_like *CIE XYZ* tristimulus values, for a 512x384 multi-spectral image with 77 bins, the output shape will be (384, 512, 3). Notes ----- +-----------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +===========+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +-----------+-----------------------+---------------+ References ---------- :cite:`Wyszecki2000bf` Examples -------- >>> msd = np.array([ ... [ ... [0.0137, 0.0913, 0.0152, 0.0281, 0.1918, 0.0430], ... [0.0159, 0.3145, 0.0842, 0.0907, 0.7103, 0.0437], ... [0.0096, 0.2582, 0.4139, 0.2228, 0.0041, 0.3744], ... [0.0111, 0.0709, 0.0220, 0.1249, 0.1817, 0.0020], ... [0.0179, 0.2971, 0.5630, 0.2375, 0.0024, 0.5819], ... [0.1057, 0.4620, 0.1918, 0.5625, 0.4209, 0.0027], ... ], ... [ ... [0.0433, 0.2683, 0.2373, 0.0518, 0.0118, 0.0823], ... [0.0258, 0.0831, 0.0430, 0.3230, 0.2302, 0.0081], ... [0.0248, 0.1203, 0.0054, 0.0065, 0.1860, 0.3625], ... [0.0186, 0.1292, 0.0079, 0.4006, 0.9404, 0.3213], ... [0.0310, 0.1682, 0.3719, 0.0861, 0.0041, 0.7849], ... [0.0473, 0.3221, 0.2268, 0.3161, 0.1124, 0.0024], ... ], ... ]) >>> multi_sds_to_XYZ(msd, SpectralShape(400, 700, 60)) ... # doctest: +ELLIPSIS array([[[ 7.6862675..., 4.0925470..., 8.4950412...], [ 27.4119366..., 15.5014764..., 29.2825122...], [ 17.1283666..., 27.7798651..., 25.5232032...], [ 11.9824544..., 8.8127109..., 6.6518695...], [ 19.1030682..., 34.4597818..., 29.7653804...], [ 46.8243374..., 39.9551652..., 43.6541858...]], <BLANKLINE> [[ 8.0978189..., 12.7544378..., 25.8004512...], [ 23.4360673..., 19.6127966..., 7.9342408...], [ 7.0933208..., 2.7894394..., 11.1527704...], [ 45.6313772..., 29.0068105..., 11.9934522...], [ 8.9327884..., 19.4008147..., 17.1534186...], [ 24.6610235..., 26.1093760..., 30.7298791...]]]) """ function = MULTI_SD_TO_XYZ_METHODS[method] return function(msd, shape, cmfs, illuminant, **filter_kwargs(function, **kwargs))
def is_within_visible_spectrum( XYZ, cmfs=MSDS_CMFS['CIE 1931 2 Degree Standard Observer'].copy().align( SPECTRAL_SHAPE_OUTER_SURFACE_XYZ), illuminant=sd_ones(SPECTRAL_SHAPE_OUTER_SURFACE_XYZ), tolerance=None, **kwargs): """ 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. 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. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.msds_to_XYZ`}, Please refer to the documentation of the previously listed definition. 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 = (hash(cmfs), hash(illuminant), six.text_type(kwargs)) vertices = _CACHE_OUTER_SURFACE_XYZ_POINTS.get(key) if vertices is None: _CACHE_OUTER_SURFACE_XYZ_POINTS[key] = vertices = (XYZ_outer_surface( cmfs, illuminant, **kwargs)) return is_within_mesh_volume(XYZ, vertices, tolerance)
def XYZ_to_spectral( XYZ, cmfs=colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], interval=5, tolerance=1e-10, maximum_iterations=5000, illuminant=sd_ones(), max_refl=1.0): XYZ = to_domain_1(XYZ) shape = SpectralShape(cmfs.shape.start, cmfs.shape.end, interval) cmfs = cmfs.copy().align(shape) illuminant = illuminant.copy().align(shape) spd = sd_zeros(shape) def function_objective(a): """ Objective function. """ return np.sum(np.diff(a)**2) def function_constraint(a): """ Function defining the constraint for XYZ=XYZ. """ spd[:] = np.exp(a) return (XYZ - (colour.colorimetry.spectral_to_XYZ_integration( spd, cmfs=cmfs, illuminant=illuminant))) def function_constraint2(a): """ Function defining constraint on emission/reflectance """ if max_refl <= 0.0: return 0.0 return max_refl - np.exp(np.max(a)) * 100. wavelengths = spd.wavelengths bins = wavelengths.size constraints = ({'type': 'eq', 'fun': function_constraint}, {'type': 'ineq', 'fun': function_constraint2}) result = minimize( function_objective, spd.values, method='SLSQP', constraints=constraints, options={ 'ftol': tolerance, 'maxiter': maximum_iterations, 'disp': True }) if not result.success: raise RuntimeError( 'Optimization failed for {0} after {1} iterations: "{2}".'.format( XYZ, result.nit, result.message)) return SpectralDistribution( from_range_100(np.exp(result.x) * 100), wavelengths, name='Meng (2015) - {0}'.format(XYZ))
def XYZ_to_sd_Meng2015( XYZ, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], interval=5, optimisation_parameters=None): """ Recovers the spectral distribution of given *CIE XYZ* tristimulus values using *Meng et al. (2015)* method. Parameters ---------- XYZ : array_like, (3,) *CIE XYZ* tristimulus values to recover the spectral distribution from. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. interval : numeric, optional Wavelength :math:`\\lambda_{i}` range interval in nm. The smaller ``interval`` is, the longer the computations will be. optimisation_parameters : dict_like, optional Parameters for :func:`scipy.optimize.minimize` definition. Returns ------- SpectralDistribution Recovered spectral distribution. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ - The definition used to convert spectrum to *CIE XYZ* tristimulus values is :func:`colour.colorimetry.spectral_to_XYZ_integration` definition because it processes any measurement interval opposed to :func:`colour.colorimetry.sd_to_XYZ_ASTME30815` definition that handles only measurement interval of 1, 5, 10 or 20nm. References ---------- :cite:`Meng2015c` Examples -------- >>> from colour.utilities import numpy_print_options >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> sd = XYZ_to_sd_Meng2015(XYZ, interval=10) >>> with numpy_print_options(suppress=True): ... # Doctests skip for Python 2.x compatibility. ... sd # doctest: +SKIP SpectralDistribution([[ 360. , 0.0780368...], [ 370. , 0.0780387...], [ 380. , 0.0780469...], [ 390. , 0.0780894...], [ 400. , 0.0780285...], [ 410. , 0.0777034...], [ 420. , 0.0769175...], [ 430. , 0.0746243...], [ 440. , 0.0691410...], [ 450. , 0.0599949...], [ 460. , 0.04779 ...], [ 470. , 0.0337270...], [ 480. , 0.0196952...], [ 490. , 0.0078056...], [ 500. , 0.0004368...], [ 510. , 0.0000065...], [ 520. , 0. ...], [ 530. , 0. ...], [ 540. , 0.0124283...], [ 550. , 0.0389186...], [ 560. , 0.0774087...], [ 570. , 0.1246716...], [ 580. , 0.1765055...], [ 590. , 0.2281652...], [ 600. , 0.2751726...], [ 610. , 0.3141208...], [ 620. , 0.3434564...], [ 630. , 0.3636521...], [ 640. , 0.3765182...], [ 650. , 0.3841561...], [ 660. , 0.3884648...], [ 670. , 0.3906975...], [ 680. , 0.3918679...], [ 690. , 0.3924590...], [ 700. , 0.3927439...], [ 710. , 0.3928570...], [ 720. , 0.3928867...], [ 730. , 0.3929099...], [ 740. , 0.3928997...], [ 750. , 0.3928827...], [ 760. , 0.3928579...], [ 770. , 0.3927857...], [ 780. , 0.3927272...], [ 790. , 0.3926867...], [ 800. , 0.3926441...], [ 810. , 0.3926385...], [ 820. , 0.3926247...], [ 830. , 0.3926105...]], interpolator=SpragueInterpolator, interpolator_args={}, extrapolator=Extrapolator, extrapolator_args={...}) >>> sd_to_XYZ_integration(sd) / 100 # doctest: +ELLIPSIS array([ 0.2065817..., 0.1219754..., 0.0514131...]) """ XYZ = to_domain_1(XYZ) shape = SpectralShape(cmfs.shape.start, cmfs.shape.end, interval) cmfs = cmfs.copy().align(shape) illuminant = sd_ones(shape) sd = sd_ones(shape) def objective_function(a): """ Objective function. """ return np.sum(np.diff(a) ** 2) def constraint_function(a): """ Function defining the constraint. """ sd[:] = a return sd_to_XYZ_integration( sd, cmfs=cmfs, illuminant=illuminant) - XYZ wavelengths = sd.wavelengths bins = wavelengths.size optimisation_settings = { 'method': 'SLSQP', 'constraints': { 'type': 'eq', 'fun': constraint_function }, 'bounds': np.tile(np.array([0, 1000]), (bins, 1)), 'options': { 'ftol': 1e-10, 'maxiter': 2000 }, } if optimisation_parameters is not None: optimisation_settings.update(optimisation_parameters) result = minimize(objective_function, sd.values, **optimisation_settings) if not result.success: raise RuntimeError( 'Optimization failed for {0} after {1} iterations: "{2}".'.format( XYZ, result.nit, result.message)) return SpectralDistribution( from_range_100(result.x * 100), wavelengths, name='Meng (2015) - {0}'.format(XYZ))
def plot_visible_spectrum( cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", out_of_gamut_clipping: Boolean = True, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the visible colours spectrum using given standard observer *CIE XYZ* colour matching functions. Parameters ---------- cmfs Standard observer colour matching functions used for computing the spectrum domain and colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. out_of_gamut_clipping Whether to clip out of gamut colours otherwise, the colours will be offset by the absolute minimal colour leading to a rendering on gray background, less saturated and smoother. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. References ---------- :cite:`Spiker2015a` Examples -------- >>> plot_visible_spectrum() # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Visible_Spectrum.png :align: center :alt: plot_visible_spectrum """ cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) bounding_box = (min(cmfs.wavelengths), max(cmfs.wavelengths), 0, 1) settings: Dict[str, Any] = {"bounding_box": bounding_box, "y_label": None} settings.update(kwargs) settings["standalone"] = False _figure, axes = plot_single_sd( sd_ones(cmfs.shape), cmfs=cmfs, out_of_gamut_clipping=out_of_gamut_clipping, **settings, ) # Removing wavelength line as it doubles with the axes spine. axes.lines.pop(0) settings = { "axes": axes, "standalone": True, "title": f"The Visible Spectrum - {cmfs.strict_name}", "x_label": "Wavelength $\\lambda$ (nm)", } settings.update(kwargs) return render(**settings)
def plot_visible_spectrum(cmfs='CIE 1931 2 Degree Standard Observer', out_of_gamut_clipping=True, **kwargs): """ Plots the visible colours spectrum using given standard observer *CIE XYZ* colour matching functions. Parameters ---------- cmfs : unicode, optional Standard observer colour matching functions used for spectrum creation. out_of_gamut_clipping : bool, optional Whether to clip out of gamut colours otherwise, the colours will be offset by the absolute minimal colour leading to a rendering on gray background, less saturated and smoother. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Returns ------- tuple Current figure and axes. References ---------- :cite:`Spiker2015a` Examples -------- >>> plot_visible_spectrum() # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, \ <matplotlib.axes._subplots.AxesSubplot object at 0x...>) .. image:: ../_static/Plotting_Plot_Visible_Spectrum.png :align: center :alt: plot_visible_spectrum """ cmfs = first_item(filter_cmfs(cmfs).values()) bounding_box = (min(cmfs.wavelengths), max(cmfs.wavelengths), 0, 1) settings = {'bounding_box': bounding_box, 'y_label': None} settings.update(kwargs) settings['standalone'] = False _figure, axes = plot_single_sd(sd_ones(cmfs.shape), cmfs=cmfs, out_of_gamut_clipping=out_of_gamut_clipping, **settings) # Removing wavelength line as it doubles with the axes spine. axes.lines.pop(0) settings = { 'axes': axes, 'standalone': True, 'title': 'The Visible Spectrum - {0}'.format(cmfs.strict_name), 'x_label': 'Wavelength $\\lambda$ (nm)', } settings.update(kwargs) return render(**settings)
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