"""
Gamut Section Plotting
======================
Define 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)