def domain(self, value): """ Setter for the **self.domain** property. """ if value is not None: if np.asarray(value).dtype != object: if not np.all(np.isfinite(value)): runtime_warning( '"{0}" new "domain" variable is not finite: {1}, ' 'unpredictable results may occur!'.format( self.name, value)) value = np.copy(value).astype(self.dtype) if self._range is not None: if value.size != self._range.size: runtime_warning( '"{0}" new "domain" and current "range" variables ' 'have different size, "range" variable will be ' 'resized to "domain" variable shape!'.format( self.name)) self._range = np.resize(self._range, value.shape) self._domain = value self._create_function()
def domain(self, value): """ Setter for the **self.domain** property. """ if value is not None: if not np.all(np.isfinite(value)): runtime_warning( '"{0}" new "domain" variable is not finite: {1}, ' 'unpredictable results may occur!'.format( self.name, value)) value = np.copy(value).astype(self.dtype) if self._range is not None: if value.size != self._range.size: runtime_warning( '"{0}" new "domain" and current "range" variables ' 'have different size, "range" variable will be ' 'resized to "domain" variable shape!'.format( self.name)) self._range = np.resize(self._range, value.shape) self._domain = value self._create_function()
def illuminant(self): # pragma: no cover # Docstrings are omitted for documentation purposes. runtime_warning( str( ObjectRenamed('RGB_Colourspace.illuminant', 'RGB_Colourspace.whitepoint_name'))) return self.whitepoint_name
def encoding_cctf(self): # pragma: no cover # Docstrings are omitted for documentation purposes. runtime_warning( str( ObjectRenamed('RGB_Colourspace.encoding_cctf', 'RGB_Colourspace.cctf_encoding'))) return self.cctf_encoding
def illuminant(self, value): # Docstrings are omitted for documentation purposes. runtime_warning( str( Renamed('RGB_Colourspace.illuminant', 'RGB_Colourspace.whitepoint_name'))) self.whitepoint_name = value
def training_data_sds_to_RGB(training_data, sensitivities, illuminant): """ Converts given training data to *RGB* tristimulus values using given illuminant and given camera *RGB* spectral sensitivities. Parameters ---------- training_data : MultiSpectralDistributions Training data multi-spectral distributions. sensitivities : RGB_CameraSensitivities Camera *RGB* spectral sensitivities. illuminant : SpectralDistribution Illuminant spectral distribution. Returns ------- ndarray Training data *RGB* tristimulus values. Examples -------- >>> path = os.path.join( ... RESOURCES_DIRECTORY_RAWTOACES, ... 'CANON_EOS_5DMark_II_RGB_Sensitivities.csv') >>> sensitivities = sds_and_msds_to_msds( ... read_sds_from_csv_file(path).values()) >>> illuminant = normalise_illuminant( ... SDS_ILLUMINANTS['D55'], sensitivities) >>> training_data = read_training_data_rawtoaces_v1() >>> training_data_sds_to_RGB(training_data, sensitivities, illuminant)[:5] ... # doctest: +ELLIPSIS array([[ 0.0207582..., 0.0196857..., 0.0213935...], [ 0.0895775..., 0.0891922..., 0.0891091...], [ 0.7810230..., 0.7801938..., 0.7764302...], [ 0.1995 ..., 0.1995 ..., 0.1995 ...], [ 0.5898478..., 0.5904015..., 0.5851076...]]) """ shape = sensitivities.shape if illuminant.shape != shape: runtime_warning('Aligning "{0}" illuminant shape to "{1}".'.format( illuminant.name, shape)) illuminant = illuminant.copy().align(shape) if training_data.shape != shape: runtime_warning('Aligning "{0}" training data shape to "{1}".'.format( training_data.name, shape)) training_data = training_data.copy().align(shape) RGB_w = white_balance_multipliers(sensitivities, illuminant) RGB = np.dot( np.transpose( illuminant.values[..., np.newaxis] * training_data.values), sensitivities.values) RGB *= RGB_w return RGB
def random_triplet_generator(size, limits=np.array([[0, 1], [0, 1], [0, 1]]), random_state=RANDOM_STATE): """ Returns a generator yielding random triplets. Parameters ---------- size : int Generator size. limits : array_like, (3, 2) Random values limits on each triplet axis. random_state : RandomState Mersenne Twister pseudo-random number generator. Returns ------- generator Random triplets generator. Notes ----- - The test is assuming that :func:`np.random.RandomState` definition will return the same sequence no matter which *OS* or *Python* version is used. There is however no formal promise about the *prng* sequence reproducibility of either *Python* or *Numpy* implementations, see :cite:`Laurent2012a`. Examples -------- >>> from pprint import pprint >>> prng = np.random.RandomState(4) >>> random_triplet_generator(10, random_state=prng) ... # doctest: +ELLIPSIS array([[ 0.9670298..., 0.7793829..., 0.4361466...], [ 0.5472322..., 0.1976850..., 0.9489773...], [ 0.9726843..., 0.8629932..., 0.7863059...], [ 0.7148159..., 0.9834006..., 0.8662893...], [ 0.6977288..., 0.1638422..., 0.1731654...], [ 0.2160895..., 0.5973339..., 0.0749485...], [ 0.9762744..., 0.0089861..., 0.6007427...], [ 0.0062302..., 0.3865712..., 0.1679721...], [ 0.2529823..., 0.0441600..., 0.7333801...], [ 0.4347915..., 0.9566529..., 0.4084438...]]) """ integer_size = DEFAULT_INT_DTYPE(size) if integer_size != size: runtime_warning( '"size" has been cast to integer: {0}'.format(integer_size)) return tstack([ random_state.uniform(*limits[0], size=integer_size), random_state.uniform(*limits[1], size=integer_size), random_state.uniform(*limits[2], size=integer_size), ])
def domain(self, value: ArrayLike): """Setter for the **self.domain** property.""" value = as_float_array(value, self.dtype) if not np.all(np.isfinite(value)): runtime_warning( f'"{self.name}" new "domain" variable is not finite: {value}, ' f"unpredictable results may occur!" ) if value.size != self._range.size: self._range = np.resize(self._range, value.shape) self._domain = value self._create_function()
def normalise_illuminant( illuminant: SpectralDistribution, sensitivities: RGB_CameraSensitivities) -> SpectralDistribution: """ Normalise given illuminant with given camera *RGB* spectral sensitivities. The multiplicative inverse scaling factor :math:`k` is computed by multiplying the illuminant by the sensitivies channel with the maximum value. Parameters ---------- illuminant Illuminant spectral distribution. sensitivities Camera *RGB* spectral sensitivities. Returns ------- :class:`colour.SpectralDistribution` Normalised illuminant. Examples -------- >>> path = os.path.join( ... RESOURCES_DIRECTORY_RAWTOACES, ... 'CANON_EOS_5DMark_II_RGB_Sensitivities.csv') >>> sensitivities = sds_and_msds_to_msds( ... read_sds_from_csv_file(path).values()) >>> illuminant = SDS_ILLUMINANTS['D55'] >>> np.sum(illuminant.values) # doctest: +ELLIPSIS 7276.1490000... >>> np.sum(normalise_illuminant(illuminant, sensitivities).values) ... # doctest: +ELLIPSIS 3.4390373... """ shape = sensitivities.shape if illuminant.shape != shape: runtime_warning( f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape) c_i = np.argmax(np.max(sensitivities.values, axis=0)) k = 1 / np.sum(illuminant.values * sensitivities.values[..., c_i]) return illuminant * k
def white_balance_multipliers(sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution) -> NDArray: """ Compute the *RGB* white balance multipliers for given camera *RGB* spectral sensitivities and illuminant. Parameters ---------- sensitivities Camera *RGB* spectral sensitivities. illuminant Illuminant spectral distribution. Returns ------- :class:`numpy.ndarray` *RGB* white balance multipliers. References ---------- :cite:`Dyer2017` Examples -------- >>> path = os.path.join( ... RESOURCES_DIRECTORY_RAWTOACES, ... 'CANON_EOS_5DMark_II_RGB_Sensitivities.csv') >>> sensitivities = sds_and_msds_to_msds( ... read_sds_from_csv_file(path).values()) >>> illuminant = SDS_ILLUMINANTS['D55'] >>> white_balance_multipliers(sensitivities, illuminant) ... # doctest: +ELLIPSIS array([ 2.3414154..., 1. , 1.5163375...]) """ shape = sensitivities.shape if illuminant.shape != shape: runtime_warning( f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape) RGB_w = 1 / np.sum( sensitivities.values * illuminant.values[..., np.newaxis], axis=0) RGB_w *= 1 / np.min(RGB_w) return RGB_w
def range(self, value: ArrayLike): """Setter for the **self.range** property.""" value = as_float_array(value, self.dtype) if not np.all(np.isfinite(value)): runtime_warning( f'"{self.name}" new "range" variable is not finite: {value}, ' f"unpredictable results may occur!" ) attest( value.size == self._domain.size, '"domain" and "range" variables must have same size!', ) self._range = value self._create_function()
def range(self, value): """ Setter for the **self.range** property. """ if value is not None: if not np.all(np.isfinite(value)): runtime_warning( '"{0}" new "range" variable is not finite: {1}, ' 'unpredictable results may occur!'.format( self.name, value)) value = np.copy(value).astype(self.dtype) if self._domain is not None: assert value.size == self._domain.size, ( '"domain" and "range" variables must have same size!') self._range = value self._create_function()
def filter_passthrough( mapping: Mapping, filterers: Union[Any, str, Sequence[Union[Any, str]]], anchors: Boolean = True, allow_non_siblings: Boolean = True, flags: Union[Integer, RegexFlag] = re.IGNORECASE, ) -> Dict: """ Return mapping objects matching given filterers while passing through class instances whose type is one of the mapping element types. This definition allows passing custom but compatible objects to the various plotting definitions that by default expect the key from a dataset element. For example, a typical call to :func:`colour.plotting.\ plot_multi_illuminant_sds` definition with a regex pattern automatically anchored at boundaries by default is as follows: >>> import colour >>> colour.plotting.plot_multi_illuminant_sds(['A']) ... # doctest: +SKIP Here, `'A'` is by default anchored at boundaries and transformed into `'^A$'`. Note that because it is a regex pattern, special characters such as parenthesis must be escaped: `'Adobe RGB (1998)'` must be written `'Adobe RGB \\(1998\\)'` instead. With the previous example, t is also possible to pass a custom spectral distribution as follows: >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360 ... } >>> colour.plotting.plot_multi_illuminant_sds( ... ['A', colour.SpectralDistribution(data)]) ... # doctest: +SKIP Similarly, a typical call to :func:`colour.plotting.\ plot_planckian_locus_in_chromaticity_diagram_CIE1931` definition is as follows: >>> colour.plotting.plot_planckian_locus_in_chromaticity_diagram_CIE1931( ... ['A']) ... # doctest: +SKIP But it is also possible to pass a custom whitepoint as follows: >>> colour.plotting.plot_planckian_locus_in_chromaticity_diagram_CIE1931( ... ['A', {'Custom': np.array([1 / 3 + 0.05, 1 / 3 + 0.05])}]) ... # doctest: +SKIP Parameters ---------- mapping Mapping to filter. filterers Filterer or object class instance (which is passed through directly if its type is one of the mapping element types) or list of filterers. anchors Whether to use Regex line anchors, i.e. *^* and *$* are added, surrounding the filterers patterns. allow_non_siblings Whether to allow non-siblings to be also passed through. flags Regex flags. Returns ------- :class:`dict` Filtered mapping. """ if is_string(filterers): filterers = [filterers] elif not isinstance(filterers, (list, tuple)): filterers = [filterers] string_filterers: List[str] = [ cast(str, filterer) for filterer in filterers if is_string(filterer) ] object_filterers: List[Any] = [ filterer for filterer in filterers if is_sibling(filterer, mapping) ] if allow_non_siblings: non_siblings = [ filterer for filterer in filterers if filterer not in string_filterers and filterer not in object_filterers ] if non_siblings: runtime_warning( f'Non-sibling elements are passed-through: "{non_siblings}"') object_filterers.extend(non_siblings) filtered_mapping = filter_mapping(mapping, string_filterers, anchors, flags) for filterer in object_filterers: # TODO: Consider using "MutableMapping" here. if isinstance(filterer, (dict, CaseInsensitiveMapping)): for key, value in filterer.items(): filtered_mapping[key] = value else: try: name = filterer.name except AttributeError: try: name = filterer.__name__ except AttributeError: name = str(id(filterer)) filtered_mapping[name] = filterer return filtered_mapping
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'))
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 filter_passthrough(mapping, filterers, anchors=True, allow_non_siblings=True, flags=re.IGNORECASE): """ Returns mapping objects matching given filterers while passing through class instances whose type is one of the mapping element types. This definition allows passing custom but compatible objects to the various plotting definitions that by default expect the key from a dataset element. For example, a typical call to :func:`colour.plotting.\ plot_multi_illuminant_sds` definition is as follows: >>> import colour >>> import colour.plotting >>> colour.plotting.plot_multi_illuminant_sds(['A']) ... # doctest: +SKIP But it is also possible to pass a custom spectral distribution as follows: >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360 ... } >>> colour.plotting.plot_multi_illuminant_sds( ... ['A', colour.SpectralDistribution(data)]) ... # doctest: +SKIP Similarly, a typical call to :func:`colour.plotting.\ plot_planckian_locus_in_chromaticity_diagram_CIE1931` definition is as follows: >>> colour.plotting.plot_planckian_locus_in_chromaticity_diagram_CIE1931( ... ['A']) ... # doctest: +SKIP But it is also possible to pass a custom whitepoint as follows: >>> colour.plotting.plot_planckian_locus_in_chromaticity_diagram_CIE1931( ... ['A', {'Custom': np.array([1 / 3 + 0.05, 1 / 3 + 0.05])}]) ... # doctest: +SKIP Parameters ---------- mapping : dict_like Mapping to filter. filterers : unicode or object or array_like Filterer or object class instance (which is passed through directly if its type is one of the mapping element types) or list of filterers. anchors : bool, optional Whether to use Regex line anchors, i.e. *^* and *$* are added, surrounding the filterers patterns. allow_non_siblings : bool, optional Whether to allow non-siblings to be also passed through. flags : int, optional Regex flags. Returns ------- dict_like Filtered mapping. """ if is_string(filterers): filterers = [filterers] elif not isinstance(filterers, (list, tuple)): filterers = [filterers] string_filterers = [ filterer for filterer in filterers if is_string(filterer) ] object_filterers = [ filterer for filterer in filterers if is_sibling(filterer, mapping) ] if allow_non_siblings: non_siblings = [ filterer for filterer in filterers if filterer not in string_filterers and filterer not in object_filterers ] if non_siblings: runtime_warning( 'Non-sibling elements are passed-through: "{0}"'.format( non_siblings)) object_filterers.extend(non_siblings) filtered_mapping = filter_mapping(mapping, string_filterers, anchors, flags) for filterer in object_filterers: if isinstance(filterer, (dict, OrderedDict, CaseInsensitiveMapping)): for key, value in filterer.items(): filtered_mapping[key] = value else: try: name = filterer.name except AttributeError: try: name = filterer.__name__ except AttributeError: name = str(id(filterer)) filtered_mapping[name] = filterer return filtered_mapping
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_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 _uv_to_CCT_Ohno2013( uv, cmfs=STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'], start=CCT_MINIMAL, end=CCT_MAXIMAL, count=CCT_SAMPLES, iterations=CCT_CALCULATION_ITERATIONS): """ Returns the correlated colour temperature :math:`T_{cp}` and :math:`\\Delta_{uv}` from given *CIE UCS* colourspace *uv* chromaticity coordinates, colour matching functions and temperature range using *Ohno (2013)* method. The iterations parameter defines the calculations precision: The higher its value, the more planckian tables will be generated through cascade expansion in order to converge to the exact solution. Parameters ---------- uv : array_like *CIE UCS* colourspace *uv* chromaticity coordinates. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. start : numeric, optional Temperature range start in kelvins. end : numeric, optional Temperature range end in kelvins. count : int, optional Temperatures count in the planckian tables. iterations : int, optional Number of planckian tables to generate. Returns ------- ndarray Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. """ # Ensuring we do at least one iteration to initialise variables. iterations = max(iterations, 1) # Planckian table creation through cascade expansion. for _i in range(iterations): table = planckian_table(uv, cmfs, start, end, count) index = planckian_table_minimal_distance_index(table) if index == 0: runtime_warning( ('Minimal distance index is on lowest planckian table bound, ' 'unpredictable results may occur!')) index += 1 elif index == len(table) - 1: runtime_warning( ('Minimal distance index is on highest planckian table bound, ' 'unpredictable results may occur!')) index -= 1 start = table[index - 1].Ti end = table[index + 1].Ti _ux, vx = uv Tuvdip, Tuvdi, Tuvdin = (table[index - 1], table[index], table[index + 1]) Tip, uip, vip, dip = Tuvdip.Ti, Tuvdip.ui, Tuvdip.vi, Tuvdip.di Ti, di = Tuvdi.Ti, Tuvdi.di Tin, uin, vin, din = Tuvdin.Ti, Tuvdin.ui, Tuvdin.vi, Tuvdin.di # Triangular solution. l = np.hypot(uin - uip, vin - vip) # noqa x = (dip ** 2 - din ** 2 + l ** 2) / (2 * l) T = Tip + (Tin - Tip) * (x / l) vtx = vip + (vin - vip) * (x / l) sign = 1 if vx - vtx >= 0 else -1 D_uv = (dip ** 2 - x ** 2) ** (1 / 2) * sign # Parabolic solution. if np.abs(D_uv) >= 0.002: X = (Tin - Ti) * (Tip - Tin) * (Ti - Tip) a = (Tip * (din - di) + Ti * (dip - din) + Tin * (di - dip)) * X ** -1 b = (-(Tip ** 2 * (din - di) + Ti ** 2 * (dip - din) + Tin ** 2 * (di - dip)) * X ** -1) c = ( -(dip * (Tin - Ti) * Ti * Tin + di * (Tip - Tin) * Tip * Tin + din * (Ti - Tip) * Tip * Ti) * X ** -1) T = -b / (2 * a) D_uv = sign * (a * T ** 2 + b * T + c) return np.array([T, D_uv])
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 _uv_to_CCT_Ohno2013( uv, cmfs=MSDS_CMFS_STANDARD_OBSERVER['CIE 1931 2 Degree Standard Observer'] .copy().trim(SPECTRAL_SHAPE_DEFAULT), start=CCT_MINIMAL, end=CCT_MAXIMAL, count=CCT_SAMPLES, iterations=CCT_CALCULATION_ITERATIONS): """ Returns the correlated colour temperature :math:`T_{cp}` and :math:`\\Delta_{uv}` from given *CIE UCS* colourspace *uv* chromaticity coordinates, colour matching functions and temperature range using *Ohno (2013)* method. The iterations parameter defines the calculations precision: The higher its value, the more planckian tables will be generated through cascade expansion in order to converge to the exact solution. Parameters ---------- uv : array_like *CIE UCS* colourspace *uv* chromaticity coordinates. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. start : numeric, optional Temperature range start in kelvins. end : numeric, optional Temperature range end in kelvins. count : int, optional Temperatures count in the planckian tables. iterations : int, optional Number of planckian tables to generate. Returns ------- ndarray Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. """ # Ensuring we do at least one iteration to initialise variables. iterations = max(iterations, 1) # Planckian table creation through cascade expansion. for _i in range(iterations): table = planckian_table(uv, cmfs, start, end, count) index = planckian_table_minimal_distance_index(table) if index == 0: runtime_warning( ('Minimal distance index is on lowest planckian table bound, ' 'unpredictable results may occur!')) index += 1 elif index == len(table) - 1: runtime_warning( ('Minimal distance index is on highest planckian table bound, ' 'unpredictable results may occur!')) index -= 1 start = table[index - 1].Ti end = table[index + 1].Ti _ux, vx = uv Tuvdip, Tuvdi, Tuvdin = (table[index - 1], table[index], table[index + 1]) Tip, uip, vip, dip = Tuvdip.Ti, Tuvdip.ui, Tuvdip.vi, Tuvdip.di Ti, di = Tuvdi.Ti, Tuvdi.di Tin, uin, vin, din = Tuvdin.Ti, Tuvdin.ui, Tuvdin.vi, Tuvdin.di # Triangular solution. l = np.hypot(uin - uip, vin - vip) # noqa x = (dip**2 - din**2 + l**2) / (2 * l) T = Tip + (Tin - Tip) * (x / l) vtx = vip + (vin - vip) * (x / l) sign = 1 if vx - vtx >= 0 else -1 D_uv = (dip**2 - x**2)**(1 / 2) * sign # Parabolic solution. if np.abs(D_uv) >= 0.002: X = (Tin - Ti) * (Tip - Tin) * (Ti - Tip) a = (Tip * (din - di) + Ti * (dip - din) + Tin * (di - dip)) * X**-1 b = (-(Tip**2 * (din - di) + Ti**2 * (dip - din) + Tin**2 * (di - dip)) * X**-1) c = (-(dip * (Tin - Ti) * Ti * Tin + di * (Tip - Tin) * Tip * Tin + din * (Ti - Tip) * Tip * Ti) * X**-1) T = -b / (2 * a) D_uv = sign * (a * T**2 + b * T + c) return np.array([T, D_uv])
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 plot_single_sd_colour_rendition_report_simple( sd, report_size=CONSTANT_REPORT_SIZE_SIMPLE, report_row_height_ratios=CONSTANT_REPORT_ROW_HEIGHT_RATIOS_SIMPLE, report_box_padding=None, **kwargs): """ Generates the simple *ANSI/IES TM-30-18 Colour Rendition Report* for given spectral distribution. Parameters ---------- sd : SpectralDistribution or SpectralDistribution_IESTM2714 Spectral distribution of the emission source to generate the report for. report_size : array_like, optional Report size, default to A4 paper size in inches. report_row_height_ratios : array_like, optional Report size row height ratios. report_box_padding : array_like, optional Report box padding, tries to define the padding around the figure and in-between the axes. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Returns ------- tuple Current figure and axes. Examples -------- >>> from colour import SDS_ILLUMINANTS >>> sd = SDS_ILLUMINANTS['FL2'] >>> plot_single_sd_colour_rendition_report_simple(sd) ... # doctest: +ELLIPSIS (<Figure size ... with ... Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_\ Plot_Single_SD_Colour_Rendition_Report_Simple.png :align: center :alt: plot_single_sd_colour_rendition_report_simple """ if six.PY2: runtime_warning( 'The "ANSI/IES TM-30-18 Colour Rendition Report" uses advanced ' '"Matplotlib" layout capabilities only available for Python 3!') return render() if report_box_padding is None: report_box_padding = CONSTANT_REPORT_PADDING_SIMPLE specification = colour_fidelity_index_ANSIIESTM3018(sd, True) figure = plt.figure(figsize=report_size, constrained_layout=True) settings = kwargs.copy() settings['standalone'] = False settings['tight_layout'] = False gridspec_report = figure.add_gridspec( 3, 1, height_ratios=report_row_height_ratios) # Title Row gridspec_title = gridspec_report[0].subgridspec(1, 1) axes_title = figure.add_subplot(gridspec_title[0]) _plot_report_header(axes_title) # Main Figures Rows & Columns gridspec_figures = gridspec_report[1].subgridspec(1, 1) axes_vector_graphics = figure.add_subplot(gridspec_figures[0, 0]) plot_colour_vector_graphic(specification, axes=axes_vector_graphics, **settings) gridspec_footer = gridspec_report[2].subgridspec(1, 1) axes_footer = figure.add_subplot(gridspec_footer[0]) _plot_report_footer(axes_footer) figure.set_constrained_layout_pads(**report_box_padding) settings = kwargs.copy() settings['tight_layout'] = False return render(**settings)
def plot_single_sd_colour_rendition_report_full( sd, source=None, date=None, manufacturer=None, model=None, notes=None, report_size=CONSTANT_REPORT_SIZE_FULL, report_row_height_ratios=CONSTANT_REPORT_ROW_HEIGHT_RATIOS_FULL, report_box_padding=None, **kwargs): """ Generates the full *ANSI/IES TM-30-18 Colour Rendition Report* for given spectral distribution. Parameters ---------- sd : SpectralDistribution or SpectralDistribution_IESTM2714 Spectral distribution of the emission source to generate the report for. source : unicode, optional Emission source name, defaults to `colour.SpectralDistribution_IESTM2714.header.description` or `colour.SpectralDistribution_IESTM2714.name` properties value. date : unicode, optional Emission source measurement date, defaults to `colour.SpectralDistribution_IESTM2714.header.report_date` property value. manufacturer : unicode, optional Emission source manufacturer, defaults to `colour.SpectralDistribution_IESTM2714.header.manufacturer` property value. model : unicode, optional Emission source model, defaults to `colour.SpectralDistribution_IESTM2714.header.catalog_number` property value. notes : unicode, optional Notes pertaining to the emission source, defaults to `colour.SpectralDistribution_IESTM2714.header.comments` property value. report_size : array_like, optional Report size, default to A4 paper size in inches. report_row_height_ratios : array_like, optional Report size row height ratios. report_box_padding : array_like, optional Report box padding, tries to define the padding around the figure and in-between the axes. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Returns ------- tuple Current figure and axes. Examples -------- >>> from colour import SDS_ILLUMINANTS >>> sd = SDS_ILLUMINANTS['FL2'] >>> plot_single_sd_colour_rendition_report_full(sd) ... # doctest: +ELLIPSIS (<Figure size ... with ... Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_\ Plot_Single_SD_Colour_Rendition_Report_Full.png :align: center :alt: plot_single_sd_colour_rendition_report_full """ if six.PY2: runtime_warning( 'The "ANSI/IES TM-30-18 Colour Rendition Report" uses advanced ' '"Matplotlib" layout capabilities only available for Python 3!') return render() if report_box_padding is None: report_box_padding = CONSTANT_REPORT_PADDING_FULL specification = colour_fidelity_index_ANSIIESTM3018(sd, True) sd = (SpectralDistribution_IESTM2714(data=sd, name=sd.name) if not isinstance(sd, SpectralDistribution_IESTM2714) else sd) source = sd.header.description if source is None else source source = sd.name if source is None else source date = sd.header.report_date if date is None else date date = _NOT_APPLICABLE_VALUE if date is None else date manufacturer = (sd.header.manufacturer if manufacturer is None else manufacturer) manufacturer = (_NOT_APPLICABLE_VALUE if manufacturer is None else manufacturer) model = sd.header.catalog_number if model is None else model model = _NOT_APPLICABLE_VALUE if model is None else model notes = sd.header.comments if notes is None else notes notes = _NOT_APPLICABLE_VALUE if notes is None else notes figure = plt.figure(figsize=report_size, constrained_layout=True) settings = kwargs.copy() settings['standalone'] = False settings['tight_layout'] = False gridspec_report = figure.add_gridspec( 5, 1, height_ratios=report_row_height_ratios) # Title Row gridspec_title = gridspec_report[0].subgridspec(1, 1) axes_title = figure.add_subplot(gridspec_title[0]) _plot_report_header(axes_title) # Description Rows & Columns gridspec_description = gridspec_report[1].subgridspec(1, 2) # Source & Date Column axes_source_date = figure.add_subplot(gridspec_description[0]) axes_source_date.set_axis_off() axes_source_date.text(0.25, 2 / 3, 'Source: ', ha='right', va='center', size='medium', weight='bold') axes_source_date.text(0.25, 2 / 3, source, va='center', size='medium') axes_source_date.text(0.25, 1 / 3, 'Date: ', ha='right', va='center', size='medium', weight='bold') axes_source_date.text(0.25, 1 / 3, date, va='center', size='medium') # Manufacturer & Model Column axes_manufacturer_model = figure.add_subplot(gridspec_description[1]) axes_manufacturer_model.set_axis_off() axes_manufacturer_model.text(0.25, 2 / 3, 'Manufacturer: ', ha='right', va='center', size='medium', weight='bold') axes_manufacturer_model.text(0.25, 2 / 3, manufacturer, va='center', size='medium') axes_manufacturer_model.text(0.25, 1 / 3, 'Model: ', ha='right', va='center', size='medium', weight='bold') axes_manufacturer_model.text(0.25, 1 / 3, model, va='center', size='medium') # Main Figures Rows & Columns gridspec_figures = gridspec_report[2].subgridspec( 4, 2, height_ratios=[1, 1, 1, 1.5]) axes_spectra = figure.add_subplot(gridspec_figures[0, 0]) plot_spectra_ANSIIESTM3018(specification, axes=axes_spectra, **settings) axes_vector_graphics = figure.add_subplot(gridspec_figures[1:3, 0]) plot_colour_vector_graphic(specification, axes=axes_vector_graphics, **settings) axes_chroma_shifts = figure.add_subplot(gridspec_figures[0, 1]) plot_local_chroma_shifts(specification, axes=axes_chroma_shifts, **settings) axes_hue_shifts = figure.add_subplot(gridspec_figures[1, 1]) plot_local_hue_shifts(specification, axes=axes_hue_shifts, **settings) axes_colour_fidelities = figure.add_subplot(gridspec_figures[2, 1]) plot_local_colour_fidelities(specification, axes=axes_colour_fidelities, x_ticker=True, **settings) # Colour Fidelity Indexes Row axes_colour_fidelity_indexes = figure.add_subplot(gridspec_figures[3, :]) plot_colour_fidelity_indexes(specification, axes=axes_colour_fidelity_indexes, **settings) # Notes & Chromaticities / CRI Row and Columns gridspec_notes_chromaticities_CRI = gridspec_report[3].subgridspec(1, 2) axes_notes = figure.add_subplot(gridspec_notes_chromaticities_CRI[0]) axes_notes.set_axis_off() axes_notes.text(0.25, 1, 'Notes: ', ha='right', va='center', size='medium', weight='bold') axes_notes.text(0.25, 1, notes, va='center', size='medium') gridspec_chromaticities_CRI = gridspec_notes_chromaticities_CRI[ 1].subgridspec(1, 2) XYZ = sd_to_XYZ(specification.sd_test) xy = XYZ_to_xy(XYZ) Luv = XYZ_to_Luv(XYZ, xy) uv_p = Luv_to_uv(Luv, xy) gridspec_chromaticities = gridspec_chromaticities_CRI[0].subgridspec(1, 1) axes_chromaticities = figure.add_subplot(gridspec_chromaticities[0]) axes_chromaticities.set_axis_off() axes_chromaticities.text(0.5, 4 / 5, '$x$ {:.4f}'.format(xy[0]), ha='center', va='center', size='medium', weight='bold') axes_chromaticities.text(0.5, 3 / 5, '$y$ {:.4f}'.format(xy[1]), ha='center', va='center', size='medium', weight='bold') axes_chromaticities.text(0.5, 2 / 5, '$u\'$ {:.4f}'.format(uv_p[0]), ha='center', va='center', size='medium', weight='bold') axes_chromaticities.text(0.5, 1 / 5, '$v\'$ {:.4f}'.format(uv_p[1]), ha='center', va='center', size='medium', weight='bold') gridspec_CRI = gridspec_chromaticities_CRI[1].subgridspec(1, 1) CRI_spec = colour_rendering_index(specification.sd_test, additional_data=True) axes_CRI = figure.add_subplot(gridspec_CRI[0]) axes_CRI.set_xticks([]) axes_CRI.set_yticks([]) axes_CRI.text(0.5, 4 / 5, 'CIE 13.31-1995', ha='center', va='center', size='medium', weight='bold') axes_CRI.text(0.5, 3 / 5, '(CRI)', ha='center', va='center', size='medium', weight='bold') axes_CRI.text(0.5, 2 / 5, '$R_a$ {:.0f}'.format(CRI_spec.Q_a), ha='center', va='center', size='medium', weight='bold') axes_CRI.text(0.5, 1 / 5, '$R_9$ {:.0f}'.format(CRI_spec.Q_as[8].Q_a), ha='center', va='center', size='medium', weight='bold') gridspec_footer = gridspec_report[4].subgridspec(1, 1) axes_footer = figure.add_subplot(gridspec_footer[0]) _plot_report_footer(axes_footer) figure.set_constrained_layout_pads(**report_box_padding) settings = kwargs.copy() settings['tight_layout'] = False return render(**settings)
warnings.warn('This is a fourth warning and it has not been filtered!') filter_warnings(python_warnings=False) warning('This is a fifth warning and it has been filtered!') filter_warnings(False, python_warnings=False) warning('This is a sixth warning and it has not been filtered!') filter_warnings(False, python_warnings=False) filter_warnings(colour_warnings=False, colour_runtime_warnings=True) runtime_warning('This is a first runtime warning and it has been filtered!') filter_warnings(colour_warnings=False, colour_usage_warnings=True) usage_warning('This is a first usage warning and it has been filtered!') print('\n') message_box('Overall "Colour" Examples') message_box('N-Dimensional Arrays Support') XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) illuminant = np.array([0.31270, 0.32900]) message_box('Using 1d "array_like" parameter:\n' '\n{0}'.format(XYZ)) print(colour.XYZ_to_Lab(XYZ, illuminant=illuminant))
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 find_coefficients_Jakob2019( XYZ, cmfs=MSDS_CMFS_STANDARD_OBSERVER['CIE 1931 2 Degree Standard Observer'] .copy().align(SPECTRAL_SHAPE_JAKOB2019), illuminant=SDS_ILLUMINANTS['D65'].copy().align( SPECTRAL_SHAPE_JAKOB2019), coefficients_0=zeros(3), max_error=JND_CIE1976 / 100, dimensionalise=True): """ Computes the coefficients for *Jakob and Hanika (2019)* reflectance spectral model. Parameters ---------- XYZ : array_like, (3,) *CIE XYZ* tristimulus values to find the coefficients for. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralDistribution Illuminant spectral distribution. coefficients_0 : array_like, (3,), optional Starting coefficients for the solver. max_error : float, optional Maximal acceptable error. Set higher to save computational time. If *None*, the solver will keep going until it is very close to the minimum. The default is ``ACCEPTABLE_DELTA_E``. dimensionalise : bool, optional If *True*, returned coefficients are dimensionful and will not work correctly if fed back as ``coefficients_0``. The default is *True*. Returns ------- coefficients : ndarray, (3,) Computed coefficients that best fit the given colour. error : float :math:`\\Delta E_{76}` between the target colour and the colour corresponding to the computed coefficients. References ---------- :cite:`Jakob2019` Examples -------- >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> find_coefficients_Jakob2019(XYZ) # doctest: +ELLIPSIS (array([ 1.3723791...e-04, -1.3514399...e-01, 3.0838973...e+01]), \ 0.0141941...) """ shape = cmfs.shape if illuminant.shape != shape: runtime_warning( 'Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant.name, cmfs.name)) illuminant = illuminant.copy().align(cmfs.shape) def optimize(target_o, coefficients_0_o): """ Minimises the error function using *L-BFGS-B* method. """ try: result = minimize( error_function, coefficients_0_o, (target_o, cmfs, illuminant, max_error), method='L-BFGS-B', jac=True) return result.x, result.fun except StopMinimizationEarly as error: return error.coefficients, error.error xy_n = XYZ_to_xy(sd_to_XYZ(illuminant, cmfs)) XYZ_good = full(3, 0.5) coefficients_good = zeros(3) divisions = 3 while divisions < 10: XYZ_r = XYZ_good coefficient_r = coefficients_good keep_divisions = False coefficients_0 = coefficient_r for i in range(1, divisions): XYZ_i = (XYZ - XYZ_r) * i / (divisions - 1) + XYZ_r Lab_i = XYZ_to_Lab(XYZ_i) coefficients_0, error = optimize(Lab_i, coefficients_0) if error > max_error: break else: XYZ_good = XYZ_i coefficients_good = coefficients_0 keep_divisions = True else: break if not keep_divisions: divisions += 2 target = XYZ_to_Lab(XYZ, xy_n) coefficients, error = optimize(target, coefficients_0) if dimensionalise: coefficients = dimensionalise_coefficients(coefficients, shape) return coefficients, error
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 generate(self, colourspace, cmfs=MSDS_CMFS_STANDARD_OBSERVER[ 'CIE 1931 2 Degree Standard Observer'] .copy().align(SPECTRAL_SHAPE_JAKOB2019), illuminant=SDS_ILLUMINANTS['D65'].copy().align( SPECTRAL_SHAPE_JAKOB2019), size=64, print_callable=print): """ Generates the lookup table data for given *RGB* colourspace, colour matching functions, illuminant and given size. Parameters ---------- colourspace: RGB_Colourspace The *RGB* colourspace to create a lookup table for. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. illuminant : SpectralDistribution, optional Illuminant spectral distribution. size : int, optional The resolution of the lookup table. Higher values will decrease errors but at the cost of a much longer run time. The published *\\*.coeff* files have a resolution of 64. print_callable : callable, optional Callable used to print progress and diagnostic information. Examples -------- >>> from colour.utilities import numpy_print_options >>> from colour.models import RGB_COLOURSPACE_sRGB >>> 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) >>> LUT = LUT3D_Jakob2019() >>> print(LUT.interpolator) None >>> LUT.generate(RGB_COLOURSPACE_sRGB, cmfs, illuminant, 3) ======================================================================\ ========= * \ * * "Jakob et al. (2018)" LUT Optimisation \ * * \ * ======================================================================\ ========= <BLANKLINE> Optimising 27 coefficients... <BLANKLINE> >>> print(LUT.interpolator) ... # doctest: +ELLIPSIS <scipy.interpolate.interpolate.RegularGridInterpolator object at 0x...> """ shape = cmfs.shape if illuminant.shape != shape: runtime_warning( 'Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant.name, cmfs.name)) illuminant = illuminant.copy().align(cmfs.shape) xy_n = XYZ_to_xy(sd_to_XYZ(illuminant, cmfs)) # It could be interesting to have different resolutions for lightness # and chromaticity, but the current file format doesn't allow it. lightness_steps = size chroma_steps = size self._lightness_scale = lightness_scale(lightness_steps) self._coefficients = np.empty( [3, chroma_steps, chroma_steps, lightness_steps, 3]) cube_indexes = np.ndindex(3, chroma_steps, chroma_steps) total_coefficients = chroma_steps ** 2 * 3 # First, create a list of all the fully bright colours with the order # matching cube_indexes. samples = np.linspace(0, 1, chroma_steps) ij = np.meshgrid(*[[1], samples, samples], indexing='ij') ij = np.transpose(ij).reshape(-1, 3) chromas = np.concatenate( [ij, np.roll(ij, 1, axis=1), np.roll(ij, 2, axis=1)]) message_box( '"Jakob et al. (2018)" LUT Optimisation', print_callable=print_callable) print_callable( '\nOptimising {0} coefficients...\n'.format(total_coefficients)) def optimize(ijkL, coefficients_0): """ Solves for a specific lightness and stores the result in the appropriate cell. """ i, j, k, L = ijkL RGB = self._lightness_scale[L] * chroma XYZ = RGB_to_XYZ(RGB, colourspace.whitepoint, xy_n, colourspace.matrix_RGB_to_XYZ) coefficients, error = find_coefficients_Jakob2019( XYZ, cmfs, illuminant, coefficients_0, dimensionalise=False) self._coefficients[i, L, j, k, :] = dimensionalise_coefficients( coefficients, shape) return coefficients with tqdm(total=total_coefficients) as progress: for ijk, chroma in zip(cube_indexes, chromas): progress.update() # Starts from somewhere in the middle, similarly to how # feedback works in "colour.recovery.\ # find_coefficients_Jakob2019" definition. L_middle = lightness_steps // 3 coefficients_middle = optimize( np.hstack([ijk, L_middle]), zeros(3)) # Goes down the lightness scale. coefficients_0 = coefficients_middle for L in reversed(range(0, L_middle)): coefficients_0 = optimize( np.hstack([ijk, L]), coefficients_0) # Goes up the lightness scale. coefficients_0 = coefficients_middle for L in range(L_middle + 1, lightness_steps): coefficients_0 = optimize( np.hstack([ijk, L]), coefficients_0) self._size = size self._create_interpolator()
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 _uv_to_CCT_Ohno2013( uv: ArrayLike, cmfs: Optional[MultiSpectralDistributions] = None, start: Floating = CCT_MINIMAL, end: Floating = CCT_MAXIMAL, count: Integer = CCT_SAMPLES, iterations: Integer = CCT_CALCULATION_ITERATIONS, ) -> NDArray: """ Return the correlated colour temperature :math:`T_{cp}` and :math:`\\Delta_{uv}` from given *CIE UCS* colourspace *uv* chromaticity coordinates, colour matching functions and temperature range using *Ohno (2013)* method. The ``iterations`` parameter defines the calculations' precision: The higher its value, the more planckian tables will be generated through cascade expansion in order to converge to the exact solution. Parameters ---------- uv *CIE UCS* colourspace *uv* chromaticity coordinates. cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. start Temperature range start in kelvin degrees. end Temperature range end in kelvin degrees. count Temperatures count in the planckian tables. iterations Number of planckian tables to generate. Returns ------- :class:`numpy.ndarray` Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. """ cmfs, _illuminant = handle_spectral_arguments(cmfs) # Ensuring that we do at least one iteration to initialise the variables. iterations = max(int(iterations), 1) # Planckian table creation through cascade expansion. for _i in range(iterations): table = planckian_table(uv, cmfs, start, end, count) index = planckian_table_minimal_distance_index(table) if index == 0: runtime_warning( "Minimal distance index is on lowest planckian table bound, " "unpredictable results may occur!") index += 1 elif index == len(table) - 1: runtime_warning( "Minimal distance index is on highest planckian table bound, " "unpredictable results may occur!") index -= 1 start = table[index - 1].Ti end = table[index + 1].Ti _ux, vx = tsplit(uv) Tuvdip, Tuvdi, Tuvdin = (table[index - 1], table[index], table[index + 1]) Tip, uip, vip, dip = Tuvdip.Ti, Tuvdip.ui, Tuvdip.vi, Tuvdip.di Ti, di = Tuvdi.Ti, Tuvdi.di Tin, uin, vin, din = Tuvdin.Ti, Tuvdin.ui, Tuvdin.vi, Tuvdin.di # Triangular solution. l = np.hypot(uin - uip, vin - vip) # noqa x = (dip**2 - din**2 + l**2) / (2 * l) T = Tip + (Tin - Tip) * (x / l) vtx = vip + (vin - vip) * (x / l) sign = 1 if vx - vtx >= 0 else -1 D_uv = (dip**2 - x**2)**(1 / 2) * sign # Parabolic solution. if np.abs(D_uv) >= 0.002: X = (Tin - Ti) * (Tip - Tin) * (Ti - Tip) a = (Tip * (din - di) + Ti * (dip - din) + Tin * (di - dip)) * X**-1 b = (-(Tip**2 * (din - di) + Ti**2 * (dip - din) + Tin**2 * (di - dip)) * X**-1) c = (-(dip * (Tin - Ti) * Ti * Tin + di * (Tip - Tin) * Tip * Tin + din * (Ti - Tip) * Tip * Ti) * X**-1) T = -b / (2 * a) D_uv = sign * (a * T**2 + b * T + c) return np.array([T, D_uv])
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)