Source code for colour.plotting.section

"""
Gamut Section Plotting
======================

Defines the gamut section plotting objects:

-   :func:`colour.plotting.section.plot_hull_section_colours`
-   :func:`colour.plotting.section.plot_hull_section_contour`
-   :func:`colour.plotting.plot_visible_spectrum_section`
-   :func:`colour.plotting.plot_RGB_colourspace_section`
"""

from __future__ import annotations

import numpy as np
from matplotlib.axes import Axes
from matplotlib.collections import LineCollection
from matplotlib.figure import Figure
from matplotlib.patches import Polygon

from colour.colorimetry import (
    MultiSpectralDistributions,
    SpectralDistribution,
    SpectralShape,
    reshape_msds,
)
from colour.geometry import hull_section, primitive_cube
from colour.graph import convert
from colour.hints import (
    Any,
    ArrayLike,
    Dict,
    Literal,
    LiteralColourspaceModel,
    LiteralRGBColourspace,
    Real,
    Sequence,
    Tuple,
    cast,
)
from colour.models import (
    COLOURSPACE_MODELS_AXIS_LABELS,
    COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE,
    RGB_Colourspace,
    RGB_to_XYZ,
)
from colour.notation import HEX_to_RGB
from colour.plotting import (
    CONSTANTS_COLOUR_STYLE,
    XYZ_to_plotting_colourspace,
    artist,
    colourspace_model_axis_reorder,
    filter_cmfs,
    filter_illuminants,
    filter_RGB_colourspaces,
    override_style,
    render,
)
from colour.utilities import (
    CanonicalMapping,
    as_int_array,
    first_item,
    full,
    optional,
    required,
    suppress_warnings,
    tstack,
    validate_method,
)
from colour.volume import solid_RoschMacAdam

__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__ = [
    "MAPPING_AXIS_TO_PLANE",
    "plot_hull_section_colours",
    "plot_hull_section_contour",
    "plot_visible_spectrum_section",
    "plot_RGB_colourspace_section",
]

MAPPING_AXIS_TO_PLANE: CanonicalMapping = CanonicalMapping(
    {"+x": (1, 2), "+y": (0, 2), "+z": (0, 1)}
)
MAPPING_AXIS_TO_PLANE.__doc__ = """Axis to plane mapping."""


[docs] @required("trimesh") @override_style() def plot_hull_section_colours( hull: trimesh.Trimesh, # pyright: ignore # noqa: F821 model: LiteralColourspaceModel | str = "CIE xyY", axis: Literal["+z", "+x", "+y"] | str = "+z", origin: float = 0.5, normalise: bool = True, section_colours: ArrayLike | str | None = None, section_opacity: float = 1, convert_kwargs: dict | None = None, samples: int = 256, **kwargs: Any, ) -> Tuple[Figure, 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 Sample 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) >>> if is_trimesh_installed: ... from trimesh import Trimesh ... ... hull = Trimesh(XYZ_vertices, faces, process=False) ... plot_hull_section_colours(hull, section_colours="RGB") ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...Axes...>) .. 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 = 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), cast(Real, 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)
[docs] @required("trimesh") @override_style() def plot_hull_section_contour( hull: trimesh.Trimesh, # pyright: ignore # noqa: F821 model: LiteralColourspaceModel | str = "CIE xyY", axis: Literal["+z", "+x", "+y"] | str = "+z", origin: float = 0.5, normalise: bool = True, contour_colours: ArrayLike | str | None = None, contour_opacity: float = 1, convert_kwargs: dict | None = None, **kwargs: Any, ) -> Tuple[Figure, Axes]: """ Plot the section contour 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. contour_colours Colours of the hull section contour, if ``contour_colours`` is set to *RGB*, the colours will be computed according to the corresponding coordinates. contour_opacity Opacity of the hull section contour. convert_kwargs Keyword arguments for the :func:`colour.convert` definition. 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) >>> if is_trimesh_installed: ... from trimesh import Trimesh ... ... hull = Trimesh(XYZ_vertices, faces, process=False) ... plot_hull_section_contour(hull, contour_colours="RGB") ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...Axes...>) .. image:: ../_static/Plotting_Plot_Hull_Section_Contour.png :align: center :alt: plot_hull_section_contour """ hull = hull.copy() contour_colours = optional(contour_colours, CONSTANTS_COLOUR_STYLE.colour.dark) settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) convert_kwargs = optional(convert_kwargs, {}) # Luminance / Lightness is re-ordered along "z-up" 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 plane = MAPPING_AXIS_TO_PLANE[axis] 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_contour_colours = str(contour_colours).upper() == "RGB" section = hull_section(hull, axis, origin, normalise) if use_RGB_contour_colours: ijk_section = ( 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, ) contour_colours = np.clip(XYZ_to_plotting_colourspace(XYZ_section), 0, 1) section = np.reshape(section[..., plane], (-1, 1, 2)) line_collection = LineCollection( np.concatenate([section[:-1], section[1:]], axis=1), # pyright: ignore colors=contour_colours, alpha=contour_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line, ) axes.add_collection(line_collection) settings = { "axes": axes, "bounding_box": extent, } settings.update(kwargs) return render(**settings)
[docs] @required("trimesh") @override_style() def plot_visible_spectrum_section( cmfs: ( MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str] ) = "CIE 1931 2 Degree Standard Observer", illuminant: SpectralDistribution | str = "D65", model: LiteralColourspaceModel | str = "CIE xyY", axis: Literal["+z", "+x", "+y"] | str = "+z", origin: float = 0.5, normalise: bool = True, show_section_colours: bool = True, show_section_contour: bool = True, **kwargs: Any, ) -> Tuple[Figure, Axes]: """ Plot the visible spectrum volume, i.e. *Rösch-MacAdam* colour solid, section colours along given axis and origin. Parameters ---------- cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.common.filter_cmfs` definition. illuminant Illuminant spectral distribution, default to *CIE Illuminant D65*. ``illuminant`` can be of any type or form supported by the :func:`colour.plotting.common.filter_illuminants` definition. 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. show_section_colours Whether to show the hull section colours. show_section_contour Whether to show the hull section contour. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`, :func:`colour.plotting.section.plot_hull_section_colours` :func:`colour.plotting.section.plot_hull_section_contour`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour.utilities import is_trimesh_installed >>> if is_trimesh_installed: ... plot_visible_spectrum_section(section_colours="RGB", section_opacity=0.15) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...Axes...>) .. image:: ../_static/Plotting_Plot_Visible_Spectrum_Section.png :align: center :alt: plot_visible_spectrum_section """ import trimesh.convex from trimesh import Trimesh settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) cmfs = cast( MultiSpectralDistributions, reshape_msds( first_item(filter_cmfs(cmfs).values()), SpectralShape(360, 780, 1), copy=False, ), ) illuminant = cast( SpectralDistribution, first_item(filter_illuminants(illuminant).values()), ) vertices = solid_RoschMacAdam( cmfs, illuminant, point_order="Pulse Wave Width", filter_jagged_points=True, ) mesh = Trimesh(vertices) hull = trimesh.convex.convex_hull(mesh) if show_section_colours: settings = {"axes": axes} settings.update(kwargs) settings["show"] = False plot_hull_section_colours(hull, model, axis, origin, normalise, **settings) if show_section_contour: settings = {"axes": axes} settings.update(kwargs) settings["show"] = False plot_hull_section_contour(hull, model, axis, origin, normalise, **settings) title = ( f"Visible Spectrum Section - " f"{f'{origin * 100}%' if normalise else origin} - " f"{model} - " f"{cmfs.display_name}" ) plane = MAPPING_AXIS_TO_PLANE[axis] labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[ as_int_array(colourspace_model_axis_reorder([0, 1, 2], model)) ] x_label, y_label = labels[plane[0]], labels[plane[1]] settings.update( { "axes": axes, "show": True, "title": title, "x_label": x_label, "y_label": y_label, } ) settings.update(kwargs) return render(**settings)
[docs] @required("trimesh") @override_style() def plot_RGB_colourspace_section( colourspace: ( RGB_Colourspace | LiteralRGBColourspace | str | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] ), model: LiteralColourspaceModel | str = "CIE xyY", axis: Literal["+z", "+x", "+y"] | str = "+z", origin: float = 0.5, normalise: bool = True, size: float = 1.0, show_section_colours: bool = True, show_section_contour: bool = True, segments: int = 64, **kwargs: Any, ) -> Tuple[Figure, Axes]: """ Plot given *RGB* colourspace section colours along given axis and origin. Parameters ---------- 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. 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. size: Size of the underlying *RGB* colourspace cube; used for plotting HDR related sections. show_section_colours Whether to show the hull section colours. show_section_contour Whether to show the hull section contour. segments Edge segments count for the *RGB* colourspace cube. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`, :func:`colour.plotting.section.plot_hull_section_colours` :func:`colour.plotting.section.plot_hull_section_contour`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour.utilities import is_trimesh_installed >>> if is_trimesh_installed: ... plot_RGB_colourspace_section( ... "sRGB", section_colours="RGB", section_opacity=0.15 ... ) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...Axes...>) .. image:: ../_static/Plotting_Plot_RGB_Colourspace_Section.png :align: center :alt: plot_RGB_colourspace_section """ from trimesh import Trimesh settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) colourspace = cast( RGB_Colourspace, first_item(filter_RGB_colourspaces(colourspace).values()), ) vertices, faces, _outline = primitive_cube(1, 1, 1, segments, segments, segments) XYZ_vertices = RGB_to_XYZ((vertices["position"] + 0.5) * size, colourspace) hull = Trimesh(XYZ_vertices, faces, process=False) if show_section_colours: settings = {"axes": axes} settings.update(kwargs) settings["show"] = False plot_hull_section_colours(hull, model, axis, origin, normalise, **settings) if show_section_contour: settings = {"axes": axes} settings.update(kwargs) settings["show"] = False plot_hull_section_contour(hull, model, axis, origin, normalise, **settings) title = ( f"{colourspace.name} Section - " f"{f'{origin * 100}%' if normalise else origin} - " f"{model}" ) plane = MAPPING_AXIS_TO_PLANE[axis] labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[ as_int_array(colourspace_model_axis_reorder([0, 1, 2], model)) ] x_label, y_label = labels[plane[0]], labels[plane[1]] settings.update( { "axes": axes, "show": True, "title": title, "x_label": x_label, "y_label": y_label, } ) settings.update(kwargs) return render(**settings)