Coverage for colour/plotting/volume.py: 100%

162 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

2Colour Models Volume Plotting 

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

4 

5Define 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 

11from __future__ import annotations 

12 

13import typing 

14 

15import matplotlib.pyplot as plt 

16import numpy as np 

17from mpl_toolkits.mplot3d.art3d import Poly3DCollection 

18 

19from colour.constants import EPSILON 

20from colour.geometry import primitive_vertices_cube_mpl, primitive_vertices_grid_mpl 

21from colour.graph import convert 

22 

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 ) 

35 

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 

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 

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. 

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 

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. 

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

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. 

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

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. 

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