Coverage for plotting/volume.py: 83%

1"""

2 Colour Models Volume Plotting

3=============================

4

5 Define the colour models volume and gamut plotting objects.

6

7- :func:`colour.plotting.plot_RGB_colourspaces_gamuts`

8- :func:`colour.plotting.plot_RGB_scatter`

9"""

10

11 from __future__ import annotations

12

13 import typing

14

15 import matplotlib.pyplot as plt

16 import numpy as np

17 from mpl_toolkits.mplot3d.art3d import Poly3DCollection

18

19 from colour.constants import EPSILON

20 from colour.geometry import primitive_vertices_cube_mpl, primitive_vertices_grid_mpl

21 from colour.graph import convert

22

23 if 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 )

35

36 from colour.hints import List, Sequence, Tuple, cast

37 from colour.models import RGB_Colourspace, RGB_to_XYZ

38 from colour.models.common import COLOURSPACE_MODELS_AXIS_LABELS

39 from 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)

47 from 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)

59 from colour.utilities.deprecation import handle_arguments_deprecation

60

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"

67

68 __all__ = [

69 "nadir_grid",

70 "RGB_identity_cube",

71 "plot_RGB_colourspaces_gamuts",

72 "plot_RGB_scatter",

73]

74

75

76 def 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.

87

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.

98

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)`.

130

131 Returns

132 -------

133 :class:`tuple`

134 Grid quads, face colours, edge colours.

135

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 """

186

187 limits = as_float_array(optional(limits, np.array([[-1, 1], [-1, 1]])))

188 labels = cast("Sequence", optional(labels, ("x", "y")))

189

190 extent = np.max(np.abs(limits[..., 1] - limits[..., 0]))

191

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)

206

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 )

215

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)])

221

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 )

230

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 )

238

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

246

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

252

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"

260

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)

266

267 tick = ( # noqa: PLW2901

268 as_int_scalar(tick) if is_integer(tick) else tick

269 )

270 c = settings[f"{axis}_ticks_colour"]

271

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 )

283

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"

288

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)

291

292 c = settings[f"{axis}_label_colour"]

293

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 )

306

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]))

310

311 return quads, RGB_f, RGB_e

312

313

314 def 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.

341

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.

352

353 Returns

354 -------

355 :class:`tuple`

356 Cube quads and *RGB* colours.

357

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 """

399

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)

410

411 return quads, RGB

412

413

414@override_style()

415 def 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.

438

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.

470

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.

487

488 Returns

489 -------

490 :class:`tuple`

491 Current figure and axes.

492

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...>)

498

499 .. image:: ../_static/Plotting_Plot_RGB_Colourspaces_Gamuts.png

500 :align: center

501 :alt: plot_RGB_colourspaces_gamuts

502 """

503

504 model = handle_arguments_deprecation(

505 {

506 "ArgumentRenamed": [["reference_colourspace", "model"]],

507 },

508 **kwargs,

509 ).get("model", model)

510

511 colourspaces = cast(

512 "List[RGB_Colourspace]",

513 list(filter_RGB_colourspaces(colourspaces).values()),

514 ) # pyright: ignore

515

516 convert_kwargs = optional(convert_kwargs, {})

517

518 count_c = len(colourspaces)

519

520 title = f"{', '.join([colourspace.name for colourspace in colourspaces])} - {model}"

521

522 illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint

523

524 convert_settings = {"illuminant": illuminant}

525 convert_settings.update(convert_kwargs)

526

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)

535

536 figure = plt.figure()

537 axes = figure.add_subplot(111, projection="3d")

538

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

545

546 points = colourspace_model_axis_reorder(

547 convert(XYZ, "CIE XYZ", model, **convert_settings),

548 model,

549 )

550

551 points[np.isnan(points)] = 0

552

553 c = (

554 (0.0, 0.0, 0.0, 0.5)

555 if spectral_locus_colour is None

556 else spectral_locus_colour

557 )

558

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 )

573

574 plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace

575

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 )

587

588 quads_cb, RGB = RGB_identity_cube(

589 width_segments=segments,

590 height_segments=segments,

591 depth_segments=segments,

592 )

593

594 XYZ = RGB_to_XYZ(quads_cb, colourspace)

595

596 # Preventing singularities for colour models such as "CIE xyY",

597 XYZ[XYZ == 0] = EPSILON

598

599 convert_settings = {"illuminant": colourspace.whitepoint}

600 convert_settings.update(convert_kwargs)

601

602 quads_c.extend(

603 colourspace_model_axis_reorder(

604 convert(XYZ, "CIE XYZ", model, **convert_settings), # pyright: ignore

605 model,

606 )

607 )

608

609 if settings.face_colours[i] is not None:

610 RGB = ones(RGB.shape) * settings.face_colours[i]

611

612 RGB_cf.extend(np.hstack([RGB, full((RGB.shape[0], 1), settings.face_alpha[i])]))

613

614 if settings.edge_colours[i] is not None:

615 RGB = ones(RGB.shape) * settings.edge_colours[i]

616

617 RGB_ce.extend(np.hstack([RGB, full((RGB.shape[0], 1), settings.edge_alpha[i])]))

618

619 quads = as_float_array(quads_c)

620 RGB_f = as_float_array(RGB_cf)

621 RGB_e = as_float_array(RGB_ce)

622

623 quads[np.isnan(quads)] = 0

624

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))

630

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])

636

637 if show_grid:

638 limits = np.array([[-1.5, 1.5], [-1.5, 1.5]])

639

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])

646

647 collection = Poly3DCollection(quads)

648 collection.set_facecolors(RGB_f) # pyright: ignore

649 collection.set_edgecolors(RGB_e) # pyright: ignore

650

651 axes.add_collection3d(collection)

652

653 settings.update({"axes": axes, "axes_visible": False, "camera_aspect": "equal"})

654 settings.update(kwargs)

655

656 return cast("Tuple[Figure, Axes3D]", render(**settings))

657

658

659@override_style()

660 def 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.

687

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.

727

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.

734

735 Returns

736 -------

737 :class:`tuple`

738 Current figure and axes.

739

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...>)

745

746 .. image:: ../_static/Plotting_Plot_RGB_Scatter.png

747 :align: center

748 :alt: plot_RGB_scatter

749 """

750

751 RGB = np.reshape(as_float_array(RGB)[..., :3], (-1, 3))

752

753 colourspace = cast(

754 "RGB_Colourspace",

755 first_item(filter_RGB_colourspaces(colourspace).values()),

756 )

757 colourspaces = cast("List[str]", optional(colourspaces, [colourspace.name]))

758

759 convert_kwargs = optional(convert_kwargs, {})

760

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

770

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 )

783

784 XYZ = RGB_to_XYZ(RGB, colourspace)

785

786 convert_settings = {"illuminant": colourspace.whitepoint}

787 convert_settings.update(convert_kwargs)

788

789 points = colourspace_model_axis_reorder(

790 convert(XYZ, "CIE XYZ", model, **convert_settings), # pyright: ignore

791 model,

792 )

793

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 )

803

804 settings.update({"axes": axes, "show": True})

805 settings.update(kwargs)

806

807 return cast("Tuple[Figure, Axes3D]", render(**settings))