Source code for colour.plotting.volume

"""
Colour Models Volume Plotting
=============================

Defines the colour models volume and gamut plotting objects:

-   :func:`colour.plotting.plot_RGB_colourspaces_gamuts`
-   :func:`colour.plotting.plot_RGB_scatter`
"""

from __future__ import annotations

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.figure import Figure
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from mpl_toolkits.mplot3d.axes3d import Axes3D

from colour.colorimetry import MultiSpectralDistributions
from colour.constants import EPSILON
from colour.geometry import (
    primitive_vertices_cube_mpl,
    primitive_vertices_grid_mpl,
)
from colour.graph import convert
from colour.hints import (
    Any,
    ArrayLike,
    List,
    Literal,
    LiteralColourspaceModel,
    LiteralRGBColourspace,
    NDArrayFloat,
    Sequence,
    Tuple,
    cast,
)
from colour.models import RGB_Colourspace, RGB_to_XYZ
from colour.models.common import COLOURSPACE_MODELS_AXIS_LABELS
from colour.plotting import (
    CONSTANTS_COLOUR_STYLE,
    colourspace_model_axis_reorder,
    filter_cmfs,
    filter_RGB_colourspaces,
    override_style,
    render,
)
from colour.utilities import (
    Structure,
    as_float_array,
    as_int_array,
    as_int_scalar,
    first_item,
    full,
    is_integer,
    ones,
    optional,
    zeros,
)
from colour.utilities.deprecation import handle_arguments_deprecation

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "nadir_grid",
    "RGB_identity_cube",
    "plot_RGB_colourspaces_gamuts",
    "plot_RGB_scatter",
]


def nadir_grid(
    limits: ArrayLike | None = None,
    segments: int = 10,
    labels: ArrayLike | Sequence[str] | None = None,
    axes: Axes3D | None = None,
    **kwargs: Any,
) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]:
    """
    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(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 = 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 = (  # noqa: PLW2901
                    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 = as_float_array(np.vstack([quads_g, quads_gs, quad_x, quad_y]))
    RGB_f = as_float_array(np.vstack([RGB_gf, RGB_gsf, RGB_x, RGB_y]))
    RGB_e = as_float_array(np.vstack([RGB_ge, RGB_gse, RGB_x, RGB_y]))

    return quads, RGB_f, RGB_e


def RGB_identity_cube(
    width_segments: int = 16,
    height_segments: int = 16,
    depth_segments: int = 16,
    planes: (
        Literal[
            "-x",
            "+x",
            "-y",
            "+y",
            "-z",
            "+z",
            "xy",
            "xz",
            "yz",
            "yx",
            "zx",
            "zy",
        ]
        | None
    ) = None,
) -> Tuple[NDArrayFloat, NDArrayFloat]:
    """
    Return an *RGB* identity cube made of quad geometric elements and its
    associated *RGB* colours.

    Parameters
    ----------
    width_segments
        Cube segments, quad counts along the width.
    height_segments
        Cube segments, quad counts along the height.
    depth_segments
        Cube segments, quad counts along the depth.
    planes
        Grid primitives to include in the cube construction.

    Returns
    -------
    :class:`tuple`
        Cube quads, *RGB* colours.

    Examples
    --------
    >>> vertices, RGB = RGB_identity_cube(1, 1, 1)
    >>> vertices
    array([[[ 0.,  0.,  0.],
            [ 1.,  0.,  0.],
            [ 1.,  1.,  0.],
            [ 0.,  1.,  0.]],
    <BLANKLINE>
           [[ 0.,  0.,  1.],
            [ 1.,  0.,  1.],
            [ 1.,  1.,  1.],
            [ 0.,  1.,  1.]],
    <BLANKLINE>
           [[ 0.,  0.,  0.],
            [ 1.,  0.,  0.],
            [ 1.,  0.,  1.],
            [ 0.,  0.,  1.]],
    <BLANKLINE>
           [[ 0.,  1.,  0.],
            [ 1.,  1.,  0.],
            [ 1.,  1.,  1.],
            [ 0.,  1.,  1.]],
    <BLANKLINE>
           [[ 0.,  0.,  0.],
            [ 0.,  1.,  0.],
            [ 0.,  1.,  1.],
            [ 0.,  0.,  1.]],
    <BLANKLINE>
           [[ 1.,  0.,  0.],
            [ 1.,  1.,  0.],
            [ 1.,  1.,  1.],
            [ 1.,  0.,  1.]]])
    >>> RGB
    array([[ 0.5,  0.5,  0. ],
           [ 0.5,  0.5,  1. ],
           [ 0.5,  0. ,  0.5],
           [ 0.5,  1. ,  0.5],
           [ 0. ,  0.5,  0.5],
           [ 1. ,  0.5,  0.5]])
    """

    quads = primitive_vertices_cube_mpl(
        width=1,
        height=1,
        depth=1,
        width_segments=width_segments,
        height_segments=height_segments,
        depth_segments=depth_segments,
        planes=planes,
    )
    RGB = np.average(quads, axis=-2)

    return quads, RGB


[docs] @override_style() def plot_RGB_colourspaces_gamuts( colourspaces: ( RGB_Colourspace | LiteralRGBColourspace | str | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] ), model: LiteralColourspaceModel | str = "CIE xyY", segments: int = 8, show_grid: bool = True, grid_segments: int = 10, show_spectral_locus: bool = False, spectral_locus_colour: ArrayLike | str | None = None, cmfs: ( MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str] ) = "CIE 1931 2 Degree Standard Observer", chromatically_adapt: bool = False, convert_kwargs: dict | None = None, **kwargs: Any, ) -> Tuple[Figure, Axes3D]: """ Plot given *RGB* colourspaces gamuts in given reference colourspace. Parameters ---------- colourspaces *RGB* colourspaces to plot the gamuts of. ``colourspaces`` elements can be of any type or form supported by the :func:`colour.plotting.common.filter_RGB_colourspaces` definition. model Colourspace model, 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.common.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>, <...Axes3D...>) .. image:: ../_static/Plotting_Plot_RGB_Colourspaces_Gamuts.png :align: center :alt: plot_RGB_colourspaces_gamuts """ model = handle_arguments_deprecation( { "ArgumentRenamed": [["reference_colourspace", "model"]], }, **kwargs, ).get("model", model) colourspaces = cast( List[RGB_Colourspace], list(filter_RGB_colourspaces(colourspaces).values()), ) # pyright: ignore convert_kwargs = optional(convert_kwargs, {}) count_c = len(colourspaces) title = f"{', '.join([colourspace.name for colourspace in colourspaces])} - {model}" 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 = cast(Axes3D, 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", model, **convert_settings), model, ) 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( # noqa: PLW2901 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) # Preventing singularities for colour models such as "CIE xyY", XYZ[XYZ == 0] = EPSILON convert_settings = {"illuminant": colourspace.whitepoint} convert_settings.update(convert_kwargs) quads_c.extend( colourspace_model_axis_reorder( convert(XYZ, "CIE XYZ", model, **convert_settings), model, ) ) 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[model])[ as_int_array(colourspace_model_axis_reorder([0, 1, 2], model)) ] 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) # pyright: ignore collection.set_edgecolors(RGB_e) # pyright: ignore axes.add_collection3d(collection) settings.update({"axes": axes, "axes_visible": False, "camera_aspect": "equal"}) settings.update(kwargs) return cast(Tuple[Figure, Axes3D], render(**settings))
[docs] @override_style() def plot_RGB_scatter( RGB: ArrayLike, colourspace: ( RGB_Colourspace | str | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] ) = "sRGB", model: LiteralColourspaceModel | str = "CIE xyY", colourspaces: ( RGB_Colourspace | str | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] | None ) = None, segments: int = 8, show_grid: bool = True, grid_segments: int = 10, show_spectral_locus: bool = False, spectral_locus_colour: ArrayLike | str | None = None, points_size: float = 12, cmfs: ( MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str] ) = "CIE 1931 2 Degree Standard Observer", chromatically_adapt: bool = False, convert_kwargs: dict | None = None, **kwargs: Any, ) -> Tuple[Figure, Axes3D]: """ Plot given *RGB* colourspace array in a scatter plot. Parameters ---------- RGB *RGB* colourspace array. colourspace *RGB* colourspace of the *RGB* array. ``colourspace`` can be of any type or form supported by the :func:`colour.plotting.common.filter_RGB_colourspaces` definition. model Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. colourspaces *RGB* colourspaces to plot the gamuts of. ``colourspaces`` elements can be of any type or form supported by the :func:`colour.plotting.common.filter_RGB_colourspaces` definition. 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. points_size Scatter points size. 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.common.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 ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_RGB_colourspaces_gamuts`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> RGB = np.random.random((128, 128, 3)) >>> plot_RGB_scatter(RGB, "ITU-R BT.709") # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...Axes3D...>) .. image:: ../_static/Plotting_Plot_RGB_Scatter.png :align: center :alt: plot_RGB_scatter """ RGB = np.reshape(as_float_array(RGB)[..., :3], (-1, 3)) colourspace = cast( RGB_Colourspace, first_item(filter_RGB_colourspaces(colourspace).values()), ) colourspaces = cast(List[str], optional(colourspaces, [colourspace.name])) convert_kwargs = optional(convert_kwargs, {}) count_c = len(colourspaces) settings = Structure( **{ "face_colours": [None] * count_c, "edge_colours": [(0.25, 0.25, 0.25)] * count_c, "face_alpha": [0.0] * count_c, "edge_alpha": [0.1] * count_c, } ) settings.update(kwargs) settings["show"] = False plot_RGB_colourspaces_gamuts( colourspaces=colourspaces, model=model, segments=segments, show_grid=show_grid, grid_segments=grid_segments, show_spectral_locus=show_spectral_locus, spectral_locus_colour=spectral_locus_colour, cmfs=cmfs, chromatically_adapt=chromatically_adapt, **settings, ) XYZ = RGB_to_XYZ(RGB, colourspace) convert_settings = {"illuminant": colourspace.whitepoint} convert_settings.update(convert_kwargs) points = colourspace_model_axis_reorder( convert(XYZ, "CIE XYZ", model, **convert_settings), model, ) axes = plt.gca() axes.scatter( points[..., 0], points[..., 1], points[..., 2], c=np.reshape(RGB, (-1, 3)), s=points_size, # pyright: ignore zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_scatter, ) settings.update({"axes": axes, "show": True}) settings.update(kwargs) return cast(Tuple[Figure, Axes3D], render(**settings))