Exemple #1
0
    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()
Exemple #2
0
    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()
Exemple #3
0
    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
Exemple #4
0
    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
Exemple #5
0
    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 illuminant(self, value):
        # Docstrings are omitted for documentation purposes.
        runtime_warning(
            str(
                Renamed('RGB_Colourspace.illuminant',
                        'RGB_Colourspace.whitepoint_name')))

        self.whitepoint_name = value
Exemple #7
0
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
Exemple #8
0
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),
    ])
Exemple #9
0
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),
    ])
Exemple #10
0
    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()
Exemple #11
0
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
Exemple #12
0
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
Exemple #13
0
    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()
Exemple #14
0
    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()
Exemple #15
0
    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()
Exemple #16
0
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
Exemple #17
0
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'))
Exemple #18
0
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))
Exemple #19
0
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
Exemple #20
0
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))
Exemple #21
0
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
Exemple #22
0
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])
Exemple #23
0
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))
Exemple #24
0
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])
Exemple #25
0
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
Exemple #26
0
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)
Exemple #27
0
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)
Exemple #28
0
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)
Exemple #29
0
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))
Exemple #30
0
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)
Exemple #31
0
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
Exemple #32
0
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))
Exemple #33
0
    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()
Exemple #34
0
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)
Exemple #35
0
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])
Exemple #36
0
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)