def msds_constant( k: Floating, labels: Sequence, shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, **kwargs: Any, ) -> MultiSpectralDistributions: """ Return the multi-spectral distributions with given labels and given spectral shape filled with constant :math:`k` values. Parameters ---------- k Constant :math:`k` to fill the multi-spectral distributions with. labels Names to use for the :class:`colour.SpectralDistribution` class instances. shape Spectral shape used to create the multi-spectral distributions. Other Parameters ---------------- kwargs {:class:`colour.MultiSpectralDistributions`}, See the documentation of the previously listed class. Returns ------- :class:`colour.MultiSpectralDistributions` Constant :math:`k` filled multi-spectral distributions. Notes ----- - By default, the multi-spectral distributions will use the shape given by :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute. Examples -------- >>> msds = msds_constant(100, labels=['a', 'b', 'c']) >>> msds.shape SpectralShape(360.0, 780.0, 1.0) >>> msds[400] array([ 100., 100., 100.]) >>> msds.labels # doctest: +SKIP ['a', 'b', 'c'] """ settings = {"name": f"{k} Constant"} settings.update(kwargs) wavelengths = shape.range() values = full((len(wavelengths), len(labels)), k) return MultiSpectralDistributions(values, wavelengths, labels=labels, **settings)
def load_TCS_CIE2017(shape: SpectralShape) -> MultiSpectralDistributions: """ Load the *CIE 2017 Test Colour Samples* dataset appropriate for the given spectral shape. The datasets are cached and won't be loaded again on subsequent calls to this definition. Parameters ---------- shape Spectral shape of the tested illuminant. Returns ------- :class:`colour.MultiSpectralDistributions` *CIE 2017 Test Colour Samples* dataset. Examples -------- >>> sds_tcs = load_TCS_CIE2017(SpectralShape(380, 780, 5)) >>> len(sds_tcs.labels) 99 """ global _CACHE_TCS_CIE2017 interval = shape.interval attest( interval in (1, 5), "Spectral shape interval must be either 1nm or 5nm!", ) filename = f"tcs_cfi2017_{as_int_scalar(interval)}_nm.csv.gz" if filename in _CACHE_TCS_CIE2017: return _CACHE_TCS_CIE2017[filename] data = np.genfromtxt(str( os.path.join(RESOURCES_DIRECTORY_CIE2017, filename)), delimiter=",") labels = [f"TCS{i} (CIE 2017)" for i in range(99)] tcs = MultiSpectralDistributions(data[:, 1:], data[:, 0], labels) _CACHE_TCS_CIE2017[filename] = tcs return tcs
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 msds_constant(k, labels, shape=SPECTRAL_SHAPE_DEFAULT, dtype=None): """ Returns the multi-spectral distributions with given labels and given spectral shape filled with constant :math:`k` values. Parameters ---------- k : numeric Constant :math:`k` to fill the multi-spectral distributions with. labels : array_like Names to use for the :class:`colour.SpectralDistribution` class instances. shape : SpectralShape, optional Spectral shape used to create the multi-spectral distributions. dtype : type Data type used for the multi-spectral distributions. Returns ------- MultiSpectralDistributions Constant :math:`k` filled multi-spectral distributions. Notes ----- - By default, the multi-spectral distributions will use the shape given by :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute. Examples -------- >>> msds = msds_constant(100, labels=['a', 'b', 'c']) >>> msds.shape SpectralShape(360.0, 780.0, 1.0) >>> msds[400] array([ 100., 100., 100.]) >>> msds.labels # doctest: +SKIP ['a', 'b', 'c'] """ if dtype is None: dtype = DEFAULT_FLOAT_DTYPE wavelengths = shape.range(dtype) values = full([len(wavelengths), len(labels)], k, dtype) name = '{0} Constant'.format(k) return MultiSpectralDistributions( values, wavelengths, name=name, labels=labels, dtype=dtype)
def load_TCS_CIE2017(shape): """ Loads the *CIE 2017 Test Colour Samples* dataset appropriate for the given spectral shape. The datasets are cached and won't be loaded again on subsequent calls to this definition. Parameters ---------- shape : SpectralShape Spectral shape of the tested illuminant. Returns ------- MultiSpectralDistributions *CIE 2017 Test Colour Samples* dataset. Examples -------- >>> sds_tcs = load_TCS_CIE2017(SpectralShape(interval=5)) >>> len(sds_tcs.labels) 99 """ global _CACHE_TCS_CIE2017 interval = shape.interval assert interval in (1, 5), ( 'Spectral shape interval must be either 1nm or 5nm!') filename = 'tcs_cfi2017_{0}_nm.csv.gz'.format(as_int(interval)) if filename in _CACHE_TCS_CIE2017: return _CACHE_TCS_CIE2017[filename] data = np.genfromtxt(str( os.path.join(RESOURCES_DIRECTORY_CIE2017, filename)), delimiter=',') labels = ['TCS{0} (CIE 2017)'.format(i) for i in range(99)] return MultiSpectralDistributions(data[:, 1:], data[:, 0], labels)
0.00413520, 0.78492409, ], [ 0.04727245, 0.32210270, 0.22679484, 0.31613642, 0.11242847, 0.00244144, ], ], ]) MSDS_TWO: MultiSpectralDistributions = MultiSpectralDistributions( np.transpose(np.reshape(DATA_TWO, [-1, 6])), SpectralShape(400, 700, 60).range(), ) TVS_D65_INTEGRATION_MSDS: NDArray = np.array([ [7.50219602, 3.95048275, 8.40152163], [26.92629005, 15.07170066, 28.71020457], [16.70060700, 28.21421317, 25.64802044], [11.57577260, 8.64108703, 6.57740493], [18.73108262, 35.07369122, 30.14365007], [45.16559608, 39.61411218, 43.68158810], [8.17318743, 13.09236381, 25.93755134], [22.46715798, 19.31066951, 7.95954422], [6.58106180, 2.52865132, 11.09122159], [43.91745731, 27.98043364, 11.73313699], [8.53693599, 19.70195654, 17.70110118], [23.91114755, 26.21471641, 30.67613685],
[[0.01367208, 0.09127947, 0.01524376, 0.02810712, 0.19176012, 0.04299992], [0.01591516, 0.31454948, 0.08416876, 0.09071489, 0.71026170, 0.04374762], [0.00959792, 0.25822842, 0.41388571, 0.22275120, 0.00407416, 0.37439537], [0.01106279, 0.07090867, 0.02204929, 0.12487984, 0.18168917, 0.00202945], [0.01791409, 0.29707789, 0.56295109, 0.23752193, 0.00236515, 0.58190280], [0.10565346, 0.46204320, 0.19180590, 0.56250858, 0.42085907, 0.00270085]], [[0.04325933, 0.26825359, 0.23732357, 0.05175860, 0.01181048, 0.08233768], [0.02577249, 0.08305486, 0.04303044, 0.32298771, 0.23022813, 0.00813306], [0.02484169, 0.12027161, 0.00541695, 0.00654612, 0.18603799, 0.36247808], [0.01861601, 0.12924391, 0.00785840, 0.40062562, 0.94044405, 0.32133976], [0.03102159, 0.16815442, 0.37186235, 0.08610666, 0.00413520, 0.78492409], [0.04727245, 0.32210270, 0.22679484, 0.31613642, 0.11242847, 0.00244144]], ]) MSDS = MultiSpectralDistributions( np.transpose(np.reshape(MSDS_ARRAY, [-1, 6])), SpectralShape(400, 700, 60).range()) XYZ_D65_INTEGRATION_MSDS = np.array([ [7.50233492, 3.95038753, 8.40338691], [26.92687731, 15.07139447, 28.71690972], [16.70142165, 28.21458593, 25.65389977], [11.57556582, 8.64089371, 6.57886800], [18.73219899, 35.07426727, 30.15041858], [45.16536627, 39.61360393, 43.69092793], [8.17423105, 13.09267724, 25.94320661], [22.46636804, 19.31025987, 7.96118575], [6.58131705, 2.52857864, 11.09362054], [43.91622393, 27.97952698, 11.73575112], [8.53774314, 19.70241814, 17.70479540], [23.91149458, 26.21467120, 30.68294836],
[0.34488011934469100, 0.33148268992224300, 0.32363699470796100], [0.34191787732329100, 0.33198455035238900, 0.32609730884690000], [0.33953109298712900, 0.33234117252254500, 0.32812736934018400], [0.33716950377436700, 0.33291200941553900, 0.32991797595888800], [0.33617201852771700, 0.33291927969521400, 0.33090790121664900], [0.33516744343336300, 0.33302767257885600, 0.33180363309599500], [0.33442162530646300, 0.33317970467326000, 0.33239662725536100], [0.33400876037640200, 0.33324703097454900, 0.33274078072682400], [0.33391579279008200, 0.33325934921060100, 0.33282085708148900], [0.33381845494636700, 0.33327505027938300, 0.33290173128344400], [0.33367277492845600, 0.33329432844873200, 0.33302596748863200], [0.33356951340559100, 0.33330942495777500, 0.33311108308149700], ] ) MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019: MultiSpectralDistributions = ( MultiSpectralDistributions( DATA_BASIS_FUNCTIONS_sRGB_MALLETT2019, SPECTRAL_SHAPE_sRGB_MALLETT2019.range(), name="Basis Functions - sRGB - Mallett 2019", labels=("red", "green", "blue"), ) ) """ *Mallett and Yuksel (2019)* basis functions for the *sRGB* colourspace. References ---------- :cite:`Mallett2019` """
[0.35760870152308100, 0.32914390227898000, 0.31324730130288600], [0.34871280010839300, 0.33080872680368200, 0.32047832512463300], [0.34488011934469100, 0.33148268992224300, 0.32363699470796100], [0.34191787732329100, 0.33198455035238900, 0.32609730884690000], [0.33953109298712900, 0.33234117252254500, 0.32812736934018400], [0.33716950377436700, 0.33291200941553900, 0.32991797595888800], [0.33617201852771700, 0.33291927969521400, 0.33090790121664900], [0.33516744343336300, 0.33302767257885600, 0.33180363309599500], [0.33442162530646300, 0.33317970467326000, 0.33239662725536100], [0.33400876037640200, 0.33324703097454900, 0.33274078072682400], [0.33391579279008200, 0.33325934921060100, 0.33282085708148900], [0.33381845494636700, 0.33327505027938300, 0.33290173128344400], [0.33367277492845600, 0.33329432844873200, 0.33302596748863200], [0.33356951340559100, 0.33330942495777500, 0.33311108308149700], ]) MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019 = MultiSpectralDistributions( DATA_BASIS_FUNCTIONS_sRGB_MALLETT2019, SPECTRAL_SHAPE_sRGB_MALLETT2019.range(), name='Basis Functions - sRGB - Mallett 2019', labels=('red', 'green', 'blue')) """ *Mallett and Yuksel (2019)* basis functions for the *sRGB* colourspace. References ---------- :cite:`Mallett2019` MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019 : MultiSpectralDistributions """
def spectral_primary_decomposition_Mallett2019( colourspace: RGB_Colourspace, cmfs: Optional[MultiSpectralDistributions] = None, illuminant: Optional[SpectralDistribution] = None, metric: Callable = np.linalg.norm, metric_args: Tuple = tuple(), optimisation_kwargs: Optional[Dict] = None, ) -> MultiSpectralDistributions: """ Perform the spectral primary decomposition as described in *Mallett and Yuksel (2019)* for given *RGB* colourspace. Parameters ---------- colourspace *RGB* colourspace. cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. illuminant Illuminant spectral distribution, default to *CIE Standard Illuminant D65*. metric Function to be minimised, i.e. the objective function. ``metric(basis, *metric_args) -> float`` where ``basis`` is three reflectances concatenated together, each with a shape matching ``shape``. metric_args Additional arguments passed to ``metric``. optimisation_kwargs Parameters for :func:`scipy.optimize.minimize` definition. Returns ------- :class:`colour.MultiSpectralDistributions` Basis functions for given *RGB* colourspace. References ---------- :cite:`Mallett2019` Notes ----- - In-addition to the *BT.709* primaries used by the *sRGB* colourspace, :cite:`Mallett2019` tried *BT.2020*, *P3 D65*, *Adobe RGB 1998*, *NTSC (1987)*, *Pal/Secam*, *ProPhoto RGB*, and *Adobe Wide Gamut RGB* primaries, every one of which encompasses a larger (albeit not-always-enveloping) set of *CIE L\\*a\\*b\\** colours than BT.709. Of these, only *Pal/Secam* produces a feasible basis, which is relatively unsurprising since it is very similar to *BT.709*, whereas the others are significantly larger. Examples -------- >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS, SpectralShape >>> from colour.models import RGB_COLOURSPACE_PAL_SECAM >>> from colour.utilities import numpy_print_options >>> cmfs = ( ... MSDS_CMFS['CIE 1931 2 Degree Standard Observer']. ... copy().align(SpectralShape(360, 780, 10)) ... ) >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape) >>> msds = spectral_primary_decomposition_Mallett2019( ... RGB_COLOURSPACE_PAL_SECAM, cmfs, illuminant, optimisation_kwargs={ ... 'options': {'ftol': 1e-5} ... } ... ) >>> with numpy_print_options(suppress=True): ... print(msds) # doctest: +SKIP [[ 360. 0.3395134... 0.3400214... 0.3204650...] [ 370. 0.3355246... 0.3338028... 0.3306724...] [ 380. 0.3376707... 0.3185578... 0.3437715...] [ 390. 0.3178866... 0.3351754... 0.3469378...] [ 400. 0.3045154... 0.3248376... 0.3706469...] [ 410. 0.2935652... 0.2919463... 0.4144884...] [ 420. 0.1875740... 0.1853729... 0.6270530...] [ 430. 0.0167983... 0.054483 ... 0.9287186...] [ 440. 0. ... 0. ... 1. ...] [ 450. 0. ... 0. ... 1. ...] [ 460. 0. ... 0. ... 1. ...] [ 470. 0. ... 0.0458044... 0.9541955...] [ 480. 0. ... 0.2960917... 0.7039082...] [ 490. 0. ... 0.5042592... 0.4957407...] [ 500. 0. ... 0.6655795... 0.3344204...] [ 510. 0. ... 0.8607541... 0.1392458...] [ 520. 0. ... 0.9999998... 0.0000001...] [ 530. 0. ... 1. ... 0. ...] [ 540. 0. ... 1. ... 0. ...] [ 550. 0. ... 1. ... 0. ...] [ 560. 0. ... 0.9924229... 0. ...] [ 570. 0. ... 0.9970703... 0.0025673...] [ 580. 0.0396002... 0.9028231... 0.0575766...] [ 590. 0.7058973... 0.2941026... 0. ...] [ 600. 1. ... 0. ... 0. ...] [ 610. 1. ... 0. ... 0. ...] [ 620. 1. ... 0. ... 0. ...] [ 630. 1. ... 0. ... 0. ...] [ 640. 0.9835925... 0.0100166... 0.0063908...] [ 650. 0.7878949... 0.1265097... 0.0855953...] [ 660. 0.5987994... 0.2051062... 0.1960942...] [ 670. 0.4724493... 0.2649623... 0.2625883...] [ 680. 0.3989806... 0.3007488... 0.3002704...] [ 690. 0.3666586... 0.3164003... 0.3169410...] [ 700. 0.3497806... 0.3242863... 0.3259329...] [ 710. 0.3563736... 0.3232441... 0.3203822...] [ 720. 0.3362624... 0.3326209... 0.3311165...] [ 730. 0.3245015... 0.3365982... 0.3389002...] [ 740. 0.3335520... 0.3320670... 0.3343808...] [ 750. 0.3441287... 0.3291168... 0.3267544...] [ 760. 0.3343705... 0.3330132... 0.3326162...] [ 770. 0.3274633... 0.3305704... 0.3419662...] [ 780. 0.3475263... 0.3262331... 0.3262404...]] """ cmfs, illuminant = handle_spectral_arguments(cmfs, illuminant) N = len(cmfs.shape) R_to_XYZ = np.transpose(illuminant.values[..., np.newaxis] * cmfs.values / (np.sum(cmfs.values[:, 1] * illuminant.values))) R_to_RGB = np.dot(colourspace.matrix_XYZ_to_RGB, R_to_XYZ) basis_to_RGB = block_diag(R_to_RGB, R_to_RGB, R_to_RGB) primaries = np.reshape(np.identity(3), 9) # Ensure that the reflectances correspond to the correct RGB colours. colour_match = LinearConstraint(basis_to_RGB, primaries, primaries) # Ensure that the reflectances are bounded by [0, 1]. energy_conservation = Bounds(np.zeros(3 * N), np.ones(3 * N)) # Ensure that the sum of the three bases is bounded by [0, 1]. sum_matrix = np.transpose(np.tile(np.identity(N), (3, 1))) sum_constraint = LinearConstraint(sum_matrix, np.zeros(N), np.ones(N)) optimisation_settings = { "method": "SLSQP", "constraints": [colour_match, sum_constraint], "bounds": energy_conservation, "options": { "ftol": 1e-10, }, } if optimisation_kwargs is not None: optimisation_settings.update(optimisation_kwargs) result = minimize(metric, args=metric_args, x0=np.zeros(3 * N), **optimisation_settings) basis_functions = np.transpose(np.reshape(result.x, (3, N))) return MultiSpectralDistributions( basis_functions, cmfs.shape.range(), name=f"Basis Functions - {colourspace.name} - Mallett (2019)", labels=("red", "green", "blue"), )
def spectral_primary_decomposition_Mallett2019( colourspace, cmfs=MSDS_CMFS_STANDARD_OBSERVER[ 'CIE 1931 2 Degree Standard Observer'], illuminant=SDS_ILLUMINANTS['D65'], metric=np.linalg.norm, metric_args=tuple(), optimisation_kwargs=None): """ Performs the spectral primary decomposition as described in *Mallett and Yuksel (2019)* for given *RGB* colourspace. Parameters ---------- colourspace: RGB_Colourspace *RGB* colourspace. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. metric : unicode, optional Function to be minimised, i.e. the objective function. ``metric(basis, *metric_args) -> float`` where ``basis`` is three reflectances concatenated together, each with a shape matching ``shape``. metric_args : tuple, optional Additional arguments passed to ``metric``. optimisation_kwargs : dict_like, optional Parameters for :func:`scipy.optimize.minimize` definition. Returns ------- MultiSpectralDistributions Basis functions for given *RGB* colourspace. References ---------- :cite:`Mallett2019` Notes ----- - In-addition to the *BT.709* primaries used by the *sRGB* colourspace, :cite:`Mallett2019` tried *BT.2020*, *P3 D65*, *Adobe RGB 1998*, *NTSC (1987)*, *Pal/Secam*, *ProPhoto RGB*, and *Adobe Wide Gamut RGB* primaries, every one of which encompasses a larger (albeit not-always-enveloping) set of *CIE L\\*a\\*b\\** colours than BT.709. Of these, only *Pal/Secam* produces a feasible basis, which is relatively unsurprising since it is very similar to *BT.709*, whereas the others are significantly larger. Examples -------- >>> from colour.colorimetry import SpectralShape >>> from colour.models import RGB_COLOURSPACE_PAL_SECAM >>> from colour.utilities import numpy_print_options >>> 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) >>> msds = spectral_primary_decomposition_Mallett2019( ... RGB_COLOURSPACE_PAL_SECAM, cmfs, illuminant, optimisation_kwargs={ ... 'options': {'ftol': 1e-5} ... } ... ) >>> with numpy_print_options(suppress=True): ... # Doctests skip for Python 2.x compatibility. ... print(msds) # doctest: +SKIP [[ 360. 0.3324728... 0.3332663... 0.3342608...] [ 370. 0.3323307... 0.3327746... 0.3348946...] [ 380. 0.3341115... 0.3323995... 0.3334889...] [ 390. 0.3337570... 0.3298092... 0.3364336...] [ 400. 0.3209352... 0.3218213... 0.3572433...] [ 410. 0.2881025... 0.2837628... 0.4281346...] [ 420. 0.1836749... 0.1838893... 0.6324357...] [ 430. 0.0187212... 0.0529655... 0.9283132...] [ 440. 0. ... 0. ... 1. ...] [ 450. 0. ... 0. ... 1. ...] [ 460. 0. ... 0. ... 1. ...] [ 470. 0. ... 0.0509556... 0.9490443...] [ 480. 0. ... 0.2933996... 0.7066003...] [ 490. 0. ... 0.5001396... 0.4998603...] [ 500. 0. ... 0.6734805... 0.3265195...] [ 510. 0. ... 0.8555213... 0.1444786...] [ 520. 0. ... 0.9999985... 0.0000014...] [ 530. 0. ... 1. ... 0. ...] [ 540. 0. ... 1. ... 0. ...] [ 550. 0. ... 1. ... 0. ...] [ 560. 0. ... 0.9924229... 0. ...] [ 570. 0. ... 0.9913344... 0.0083032...] [ 580. 0.0370289... 0.9145168... 0.0484542...] [ 590. 0.7100075... 0.2898477... 0.0001446...] [ 600. 1. ... 0. ... 0. ...] [ 610. 1. ... 0. ... 0. ...] [ 620. 1. ... 0. ... 0. ...] [ 630. 1. ... 0. ... 0. ...] [ 640. 0.9711347... 0.0137659... 0.0150993...] [ 650. 0.7996619... 0.1119379... 0.0884001...] [ 660. 0.6064640... 0.202815 ... 0.1907209...] [ 670. 0.4662959... 0.2675037... 0.2662005...] [ 680. 0.4010958... 0.2998989... 0.2990052...] [ 690. 0.3617485... 0.3208921... 0.3173592...] [ 700. 0.3496691... 0.3247855... 0.3255453...] [ 710. 0.3433979... 0.3273540... 0.329248 ...] [ 720. 0.3358860... 0.3345583... 0.3295556...] [ 730. 0.3349498... 0.3314232... 0.3336269...] [ 740. 0.3359954... 0.3340147... 0.3299897...] [ 750. 0.3310392... 0.3327595... 0.3362012...] [ 760. 0.3346883... 0.3314158... 0.3338957...] [ 770. 0.3332167... 0.333371 ... 0.3334122...] [ 780. 0.3319670... 0.3325476... 0.3354852...]] """ 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) N = len(cmfs.shape) R_to_XYZ = np.transpose( np.expand_dims(illuminant.values, axis=1) * cmfs.values / (np.sum(cmfs.values[:, 1] * illuminant.values))) R_to_RGB = np.dot(colourspace.matrix_XYZ_to_RGB, R_to_XYZ) basis_to_RGB = block_diag(R_to_RGB, R_to_RGB, R_to_RGB) primaries = np.identity(3).reshape(9) # Ensure the reflectances correspond to the correct RGB colours. colour_match = LinearConstraint(basis_to_RGB, primaries, primaries) # Ensure the reflectances are bounded by [0, 1]. energy_conservation = Bounds(np.zeros(3 * N), np.ones(3 * N)) # Ensure the sum of the three bases is bounded by [0, 1]. sum_matrix = np.transpose(np.tile(np.identity(N), (3, 1))) sum_constraint = LinearConstraint(sum_matrix, np.zeros(N), np.ones(N)) optimisation_settings = { 'method': 'SLSQP', 'constraints': [colour_match, sum_constraint], 'bounds': energy_conservation, 'options': { 'ftol': 1e-10, } } if optimisation_kwargs is not None: optimisation_settings.update(optimisation_kwargs) result = minimize(metric, args=metric_args, x0=np.zeros(3 * N), **optimisation_settings) basis_functions = np.transpose(result.x.reshape(3, N)) return MultiSpectralDistributions( basis_functions, cmfs.shape.range(), name='Basis Functions - {0} - Mallett (2019)'.format(colourspace.name), labels=('red', 'green', 'blue'))