Coverage for colour/plotting/section.py: 93%
162 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2Gamut Section Plotting
3======================
5Define the gamut section plotting objects.
7- :func:`colour.plotting.section.plot_hull_section_colours`
8- :func:`colour.plotting.section.plot_hull_section_contour`
9- :func:`colour.plotting.plot_visible_spectrum_section`
10- :func:`colour.plotting.plot_RGB_colourspace_section`
11"""
13from __future__ import annotations
15import typing
17import numpy as np
19if typing.TYPE_CHECKING:
20 from matplotlib.axes import Axes
22from matplotlib.collections import LineCollection
24if typing.TYPE_CHECKING:
25 from matplotlib.figure import Figure
27from matplotlib.patches import Polygon
29from colour.colorimetry import (
30 MultiSpectralDistributions,
31 SpectralDistribution,
32 SpectralShape,
33 reshape_msds,
34)
35from colour.geometry import hull_section, primitive_cube
36from colour.graph import colourspace_model_to_reference, convert
38if typing.TYPE_CHECKING:
39 from colour.hints import (
40 Any,
41 ArrayLike,
42 Dict,
43 Literal,
44 LiteralColourspaceModel,
45 LiteralRGBColourspace,
46 Sequence,
47 Tuple,
48 )
50from colour.hints import Real, cast
51from colour.models import (
52 COLOURSPACE_MODELS_AXIS_LABELS,
53 RGB_Colourspace,
54 RGB_to_XYZ,
55)
56from colour.notation import HEX_to_RGB
57from colour.plotting import (
58 CONSTANTS_COLOUR_STYLE,
59 XYZ_to_plotting_colourspace,
60 artist,
61 colourspace_model_axis_reorder,
62 filter_cmfs,
63 filter_illuminants,
64 filter_RGB_colourspaces,
65 override_style,
66 render,
67)
68from colour.utilities import (
69 CanonicalMapping,
70 as_int_array,
71 first_item,
72 full,
73 ones,
74 optional,
75 required,
76 suppress_warnings,
77 tstack,
78 validate_method,
79)
80from colour.volume import solid_RoschMacAdam
82__author__ = "Colour Developers"
83__copyright__ = "Copyright 2013 Colour Developers"
84__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
85__maintainer__ = "Colour Developers"
86__email__ = "colour-developers@colour-science.org"
87__status__ = "Production"
89__all__ = [
90 "MAPPING_AXIS_TO_PLANE",
91 "plot_hull_section_colours",
92 "plot_hull_section_contour",
93 "plot_visible_spectrum_section",
94 "plot_RGB_colourspace_section",
95]
97MAPPING_AXIS_TO_PLANE: CanonicalMapping = CanonicalMapping(
98 {"+x": (1, 2), "+y": (0, 2), "+z": (0, 1)}
99)
100MAPPING_AXIS_TO_PLANE.__doc__ = """
101Mapping from axes to their orthogonal planes.
103Maps each positive axis ('+x', '+y', '+z') to the indices of the two
104dimensions that form the perpendicular plane in 3D space.
105"""
108@required("trimesh")
109@override_style()
110def plot_hull_section_colours(
111 hull: trimesh.Trimesh, # pyright: ignore # noqa: F821
112 model: LiteralColourspaceModel | str = "CIE xyY",
113 axis: Literal["+z", "+x", "+y"] | str = "+z",
114 origin: float = 0.5,
115 normalise: bool = True,
116 section_colours: ArrayLike | str | None = None,
117 section_opacity: float = 1,
118 convert_kwargs: dict | None = None,
119 samples: int = 256,
120 **kwargs: Any,
121) -> Tuple[Figure, Axes]:
122 """
123 Plot the section colours of the specified *trimesh* hull along the
124 specified axis and origin.
126 Parameters
127 ----------
128 hull
129 *Trimesh* hull.
130 model
131 Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute
132 for the list of supported colourspace models.
133 axis
134 Axis the hull section will be normal to.
135 origin
136 Coordinate along ``axis`` at which to plot the hull section.
137 normalise
138 Whether to normalise ``axis`` to the extent of the hull along it.
139 section_colours
140 Colours of the hull section, if ``section_colours`` is set to *RGB*,
141 the colours will be computed using the corresponding coordinates.
142 section_opacity
143 Opacity of the hull section colours.
144 convert_kwargs
145 Keyword arguments for the :func:`colour.convert` definition.
146 samples
147 Sample count on one axis when computing the hull section colours.
149 Other Parameters
150 ----------------
151 kwargs
152 {:func:`colour.plotting.artist`,
153 :func:`colour.plotting.render`},
154 See the documentation of the previously listed definitions.
156 Returns
157 -------
158 :class:`tuple`
159 Current figure and axes.
161 Examples
162 --------
163 >>> from colour.models import RGB_COLOURSPACE_sRGB
164 >>> from colour.utilities import is_trimesh_installed
165 >>> vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64)
166 >>> XYZ_vertices = RGB_to_XYZ(vertices["position"] + 0.5, RGB_COLOURSPACE_sRGB)
167 >>> if is_trimesh_installed:
168 ... from trimesh import Trimesh
169 ...
170 ... hull = Trimesh(XYZ_vertices, faces, process=False)
171 ... plot_hull_section_colours(hull, section_colours="RGB")
172 ... # doctest: +ELLIPSIS
173 (<Figure size ... with 1 Axes>, <...Axes...>)
175 .. image:: ../_static/Plotting_Plot_Hull_Section_Colours.png
176 :align: center
177 :alt: plot_hull_section_colours
178 """
180 axis = validate_method(
181 axis,
182 ("+z", "+x", "+y"),
183 '"{0}" axis is invalid, it must be one of {1}!',
184 )
186 hull = hull.copy()
188 settings: Dict[str, Any] = {"uniform": True}
189 settings.update(kwargs)
191 _figure, axes = artist(**settings)
193 section_colours = optional(
194 section_colours, HEX_to_RGB(CONSTANTS_COLOUR_STYLE.colour.average)
195 )
197 convert_kwargs = optional(convert_kwargs, {})
199 # Luminance / Lightness reordered along "z" axis.
200 with suppress_warnings(python_warnings=True):
201 ijk_vertices = colourspace_model_axis_reorder(
202 convert(hull.vertices, "CIE XYZ", model, **convert_kwargs), model
203 )
204 ijk_vertices = np.nan_to_num(ijk_vertices)
205 ijk_vertices = colourspace_model_to_reference(ijk_vertices, model)
207 hull.vertices = ijk_vertices
209 if axis == "+x":
210 index_origin = 0
211 elif axis == "+y":
212 index_origin = 1
213 elif axis == "+z":
214 index_origin = 2
215 plane = MAPPING_AXIS_TO_PLANE[axis]
217 section = hull_section(hull, axis, origin, normalise)
219 padding = 0.1 * np.mean(colourspace_model_to_reference(ones(3), model))
220 min_x = np.min(ijk_vertices[..., plane[0]]) - padding
221 max_x = np.max(ijk_vertices[..., plane[0]]) + padding
222 min_y = np.min(ijk_vertices[..., plane[1]]) - padding
223 max_y = np.max(ijk_vertices[..., plane[1]]) + padding
224 extent = (min_x, max_x, min_y, max_y)
226 use_RGB_section_colours = str(section_colours).upper() == "RGB"
227 if use_RGB_section_colours:
228 ii, jj = np.meshgrid(
229 np.linspace(min_x, max_x, samples),
230 np.linspace(max_y, min_y, samples),
231 )
232 ij = tstack([ii, jj])
233 ijk_section = full(
234 (samples, samples, 3),
235 cast("Real", np.median(section[..., index_origin])),
236 )
237 ijk_section[..., plane] = ij
238 ijk_section = ijk_section / colourspace_model_to_reference(ones(3), model)
239 XYZ_section = convert(
240 colourspace_model_axis_reorder(ijk_section, model, "Inverse"),
241 model,
242 "CIE XYZ",
243 **convert_kwargs,
244 )
245 RGB_section = XYZ_to_plotting_colourspace(XYZ_section)
246 else:
247 section_colours = np.hstack([section_colours, section_opacity])
249 facecolor = "none" if use_RGB_section_colours else section_colours
250 polygon = Polygon(
251 section[..., plane],
252 facecolor=facecolor,
253 edgecolor="none",
254 zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
255 )
256 axes.add_patch(polygon)
258 if use_RGB_section_colours:
259 image = axes.imshow(
260 np.clip(RGB_section, 0, 1),
261 interpolation="bilinear",
262 extent=extent, # type: ignore
263 clip_path=None,
264 alpha=section_opacity,
265 zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
266 )
267 image.set_clip_path(polygon)
269 settings = {
270 "axes": axes,
271 "bounding_box": extent,
272 }
273 settings.update(kwargs)
275 return render(**settings)
278@required("trimesh")
279@override_style()
280def plot_hull_section_contour(
281 hull: trimesh.Trimesh, # pyright: ignore # noqa: F821
282 model: LiteralColourspaceModel | str = "CIE xyY",
283 axis: Literal["+z", "+x", "+y"] | str = "+z",
284 origin: float = 0.5,
285 normalise: bool = True,
286 contour_colours: ArrayLike | str | None = None,
287 contour_opacity: float = 1,
288 convert_kwargs: dict | None = None,
289 **kwargs: Any,
290) -> Tuple[Figure, Axes]:
291 """
292 Plot the section contour of the specified *trimesh* hull along the
293 specified axis and origin.
295 Parameters
296 ----------
297 hull
298 *Trimesh* hull.
299 model
300 Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute
301 for the list of supported colourspace models.
302 axis
303 Axis the hull section will be normal to.
304 origin
305 Coordinate along ``axis`` at which to plot the hull section.
306 normalise
307 Whether to normalise ``axis`` to the extent of the hull along it.
308 contour_colours
309 Colours of the hull section contour, if ``contour_colours`` is set
310 to *RGB*, the colours will be computed using the corresponding
311 coordinates.
312 contour_opacity
313 Opacity of the hull section contour.
314 convert_kwargs
315 Keyword arguments for the :func:`colour.convert` definition.
317 Other Parameters
318 ----------------
319 kwargs
320 {:func:`colour.plotting.artist`,
321 :func:`colour.plotting.render`},
322 See the documentation of the previously listed definitions.
324 Returns
325 -------
326 :class:`tuple`
327 Current figure and axes.
329 Examples
330 --------
331 >>> from colour.models import RGB_COLOURSPACE_sRGB
332 >>> from colour.utilities import is_trimesh_installed
333 >>> vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64)
334 >>> XYZ_vertices = RGB_to_XYZ(vertices["position"] + 0.5, RGB_COLOURSPACE_sRGB)
335 >>> if is_trimesh_installed:
336 ... from trimesh import Trimesh
337 ...
338 ... hull = Trimesh(XYZ_vertices, faces, process=False)
339 ... plot_hull_section_contour(hull, contour_colours="RGB")
340 ... # doctest: +ELLIPSIS
341 (<Figure size ... with 1 Axes>, <...Axes...>)
343 .. image:: ../_static/Plotting_Plot_Hull_Section_Contour.png
344 :align: center
345 :alt: plot_hull_section_contour
346 """
348 hull = hull.copy()
350 contour_colours = optional(contour_colours, CONSTANTS_COLOUR_STYLE.colour.dark)
352 settings: Dict[str, Any] = {"uniform": True}
353 settings.update(kwargs)
355 _figure, axes = artist(**settings)
357 convert_kwargs = optional(convert_kwargs, {})
359 # Luminance / Lightness is re-ordered along "z-up" axis.
360 with suppress_warnings(python_warnings=True):
361 ijk_vertices = colourspace_model_axis_reorder(
362 convert(hull.vertices, "CIE XYZ", model, **convert_kwargs), model
363 )
364 ijk_vertices = np.nan_to_num(ijk_vertices)
365 ijk_vertices = colourspace_model_to_reference(ijk_vertices, model)
367 hull.vertices = ijk_vertices
369 plane = MAPPING_AXIS_TO_PLANE[axis]
371 padding = 0.1 * np.mean(colourspace_model_to_reference(np.ones(3), model))
372 min_x = np.min(ijk_vertices[..., plane[0]]) - padding
373 max_x = np.max(ijk_vertices[..., plane[0]]) + padding
374 min_y = np.min(ijk_vertices[..., plane[1]]) - padding
375 max_y = np.max(ijk_vertices[..., plane[1]]) + padding
376 extent = (min_x, max_x, min_y, max_y)
378 use_RGB_contour_colours = str(contour_colours).upper() == "RGB"
379 section = hull_section(hull, axis, origin, normalise)
380 if use_RGB_contour_colours:
381 ijk_section = section / colourspace_model_to_reference(np.ones(3), model)
382 XYZ_section = convert(
383 colourspace_model_axis_reorder(ijk_section, model, "Inverse"),
384 model,
385 "CIE XYZ",
386 **convert_kwargs,
387 )
388 contour_colours = np.clip(XYZ_to_plotting_colourspace(XYZ_section), 0, 1)
390 section = np.reshape(section[..., plane], (-1, 1, 2))
391 line_collection = LineCollection(
392 np.concatenate([section[:-1], section[1:]], axis=1), # pyright: ignore
393 colors=contour_colours,
394 alpha=contour_opacity,
395 zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line,
396 )
397 axes.add_collection(line_collection)
399 settings = {
400 "axes": axes,
401 "bounding_box": extent,
402 }
403 settings.update(kwargs)
405 return render(**settings)
408@required("trimesh")
409@override_style()
410def plot_visible_spectrum_section(
411 cmfs: (
412 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
413 ) = "CIE 1931 2 Degree Standard Observer",
414 illuminant: SpectralDistribution | str = "D65",
415 model: LiteralColourspaceModel | str = "CIE xyY",
416 axis: Literal["+z", "+x", "+y"] | str = "+z",
417 origin: float = 0.5,
418 normalise: bool = True,
419 show_section_colours: bool = True,
420 show_section_contour: bool = True,
421 **kwargs: Any,
422) -> Tuple[Figure, Axes]:
423 """
424 Plot the visible spectrum volume section colours along the specified axis
425 and origin.
427 The visible spectrum volume represents the *Rösch-MacAdam* colour solid.
429 Parameters
430 ----------
431 cmfs
432 Standard observer colour matching functions, default to the
433 *CIE 1931 2 Degree Standard Observer*. ``cmfs`` can be of any type
434 or form supported by the :func:`colour.plotting.common.filter_cmfs`
435 definition.
436 illuminant
437 Illuminant spectral distribution, default to *CIE Illuminant D65*.
438 ``illuminant`` can be of any type or form supported by the
439 :func:`colour.plotting.common.filter_illuminants` definition.
440 model
441 Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute
442 for the list of supported colourspace models.
443 axis
444 Axis the hull section will be normal to.
445 origin
446 Coordinate along ``axis`` at which to plot the hull section.
447 normalise
448 Whether to normalise ``axis`` to the extent of the hull along it.
449 show_section_colours
450 Whether to show the hull section colours.
451 show_section_contour
452 Whether to show the hull section contour.
454 Other Parameters
455 ----------------
456 kwargs
457 {:func:`colour.plotting.artist`,
458 :func:`colour.plotting.render`,
459 :func:`colour.plotting.section.plot_hull_section_colours`
460 :func:`colour.plotting.section.plot_hull_section_contour`},
461 See the documentation of the previously listed definitions.
463 Returns
464 -------
465 :class:`tuple`
466 Current figure and axes.
468 Examples
469 --------
470 >>> from colour.utilities import is_trimesh_installed
471 >>> if is_trimesh_installed:
472 ... plot_visible_spectrum_section(section_colours="RGB", section_opacity=0.15)
473 ... # doctest: +ELLIPSIS
474 (<Figure size ... with 1 Axes>, <...Axes...>)
476 .. image:: ../_static/Plotting_Plot_Visible_Spectrum_Section.png
477 :align: center
478 :alt: plot_visible_spectrum_section
479 """
481 import trimesh.convex # noqa: PLC0415
482 from trimesh import Trimesh # noqa: PLC0415
484 settings: Dict[str, Any] = {"uniform": True}
485 settings.update(kwargs)
487 _figure, axes = artist(**settings)
489 cmfs = cast(
490 "MultiSpectralDistributions",
491 reshape_msds(
492 first_item(filter_cmfs(cmfs).values()),
493 SpectralShape(360, 780, 1),
494 copy=False,
495 ),
496 )
497 illuminant = cast(
498 "SpectralDistribution",
499 first_item(filter_illuminants(illuminant).values()),
500 )
502 vertices = solid_RoschMacAdam(
503 cmfs,
504 illuminant,
505 point_order="Pulse Wave Width",
506 filter_jagged_points=True,
507 )
508 mesh = Trimesh(vertices)
509 hull = trimesh.convex.convex_hull(mesh)
511 if show_section_colours:
512 settings = {"axes": axes}
513 settings.update(kwargs)
514 settings["show"] = False
516 plot_hull_section_colours(hull, model, axis, origin, normalise, **settings)
518 if show_section_contour:
519 settings = {"axes": axes}
520 settings.update(kwargs)
521 settings["show"] = False
523 plot_hull_section_contour(hull, model, axis, origin, normalise, **settings)
525 title = (
526 f"Visible Spectrum Section - "
527 f"{f'{origin * 100}%' if normalise else origin} - "
528 f"{model} - "
529 f"{cmfs.display_name}"
530 )
532 plane = MAPPING_AXIS_TO_PLANE[axis]
534 labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[
535 as_int_array(colourspace_model_axis_reorder([0, 1, 2], model))
536 ]
537 x_label, y_label = labels[plane[0]], labels[plane[1]]
539 settings.update(
540 {
541 "axes": axes,
542 "show": True,
543 "title": title,
544 "x_label": x_label,
545 "y_label": y_label,
546 }
547 )
548 settings.update(kwargs)
550 return render(**settings)
553@required("trimesh")
554@override_style()
555def plot_RGB_colourspace_section(
556 colourspace: (
557 RGB_Colourspace
558 | LiteralRGBColourspace
559 | str
560 | Sequence[RGB_Colourspace | LiteralRGBColourspace | str]
561 ),
562 model: LiteralColourspaceModel | str = "CIE xyY",
563 axis: Literal["+z", "+x", "+y"] | str = "+z",
564 origin: float = 0.5,
565 normalise: bool = True,
566 size: float = 1.0,
567 show_section_colours: bool = True,
568 show_section_contour: bool = True,
569 segments: int = 64,
570 **kwargs: Any,
571) -> Tuple[Figure, Axes]:
572 """
573 Plot the specified *RGB* colourspace section colours along the
574 specified axis and origin.
576 Parameters
577 ----------
578 colourspace
579 *RGB* colourspace of the *RGB* array. ``colourspace`` can be of
580 any type or form supported by the
581 :func:`colour.plotting.common.filter_RGB_colourspaces`
582 definition.
583 model
584 Colourspace model, see :attr:`colour.COLOURSPACE_MODELS`
585 attribute for the list of supported colourspace models.
586 axis
587 Axis the hull section will be normal to.
588 origin
589 Coordinate along ``axis`` at which to plot the hull section.
590 normalise
591 Whether to normalise ``axis`` to the extent of the hull along
592 it.
593 size:
594 Size of the underlying *RGB* colourspace cube; used for plotting
595 HDR related sections.
596 show_section_colours
597 Whether to show the hull section colours.
598 show_section_contour
599 Whether to show the hull section contour.
600 segments
601 Edge segments count for the *RGB* colourspace cube.
603 Other Parameters
604 ----------------
605 kwargs
606 {:func:`colour.plotting.artist`,
607 :func:`colour.plotting.render`,
608 :func:`colour.plotting.section.plot_hull_section_colours`
609 :func:`colour.plotting.section.plot_hull_section_contour`},
610 See the documentation of the previously listed definitions.
612 Returns
613 -------
614 :class:`tuple`
615 Current figure and axes.
617 Examples
618 --------
619 >>> from colour.utilities import is_trimesh_installed
620 >>> if is_trimesh_installed:
621 ... plot_RGB_colourspace_section(
622 ... "sRGB", section_colours="RGB", section_opacity=0.15
623 ... )
624 ... # doctest: +ELLIPSIS
625 (<Figure size ... with 1 Axes>, <...Axes...>)
627 .. image:: ../_static/Plotting_Plot_RGB_Colourspace_Section.png
628 :align: center
629 :alt: plot_RGB_colourspace_section
630 """
632 from trimesh import Trimesh # noqa: PLC0415
634 settings: Dict[str, Any] = {"uniform": True}
635 settings.update(kwargs)
637 _figure, axes = artist(**settings)
639 colourspace = cast(
640 "RGB_Colourspace",
641 first_item(filter_RGB_colourspaces(colourspace).values()),
642 )
644 vertices, faces, _outline = primitive_cube(1, 1, 1, segments, segments, segments)
645 XYZ_vertices = RGB_to_XYZ((vertices["position"] + 0.5) * size, colourspace)
646 hull = Trimesh(XYZ_vertices, faces, process=False)
648 if show_section_colours:
649 settings = {"axes": axes}
650 settings.update(kwargs)
651 settings["show"] = False
653 plot_hull_section_colours(hull, model, axis, origin, normalise, **settings)
655 if show_section_contour:
656 settings = {"axes": axes}
657 settings.update(kwargs)
658 settings["show"] = False
660 plot_hull_section_contour(hull, model, axis, origin, normalise, **settings)
662 title = (
663 f"{colourspace.name} Section - "
664 f"{f'{origin * 100}%' if normalise else origin} - "
665 f"{model}"
666 )
668 plane = MAPPING_AXIS_TO_PLANE[axis]
670 labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[
671 as_int_array(colourspace_model_axis_reorder([0, 1, 2], model))
672 ]
673 x_label, y_label = labels[plane[0]], labels[plane[1]]
675 settings.update(
676 {
677 "axes": axes,
678 "show": True,
679 "title": title,
680 "x_label": x_label,
681 "y_label": y_label,
682 }
683 )
684 settings.update(kwargs)
686 return render(**settings)