Ejemplo n.º 1
0
def msds_constant(
    k: Floating,
    labels: Sequence,
    shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT,
    **kwargs: Any,
) -> MultiSpectralDistributions:
    """
    Return the multi-spectral distributions with given labels and given
    spectral shape filled with constant :math:`k` values.

    Parameters
    ----------
    k
        Constant :math:`k` to fill the multi-spectral distributions with.
    labels
        Names to use for the :class:`colour.SpectralDistribution` class
        instances.
    shape
        Spectral shape used to create the multi-spectral distributions.

    Other Parameters
    ----------------
    kwargs
        {:class:`colour.MultiSpectralDistributions`},
        See the documentation of the previously listed class.

    Returns
    -------
    :class:`colour.MultiSpectralDistributions`
        Constant :math:`k` filled multi-spectral distributions.

    Notes
    -----
    -   By default, the multi-spectral distributions will use the shape given
        by :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute.

    Examples
    --------
    >>> msds = msds_constant(100, labels=['a', 'b', 'c'])
    >>> msds.shape
    SpectralShape(360.0, 780.0, 1.0)
    >>> msds[400]
    array([ 100.,  100.,  100.])
    >>> msds.labels  # doctest: +SKIP
    ['a', 'b', 'c']
    """

    settings = {"name": f"{k} Constant"}
    settings.update(kwargs)

    wavelengths = shape.range()
    values = full((len(wavelengths), len(labels)), k)

    return MultiSpectralDistributions(values,
                                      wavelengths,
                                      labels=labels,
                                      **settings)
Ejemplo n.º 2
0
def msds_constant(k, labels, shape=SPECTRAL_SHAPE_DEFAULT, dtype=None):
    """
    Returns the multi-spectral distributions with given labels and given
    spectral shape filled with constant :math:`k` values.

    Parameters
    ----------
    k : numeric
        Constant :math:`k` to fill the multi-spectral distributions with.
    labels : array_like
        Names to use for the :class:`colour.SpectralDistribution` class
        instances.
    shape : SpectralShape, optional
        Spectral shape used to create the multi-spectral distributions.
    dtype : type
        Data type used for the multi-spectral distributions.

    Returns
    -------
    MultiSpectralDistributions
        Constant :math:`k` filled multi-spectral distributions.

    Notes
    -----
    -   By default, the multi-spectral distributions will use the shape given
        by :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute.

    Examples
    --------
    >>> msds = msds_constant(100, labels=['a', 'b', 'c'])
    >>> msds.shape
    SpectralShape(360.0, 780.0, 1.0)
    >>> msds[400]
    array([ 100.,  100.,  100.])
    >>> msds.labels  # doctest: +SKIP
    ['a', 'b', 'c']
    """

    if dtype is None:
        dtype = DEFAULT_FLOAT_DTYPE

    wavelengths = shape.range(dtype)
    values = full([len(wavelengths), len(labels)], k, dtype)

    name = '{0} Constant'.format(k)
    return MultiSpectralDistributions(
        values, wavelengths, name=name, labels=labels, dtype=dtype)
Ejemplo n.º 3
0
def sd_constant(k: Floating,
                shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT,
                **kwargs: Any) -> SpectralDistribution:
    """
    Return a spectral distribution of given spectral shape filled with
    constant :math:`k` values.

    Parameters
    ----------
    k
        Constant :math:`k` to fill the spectral distribution with.
    shape
        Spectral shape used to create the spectral distribution.

    Other Parameters
    ----------------
    kwargs
        {:class:`colour.SpectralDistribution`},
        See the documentation of the previously listed class.

    Returns
    -------
    :class:`colour.SpectralDistribution`
        Constant :math:`k` filled spectral distribution.

    Notes
    -----
    -   By default, the spectral distribution will use the shape given by
        :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute.

    Examples
    --------
    >>> sd = sd_constant(100)
    >>> sd.shape
    SpectralShape(360.0, 780.0, 1.0)
    >>> sd[400]
    100.0
    """

    settings = {"name": f"{k} Constant"}
    settings.update(kwargs)

    wavelengths = shape.range()
    values = full(len(wavelengths), k)

    return SpectralDistribution(values, wavelengths, **settings)
Ejemplo n.º 4
0
def sd_constant(k, shape=SPECTRAL_SHAPE_DEFAULT, dtype=None):
    """
    Returns a spectral distribution of given spectral shape filled with
    constant :math:`k` values.

    Parameters
    ----------
    k : numeric
        Constant :math:`k` to fill the spectral distribution with.
    shape : SpectralShape, optional
        Spectral shape used to create the spectral distribution.
    dtype : type
        Data type used for the spectral distribution.

    Returns
    -------
    SpectralDistribution
        Constant :math:`k` filled spectral distribution.

    Notes
    -----
    -   By default, the spectral distribution will use the shape given by
        :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute.

    Examples
    --------
    >>> sd = sd_constant(100)
    >>> sd.shape
    SpectralShape(360.0, 780.0, 1.0)
    >>> sd[400]
    100.0
    """

    if dtype is None:
        dtype = DEFAULT_FLOAT_DTYPE

    wavelengths = shape.range(dtype)
    values = full(len(wavelengths), k, dtype)

    name = '{0} Constant'.format(k)
    return SpectralDistribution(values, wavelengths, name=name, dtype=dtype)
Ejemplo n.º 5
0
def uv_to_UCS(uv, V=1):
    """
    Returns the *CIE 1960 UCS* colourspace array from given *uv* chromaticity
    coordinates.

    Parameters
    ----------
    uv : array_like
        *uv* chromaticity coordinates.
    V : numeric, optional
        Optional :math:`V` *luminance* value used to construct the
        *CIE 1960 UCS* colourspace array, the default :math:`V` *luminance* is
        set to 1.

    Returns
    -------
    ndarray
        *CIE 1960 UCS* colourspace array.

    References
    ----------
    :cite:`Wikipedia2008c`

    Examples
    --------
    >>> import numpy as np
    >>> uv = np.array([0.37720213, 0.33413508])
    >>> uv_to_UCS(uv)  # doctest: +ELLIPSIS
    array([ 1.1288911...,  1.        ,  0.8639104...])
    """

    u, v = tsplit(uv)
    V = full(u.shape, V)

    U = V * u / v
    W = -V * (u + v - 1) / v

    UVW = tstack([U, V, W])

    return from_range_1(UVW)
Ejemplo n.º 6
0
    def test_LUT3D_Jakob2019(self):
        """
        Tests the entirety of the
        :class:`colour.recovery.jakob2019.LUT3D_Jakob2019`class.
        """

        LUT = LUT3D_Jakob2019()
        LUT.generate(self._RGB_colourspace, self._cmfs, self._sd_D65, 5)

        path = os.path.join(self._temporary_directory, 'Test_Jakob2019.coeff')

        LUT.write(path)
        LUT.read(path)

        for RGB in [
                np.array([1, 0, 0]),
                np.array([0, 1, 0]),
                np.array([0, 0, 1]),
                zeros(3),
                full(3, 0.5),
                ones(3),
        ]:
            XYZ = RGB_to_XYZ(RGB, self._RGB_colourspace.whitepoint,
                             self._xy_D65,
                             self._RGB_colourspace.matrix_RGB_to_XYZ)
            Lab = XYZ_to_Lab(XYZ, self._xy_D65)

            recovered_sd = LUT.RGB_to_sd(RGB)
            recovered_XYZ = sd_to_XYZ(recovered_sd, self._cmfs,
                                      self._sd_D65) / 100
            recovered_Lab = XYZ_to_Lab(recovered_XYZ, self._xy_D65)

            error = delta_E_CIE1976(Lab, recovered_Lab)

            if error > 2 * JND_CIE1976 / 100:
                self.fail(
                    'Delta E for RGB={0} in colourspace {1} is {2}!'.format(
                        RGB, self._RGB_colourspace.name, error))
Ejemplo n.º 7
0
def uv_to_UCS(uv: ArrayLike, V: Floating = 1) -> NDArray:
    """
    Return the *CIE 1960 UCS* colourspace array from given *uv* chromaticity
    coordinates.

    Parameters
    ----------
    uv
        *uv* chromaticity coordinates.
    V
        Optional :math:`V` *luminance* value used to construct the
        *CIE 1960 UCS* colourspace array, the default :math:`V` *luminance* is
        set to 1.

    Returns
    -------
    :class:`numpy.ndarray`
        *CIE 1960 UCS* colourspace array.

    References
    ----------
    :cite:`Wikipedia2008c`

    Examples
    --------
    >>> import numpy as np
    >>> uv = np.array([0.37720213, 0.33413508])
    >>> uv_to_UCS(uv)  # doctest: +ELLIPSIS
    array([ 1.1288911...,  1.        ,  0.8639104...])
    """

    u, v = tsplit(uv)
    V = as_float_scalar(to_domain_1(V))

    UVW = tstack([V * u / v, full(u.shape, V), -V * (u + v - 1) / v])

    return from_range_1(UVW)
Ejemplo n.º 8
0
def plot_RGB_colourspaces_gamuts(colourspaces,
                                 reference_colourspace='CIE xyY',
                                 segments=8,
                                 show_grid=True,
                                 grid_segments=10,
                                 show_spectral_locus=False,
                                 spectral_locus_colour=None,
                                 cmfs='CIE 1931 2 Degree Standard Observer',
                                 chromatically_adapt=False,
                                 **kwargs):
    """
    Plots given *RGB* colourspaces gamuts in given reference colourspace.

    Parameters
    ----------
    colourspaces : unicode or RGB_Colourspace or array_like
        *RGB* colourspaces to plot the gamuts. ``colourspaces`` elements
        can be of any type or form supported by the
        :func:`colour.plotting.filter_RGB_colourspaces` definition.
    reference_colourspace : unicode, optional
        **{'CIE XYZ', 'CIE xyY', 'CIE xy', 'CIE Lab', 'CIE LCHab', 'CIE Luv',
        'CIE Luv uv', 'CIE LCHuv', 'CIE UCS', 'CIE UCS uv', 'CIE UVW',
        'DIN 99', 'Hunter Lab', 'Hunter Rdab', 'IPT', 'JzAzBz', 'OSA UCS',
        'hdr-CIELAB', 'hdr-IPT'}**,
        Reference colourspace to plot the gamuts into.
    segments : int, optional
        Edge segments count for each *RGB* colourspace cubes.
    show_grid : bool, optional
        Whether to show a grid at the bottom of the *RGB* colourspace cubes.
    grid_segments : bool, optional
        Edge segments count for the grid.
    show_spectral_locus : bool, optional
        Whether to show the spectral locus.
    spectral_locus_colour : array_like, optional
        Spectral locus colour.
    cmfs : unicode or XYZ_ColourMatchingFunctions, optional
        Standard observer colour matching functions used for computing the
        spectral locus boundaries. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.filter_cmfs` definition.
    chromatically_adapt : bool, optional
        Whether to chromatically adapt the *RGB* colourspaces given in
        ``colourspaces`` to the whitepoint of the default plotting colourspace.

    Other Parameters
    ----------------
    \\**kwargs : dict, optional
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.volume.nadir_grid`},
        Please refer to the documentation of the previously listed definitions.
    face_colours : array_like, optional
        Face colours array such as `face_colours = (None, (0.5, 0.5, 1.0))`.
    edge_colours : array_like, optional
        Edge colours array such as `edge_colours = (None, (0.5, 0.5, 1.0))`.
    face_alpha : numeric, optional
        Face opacity value such as `face_alpha = (0.5, 1.0)`.
    edge_alpha : numeric, optional
        Edge opacity value such as `edge_alpha = (0.0, 1.0)`.

    Returns
    -------
    tuple
        Current figure and axes.

    Examples
    --------
    >>> plot_RGB_colourspaces_gamuts(['ITU-R BT.709', 'ACEScg', 'S-Gamut'])
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes3DSubplot...>)

    .. image:: ../_static/Plotting_Plot_RGB_Colourspaces_Gamuts.png
        :align: center
        :alt: plot_RGB_colourspaces_gamuts
    """

    colourspaces = filter_RGB_colourspaces(colourspaces).values()

    count_c = len(colourspaces)

    title = '{0} - {1} Reference Colourspace'.format(
        ', '.join([colourspace.name for colourspace in colourspaces]),
        reference_colourspace,
    )

    settings = Structure(
        **{
            'face_colours': [None] * count_c,
            'edge_colours': [None] * count_c,
            'face_alpha': [1] * count_c,
            'edge_alpha': [1] * count_c,
            'title': title,
        })
    settings.update(kwargs)

    figure = plt.figure()
    axes = figure.add_subplot(111, projection='3d')

    illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint

    points = zeros([4, 3])
    if show_spectral_locus:
        cmfs = first_item(filter_cmfs(cmfs).values())
        XYZ = cmfs.values

        points = common_colourspace_model_axis_reorder(
            XYZ_to_colourspace_model(XYZ, illuminant, reference_colourspace),
            reference_colourspace)

        points[np.isnan(points)] = 0

        c = ((0.0, 0.0, 0.0, 0.5)
             if spectral_locus_colour is None else spectral_locus_colour)

        axes.plot(
            points[..., 0], points[..., 1], points[..., 2], color=c, zorder=10)
        axes.plot(
            (points[-1][0], points[0][0]), (points[-1][1], points[0][1]),
            (points[-1][2], points[0][2]),
            color=c,
            zorder=10)

    plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace

    quads, RGB_f, RGB_e = [], [], []
    for i, colourspace in enumerate(colourspaces):

        if chromatically_adapt and not np.array_equal(
                colourspace.whitepoint, plotting_colourspace.whitepoint):
            colourspace = colourspace.chromatically_adapt(
                plotting_colourspace.whitepoint,
                plotting_colourspace.whitepoint_name)

        quads_c, RGB = RGB_identity_cube(
            width_segments=segments,
            height_segments=segments,
            depth_segments=segments)

        XYZ = RGB_to_XYZ(
            quads_c,
            colourspace.whitepoint,
            colourspace.whitepoint,
            colourspace.matrix_RGB_to_XYZ,
        )

        quads.extend(
            common_colourspace_model_axis_reorder(
                XYZ_to_colourspace_model(
                    XYZ,
                    colourspace.whitepoint,
                    reference_colourspace,
                ), reference_colourspace))

        if settings.face_colours[i] is not None:
            RGB = ones(RGB.shape) * settings.face_colours[i]

        RGB_f.extend(
            np.hstack([RGB,
                       full([RGB.shape[0], 1], settings.face_alpha[i])]))

        if settings.edge_colours[i] is not None:
            RGB = ones(RGB.shape) * settings.edge_colours[i]

        RGB_e.extend(
            np.hstack([RGB,
                       full([RGB.shape[0], 1], settings.edge_alpha[i])]))

    quads = as_float_array(quads)
    quads[np.isnan(quads)] = 0

    if quads.size != 0:
        for i, axis in enumerate('xyz'):
            min_a = min(np.min(quads[..., i]), np.min(points[..., i]))
            max_a = max(np.max(quads[..., i]), np.max(points[..., i]))
            getattr(axes, 'set_{}lim'.format(axis))((min_a, max_a))

    labels = np.array(
        COLOURSPACE_MODELS_AXIS_LABELS[reference_colourspace])[as_int_array(
            common_colourspace_model_axis_reorder([0, 1, 2],
                                                  reference_colourspace))]
    for i, axis in enumerate('xyz'):
        getattr(axes, 'set_{}label'.format(axis))(labels[i])

    if show_grid:
        limits = np.array([[-1.5, 1.5], [-1.5, 1.5]])

        quads_g, RGB_gf, RGB_ge = nadir_grid(limits, grid_segments, labels,
                                             axes, **settings)
        quads = np.vstack([quads_g, quads])
        RGB_f = np.vstack([RGB_gf, RGB_f])
        RGB_e = np.vstack([RGB_ge, RGB_e])

    collection = Poly3DCollection(quads)
    collection.set_facecolors(RGB_f)
    collection.set_edgecolors(RGB_e)

    axes.add_collection3d(collection)

    settings.update({
        'axes': axes,
        'axes_visible': False,
        'camera_aspect': 'equal'
    })
    settings.update(kwargs)

    return render(**settings)
Ejemplo n.º 9
0
    def test_process_image_OpenColorIO(self):
        """Test :func:`colour.io.ocio.process_image_OpenColorIO` definition."""

        # TODO: Remove when "Pypi" wheel compatible with "ARM" on "macOS" is
        # released.
        if not is_opencolorio_installed():  # pragma: no cover
            return

        import PyOpenColorIO as ocio

        config = os.path.join(
            RESOURCES_DIRECTORY, "config-aces-reference.ocio.yaml"
        )

        a = full([4, 2, 3], 0.18)

        np.testing.assert_almost_equal(
            process_image_OpenColorIO(
                a, "ACES - ACES2065-1", "ACES - ACEScct", config=config
            ),
            np.array(
                [
                    [
                        [0.41358781, 0.41358781, 0.41358781],
                        [0.41358781, 0.41358781, 0.41358781],
                    ],
                    [
                        [0.41358781, 0.41358781, 0.41358781],
                        [0.41358781, 0.41358781, 0.41358781],
                    ],
                    [
                        [0.41358781, 0.41358781, 0.41358781],
                        [0.41358781, 0.41358781, 0.41358781],
                    ],
                    [
                        [0.41358781, 0.41358781, 0.41358781],
                        [0.41358781, 0.41358781, 0.41358781],
                    ],
                ]
            ),
            decimal=5,
        )

        np.testing.assert_almost_equal(
            process_image_OpenColorIO(
                a,
                "ACES - ACES2065-1",
                "Display - sRGB",
                "Output - SDR Video - ACES 1.0",
                ocio.TRANSFORM_DIR_FORWARD,
                config=config,
            ),
            np.array(
                [
                    [
                        [0.35595229, 0.35595256, 0.35595250],
                        [0.35595229, 0.35595256, 0.35595250],
                    ],
                    [
                        [0.35595229, 0.35595256, 0.35595250],
                        [0.35595229, 0.35595256, 0.35595250],
                    ],
                    [
                        [0.35595229, 0.35595256, 0.35595250],
                        [0.35595229, 0.35595256, 0.35595250],
                    ],
                    [
                        [0.35595229, 0.35595256, 0.35595250],
                        [0.35595229, 0.35595256, 0.35595250],
                    ],
                ]
            ),
            decimal=5,
        )
Ejemplo n.º 10
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
Ejemplo n.º 11
0
def convert_experiment_results_Breneman1987(experiment):
    """
    Converts *Breneman (1987)* experiment results to a
    :class:`colour.CorrespondingColourDataset` class instance.

    Parameters
    ----------
    experiment : integer
        {1, 2, 3, 4, 6, 8, 9, 11, 12}
        *Breneman (1987)* experiment number.

    Returns
    -------
    CorrespondingColourDataset
        :class:`colour.CorrespondingColourDataset` class instance.

    Examples
    --------
    >>> from pprint import pprint
    >>> pprint(tuple(convert_experiment_results_Breneman1987(2)))
    ... # doctest: +ELLIPSIS
    (2,
     array([ 0.9582463...,  1.        ,  0.9436325...]),
     array([ 0.9587332...,  1.        ,  0.4385796...]),
     array([[ 388.125     ,  405.        ,  345.625     ],
           [ 266.8957925...,  135.        ,   28.5983365...],
           [ 474.5717821...,  405.        ,  222.75     ...],
           [ 538.3899082...,  405.        ,   24.8944954...],
           [ 178.7430167...,  135.        ,   19.6089385...],
           [ 436.6749547...,  405.        ,   26.5483725...],
           [ 124.7746282...,  135.        ,   36.1965613...],
           [  77.0794172...,  135.        ,   60.5850563...],
           [ 279.9390889...,  405.        ,  455.8395127...],
           [ 149.5808157...,  135.        ,  498.7046827...],
           [ 372.1113689...,  405.        ,  669.9883990...],
           [ 212.3638968...,  135.        ,  414.6704871...]]),
     array([[ 400.1039651...,  405.        ,  191.7287234...],
           [ 271.0384615...,  135.        ,   13.5      ...],
           [ 495.4705323...,  405.        ,  119.7290874...],
           [ 580.7967033...,  405.        ,    6.6758241...],
           [ 190.1933701...,  135.        ,    7.4585635...],
           [ 473.7184115...,  405.        ,   10.2346570...],
           [ 135.4936014...,  135.        ,   20.2376599...],
           [  86.4689781...,  135.        ,   35.2281021...],
           [ 283.5396281...,  405.        ,  258.1775929...],
           [ 119.7044335...,  135.        ,  282.6354679...],
           [ 359.9532224...,  405.        ,  381.0031185...],
           [ 181.8271461...,  135.        ,  204.0661252...]]),
     array(1500),
     array(1500),
     0.3,
     0.3,
     {})
    """

    valid_experiment_results = (1, 2, 3, 4, 6, 8, 9, 11, 12)
    assert experiment in valid_experiment_results, (
        '"Breneman (1987)" experiment result must be one of "{0}"!'.format(
            valid_experiment_results))

    samples_luminance = [
        0.270,
        0.090,
        0.270,
        0.270,
        0.090,
        0.270,
        0.090,
        0.090,
        0.270,
        0.090,
        0.270,
        0.090,
    ]

    experiment_results = list(BRENEMAN_EXPERIMENTS[experiment])
    illuminant_chromaticities = experiment_results.pop(0)
    Y_r = Y_t = BRENEMAN_EXPERIMENT_PRIMARIES_CHROMATICITIES[experiment].Y
    B_r = B_t = 0.3

    XYZ_t, XYZ_r = xy_to_XYZ(
        np.hstack(
            [Luv_uv_to_xy(illuminant_chromaticities[1:3]),
             full([2, 1], Y_r)])) / Y_r

    xyY_cr, xyY_ct = [], []
    for i, experiment_result in enumerate(experiment_results):
        xyY_cr.append(
            np.hstack([
                Luv_uv_to_xy(experiment_result[2]), samples_luminance[i] * Y_r
            ]))
        xyY_ct.append(
            np.hstack([
                Luv_uv_to_xy(experiment_result[1]), samples_luminance[i] * Y_t
            ]))

    XYZ_cr = xyY_to_XYZ(xyY_cr)
    XYZ_ct = xyY_to_XYZ(xyY_ct)

    return CorrespondingColourDataset(experiment, XYZ_r, XYZ_t, XYZ_cr, XYZ_ct,
                                      Y_r, Y_t, B_r, B_t, {})
Ejemplo n.º 12
0
    def arithmetical_operation(self, a, operation, in_place=False):
        """
        Performs given arithmetical operation with :math:`a` operand, the
        operation can be either performed on a copy or in-place.

        Parameters
        ----------
        a : numeric or ndarray or Signal
            Operand.
        operation : object
            Operation to perform.
        in_place : bool, optional
            Operation happens in place.

        Returns
        -------
        Signal
            Continuous signal.

        Examples
        --------
        Adding a single *numeric* variable:

        >>> range_ = np.linspace(10, 100, 10)
        >>> signal_1 = Signal(range_)
        >>> print(signal_1)
        [[   0.   10.]
         [   1.   20.]
         [   2.   30.]
         [   3.   40.]
         [   4.   50.]
         [   5.   60.]
         [   6.   70.]
         [   7.   80.]
         [   8.   90.]
         [   9.  100.]]
        >>> print(signal_1.arithmetical_operation(10, '+', True))
        [[   0.   20.]
         [   1.   30.]
         [   2.   40.]
         [   3.   50.]
         [   4.   60.]
         [   5.   70.]
         [   6.   80.]
         [   7.   90.]
         [   8.  100.]
         [   9.  110.]]

        Adding an *array_like* variable:

        >>> a = np.linspace(10, 100, 10)
        >>> print(signal_1.arithmetical_operation(a, '+', True))
        [[   0.   30.]
         [   1.   50.]
         [   2.   70.]
         [   3.   90.]
         [   4.  110.]
         [   5.  130.]
         [   6.  150.]
         [   7.  170.]
         [   8.  190.]
         [   9.  210.]]

        Adding a :class:`colour.continuous.Signal` class:

        >>> signal_2 = Signal(range_)
        >>> print(signal_1.arithmetical_operation(signal_2, '+', True))
        [[   0.   40.]
         [   1.   70.]
         [   2.  100.]
         [   3.  130.]
         [   4.  160.]
         [   5.  190.]
         [   6.  220.]
         [   7.  250.]
         [   8.  280.]
         [   9.  310.]]
        """

        operation, ioperator = {
            '+': (add, iadd),
            '-': (sub, isub),
            '*': (mul, imul),
            '/': (div, idiv),
            '**': (pow, ipow)
        }[operation]

        if in_place:
            if isinstance(a, Signal):
                self[self._domain] = operation(self._range, a[self._domain])
                exclusive_or = np.setxor1d(self._domain, a.domain)
                self[exclusive_or] = full(exclusive_or.shape, np.nan)
            else:
                self.range = ioperator(self.range, a)

            return self
        else:
            copy = ioperator(self.copy(), a)

            return copy
Ejemplo n.º 13
0
    def arithmetical_operation(
        self,
        a: Union[FloatingOrArrayLike, AbstractContinuousFunction],
        operation: Literal["+", "-", "*", "/", "**"],
        in_place: Boolean = False,
    ) -> AbstractContinuousFunction:
        """
        Perform given arithmetical operation with operand :math:`a`, the
        operation can be either performed on a copy or in-place.

        Parameters
        ----------
        a
            Operand :math:`a`.
        operation
            Operation to perform.
        in_place
            Operation happens in place.

        Returns
        -------
        :class:`colour.continuous.Signal`
            Continuous signal.

        Examples
        --------
        Adding a single *numeric* variable:

        >>> range_ = np.linspace(10, 100, 10)
        >>> signal_1 = Signal(range_)
        >>> print(signal_1)
        [[   0.   10.]
         [   1.   20.]
         [   2.   30.]
         [   3.   40.]
         [   4.   50.]
         [   5.   60.]
         [   6.   70.]
         [   7.   80.]
         [   8.   90.]
         [   9.  100.]]
        >>> print(signal_1.arithmetical_operation(10, '+', True))
        [[   0.   20.]
         [   1.   30.]
         [   2.   40.]
         [   3.   50.]
         [   4.   60.]
         [   5.   70.]
         [   6.   80.]
         [   7.   90.]
         [   8.  100.]
         [   9.  110.]]

        Adding an `ArrayLike` variable:

        >>> a = np.linspace(10, 100, 10)
        >>> print(signal_1.arithmetical_operation(a, '+', True))
        [[   0.   30.]
         [   1.   50.]
         [   2.   70.]
         [   3.   90.]
         [   4.  110.]
         [   5.  130.]
         [   6.  150.]
         [   7.  170.]
         [   8.  190.]
         [   9.  210.]]

        Adding a :class:`colour.continuous.Signal` class:

        >>> signal_2 = Signal(range_)
        >>> print(signal_1.arithmetical_operation(signal_2, '+', True))
        [[   0.   40.]
         [   1.   70.]
         [   2.  100.]
         [   3.  130.]
         [   4.  160.]
         [   5.  190.]
         [   6.  220.]
         [   7.  250.]
         [   8.  280.]
         [   9.  310.]]
        """

        operator, ioperator = {
            "+": (add, iadd),
            "-": (sub, isub),
            "*": (mul, imul),
            "/": (truediv, itruediv),
            "**": (pow, ipow),
        }[operation]

        if in_place:
            if isinstance(a, Signal):
                self[self._domain] = operator(self._range, a[self._domain])
                exclusive_or = np.setxor1d(self._domain, a.domain)
                self[exclusive_or] = full(exclusive_or.shape, np.nan)
            else:
                self.range = ioperator(self.range, a)

            return self
        else:
            copy = ioperator(self.copy(), a)

            return copy
Ejemplo n.º 14
0
def plot_RGB_colourspaces_gamuts(
    colourspaces: Union[RGB_Colourspace, str, Sequence[Union[RGB_Colourspace,
                                                             str]]],
    reference_colourspace: Union[Literal["CAM02LCD", "CAM02SCD", "CAM02UCS",
                                         "CAM16LCD", "CAM16SCD", "CAM16UCS",
                                         "CIE XYZ", "CIE xyY", "CIE Lab",
                                         "CIE Luv", "CIE UCS", "CIE UVW",
                                         "DIN99", "Hunter Lab", "Hunter Rdab",
                                         "ICaCb", "ICtCp", "IPT", "IgPgTg",
                                         "Jzazbz", "OSA UCS", "Oklab",
                                         "hdr-CIELAB", "hdr-IPT", ],
                                 str, ] = "CIE xyY",
    segments: Integer = 8,
    show_grid: Boolean = True,
    grid_segments: Integer = 10,
    show_spectral_locus: Boolean = False,
    spectral_locus_colour: Optional[Union[ArrayLike, str]] = None,
    cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[
        MultiSpectralDistributions,
        str]], ] = "CIE 1931 2 Degree Standard Observer",
    chromatically_adapt: Boolean = False,
    convert_kwargs: Optional[Dict] = None,
    **kwargs: Any,
) -> Tuple[plt.Figure, plt.Axes]:
    """
    Plot given *RGB* colourspaces gamuts in given reference colourspace.

    Parameters
    ----------
    colourspaces
        *RGB* colourspaces to plot the gamuts. ``colourspaces`` elements
        can be of any type or form supported by the
        :func:`colour.plotting.filter_RGB_colourspaces` definition.
    reference_colourspace
        Reference colourspace model to plot the gamuts into, see
        :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported
        colourspace models.
    segments
        Edge segments count for each *RGB* colourspace cubes.
    show_grid
        Whether to show a grid at the bottom of the *RGB* colourspace cubes.
    grid_segments
        Edge segments count for the grid.
    show_spectral_locus
        Whether to show the spectral locus.
    spectral_locus_colour
        Spectral locus colour.
    cmfs
        Standard observer colour matching functions used for computing the
        spectral locus boundaries. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.filter_cmfs` definition.
    chromatically_adapt
        Whether to chromatically adapt the *RGB* colourspaces given in
        ``colourspaces`` to the whitepoint of the default plotting colourspace.
    convert_kwargs
        Keyword arguments for the :func:`colour.convert` definition.

    Other Parameters
    ----------------
    edge_colours
        Edge colours array such as `edge_colours = (None, (0.5, 0.5, 1.0))`.
    edge_alpha
        Edge opacity value such as `edge_alpha = (0.0, 1.0)`.
    face_alpha
        Face opacity value such as `face_alpha = (0.5, 1.0)`.
    face_colours
        Face colours array such as `face_colours = (None, (0.5, 0.5, 1.0))`.
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.volume.nadir_grid`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_RGB_colourspaces_gamuts(['ITU-R BT.709', 'ACEScg', 'S-Gamut'])
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes3DSubplot...>)

    .. image:: ../_static/Plotting_Plot_RGB_Colourspaces_Gamuts.png
        :align: center
        :alt: plot_RGB_colourspaces_gamuts
    """

    colourspaces = cast(
        List[RGB_Colourspace],
        list(filter_RGB_colourspaces(colourspaces).values()),
    )

    convert_kwargs = optional(convert_kwargs, {})

    count_c = len(colourspaces)

    title = (
        f"{', '.join([colourspace.name for colourspace in colourspaces])} "
        f"- {reference_colourspace} Reference Colourspace")

    illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint

    convert_settings = {"illuminant": illuminant}
    convert_settings.update(convert_kwargs)

    settings = Structure(
        **{
            "face_colours": [None] * count_c,
            "edge_colours": [None] * count_c,
            "face_alpha": [1] * count_c,
            "edge_alpha": [1] * count_c,
            "title": title,
        })
    settings.update(kwargs)

    figure = plt.figure()
    axes = figure.add_subplot(111, projection="3d")

    points = zeros((4, 3))
    if show_spectral_locus:
        cmfs = cast(MultiSpectralDistributions,
                    first_item(filter_cmfs(cmfs).values()))
        XYZ = cmfs.values

        points = colourspace_model_axis_reorder(
            convert(XYZ, "CIE XYZ", reference_colourspace, **convert_settings),
            reference_colourspace,
        )

        points[np.isnan(points)] = 0

        c = ((0.0, 0.0, 0.0,
              0.5) if spectral_locus_colour is None else spectral_locus_colour)

        axes.plot(
            points[..., 0],
            points[..., 1],
            points[..., 2],
            color=c,
            zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
        )
        axes.plot(
            (points[-1][0], points[0][0]),
            (points[-1][1], points[0][1]),
            (points[-1][2], points[0][2]),
            color=c,
            zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
        )

    plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace

    quads_c: List = []
    RGB_cf: List = []
    RGB_ce: List = []
    for i, colourspace in enumerate(colourspaces):

        if chromatically_adapt and not np.array_equal(
                colourspace.whitepoint, plotting_colourspace.whitepoint):
            colourspace = colourspace.chromatically_adapt(
                plotting_colourspace.whitepoint,
                plotting_colourspace.whitepoint_name,
            )

        quads_cb, RGB = RGB_identity_cube(
            width_segments=segments,
            height_segments=segments,
            depth_segments=segments,
        )

        XYZ = RGB_to_XYZ(
            quads_cb,
            colourspace.whitepoint,
            colourspace.whitepoint,
            colourspace.matrix_RGB_to_XYZ,
        )

        convert_settings = {"illuminant": colourspace.whitepoint}
        convert_settings.update(convert_kwargs)

        quads_c.extend(
            colourspace_model_axis_reorder(
                convert(XYZ, "CIE XYZ", reference_colourspace,
                        **convert_settings),
                reference_colourspace,
            ))

        if settings.face_colours[i] is not None:
            RGB = ones(RGB.shape) * settings.face_colours[i]

        RGB_cf.extend(
            np.hstack([RGB,
                       full((RGB.shape[0], 1), settings.face_alpha[i])]))

        if settings.edge_colours[i] is not None:
            RGB = ones(RGB.shape) * settings.edge_colours[i]

        RGB_ce.extend(
            np.hstack([RGB,
                       full((RGB.shape[0], 1), settings.edge_alpha[i])]))

    quads = as_float_array(quads_c)
    RGB_f = as_float_array(RGB_cf)
    RGB_e = as_float_array(RGB_ce)

    quads[np.isnan(quads)] = 0

    if quads.size != 0:
        for i, axis in enumerate("xyz"):
            min_a = np.minimum(np.min(quads[..., i]), np.min(points[..., i]))
            max_a = np.maximum(np.max(quads[..., i]), np.max(points[..., i]))
            getattr(axes, f"set_{axis}lim")((min_a, max_a))

    labels = np.array(
        COLOURSPACE_MODELS_AXIS_LABELS[reference_colourspace])[as_int_array(
            colourspace_model_axis_reorder([0, 1, 2], reference_colourspace))]
    for i, axis in enumerate("xyz"):
        getattr(axes, f"set_{axis}label")(labels[i])

    if show_grid:
        limits = np.array([[-1.5, 1.5], [-1.5, 1.5]])

        quads_g, RGB_gf, RGB_ge = nadir_grid(limits, grid_segments, labels,
                                             axes, **settings)
        quads = np.vstack([quads_g, quads])
        RGB_f = np.vstack([RGB_gf, RGB_f])
        RGB_e = np.vstack([RGB_ge, RGB_e])

    collection = Poly3DCollection(quads)
    collection.set_facecolors(RGB_f)
    collection.set_edgecolors(RGB_e)

    axes.add_collection3d(collection)

    settings.update({
        "axes": axes,
        "axes_visible": False,
        "camera_aspect": "equal"
    })
    settings.update(kwargs)

    return render(**settings)
Ejemplo n.º 15
0
def find_coefficients_Jakob2019(
    XYZ: ArrayLike,
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    coefficients_0: ArrayLike = zeros(3),
    max_error: Floating = JND_CIE1976 / 100,
    dimensionalise: Boolean = True,
) -> Tuple[NDArray, Floating]:
    """
    Compute the coefficients for *Jakob and Hanika (2019)* reflectance
    spectral model.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values to find the coefficients for.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution, default to
        *CIE Standard Illuminant D65*.
    coefficients_0
        Starting coefficients for the solver.
    max_error
        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
        If *True*, returned coefficients are dimensionful and will not work
        correctly if fed back as ``coefficients_0``. The default is *True*.

    Returns
    -------
    :class:`tuple`
        Tuple of computed coefficients that best fit the given colour and
        :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...)
    """

    coefficients_0 = as_float_array(coefficients_0)

    cmfs, illuminant = handle_spectral_arguments(
        cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019
    )

    def optimize(
        target_o: NDArray, coefficients_0_o: NDArray
    ) -> Tuple[NDArray, Floating]:
        """Minimise 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_integration(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, cmfs.shape)

    return coefficients, error
Ejemplo n.º 16
0
def nadir_grid(limits=None, segments=10, labels=None, axes=None, **kwargs):
    """
    Returns a grid on *CIE xy* plane made of quad geometric elements and its
    associated faces and edges colours. Ticks and labels are added to the
    given axes according to the extended grid settings.

    Parameters
    ----------
    limits : array_like, optional
        Extended grid limits.
    segments : int, optional
        Edge segments count for the extended grid.
    labels : array_like, optional
        Axis labels.
    axes : matplotlib.axes.Axes, optional
        Axes to add the grid.

    Other Parameters
    ----------------
    grid_face_colours : array_like, optional
        Grid face colours array such as
        `grid_face_colours = (0.25, 0.25, 0.25)`.
    grid_edge_colours : array_like, optional
        Grid edge colours array such as
        `grid_edge_colours = (0.25, 0.25, 0.25)`.
    grid_face_alpha : numeric, optional
        Grid face opacity value such as `grid_face_alpha = 0.1`.
    grid_edge_alpha : numeric, optional
        Grid edge opacity value such as `grid_edge_alpha = 0.5`.
    x_axis_colour : array_like, optional
        *X* axis colour array such as `x_axis_colour = (0.0, 0.0, 0.0, 1.0)`.
    y_axis_colour : array_like, optional
        *Y* axis colour array such as `y_axis_colour = (0.0, 0.0, 0.0, 1.0)`.
    x_ticks_colour : array_like, optional
        *X* axis ticks colour array such as
        `x_ticks_colour = (0.0, 0.0, 0.0, 0.85)`.
    y_ticks_colour : array_like, optional
        *Y* axis ticks colour array such as
        `y_ticks_colour = (0.0, 0.0, 0.0, 0.85)`.
    x_label_colour : array_like, optional
        *X* axis label colour array such as
        `x_label_colour = (0.0, 0.0, 0.0, 0.85)`.
    y_label_colour : array_like, optional
        *Y* axis label colour array such as
        `y_label_colour = (0.0, 0.0, 0.0, 0.85)`.
    ticks_and_label_location : array_like, optional
        Location of the *X* and *Y* axis ticks and labels such as
        `ticks_and_label_location = ('-x', '-y')`.

    Returns
    -------
    tuple
        Grid quads, faces colours, edges colours.

    Examples
    --------
    >>> nadir_grid(segments=1)
    (array([[[-1.   , -1.   ,  0.   ],
            [ 1.   , -1.   ,  0.   ],
            [ 1.   ,  1.   ,  0.   ],
            [-1.   ,  1.   ,  0.   ]],
    <BLANKLINE>
           [[-1.   , -1.   ,  0.   ],
            [ 0.   , -1.   ,  0.   ],
            [ 0.   ,  0.   ,  0.   ],
            [-1.   ,  0.   ,  0.   ]],
    <BLANKLINE>
           [[-1.   ,  0.   ,  0.   ],
            [ 0.   ,  0.   ,  0.   ],
            [ 0.   ,  1.   ,  0.   ],
            [-1.   ,  1.   ,  0.   ]],
    <BLANKLINE>
           [[ 0.   , -1.   ,  0.   ],
            [ 1.   , -1.   ,  0.   ],
            [ 1.   ,  0.   ,  0.   ],
            [ 0.   ,  0.   ,  0.   ]],
    <BLANKLINE>
           [[ 0.   ,  0.   ,  0.   ],
            [ 1.   ,  0.   ,  0.   ],
            [ 1.   ,  1.   ,  0.   ],
            [ 0.   ,  1.   ,  0.   ]],
    <BLANKLINE>
           [[-1.   , -0.001,  0.   ],
            [ 1.   , -0.001,  0.   ],
            [ 1.   ,  0.001,  0.   ],
            [-1.   ,  0.001,  0.   ]],
    <BLANKLINE>
           [[-0.001, -1.   ,  0.   ],
            [ 0.001, -1.   ,  0.   ],
            [ 0.001,  1.   ,  0.   ],
            [-0.001,  1.   ,  0.   ]]]), array([[ 0.25,  0.25,  0.25,  0.1 ],
           [ 0.  ,  0.  ,  0.  ,  0.  ],
           [ 0.  ,  0.  ,  0.  ,  0.  ],
           [ 0.  ,  0.  ,  0.  ,  0.  ],
           [ 0.  ,  0.  ,  0.  ,  0.  ],
           [ 0.  ,  0.  ,  0.  ,  1.  ],
           [ 0.  ,  0.  ,  0.  ,  1.  ]]), array([[ 0.5 ,  0.5 ,  0.5 ,  0.5 ],
           [ 0.75,  0.75,  0.75,  0.25],
           [ 0.75,  0.75,  0.75,  0.25],
           [ 0.75,  0.75,  0.75,  0.25],
           [ 0.75,  0.75,  0.75,  0.25],
           [ 0.  ,  0.  ,  0.  ,  1.  ],
           [ 0.  ,  0.  ,  0.  ,  1.  ]]))
    """

    if limits is None:
        limits = np.array([[-1, 1], [-1, 1]])

    if labels is None:
        labels = ('x', 'y')

    extent = np.max(np.abs(limits[..., 1] - limits[..., 0]))

    settings = Structure(
        **{
            'grid_face_colours': (0.25, 0.25, 0.25),
            'grid_edge_colours': (0.50, 0.50, 0.50),
            'grid_face_alpha': 0.1,
            'grid_edge_alpha': 0.5,
            'x_axis_colour': (0.0, 0.0, 0.0, 1.0),
            'y_axis_colour': (0.0, 0.0, 0.0, 1.0),
            'x_ticks_colour': (0.0, 0.0, 0.0, 0.85),
            'y_ticks_colour': (0.0, 0.0, 0.0, 0.85),
            'x_label_colour': (0.0, 0.0, 0.0, 0.85),
            'y_label_colour': (0.0, 0.0, 0.0, 0.85),
            'ticks_and_label_location': ('-x', '-y')
        })
    settings.update(**kwargs)

    # Outer grid.
    quads_g = primitive_vertices_grid_mpl(
        origin=(-extent / 2, -extent / 2),
        width=extent,
        height=extent,
        height_segments=segments,
        width_segments=segments)

    RGB_g = ones([quads_g.shape[0], quads_g.shape[-1]])
    RGB_gf = RGB_g * settings.grid_face_colours
    RGB_gf = np.hstack(
        [RGB_gf, full([RGB_gf.shape[0], 1], settings.grid_face_alpha)])
    RGB_ge = RGB_g * settings.grid_edge_colours
    RGB_ge = np.hstack(
        [RGB_ge, full([RGB_ge.shape[0], 1], settings.grid_edge_alpha)])

    # Inner grid.
    quads_gs = primitive_vertices_grid_mpl(
        origin=(-extent / 2, -extent / 2),
        width=extent,
        height=extent,
        height_segments=segments * 2,
        width_segments=segments * 2)

    RGB_gs = ones([quads_gs.shape[0], quads_gs.shape[-1]])
    RGB_gsf = RGB_gs * 0
    RGB_gsf = np.hstack([RGB_gsf, full([RGB_gsf.shape[0], 1], 0)])
    RGB_gse = np.clip(RGB_gs * settings.grid_edge_colours * 1.5, 0, 1)
    RGB_gse = np.hstack((RGB_gse,
                         full([RGB_gse.shape[0], 1],
                              settings.grid_edge_alpha / 2)))

    # Axis.
    thickness = extent / 1000
    quad_x = primitive_vertices_grid_mpl(
        origin=(limits[0, 0], -thickness / 2), width=extent, height=thickness)
    RGB_x = ones([quad_x.shape[0], quad_x.shape[-1] + 1])
    RGB_x = RGB_x * settings.x_axis_colour

    quad_y = primitive_vertices_grid_mpl(
        origin=(-thickness / 2, limits[1, 0]), width=thickness, height=extent)
    RGB_y = ones([quad_y.shape[0], quad_y.shape[-1] + 1])
    RGB_y = RGB_y * settings.y_axis_colour

    if axes is not None:
        # Ticks.
        x_s = 1 if '+x' in settings.ticks_and_label_location else -1
        y_s = 1 if '+y' in settings.ticks_and_label_location else -1
        for i, axis in enumerate('xy'):
            h_a = 'center' if axis == 'x' else 'left' if x_s == 1 else 'right'
            v_a = 'center'

            ticks = list(sorted(set(quads_g[..., 0, i])))
            ticks += [ticks[-1] + ticks[-1] - ticks[-2]]
            for tick in ticks:
                x = (limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 25)
                     if i else tick)
                y = (tick if i else
                     limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 25))

                tick = DEFAULT_INT_DTYPE(tick) if DEFAULT_FLOAT_DTYPE(
                    tick).is_integer() else tick
                c = settings['{0}_ticks_colour'.format(axis)]

                axes.text(
                    x,
                    y,
                    0,
                    tick,
                    'x',
                    horizontalalignment=h_a,
                    verticalalignment=v_a,
                    color=c,
                    clip_on=True)

        # Labels.
        for i, axis in enumerate('xy'):
            h_a = 'center' if axis == 'x' else 'left' if x_s == 1 else 'right'
            v_a = 'center'

            x = (limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 10)
                 if i else 0)
            y = (0 if i else
                 limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 10))

            c = settings['{0}_label_colour'.format(axis)]

            axes.text(
                x,
                y,
                0,
                labels[i],
                'x',
                horizontalalignment=h_a,
                verticalalignment=v_a,
                color=c,
                size=20,
                clip_on=True)

    quads = np.vstack([quads_g, quads_gs, quad_x, quad_y])
    RGB_f = np.vstack([RGB_gf, RGB_gsf, RGB_x, RGB_y])
    RGB_e = np.vstack([RGB_ge, RGB_gse, RGB_x, RGB_y])

    return quads, RGB_f, RGB_e
Ejemplo n.º 17
0
def primitive_vertices_sphere(radius=0.5,
                              segments=8,
                              intermediate=False,
                              origin=np.array([0, 0, 0]),
                              axis='+z'):
    """
    Returns the vertices of a latitude-longitude sphere primitive.

    Parameters
    ----------
    radius: numeric, optional
        Sphere radius.
    segments: numeric, optional
        Latitude-longitude segments, if the ``intermediate`` argument is
        *True*, then the sphere will have one less segment along its longitude.
    intermediate: bool, optional
        Whether to generate the sphere vertices at the center of the faces
        outlined by the segments of a regular sphere generated without
        the ``intermediate`` argument set to *True*. The resulting sphere is
        inscribed on the regular sphere faces but possesses the same poles.
    origin: array_like, optional
        Sphere origin on the construction plane.
    axis : array_like, optional
        **{'+z', '+x', '+y', 'yz', 'xz', 'xy'}**,
        Axis (or normal of the plane) the poles of the sphere will be aligned
        with.

    Returns
    -------
    ndarray
        Sphere primitive vertices.

    Notes
    -----
    -   The sphere poles have latitude segments count - 1 co-located vertices.

    Examples
    --------
    >>> primitive_vertices_sphere(segments=4)  # doctest: +ELLIPSIS
    array([[[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [ -3.5355339...e-01,  -4.3297802...e-17,   3.5355339...e-01],
            [ -5.0000000...e-01,  -6.1232340...e-17,   3.0616170...e-17],
            [ -3.5355339...e-01,  -4.3297802...e-17,  -3.5355339...e-01],
            [ -6.1232340...e-17,  -7.4987989...e-33,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  2.1648901...e-17,  -3.5355339...e-01,   3.5355339...e-01],
            [  3.0616170...e-17,  -5.0000000...e-01,   3.0616170...e-17],
            [  2.1648901...e-17,  -3.5355339...e-01,  -3.5355339...e-01],
            [  3.7493994...e-33,  -6.1232340...e-17,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  3.5355339...e-01,   0.0000000...e+00,   3.5355339...e-01],
            [  5.0000000...e-01,   0.0000000...e+00,   3.0616170...e-17],
            [  3.5355339...e-01,   0.0000000...e+00,  -3.5355339...e-01],
            [  6.1232340...e-17,   0.0000000...e+00,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  2.1648901...e-17,   3.5355339...e-01,   3.5355339...e-01],
            [  3.0616170...e-17,   5.0000000...e-01,   3.0616170...e-17],
            [  2.1648901...e-17,   3.5355339...e-01,  -3.5355339...e-01],
            [  3.7493994...e-33,   6.1232340...e-17,  -5.0000000...e-01]]])
    """

    axis = PLANE_TO_AXIS_MAPPING.get(axis, axis).lower()

    if not intermediate:
        theta = np.tile(np.radians(np.linspace(0, 180, segments + 1)),
                        (segments + 1, 1))
        phi = np.transpose(
            np.tile(np.radians(np.linspace(-180, 180, segments + 1)),
                    (segments + 1, 1)))
    else:
        theta = np.tile(
            np.radians(np.linspace(0, 180, segments * 2 + 1)[1::2][1:-1]),
            (segments + 1, 1))
        theta = np.hstack([
            zeros([segments + 1, 1]),
            theta,
            full([segments + 1, 1], np.pi),
        ])
        phi = np.transpose(
            np.tile(
                np.radians(np.linspace(-180, 180, segments + 1)) +
                np.radians(360 / segments / 2), (segments, 1)))

    rho = ones(phi.shape) * radius
    rho_theta_phi = tstack([rho, theta, phi])

    vertices = spherical_to_cartesian(rho_theta_phi)

    # Removing extra longitude vertices.
    vertices = vertices[:-1, :, :]

    if axis == '+z':
        pass
    elif axis == '+y':
        vertices = np.roll(vertices, 2, -1)
    elif axis == '+x':
        vertices = np.roll(vertices, 1, -1)
    else:
        raise ValueError('Axis must be one of "{0}"!'.format(
            ['+x', '+y', '+z']))

    vertices += origin

    return vertices
Ejemplo n.º 18
0
def plot_planckian_locus(
    planckian_locus_colours: Optional[Union[ArrayLike, str]] = None,
    planckian_locus_opacity: Floating = 1,
    planckian_locus_labels: Optional[Sequence] = None,
    method: Union[Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"],
                  str] = "CIE 1931",
    **kwargs: Any,
) -> Tuple[plt.Figure, plt.Axes]:
    """
    Plot the *Planckian Locus* according to given method.

    Parameters
    ----------
    planckian_locus_colours
        Colours of the *Planckian Locus*, if ``planckian_locus_colours`` is set
        to *RGB*, the colours will be computed according to the corresponding
        chromaticity coordinates.
    planckian_locus_opacity
       Opacity of the *Planckian Locus*.
    planckian_locus_labels
        Array of labels used to customise which iso-temperature lines will be
        drawn along the *Planckian Locus*. Passing an empty array will result
        in no iso-temperature lines being drawn.
    method
        *Chromaticity Diagram* method.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_planckian_locus(planckian_locus_colours='RGB')
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...AxesSubplot...>)

    .. image:: ../_static/Plotting_Plot_Planckian_Locus.png
        :align: center
        :alt: plot_planckian_locus
    """

    method = validate_method(method,
                             ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"])

    planckian_locus_colours = optional(planckian_locus_colours,
                                       CONSTANTS_COLOUR_STYLE.colour.dark)

    labels = cast(
        Tuple,
        optional(
            planckian_locus_labels,
            (10**6 / 600, 2000, 2500, 3000, 4000, 6000, 10**6 / 100),
        ),
    )
    D_uv = 0.05

    settings: Dict[str, Any] = {"uniform": True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    if method == "cie 1931":

        def uv_to_ij(uv: NDArray) -> NDArray:
            """
            Convert given *uv* chromaticity coordinates to *ij* chromaticity
            coordinates.
            """

            return UCS_uv_to_xy(uv)

    elif method == "cie 1960 ucs":

        def uv_to_ij(uv: NDArray) -> NDArray:
            """
            Convert given *uv* chromaticity coordinates to *ij* chromaticity
            coordinates.
            """

            return uv

    elif method == "cie 1976 ucs":

        def uv_to_ij(uv: NDArray) -> NDArray:
            """
            Convert given *uv* chromaticity coordinates to *ij* chromaticity
            coordinates.
            """

            return xy_to_Luv_uv(UCS_uv_to_xy(uv))

    def CCT_D_uv_to_plotting_colourspace(CCT_D_uv):
        """
        Convert given *uv* chromaticity coordinates to the default plotting
        colourspace.
        """

        return normalise_maximum(
            XYZ_to_plotting_colourspace(
                xy_to_XYZ(UCS_uv_to_xy(CCT_to_uv(CCT_D_uv,
                                                 "Robertson 1968")))),
            axis=-1,
        )

    start, end = 10**6 / 600, 10**6 / 10
    CCT = np.arange(start, end + 100, 100)
    CCT_D_uv = np.reshape(tstack([CCT, zeros(CCT.shape)]), (-1, 1, 2))
    ij = uv_to_ij(CCT_to_uv(CCT_D_uv, "Robertson 1968"))

    use_RGB_planckian_locus_colours = (
        str(planckian_locus_colours).upper() == "RGB")
    if use_RGB_planckian_locus_colours:
        pl_colours = CCT_D_uv_to_plotting_colourspace(CCT_D_uv)
    else:
        pl_colours = planckian_locus_colours

    line_collection = LineCollection(
        np.concatenate([ij[:-1], ij[1:]], axis=1),
        colors=pl_colours,
        alpha=planckian_locus_opacity,
        zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line,
    )
    axes.add_collection(line_collection)

    for label in labels:
        CCT_D_uv = np.reshape(
            tstack([full(10, label),
                    np.linspace(-D_uv, D_uv, 10)]), (-1, 1, 2))

        if use_RGB_planckian_locus_colours:
            itl_colours = CCT_D_uv_to_plotting_colourspace(CCT_D_uv)
        else:
            itl_colours = planckian_locus_colours

        ij = uv_to_ij(CCT_to_uv(CCT_D_uv, "Robertson 1968"))

        line_collection = LineCollection(
            np.concatenate([ij[:-1], ij[1:]], axis=1),
            colors=itl_colours,
            alpha=planckian_locus_opacity,
            zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line,
        )
        axes.add_collection(line_collection)
        axes.annotate(
            f"{as_int_scalar(label)}K",
            xy=(ij[-1, :, 0], ij[-1, :, 1]),
            xytext=(0, CONSTANTS_COLOUR_STYLE.geometry.long / 2),
            textcoords="offset points",
            size="x-small",
            zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_label,
        )

    settings = {"axes": axes}
    settings.update(kwargs)

    return render(**settings)
Ejemplo n.º 19
0
def uv_to_Luv(uv,
              illuminant=CCS_ILLUMINANTS['CIE 1931 2 Degree Standard Observer']
              ['D65'],
              Y=1):
    """
    Returns the *CIE L\\*u\\*v\\** colourspace array from given :math:`uv^p`
    chromaticity coordinates by extending the array last dimension with given
    :math:`L` *Lightness*.

    Parameters
    ----------
    uv : array_like
        :math:`uv^p` chromaticity coordinates.
    illuminant : array_like, optional
        Reference *illuminant* *CIE xy* chromaticity coordinates or *CIE xyY*
        colourspace array.
    Y : numeric, optional
        Optional :math:`Y` *luminance* value used to construct the intermediate
        *CIE XYZ* colourspace array, the default :math:`Y` *luminance* value is
        1.

    Returns
    -------
    ndarray
        *CIE L\\*u\\*v\\** colourspace array.

    Notes
    -----

    +----------------+-----------------------+-----------------+
    | **Range**      | **Scale - Reference** | **Scale - 1**   |
    +================+=======================+=================+
    | ``Luv``        | ``L`` : [0, 100]      | ``L`` : [0, 1]  |
    |                |                       |                 |
    |                | ``u`` : [-100, 100]   | ``u`` : [-1, 1] |
    |                |                       |                 |
    |                | ``v`` : [-100, 100]   | ``v`` : [-1, 1] |
    +----------------+-----------------------+-----------------+
    | ``illuminant`` | [0, 1]                | [0, 1]          |
    +----------------+-----------------------+-----------------+

    References
    ----------
    :cite:`CIETC1-482004j`

    Examples
    --------
    >>> import numpy as np
    >>> uv = np.array([0.37720213, 0.50120264])
    >>> uv_to_Luv(uv)  # doctest: +ELLIPSIS
    array([ 100.        ,  233.1837603...,   42.7474385...])
    """

    u, v = tsplit(uv)
    Y = to_domain_1(Y)

    X = 9 * u / (4 * v)
    Z = (-5 * Y * v - 3 * u / 4 + 3) / v
    Y = full(u.shape, Y)

    return XYZ_to_Luv(from_range_1(tstack([X, Y, Z])), illuminant)
Ejemplo n.º 20
0
def nadir_grid(
    limits: Optional[ArrayLike] = None,
    segments: Integer = 10,
    labels: Optional[Sequence[str]] = None,
    axes: Optional[plt.Axes] = None,
    **kwargs: Any,
) -> Tuple[NDArray, NDArray, NDArray]:
    """
    Return a grid on *CIE xy* plane made of quad geometric elements and its
    associated faces and edges colours. Ticks and labels are added to the
    given axes according to the extended grid settings.

    Parameters
    ----------
    limits
        Extended grid limits.
    segments
        Edge segments count for the extended grid.
    labels
        Axis labels.
    axes
        Axes to add the grid.

    Other Parameters
    ----------------
    grid_edge_alpha
        Grid edge opacity value such as `grid_edge_alpha = 0.5`.
    grid_edge_colours
        Grid edge colours array such as
        `grid_edge_colours = (0.25, 0.25, 0.25)`.
    grid_face_alpha
        Grid face opacity value such as `grid_face_alpha = 0.1`.
    grid_face_colours
        Grid face colours array such as
        `grid_face_colours = (0.25, 0.25, 0.25)`.
    ticks_and_label_location
        Location of the *X* and *Y* axis ticks and labels such as
        `ticks_and_label_location = ('-x', '-y')`.
    x_axis_colour
        *X* axis colour array such as `x_axis_colour = (0.0, 0.0, 0.0, 1.0)`.
    x_label_colour
        *X* axis label colour array such as
        `x_label_colour = (0.0, 0.0, 0.0, 0.85)`.
    x_ticks_colour
        *X* axis ticks colour array such as
        `x_ticks_colour = (0.0, 0.0, 0.0, 0.85)`.
    y_axis_colour
        *Y* axis colour array such as `y_axis_colour = (0.0, 0.0, 0.0, 1.0)`.
    y_label_colour
        *Y* axis label colour array such as
        `y_label_colour = (0.0, 0.0, 0.0, 0.85)`.
    y_ticks_colour
        *Y* axis ticks colour array such as
        `y_ticks_colour = (0.0, 0.0, 0.0, 0.85)`.

    Returns
    -------
    :class:`tuple`
        Grid quads, faces colours, edges colours.

    Examples
    --------
    >>> nadir_grid(segments=1)
    (array([[[-1.   , -1.   ,  0.   ],
            [ 1.   , -1.   ,  0.   ],
            [ 1.   ,  1.   ,  0.   ],
            [-1.   ,  1.   ,  0.   ]],
    <BLANKLINE>
           [[-1.   , -1.   ,  0.   ],
            [ 0.   , -1.   ,  0.   ],
            [ 0.   ,  0.   ,  0.   ],
            [-1.   ,  0.   ,  0.   ]],
    <BLANKLINE>
           [[-1.   ,  0.   ,  0.   ],
            [ 0.   ,  0.   ,  0.   ],
            [ 0.   ,  1.   ,  0.   ],
            [-1.   ,  1.   ,  0.   ]],
    <BLANKLINE>
           [[ 0.   , -1.   ,  0.   ],
            [ 1.   , -1.   ,  0.   ],
            [ 1.   ,  0.   ,  0.   ],
            [ 0.   ,  0.   ,  0.   ]],
    <BLANKLINE>
           [[ 0.   ,  0.   ,  0.   ],
            [ 1.   ,  0.   ,  0.   ],
            [ 1.   ,  1.   ,  0.   ],
            [ 0.   ,  1.   ,  0.   ]],
    <BLANKLINE>
           [[-1.   , -0.001,  0.   ],
            [ 1.   , -0.001,  0.   ],
            [ 1.   ,  0.001,  0.   ],
            [-1.   ,  0.001,  0.   ]],
    <BLANKLINE>
           [[-0.001, -1.   ,  0.   ],
            [ 0.001, -1.   ,  0.   ],
            [ 0.001,  1.   ,  0.   ],
            [-0.001,  1.   ,  0.   ]]]), array([[ 0.25,  0.25,  0.25,  0.1 ],
           [ 0.  ,  0.  ,  0.  ,  0.  ],
           [ 0.  ,  0.  ,  0.  ,  0.  ],
           [ 0.  ,  0.  ,  0.  ,  0.  ],
           [ 0.  ,  0.  ,  0.  ,  0.  ],
           [ 0.  ,  0.  ,  0.  ,  1.  ],
           [ 0.  ,  0.  ,  0.  ,  1.  ]]), array([[ 0.5 ,  0.5 ,  0.5 ,  0.5 ],
           [ 0.75,  0.75,  0.75,  0.25],
           [ 0.75,  0.75,  0.75,  0.25],
           [ 0.75,  0.75,  0.75,  0.25],
           [ 0.75,  0.75,  0.75,  0.25],
           [ 0.  ,  0.  ,  0.  ,  1.  ],
           [ 0.  ,  0.  ,  0.  ,  1.  ]]))
    """

    limits = as_float_array(
        cast(ArrayLike, optional(limits, np.array([[-1, 1], [-1, 1]]))))
    labels = cast(Sequence, optional(labels, ("x", "y")))

    extent = np.max(np.abs(limits[..., 1] - limits[..., 0]))

    settings = Structure(
        **{
            "grid_face_colours": (0.25, 0.25, 0.25),
            "grid_edge_colours": (0.50, 0.50, 0.50),
            "grid_face_alpha": 0.1,
            "grid_edge_alpha": 0.5,
            "x_axis_colour": (0.0, 0.0, 0.0, 1.0),
            "y_axis_colour": (0.0, 0.0, 0.0, 1.0),
            "x_ticks_colour": (0.0, 0.0, 0.0, 0.85),
            "y_ticks_colour": (0.0, 0.0, 0.0, 0.85),
            "x_label_colour": (0.0, 0.0, 0.0, 0.85),
            "y_label_colour": (0.0, 0.0, 0.0, 0.85),
            "ticks_and_label_location": ("-x", "-y"),
        })
    settings.update(**kwargs)

    # Outer grid.
    quads_g = primitive_vertices_grid_mpl(
        origin=(-extent / 2, -extent / 2),
        width=extent,
        height=extent,
        height_segments=segments,
        width_segments=segments,
    )

    RGB_g = ones((quads_g.shape[0], quads_g.shape[-1]))
    RGB_gf = RGB_g * settings.grid_face_colours
    RGB_gf = np.hstack(
        [RGB_gf, full((RGB_gf.shape[0], 1), settings.grid_face_alpha)])
    RGB_ge = RGB_g * settings.grid_edge_colours
    RGB_ge = np.hstack(
        [RGB_ge, full((RGB_ge.shape[0], 1), settings.grid_edge_alpha)])

    # Inner grid.
    quads_gs = primitive_vertices_grid_mpl(
        origin=(-extent / 2, -extent / 2),
        width=extent,
        height=extent,
        height_segments=segments * 2,
        width_segments=segments * 2,
    )

    RGB_gs = ones((quads_gs.shape[0], quads_gs.shape[-1]))
    RGB_gsf = RGB_gs * 0
    RGB_gsf = np.hstack([RGB_gsf, full((RGB_gsf.shape[0], 1), 0)])
    RGB_gse = np.clip(RGB_gs * settings.grid_edge_colours * 1.5, 0, 1)
    RGB_gse = np.hstack(
        (RGB_gse, full((RGB_gse.shape[0], 1), settings.grid_edge_alpha / 2)))

    # Axis.
    thickness = extent / 1000
    quad_x = primitive_vertices_grid_mpl(origin=(limits[0, 0], -thickness / 2),
                                         width=extent,
                                         height=thickness)
    RGB_x = ones((quad_x.shape[0], quad_x.shape[-1] + 1))
    RGB_x = RGB_x * settings.x_axis_colour

    quad_y = primitive_vertices_grid_mpl(origin=(-thickness / 2, limits[1, 0]),
                                         width=thickness,
                                         height=extent)
    RGB_y = ones((quad_y.shape[0], quad_y.shape[-1] + 1))
    RGB_y = RGB_y * settings.y_axis_colour

    if axes is not None:
        # Ticks.
        x_s = 1 if "+x" in settings.ticks_and_label_location else -1
        y_s = 1 if "+y" in settings.ticks_and_label_location else -1
        for i, axis in enumerate("xy"):
            h_a = "center" if axis == "x" else "left" if x_s == 1 else "right"
            v_a = "center"

            ticks = list(sorted(set(quads_g[..., 0, i])))
            ticks += [ticks[-1] + ticks[-1] - ticks[-2]]
            for tick in ticks:
                x = (limits[1, 1 if x_s == 1 else 0] +
                     (x_s * extent / 25) if i else tick)
                y = (tick if i else limits[0, 1 if y_s == 1 else 0] +
                     (y_s * extent / 25))

                tick = as_int_scalar(tick) if is_integer(tick) else tick
                c = settings[f"{axis}_ticks_colour"]

                axes.text(
                    x,
                    y,
                    0,
                    tick,
                    "x",
                    horizontalalignment=h_a,
                    verticalalignment=v_a,
                    color=c,
                    clip_on=True,
                )

        # Labels.
        for i, axis in enumerate("xy"):
            h_a = "center" if axis == "x" else "left" if x_s == 1 else "right"
            v_a = "center"

            x = (limits[1, 1 if x_s == 1 else 0] +
                 (x_s * extent / 10) if i else 0)
            y = (0 if i else limits[0, 1 if y_s == 1 else 0] +
                 (y_s * extent / 10))

            c = settings[f"{axis}_label_colour"]

            axes.text(
                x,
                y,
                0,
                labels[i],
                "x",
                horizontalalignment=h_a,
                verticalalignment=v_a,
                color=c,
                size=20,
                clip_on=True,
            )

    quads = np.vstack([quads_g, quads_gs, quad_x, quad_y])
    RGB_f = np.vstack([RGB_gf, RGB_gsf, RGB_x, RGB_y])
    RGB_e = np.vstack([RGB_ge, RGB_gse, RGB_x, RGB_y])

    return quads, RGB_f, RGB_e
Ejemplo n.º 21
0
def xy_to_xyY(xy, Y=1):
    """
    Converts from *CIE xy* chromaticity coordinates to *CIE xyY* colourspace by
    extending the array last dimension with given :math:`Y` *luminance*.

    ``xy`` argument with last dimension being equal to 3 will be assumed to be
    a *CIE xyY* colourspace array argument and will be returned directly by the
    definition.

    Parameters
    ----------
    xy : array_like
        *CIE xy* chromaticity coordinates or *CIE xyY* colourspace array.
    Y : numeric, optional
        Optional :math:`Y` *luminance* value used to construct the *CIE xyY*
        colourspace array, the default :math:`Y` *luminance* value is 1.

    Returns
    -------
    ndarray
        *CIE xyY* colourspace array.

    Notes
    -----

    +------------+-----------------------+---------------+
    | **Domain** | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``xy``     | [0, 1]                | [0, 1]        |
    +------------+-----------------------+---------------+

    +------------+-----------------------+---------------+
    | **Range**  | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``xyY``    | [0, 1]                | [0, 1]        |
    +------------+-----------------------+---------------+

    -   This definition is a convenient object provided to implement support of
        illuminant argument *luminance* value in various :mod:`colour.models`
        package objects such as :func:`colour.Lab_to_XYZ` or
        :func:`colour.Luv_to_XYZ`.

    References
    ----------
    :cite:`Wikipedia2005`

    Examples
    --------
    >>> xy = np.array([0.54369557, 0.32107944])
    >>> xy_to_xyY(xy)  # doctest: +ELLIPSIS
    array([ 0.5436955...,  0.3210794...,  1.        ])
    >>> xy = np.array([0.54369557, 0.32107944, 1.00000000])
    >>> xy_to_xyY(xy)  # doctest: +ELLIPSIS
    array([ 0.5436955...,  0.3210794...,  1.        ])
    >>> xy = np.array([0.54369557, 0.32107944])
    >>> xy_to_xyY(xy, 100)  # doctest: +ELLIPSIS
    array([   0.5436955...,    0.3210794...,  100.        ])
    """

    xy = as_float_array(xy)
    Y = to_domain_1(Y)

    shape = xy.shape
    # Assuming ``xy`` is actually a *CIE xyY* colourspace array argument and
    # returning it directly.
    if shape[-1] == 3:
        return xy

    x, y = tsplit(xy)

    Y = full(x.shape, from_range_1(Y))
    xyY = tstack([x, y, Y])

    return xyY
Ejemplo n.º 22
0
def plot_hull_section_colours(
    hull: trimesh.Trimesh,  # type: ignore[name-defined]  # noqa
    model: Union[Literal["CAM02LCD", "CAM02SCD", "CAM02UCS", "CAM16LCD",
                         "CAM16SCD", "CAM16UCS", "CIE XYZ", "CIE xyY",
                         "CIE Lab", "CIE Luv", "CIE UCS", "CIE UVW", "DIN99",
                         "Hunter Lab", "Hunter Rdab", "ICaCb", "ICtCp", "IPT",
                         "IgPgTg", "Jzazbz", "OSA UCS", "Oklab", "hdr-CIELAB",
                         "hdr-IPT", ], str, ] = "CIE xyY",
    axis: Union[Literal["+z", "+x", "+y"], str] = "+z",
    origin: Floating = 0.5,
    normalise: Boolean = True,
    section_colours: Optional[Union[ArrayLike, str]] = None,
    section_opacity: Floating = 1,
    convert_kwargs: Optional[Dict] = None,
    samples: Integer = 256,
    **kwargs: Any,
) -> Tuple[plt.Figure, plt.Axes]:
    """
    Plot the section colours of given *trimesh* hull along given axis and
    origin.

    Parameters
    ----------
    hull
        *Trimesh* hull.
    model
        Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for
        the list of supported colourspace models.
    axis
        Axis the hull section will be normal to.
    origin
        Coordinate along ``axis`` at which to plot the hull section.
    normalise
        Whether to normalise ``axis`` to the extent of the hull along it.
    section_colours
        Colours of the hull section, if ``section_colours`` is set to *RGB*,
        the colours will be computed according to the corresponding
        coordinates.
    section_opacity
        Opacity of the hull section colours.
    convert_kwargs
        Keyword arguments for the :func:`colour.convert` definition.
    samples
        Samples count on one axis when computing the hull section colours.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> from colour.models import RGB_COLOURSPACE_sRGB
    >>> from colour.utilities import is_trimesh_installed
    >>> vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64)
    >>> XYZ_vertices = RGB_to_XYZ(
    ...     vertices['position'] + 0.5,
    ...     RGB_COLOURSPACE_sRGB.whitepoint,
    ...     RGB_COLOURSPACE_sRGB.whitepoint,
    ...     RGB_COLOURSPACE_sRGB.matrix_RGB_to_XYZ,
    ... )
    >>> if is_trimesh_installed:
    ...     import trimesh
    ...     hull = trimesh.Trimesh(XYZ_vertices, faces, process=False)
    ...     plot_hull_section_colours(hull, section_colours='RGB')
    ...     # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...AxesSubplot...>)

    .. image:: ../_static/Plotting_Plot_Hull_Section_Colours.png
        :align: center
        :alt: plot_hull_section_colours
    """

    axis = validate_method(
        axis,
        ["+z", "+x", "+y"],
        '"{0}" axis is invalid, it must be one of {1}!',
    )

    hull = hull.copy()

    settings: Dict[str, Any] = {"uniform": True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    section_colours = cast(
        ArrayLike,
        optional(section_colours,
                 HEX_to_RGB(CONSTANTS_COLOUR_STYLE.colour.average)),
    )

    convert_kwargs = optional(convert_kwargs, {})

    # Luminance / Lightness reordered along "z" axis.
    with suppress_warnings(python_warnings=True):
        ijk_vertices = colourspace_model_axis_reorder(
            convert(hull.vertices, "CIE XYZ", model, **convert_kwargs), model)
        ijk_vertices = np.nan_to_num(ijk_vertices)
        ijk_vertices *= COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[
            model]

    hull.vertices = ijk_vertices

    if axis == "+x":
        index_origin = 0
    elif axis == "+y":
        index_origin = 1
    elif axis == "+z":
        index_origin = 2
    plane = MAPPING_AXIS_TO_PLANE[axis]

    section = hull_section(hull, axis, origin, normalise)

    padding = 0.1 * np.mean(
        COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[model])
    min_x = np.min(ijk_vertices[..., plane[0]]) - padding
    max_x = np.max(ijk_vertices[..., plane[0]]) + padding
    min_y = np.min(ijk_vertices[..., plane[1]]) - padding
    max_y = np.max(ijk_vertices[..., plane[1]]) + padding
    extent = (min_x, max_x, min_y, max_y)

    use_RGB_section_colours = str(section_colours).upper() == "RGB"
    if use_RGB_section_colours:
        ii, jj = np.meshgrid(
            np.linspace(min_x, max_x, samples),
            np.linspace(max_y, min_y, samples),
        )
        ij = tstack([ii, jj])
        ijk_section = full((samples, samples, 3),
                           np.median(section[..., index_origin]))
        ijk_section[..., plane] = ij
        ijk_section /= COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[
            model]
        XYZ_section = convert(
            colourspace_model_axis_reorder(ijk_section, model, "Inverse"),
            model,
            "CIE XYZ",
            **convert_kwargs,
        )
        RGB_section = XYZ_to_plotting_colourspace(XYZ_section)
    else:
        section_colours = np.hstack([section_colours, section_opacity])

    facecolor = "none" if use_RGB_section_colours else section_colours
    polygon = Polygon(
        section[..., plane],
        facecolor=facecolor,
        edgecolor="none",
        zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
    )
    axes.add_patch(polygon)
    if use_RGB_section_colours:
        image = axes.imshow(
            np.clip(RGB_section, 0, 1),
            interpolation="bilinear",
            extent=extent,
            clip_path=None,
            alpha=section_opacity,
            zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
        )
        image.set_clip_path(polygon)

    settings = {
        "axes": axes,
        "bounding_box": extent,
    }
    settings.update(kwargs)

    return render(**settings)
Ejemplo n.º 23
0
def uv_to_Luv(
    uv: ArrayLike,
    illuminant: ArrayLike = CCS_ILLUMINANTS[
        "CIE 1931 2 Degree Standard Observer"]["D65"],
    Y: Floating = 1,
) -> NDArray:
    """
    Return the *CIE L\\*u\\*v\\** colourspace array from given :math:`uv^p`
    chromaticity coordinates by extending the array last dimension with given
    :math:`L` *Lightness*.

    Parameters
    ----------
    uv
        :math:`uv^p` chromaticity coordinates.
    illuminant
        Reference *illuminant* *CIE xy* chromaticity coordinates or *CIE xyY*
        colourspace array.
    Y
        Optional :math:`Y` *luminance* value used to construct the intermediate
        *CIE XYZ* colourspace array, the default :math:`Y` *luminance* value is
        1.

    Returns
    -------
    :class:`numpy.ndarray`
        *CIE L\\*u\\*v\\** colourspace array.

    Notes
    -----
    +----------------+-----------------------+-----------------+
    | **Range**      | **Scale - Reference** | **Scale - 1**   |
    +================+=======================+=================+
    | ``Luv``        | ``L`` : [0, 100]      | ``L`` : [0, 1]  |
    |                |                       |                 |
    |                | ``u`` : [-100, 100]   | ``u`` : [-1, 1] |
    |                |                       |                 |
    |                | ``v`` : [-100, 100]   | ``v`` : [-1, 1] |
    +----------------+-----------------------+-----------------+
    | ``illuminant`` | [0, 1]                | [0, 1]          |
    +----------------+-----------------------+-----------------+

    References
    ----------
    :cite:`CIETC1-482004j`

    Examples
    --------
    >>> import numpy as np
    >>> uv = np.array([0.37720213, 0.50120264])
    >>> uv_to_Luv(uv)  # doctest: +ELLIPSIS
    array([ 100.        ,  233.1837603...,   42.7474385...])
    """

    u, v = tsplit(uv)
    Y = as_float_scalar(to_domain_1(Y))

    X = 9 * u / (4 * v)
    Z = (-5 * Y * v - 3 * u / 4 + 3) / v

    XYZ = tstack([X, full(u.shape, Y), Z])

    return XYZ_to_Luv(from_range_1(XYZ), illuminant)
Ejemplo n.º 24
0
    def test_full(self):
        """
        Tests :func:`colour.utilities.array.full` definition.
        """

        np.testing.assert_equal(full(3, 0.5), np.full(3, 0.5))
Ejemplo n.º 25
0
def primitive_vertices_sphere(
    radius: Floating = 0.5,
    segments: Integer = 8,
    intermediate: Boolean = False,
    origin: ArrayLike = np.array([0, 0, 0]),
    axis: Union[Literal["+z", "+x", "+y", "yz", "xz", "xy"], str] = "+z",
) -> NDArray:
    """
    Return the vertices of a latitude-longitude sphere primitive.

    Parameters
    ----------
    radius
        Sphere radius.
    segments
        Latitude-longitude segments, if the ``intermediate`` argument is
        *True*, then the sphere will have one less segment along its longitude.
    intermediate
        Whether to generate the sphere vertices at the center of the faces
        outlined by the segments of a regular sphere generated without
        the ``intermediate`` argument set to *True*. The resulting sphere is
        inscribed on the regular sphere faces but possesses the same poles.
    origin
        Sphere origin on the construction plane.
    axis
        Axis (or normal of the plane) the poles of the sphere will be aligned
        with.

    Returns
    -------
    :class:`numpy.ndarray`
        Sphere primitive vertices.

    Notes
    -----
    -   The sphere poles have latitude segments count - 1 co-located vertices.

    Examples
    --------
    >>> primitive_vertices_sphere(segments=4)  # doctest: +ELLIPSIS
    array([[[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [ -3.5355339...e-01,  -4.3297802...e-17,   3.5355339...e-01],
            [ -5.0000000...e-01,  -6.1232340...e-17,   3.0616170...e-17],
            [ -3.5355339...e-01,  -4.3297802...e-17,  -3.5355339...e-01],
            [ -6.1232340...e-17,  -7.4987989...e-33,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  2.1648901...e-17,  -3.5355339...e-01,   3.5355339...e-01],
            [  3.0616170...e-17,  -5.0000000...e-01,   3.0616170...e-17],
            [  2.1648901...e-17,  -3.5355339...e-01,  -3.5355339...e-01],
            [  3.7493994...e-33,  -6.1232340...e-17,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  3.5355339...e-01,   0.0000000...e+00,   3.5355339...e-01],
            [  5.0000000...e-01,   0.0000000...e+00,   3.0616170...e-17],
            [  3.5355339...e-01,   0.0000000...e+00,  -3.5355339...e-01],
            [  6.1232340...e-17,   0.0000000...e+00,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  2.1648901...e-17,   3.5355339...e-01,   3.5355339...e-01],
            [  3.0616170...e-17,   5.0000000...e-01,   3.0616170...e-17],
            [  2.1648901...e-17,   3.5355339...e-01,  -3.5355339...e-01],
            [  3.7493994...e-33,   6.1232340...e-17,  -5.0000000...e-01]]])
    """

    axis = MAPPING_PLANE_TO_AXIS.get(axis, axis).lower()
    axis = validate_method(axis, ["+x", "+y", "+z"],
                           '"{0}" axis invalid, it must be one of {1}!')

    if not intermediate:
        theta = np.tile(
            np.radians(np.linspace(0, 180, segments + 1)),
            (int(segments) + 1, 1),
        )
        phi = np.transpose(
            np.tile(
                np.radians(np.linspace(-180, 180, segments + 1)),
                (int(segments) + 1, 1),
            ))
    else:
        theta = np.tile(
            np.radians(np.linspace(0, 180, segments * 2 + 1)[1::2][1:-1]),
            (int(segments) + 1, 1),
        )
        theta = np.hstack([
            zeros((segments + 1, 1)),
            theta,
            full((segments + 1, 1), np.pi),
        ])
        phi = np.transpose(
            np.tile(
                np.radians(np.linspace(-180, 180, segments + 1)) +
                np.radians(360 / segments / 2),
                (int(segments), 1),
            ))

    rho = ones(phi.shape) * radius
    rho_theta_phi = tstack([rho, theta, phi])

    vertices = spherical_to_cartesian(rho_theta_phi)

    # Removing extra longitude vertices.
    vertices = vertices[:-1, :, :]

    if axis == "+z":
        pass
    elif axis == "+y":
        vertices = np.roll(vertices, 2, -1)
    elif axis == "+x":
        vertices = np.roll(vertices, 1, -1)

    vertices += origin

    return vertices