Coverage for plotting/volume.py: 83%
162 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Colour Models Volume Plotting
3=============================
5Define the colour models volume and gamut plotting objects.
7- :func:`colour.plotting.plot_RGB_colourspaces_gamuts`
8- :func:`colour.plotting.plot_RGB_scatter`
9"""
11from __future__ import annotations
13import typing
15import matplotlib.pyplot as plt
16import numpy as np
17from mpl_toolkits.mplot3d.art3d import Poly3DCollection
19from colour.constants import EPSILON
20from colour.geometry import primitive_vertices_cube_mpl, primitive_vertices_grid_mpl
21from colour.graph import convert
23if typing.TYPE_CHECKING:
24 from matplotlib.figure import Figure
25 from mpl_toolkits.mplot3d.axes3d import Axes3D
26 from colour.colorimetry import MultiSpectralDistributions
27 from colour.hints import (
28 Any,
29 ArrayLike,
30 Literal,
31 LiteralColourspaceModel,
32 LiteralRGBColourspace,
33 NDArrayFloat,
34 )
36from colour.hints import List, Sequence, Tuple, cast
37from colour.models import RGB_Colourspace, RGB_to_XYZ
38from colour.models.common import COLOURSPACE_MODELS_AXIS_LABELS
39from colour.plotting import (
40 CONSTANTS_COLOUR_STYLE,
41 colourspace_model_axis_reorder,
42 filter_cmfs,
43 filter_RGB_colourspaces,
44 override_style,
45 render,
46)
47from colour.utilities import (
48 Structure,
49 as_float_array,
50 as_int_array,
51 as_int_scalar,
52 first_item,
53 full,
54 is_integer,
55 ones,
56 optional,
57 zeros,
58)
59from colour.utilities.deprecation import handle_arguments_deprecation
61__author__ = "Colour Developers"
62__copyright__ = "Copyright 2013 Colour Developers"
63__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
64__maintainer__ = "Colour Developers"
65__email__ = "colour-developers@colour-science.org"
66__status__ = "Production"
68__all__ = [
69 "nadir_grid",
70 "RGB_identity_cube",
71 "plot_RGB_colourspaces_gamuts",
72 "plot_RGB_scatter",
73]
76def nadir_grid(
77 limits: ArrayLike | None = None,
78 segments: int = 10,
79 labels: ArrayLike | Sequence[str] | None = None,
80 axes: Axes3D | None = None,
81 **kwargs: Any,
82) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]:
83 """
84 Generate a grid on the *CIE xy* plane made of quad geometric elements
85 with associated face and edge colours. Add ticks and labels to the
86 specified axes according to the extended grid settings.
88 Parameters
89 ----------
90 limits
91 Extended grid limits.
92 segments
93 Edge segments count for the extended grid.
94 labels
95 Axis labels.
96 axes
97 Axes to add the grid.
99 Other Parameters
100 ----------------
101 grid_edge_alpha
102 Grid edge opacity value such as `grid_edge_alpha = 0.5`.
103 grid_edge_colours
104 Grid edge colours array such as
105 `grid_edge_colours = (0.25, 0.25, 0.25)`.
106 grid_face_alpha
107 Grid face opacity value such as `grid_face_alpha = 0.1`.
108 grid_face_colours
109 Grid face colours array such as
110 `grid_face_colours = (0.25, 0.25, 0.25)`.
111 ticks_and_label_location
112 Location of the *X* and *Y* axis ticks and labels such as
113 `ticks_and_label_location = ('-x', '-y')`.
114 x_axis_colour
115 *X* axis colour array such as `x_axis_colour = (0.0, 0.0, 0.0, 1.0)`.
116 x_label_colour
117 *X* axis label colour array such as
118 `x_label_colour = (0.0, 0.0, 0.0, 0.85)`.
119 x_ticks_colour
120 *X* axis ticks colour array such as
121 `x_ticks_colour = (0.0, 0.0, 0.0, 0.85)`.
122 y_axis_colour
123 *Y* axis colour array such as `y_axis_colour = (0.0, 0.0, 0.0, 1.0)`.
124 y_label_colour
125 *Y* axis label colour array such as
126 `y_label_colour = (0.0, 0.0, 0.0, 0.85)`.
127 y_ticks_colour
128 *Y* axis ticks colour array such as
129 `y_ticks_colour = (0.0, 0.0, 0.0, 0.85)`.
131 Returns
132 -------
133 :class:`tuple`
134 Grid quads, face colours, edge colours.
136 Examples
137 --------
138 >>> nadir_grid(segments=1)
139 (array([[[-1. , -1. , 0. ],
140 [ 1. , -1. , 0. ],
141 [ 1. , 1. , 0. ],
142 [-1. , 1. , 0. ]],
143 <BLANKLINE>
144 [[-1. , -1. , 0. ],
145 [ 0. , -1. , 0. ],
146 [ 0. , 0. , 0. ],
147 [-1. , 0. , 0. ]],
148 <BLANKLINE>
149 [[-1. , 0. , 0. ],
150 [ 0. , 0. , 0. ],
151 [ 0. , 1. , 0. ],
152 [-1. , 1. , 0. ]],
153 <BLANKLINE>
154 [[ 0. , -1. , 0. ],
155 [ 1. , -1. , 0. ],
156 [ 1. , 0. , 0. ],
157 [ 0. , 0. , 0. ]],
158 <BLANKLINE>
159 [[ 0. , 0. , 0. ],
160 [ 1. , 0. , 0. ],
161 [ 1. , 1. , 0. ],
162 [ 0. , 1. , 0. ]],
163 <BLANKLINE>
164 [[-1. , -0.001, 0. ],
165 [ 1. , -0.001, 0. ],
166 [ 1. , 0.001, 0. ],
167 [-1. , 0.001, 0. ]],
168 <BLANKLINE>
169 [[-0.001, -1. , 0. ],
170 [ 0.001, -1. , 0. ],
171 [ 0.001, 1. , 0. ],
172 [-0.001, 1. , 0. ]]]), array([[ 0.25, 0.25, 0.25, 0.1 ],
173 [ 0. , 0. , 0. , 0. ],
174 [ 0. , 0. , 0. , 0. ],
175 [ 0. , 0. , 0. , 0. ],
176 [ 0. , 0. , 0. , 0. ],
177 [ 0. , 0. , 0. , 1. ],
178 [ 0. , 0. , 0. , 1. ]]), array([[ 0.5 , 0.5 , 0.5 , 0.5 ],
179 [ 0.75, 0.75, 0.75, 0.25],
180 [ 0.75, 0.75, 0.75, 0.25],
181 [ 0.75, 0.75, 0.75, 0.25],
182 [ 0.75, 0.75, 0.75, 0.25],
183 [ 0. , 0. , 0. , 1. ],
184 [ 0. , 0. , 0. , 1. ]]))
185 """
187 limits = as_float_array(optional(limits, np.array([[-1, 1], [-1, 1]])))
188 labels = cast("Sequence", optional(labels, ("x", "y")))
190 extent = np.max(np.abs(limits[..., 1] - limits[..., 0]))
192 settings = Structure(
193 grid_face_colours=(0.25, 0.25, 0.25),
194 grid_edge_colours=(0.50, 0.50, 0.50),
195 grid_face_alpha=0.1,
196 grid_edge_alpha=0.5,
197 x_axis_colour=(0.0, 0.0, 0.0, 1.0),
198 y_axis_colour=(0.0, 0.0, 0.0, 1.0),
199 x_ticks_colour=(0.0, 0.0, 0.0, 0.85),
200 y_ticks_colour=(0.0, 0.0, 0.0, 0.85),
201 x_label_colour=(0.0, 0.0, 0.0, 0.85),
202 y_label_colour=(0.0, 0.0, 0.0, 0.85),
203 ticks_and_label_location=("-x", "-y"),
204 )
205 settings.update(**kwargs)
207 # Outer grid.
208 quads_g = primitive_vertices_grid_mpl(
209 origin=(-extent / 2, -extent / 2),
210 width=extent,
211 height=extent,
212 height_segments=segments,
213 width_segments=segments,
214 )
216 RGB_g = ones((quads_g.shape[0], quads_g.shape[-1]))
217 RGB_gf = RGB_g * settings.grid_face_colours
218 RGB_gf = np.hstack([RGB_gf, full((RGB_gf.shape[0], 1), settings.grid_face_alpha)])
219 RGB_ge = RGB_g * settings.grid_edge_colours
220 RGB_ge = np.hstack([RGB_ge, full((RGB_ge.shape[0], 1), settings.grid_edge_alpha)])
222 # Inner grid.
223 quads_gs = primitive_vertices_grid_mpl(
224 origin=(-extent / 2, -extent / 2),
225 width=extent,
226 height=extent,
227 height_segments=segments * 2,
228 width_segments=segments * 2,
229 )
231 RGB_gs = ones((quads_gs.shape[0], quads_gs.shape[-1]))
232 RGB_gsf = RGB_gs * 0
233 RGB_gsf = np.hstack([RGB_gsf, full((RGB_gsf.shape[0], 1), 0)])
234 RGB_gse = np.clip(RGB_gs * settings.grid_edge_colours * 1.5, 0, 1)
235 RGB_gse = np.hstack(
236 (RGB_gse, full((RGB_gse.shape[0], 1), settings.grid_edge_alpha / 2))
237 )
239 # Axis.
240 thickness = extent / 1000
241 quad_x = primitive_vertices_grid_mpl(
242 origin=(limits[0, 0], -thickness / 2), width=extent, height=thickness
243 )
244 RGB_x = ones((quad_x.shape[0], quad_x.shape[-1] + 1))
245 RGB_x = RGB_x * settings.x_axis_colour
247 quad_y = primitive_vertices_grid_mpl(
248 origin=(-thickness / 2, limits[1, 0]), width=thickness, height=extent
249 )
250 RGB_y = ones((quad_y.shape[0], quad_y.shape[-1] + 1))
251 RGB_y = RGB_y * settings.y_axis_colour
253 if axes is not None:
254 # Ticks.
255 x_s = 1 if "+x" in settings.ticks_and_label_location else -1
256 y_s = 1 if "+y" in settings.ticks_and_label_location else -1
257 for i, axis in enumerate("xy"):
258 h_a = "center" if axis == "x" else "left" if x_s == 1 else "right"
259 v_a = "center"
261 ticks = sorted(set(quads_g[..., 0, i]))
262 ticks += [ticks[-1] + ticks[-1] - ticks[-2]]
263 for tick in ticks:
264 x = limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 25) if i else tick
265 y = tick if i else limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 25)
267 tick = ( # noqa: PLW2901
268 as_int_scalar(tick) if is_integer(tick) else tick
269 )
270 c = settings[f"{axis}_ticks_colour"]
272 axes.text(
273 x,
274 y,
275 0,
276 tick,
277 "x",
278 horizontalalignment=h_a,
279 verticalalignment=v_a,
280 color=c,
281 clip_on=True,
282 )
284 # Labels.
285 for i, axis in enumerate("xy"):
286 h_a = "center" if axis == "x" else "left" if x_s == 1 else "right"
287 v_a = "center"
289 x = limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 10) if i else 0
290 y = 0 if i else limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 10)
292 c = settings[f"{axis}_label_colour"]
294 axes.text(
295 x,
296 y,
297 0,
298 labels[i],
299 "x",
300 horizontalalignment=h_a,
301 verticalalignment=v_a,
302 color=c,
303 size=20,
304 clip_on=True,
305 )
307 quads = as_float_array(np.vstack([quads_g, quads_gs, quad_x, quad_y]))
308 RGB_f = as_float_array(np.vstack([RGB_gf, RGB_gsf, RGB_x, RGB_y]))
309 RGB_e = as_float_array(np.vstack([RGB_ge, RGB_gse, RGB_x, RGB_y]))
311 return quads, RGB_f, RGB_e
314def RGB_identity_cube(
315 width_segments: int = 16,
316 height_segments: int = 16,
317 depth_segments: int = 16,
318 planes: (
319 Sequence[
320 Literal[
321 "-x",
322 "+x",
323 "-y",
324 "+y",
325 "-z",
326 "+z",
327 "xy",
328 "xz",
329 "yz",
330 "yx",
331 "zx",
332 "zy",
333 ]
334 ]
335 | None
336 ) = None,
337) -> Tuple[NDArrayFloat, NDArrayFloat]:
338 """
339 Generate an *RGB* identity cube composed of quad geometric elements with
340 its associated *RGB* colours.
342 Parameters
343 ----------
344 width_segments
345 Number of quad segments along the cube width.
346 height_segments
347 Number of quad segments along the cube height.
348 depth_segments
349 Number of quad segments along the cube depth.
350 planes
351 Grid primitives to include in the cube construction.
353 Returns
354 -------
355 :class:`tuple`
356 Cube quads and *RGB* colours.
358 Examples
359 --------
360 >>> vertices, RGB = RGB_identity_cube(1, 1, 1)
361 >>> vertices
362 array([[[ 0., 0., 0.],
363 [ 1., 0., 0.],
364 [ 1., 1., 0.],
365 [ 0., 1., 0.]],
366 <BLANKLINE>
367 [[ 0., 0., 1.],
368 [ 1., 0., 1.],
369 [ 1., 1., 1.],
370 [ 0., 1., 1.]],
371 <BLANKLINE>
372 [[ 0., 0., 0.],
373 [ 1., 0., 0.],
374 [ 1., 0., 1.],
375 [ 0., 0., 1.]],
376 <BLANKLINE>
377 [[ 0., 1., 0.],
378 [ 1., 1., 0.],
379 [ 1., 1., 1.],
380 [ 0., 1., 1.]],
381 <BLANKLINE>
382 [[ 0., 0., 0.],
383 [ 0., 1., 0.],
384 [ 0., 1., 1.],
385 [ 0., 0., 1.]],
386 <BLANKLINE>
387 [[ 1., 0., 0.],
388 [ 1., 1., 0.],
389 [ 1., 1., 1.],
390 [ 1., 0., 1.]]])
391 >>> RGB
392 array([[ 0.5, 0.5, 0. ],
393 [ 0.5, 0.5, 1. ],
394 [ 0.5, 0. , 0.5],
395 [ 0.5, 1. , 0.5],
396 [ 0. , 0.5, 0.5],
397 [ 1. , 0.5, 0.5]])
398 """
400 quads = primitive_vertices_cube_mpl(
401 width=1,
402 height=1,
403 depth=1,
404 width_segments=width_segments,
405 height_segments=height_segments,
406 depth_segments=depth_segments,
407 planes=planes,
408 )
409 RGB = np.average(quads, axis=-2)
411 return quads, RGB
414@override_style()
415def plot_RGB_colourspaces_gamuts(
416 colourspaces: (
417 RGB_Colourspace
418 | LiteralRGBColourspace
419 | str
420 | Sequence[RGB_Colourspace | LiteralRGBColourspace | str]
421 ),
422 model: LiteralColourspaceModel | str = "CIE xyY",
423 segments: int = 8,
424 show_grid: bool = True,
425 grid_segments: int = 10,
426 show_spectral_locus: bool = False,
427 spectral_locus_colour: ArrayLike | str | None = None,
428 cmfs: (
429 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
430 ) = "CIE 1931 2 Degree Standard Observer",
431 chromatically_adapt: bool = False,
432 convert_kwargs: dict | None = None,
433 **kwargs: Any,
434) -> Tuple[Figure, Axes3D]:
435 """
436 Plot the gamuts of the specified *RGB* colourspaces in the specified
437 reference colourspace.
439 Parameters
440 ----------
441 colourspaces
442 *RGB* colourspaces to plot the gamuts of. ``colourspaces`` elements
443 can be of any type or form supported by the
444 :func:`colour.plotting.common.filter_RGB_colourspaces` definition.
445 model
446 Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute
447 for the list of supported colourspace models.
448 segments
449 Edge segments count for each *RGB* colourspace cube.
450 show_grid
451 Whether to show a grid at the bottom of the *RGB* colourspace
452 cubes.
453 grid_segments
454 Edge segments count for the grid.
455 show_spectral_locus
456 Whether to show the spectral locus.
457 spectral_locus_colour
458 Spectral locus colour.
459 cmfs
460 Standard observer colour matching functions used for computing the
461 spectral locus boundaries. ``cmfs`` can be of any type or form
462 supported by the :func:`colour.plotting.common.filter_cmfs`
463 definition.
464 chromatically_adapt
465 Whether to chromatically adapt the *RGB* colourspaces specified in
466 ``colourspaces`` to the whitepoint of the default plotting
467 colourspace.
468 convert_kwargs
469 Keyword arguments for the :func:`colour.convert` definition.
471 Other Parameters
472 ----------------
473 edge_colours
474 Edge colours array such as
475 `edge_colours = (None, (0.5, 0.5, 1.0))`.
476 edge_alpha
477 Edge opacity value such as `edge_alpha = (0.0, 1.0)`.
478 face_alpha
479 Face opacity value such as `face_alpha = (0.5, 1.0)`.
480 face_colours
481 Face colours array such as
482 `face_colours = (None, (0.5, 0.5, 1.0))`.
483 kwargs
484 {:func:`colour.plotting.artist`,
485 :func:`colour.plotting.volume.nadir_grid`},
486 See the documentation of the previously listed definitions.
488 Returns
489 -------
490 :class:`tuple`
491 Current figure and axes.
493 Examples
494 --------
495 >>> plot_RGB_colourspaces_gamuts(["ITU-R BT.709", "ACEScg", "S-Gamut"])
496 ... # doctest: +ELLIPSIS
497 (<Figure size ... with 1 Axes>, <...Axes3D...>)
499 .. image:: ../_static/Plotting_Plot_RGB_Colourspaces_Gamuts.png
500 :align: center
501 :alt: plot_RGB_colourspaces_gamuts
502 """
504 model = handle_arguments_deprecation(
505 {
506 "ArgumentRenamed": [["reference_colourspace", "model"]],
507 },
508 **kwargs,
509 ).get("model", model)
511 colourspaces = cast(
512 "List[RGB_Colourspace]",
513 list(filter_RGB_colourspaces(colourspaces).values()),
514 ) # pyright: ignore
516 convert_kwargs = optional(convert_kwargs, {})
518 count_c = len(colourspaces)
520 title = f"{', '.join([colourspace.name for colourspace in colourspaces])} - {model}"
522 illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint
524 convert_settings = {"illuminant": illuminant}
525 convert_settings.update(convert_kwargs)
527 settings = Structure(
528 face_colours=[None] * count_c,
529 edge_colours=[None] * count_c,
530 face_alpha=[1] * count_c,
531 edge_alpha=[1] * count_c,
532 title=title,
533 )
534 settings.update(kwargs)
536 figure = plt.figure()
537 axes = figure.add_subplot(111, projection="3d")
539 points = zeros((4, 3))
540 if show_spectral_locus:
541 cmfs = cast(
542 "MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values())
543 )
544 XYZ = cmfs.values
546 points = colourspace_model_axis_reorder(
547 convert(XYZ, "CIE XYZ", model, **convert_settings),
548 model,
549 )
551 points[np.isnan(points)] = 0
553 c = (
554 (0.0, 0.0, 0.0, 0.5)
555 if spectral_locus_colour is None
556 else spectral_locus_colour
557 )
559 axes.plot(
560 points[..., 0],
561 points[..., 1],
562 points[..., 2],
563 color=c,
564 zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
565 )
566 axes.plot(
567 (points[-1][0], points[0][0]),
568 (points[-1][1], points[0][1]),
569 (points[-1][2], points[0][2]),
570 color=c,
571 zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
572 )
574 plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace
576 quads_c: list = []
577 RGB_cf: list = []
578 RGB_ce: list = []
579 for i, colourspace in enumerate(colourspaces):
580 if chromatically_adapt and not np.array_equal(
581 colourspace.whitepoint, plotting_colourspace.whitepoint
582 ):
583 colourspace = colourspace.chromatically_adapt( # noqa: PLW2901
584 plotting_colourspace.whitepoint,
585 plotting_colourspace.whitepoint_name,
586 )
588 quads_cb, RGB = RGB_identity_cube(
589 width_segments=segments,
590 height_segments=segments,
591 depth_segments=segments,
592 )
594 XYZ = RGB_to_XYZ(quads_cb, colourspace)
596 # Preventing singularities for colour models such as "CIE xyY",
597 XYZ[XYZ == 0] = EPSILON
599 convert_settings = {"illuminant": colourspace.whitepoint}
600 convert_settings.update(convert_kwargs)
602 quads_c.extend(
603 colourspace_model_axis_reorder(
604 convert(XYZ, "CIE XYZ", model, **convert_settings), # pyright: ignore
605 model,
606 )
607 )
609 if settings.face_colours[i] is not None:
610 RGB = ones(RGB.shape) * settings.face_colours[i]
612 RGB_cf.extend(np.hstack([RGB, full((RGB.shape[0], 1), settings.face_alpha[i])]))
614 if settings.edge_colours[i] is not None:
615 RGB = ones(RGB.shape) * settings.edge_colours[i]
617 RGB_ce.extend(np.hstack([RGB, full((RGB.shape[0], 1), settings.edge_alpha[i])]))
619 quads = as_float_array(quads_c)
620 RGB_f = as_float_array(RGB_cf)
621 RGB_e = as_float_array(RGB_ce)
623 quads[np.isnan(quads)] = 0
625 if quads.size != 0:
626 for i, axis in enumerate("xyz"):
627 min_a = np.minimum(np.min(quads[..., i]), np.min(points[..., i]))
628 max_a = np.maximum(np.max(quads[..., i]), np.max(points[..., i]))
629 getattr(axes, f"set_{axis}lim")((min_a, max_a))
631 labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[
632 as_int_array(colourspace_model_axis_reorder([0, 1, 2], model))
633 ]
634 for i, axis in enumerate("xyz"):
635 getattr(axes, f"set_{axis}label")(labels[i])
637 if show_grid:
638 limits = np.array([[-1.5, 1.5], [-1.5, 1.5]])
640 quads_g, RGB_gf, RGB_ge = nadir_grid(
641 limits, grid_segments, labels, axes, **settings
642 )
643 quads = np.vstack([quads_g, quads])
644 RGB_f = np.vstack([RGB_gf, RGB_f])
645 RGB_e = np.vstack([RGB_ge, RGB_e])
647 collection = Poly3DCollection(quads)
648 collection.set_facecolors(RGB_f) # pyright: ignore
649 collection.set_edgecolors(RGB_e) # pyright: ignore
651 axes.add_collection3d(collection)
653 settings.update({"axes": axes, "axes_visible": False, "camera_aspect": "equal"})
654 settings.update(kwargs)
656 return cast("Tuple[Figure, Axes3D]", render(**settings))
659@override_style()
660def plot_RGB_scatter(
661 RGB: ArrayLike,
662 colourspace: (
663 RGB_Colourspace | str | Sequence[RGB_Colourspace | LiteralRGBColourspace | str]
664 ) = "sRGB",
665 model: LiteralColourspaceModel | str = "CIE xyY",
666 colourspaces: (
667 RGB_Colourspace
668 | str
669 | Sequence[RGB_Colourspace | LiteralRGBColourspace | str]
670 | None
671 ) = None,
672 segments: int = 8,
673 show_grid: bool = True,
674 grid_segments: int = 10,
675 show_spectral_locus: bool = False,
676 spectral_locus_colour: ArrayLike | str | None = None,
677 points_size: float = 12,
678 cmfs: (
679 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
680 ) = "CIE 1931 2 Degree Standard Observer",
681 chromatically_adapt: bool = False,
682 convert_kwargs: dict | None = None,
683 **kwargs: Any,
684) -> Tuple[Figure, Axes3D]:
685 """
686 Plot the specified *RGB* colourspace array in a scatter plot.
688 Parameters
689 ----------
690 RGB
691 *RGB* colourspace array.
692 colourspace
693 *RGB* colourspace of the *RGB* array. ``colourspace`` can be of any
694 type or form supported by the
695 :func:`colour.plotting.common.filter_RGB_colourspaces` definition.
696 model
697 Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute
698 for the list of supported colourspace models.
699 colourspaces
700 *RGB* colourspaces to plot the gamuts of. ``colourspaces`` elements
701 can be of any type or form supported by the
702 :func:`colour.plotting.common.filter_RGB_colourspaces` definition.
703 segments
704 Edge segments count for each *RGB* colourspace cube.
705 show_grid
706 Whether to show a grid at the bottom of the *RGB* colourspace
707 cubes.
708 grid_segments
709 Edge segments count for the grid.
710 show_spectral_locus
711 Whether to show the spectral locus.
712 spectral_locus_colour
713 Spectral locus colour.
714 points_size
715 Scatter points size.
716 cmfs
717 Standard observer colour matching functions used for computing the
718 spectral locus boundaries. ``cmfs`` can be of any type or form
719 supported by the :func:`colour.plotting.common.filter_cmfs`
720 definition.
721 chromatically_adapt
722 Whether to chromatically adapt the *RGB* colourspaces specified in
723 ``colourspaces`` to the whitepoint of the default plotting
724 colourspace.
725 convert_kwargs
726 Keyword arguments for the :func:`colour.convert` definition.
728 Other Parameters
729 ----------------
730 kwargs
731 {:func:`colour.plotting.artist`,
732 :func:`colour.plotting.plot_RGB_colourspaces_gamuts`},
733 See the documentation of the previously listed definitions.
735 Returns
736 -------
737 :class:`tuple`
738 Current figure and axes.
740 Examples
741 --------
742 >>> RGB = np.random.random((128, 128, 3))
743 >>> plot_RGB_scatter(RGB, "ITU-R BT.709") # doctest: +ELLIPSIS
744 (<Figure size ... with 1 Axes>, <...Axes3D...>)
746 .. image:: ../_static/Plotting_Plot_RGB_Scatter.png
747 :align: center
748 :alt: plot_RGB_scatter
749 """
751 RGB = np.reshape(as_float_array(RGB)[..., :3], (-1, 3))
753 colourspace = cast(
754 "RGB_Colourspace",
755 first_item(filter_RGB_colourspaces(colourspace).values()),
756 )
757 colourspaces = cast("List[str]", optional(colourspaces, [colourspace.name]))
759 convert_kwargs = optional(convert_kwargs, {})
761 count_c = len(colourspaces)
762 settings = Structure(
763 face_colours=[None] * count_c,
764 edge_colours=[(0.25, 0.25, 0.25)] * count_c,
765 face_alpha=[0.0] * count_c,
766 edge_alpha=[0.1] * count_c,
767 )
768 settings.update(kwargs)
769 settings["show"] = False
771 plot_RGB_colourspaces_gamuts(
772 colourspaces=colourspaces,
773 model=model,
774 segments=segments,
775 show_grid=show_grid,
776 grid_segments=grid_segments,
777 show_spectral_locus=show_spectral_locus,
778 spectral_locus_colour=spectral_locus_colour,
779 cmfs=cmfs,
780 chromatically_adapt=chromatically_adapt,
781 **settings,
782 )
784 XYZ = RGB_to_XYZ(RGB, colourspace)
786 convert_settings = {"illuminant": colourspace.whitepoint}
787 convert_settings.update(convert_kwargs)
789 points = colourspace_model_axis_reorder(
790 convert(XYZ, "CIE XYZ", model, **convert_settings), # pyright: ignore
791 model,
792 )
794 axes = plt.gca()
795 axes.scatter(
796 points[..., 0],
797 points[..., 1],
798 points[..., 2],
799 c=np.reshape(RGB, (-1, 3)),
800 s=points_size, # pyright: ignore
801 zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_scatter,
802 )
804 settings.update({"axes": axes, "show": True})
805 settings.update(kwargs)
807 return cast("Tuple[Figure, Axes3D]", render(**settings))