# !/usr/bin/env python
"""
RGB Colourspace Visuals
=======================
Define the *RGB colourspace* visuals:
- :class:`colour_visuals.VisualRGBColourspace2D`
- :class:`colour_visuals.VisualRGBColourspace3D`
"""
from __future__ import annotations
import typing
import numpy as np
import pygfx as gfx
from colour.constants import EPSILON
from colour.geometry import primitive_cube
if typing.TYPE_CHECKING:
from colour.hints import (
Any,
ArrayLike,
Literal,
LiteralColourspaceModel,
LiteralRGBColourspace,
Sequence,
Type,
)
from colour.models import RGB_Colourspace, RGB_to_XYZ, XYZ_to_RGB, xy_to_XYZ
from colour.plotting import (
CONSTANTS_COLOUR_STYLE,
METHODS_CHROMATICITY_DIAGRAM,
colourspace_model_axis_reorder,
)
from colour.utilities import full
from colour_visuals.common import (
XYZ_to_colourspace_model,
append_channel,
as_contiguous_array,
conform_primitive_dtype,
)
from colour_visuals.visual import (
MixinPropertyColour,
MixinPropertyColourspace,
MixinPropertyKwargs,
MixinPropertyMethod,
MixinPropertyModel,
MixinPropertyOpacity,
MixinPropertySegments,
MixinPropertySize,
MixinPropertyThickness,
MixinPropertyTypeMaterial,
MixinPropertyWireframe,
Visual,
)
__author__ = "Colour Developers"
__copyright__ = "Copyright 2023 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__ = [
"VisualRGBColourspace2D",
"VisualRGBColourspace3D",
]
[docs]
class VisualRGBColourspace2D(
MixinPropertyColourspace,
MixinPropertyMethod,
MixinPropertyColour,
MixinPropertyOpacity,
MixinPropertyThickness,
Visual,
):
"""
Create a 2D *RGB* colourspace gamut visual.
Parameters
----------
colourspace
*RGB* colourspace to plot the gamut of. ``colourspaces`` elements
can be of any type or form supported by the
:func:`colour.plotting.common.filter_RGB_colourspaces` definition.
method
*Chromaticity Diagram* method.
colour
Colour of the visual, if *None*, the colour is computed from the visual
geometry.
opacity
Opacity of the visual.
thickness
Thickness of the visual lines.
Attributes
----------
- :attr:`~colour_visuals.VisualRGBColourspace2D.colourspace`
- :attr:`~colour_visuals.VisualRGBColourspace2D.method`
- :attr:`~colour_visuals.VisualRGBColourspace2D.colour`
- :attr:`~colour_visuals.VisualRGBColourspace2D.opacity`
- :attr:`~colour_visuals.VisualRGBColourspace2D.thickness`
Methods
-------
- :meth:`~colour_visuals.VisualRGBColourspace2D.__init__`
- :meth:`~colour_visuals.VisualRGBColourspace2D.update`
Examples
--------
>>> import os
>>> from colour.utilities import suppress_stdout
>>> from wgpu.gui.auto import WgpuCanvas
>>> with suppress_stdout():
... canvas = WgpuCanvas(size=(960, 540))
... scene = gfx.Scene()
... scene.add(
... gfx.Background(
... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18]))
... )
... )
... visual = VisualRGBColourspace2D()
... camera = gfx.PerspectiveCamera(50, 16 / 9)
... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25)
... scene.add(visual)
... if os.environ.get("CI") is None:
... gfx.show(scene, camera=camera, canvas=canvas)
.. image:: ../_static/Plotting_VisualRGBColourspace2D.png
:align: center
:alt: visual-rgbcolourspace-2-d
"""
[docs]
def __init__(
self,
colourspace: (
RGB_Colourspace
| str
| Sequence[RGB_Colourspace | LiteralRGBColourspace | str]
) = "sRGB",
method: (
Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] | str
) = "CIE 1931",
colour: ArrayLike | None = None,
opacity: float = 1,
thickness: float = 1,
) -> None:
super().__init__()
self._gamut = None
self._whitepoint = None
with self.block_update():
self.colourspace = colourspace
self.method = method
self.colour = colour
self.opacity = opacity
self.thickness = thickness
self.update()
[docs]
def update(self) -> None:
"""Update the visual."""
if self._is_update_blocked:
return
self.clear()
plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace
XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[self._method]["XYZ_to_ij"]
primaries = self._colourspace.primaries
primaries[primaries == 0] = EPSILON
ij = XYZ_to_ij(
xy_to_XYZ(primaries),
plotting_colourspace.whitepoint,
)
positions = append_channel(
np.array([ij[0], ij[1], ij[1], ij[2], ij[2], ij[0]]), 0
)
if self._colour is None:
RGB = XYZ_to_RGB(xy_to_XYZ(primaries), plotting_colourspace)
colour_g = np.array([RGB[0], RGB[1], RGB[1], RGB[2], RGB[2], RGB[0]])
else:
colour_g = np.tile(self._colour, (positions.shape[0], 1))
self._gamut = gfx.Line(
gfx.Geometry(
positions=as_contiguous_array(positions),
colors=as_contiguous_array(append_channel(colour_g, self._opacity)),
),
gfx.LineSegmentMaterial(thickness=self._thickness, color_mode="vertex"),
)
self.add(self._gamut)
ij = XYZ_to_ij(
xy_to_XYZ(self._colourspace.whitepoint),
plotting_colourspace.whitepoint,
)
positions = np.reshape(append_channel(ij, 0), (-1, 3))
if self._colour is None:
colour_w = np.reshape(
XYZ_to_RGB(
xy_to_XYZ(self._colourspace.whitepoint), plotting_colourspace
),
(-1, 3),
)
else:
colour_w = np.tile(self._colour, (positions.shape[0], 1))
self._whitepoint = gfx.Points(
gfx.Geometry(
positions=as_contiguous_array(positions),
sizes=as_contiguous_array(
full(positions.shape[0], self._thickness * 5)
),
colors=as_contiguous_array(append_channel(colour_w, self._opacity)),
),
gfx.PointsMaterial(
color_mode="vertex", size_mode="vertex", size_space="screen"
),
)
self.add(self._whitepoint)
[docs]
class VisualRGBColourspace3D(
MixinPropertyColourspace,
MixinPropertyModel,
MixinPropertyColour,
MixinPropertyOpacity,
MixinPropertyThickness,
MixinPropertyTypeMaterial,
MixinPropertyWireframe,
MixinPropertySegments,
MixinPropertySize,
MixinPropertyKwargs,
Visual,
):
"""
Create a 3D *RGB* colourspace volume visual.
Parameters
----------
colourspace
*RGB* colourspace to plot the gamut of. ``colourspaces`` elements
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.
colour
Colour of the visual, if *None*, the colour is computed from the visual
geometry.
opacity
Opacity of the visual.
thickness
Thickness of the visual lines.
material
Material used to surface the visual geometry.
wireframe
Whether to render the visual as a wireframe, i.e., only render edges.
segments
Edge segments count for the *RGB* colourspace cube.
size
Size of the underlying *RGB* colourspace cube; used for plotting HDR
related volumes.
Other Parameters
----------------
kwargs
See the documentation of the supported conversion definitions.
Attributes
----------
- :attr:`~colour_visuals.VisualRGBColourspace3D.colourspace`
- :attr:`~colour_visuals.VisualRGBColourspace3D.model`
- :attr:`~colour_visuals.VisualRGBColourspace3D.colour`
- :attr:`~colour_visuals.VisualRGBColourspace3D.opacity`
- :attr:`~colour_visuals.VisualRGBColourspace3D.thickness`
- :attr:`~colour_visuals.VisualRGBColourspace3D.type_material`
- :attr:`~colour_visuals.VisualRGBColourspace3D.wireframe`
- :attr:`~colour_visuals.VisualRGBColourspace3D.segments`
- :attr:`~colour_visuals.VisualRGBColourspace3D.size`
Methods
-------
- :meth:`~colour_visuals.VisualRGBColourspace3D.__init__`
- :meth:`~colour_visuals.VisualRGBColourspace3D.update`
Examples
--------
>>> import os
>>> import pylinalg as la
>>> from colour.utilities import suppress_stdout
>>> from wgpu.gui.auto import WgpuCanvas
>>> with suppress_stdout():
... canvas = WgpuCanvas(size=(960, 540))
... scene = gfx.Scene()
... scene.add(
... gfx.Background(
... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18]))
... )
... )
... visual = VisualRGBColourspace3D(wireframe=True)
... visual.local.rotation = la.quat_from_euler((-np.pi / 4, 0), order="XY")
... camera = gfx.PerspectiveCamera(50, 16 / 9)
... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25)
... scene.add(visual)
... if os.environ.get("CI") is None:
... gfx.show(scene, camera=camera, canvas=canvas)
.. image:: ../_static/Plotting_VisualRGBColourspace3D.png
:align: center
:alt: visual-rgbcolourspace-3-d
"""
[docs]
def __init__(
self,
colourspace: (
RGB_Colourspace
| str
| Sequence[RGB_Colourspace | LiteralRGBColourspace | str]
) = "sRGB",
model: LiteralColourspaceModel | str = "CIE xyY",
colour: ArrayLike | None = None,
opacity: float = 1,
material: Type[gfx.MeshAbstractMaterial] = gfx.MeshBasicMaterial,
wireframe: bool = False,
segments: int = 16,
size: float = 1,
**kwargs: Any,
) -> None:
super().__init__()
self._gamut = None
self._whitepoint = None
with self.block_update():
self.colourspace = colourspace
self.model = model
self.colour = colour
self.opacity = opacity
self.type_material = material
self.wireframe = wireframe
self.segments = segments
self.size = size
self.kwargs = kwargs
self.update()
[docs]
def update(self) -> None:
"""Update the visual."""
if self._is_update_blocked:
return
self.clear()
vertices, faces, outline = conform_primitive_dtype(
primitive_cube(
width_segments=self._segments,
height_segments=self._segments,
depth_segments=self._segments,
)
)
positions = (vertices["position"] + 0.5) * self._size
positions[positions == 0] = EPSILON
if self._colour is None:
# NOTE: The colours are normalised by the reciprocal of *self._size*
# to avoid unpleasant clipping when displaying HDR volumes, e.g.,
# the full ITU-R BT.2020 volume within ICtCp which is using a size
# of 10000.
colour = positions * (1.0 / self._size)
else:
colour = np.tile(self._colour, (positions.shape[0], 1))
positions = colourspace_model_axis_reorder(
XYZ_to_colourspace_model(
RGB_to_XYZ(positions, self._colourspace),
self._colourspace.whitepoint,
self._model,
**self._kwargs,
),
self._model,
)
self._gamut = gfx.Mesh(
gfx.Geometry(
positions=as_contiguous_array(positions),
normals=vertices["normal"],
indices=np.reshape(outline[..., 1], (-1, 4)),
colors=as_contiguous_array(append_channel(colour, self._opacity)),
),
(
self._type_material(color_mode="vertex", wireframe=self._wireframe)
if self._wireframe
else self._type_material(color_mode="vertex")
),
)
self.add(self._gamut)
if __name__ == "__main__":
scene = gfx.Scene()
scene.add(
gfx.Background(None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])))
)
light_1 = gfx.AmbientLight()
scene.add(light_1)
light_2 = gfx.DirectionalLight()
light_2.local.position = np.array([1, 1, 0])
scene.add(light_2)
visual_1 = VisualRGBColourspace3D()
scene.add(visual_1)
visual_2 = VisualRGBColourspace3D(wireframe=True)
visual_2.local.position = np.array([0.5, 0, 0])
scene.add(visual_2)
visual_3 = VisualRGBColourspace3D(material=gfx.MeshNormalMaterial)
visual_3.local.position = np.array([1, 0, 0])
scene.add(visual_3)
visual_4 = VisualRGBColourspace3D(
model="CIE Lab",
colour=np.array([0.5, 0.5, 0.5]),
opacity=1,
material=gfx.MeshStandardMaterial,
)
visual_4.local.position = np.array([2.5, 0, 0])
scene.add(visual_4)
visual_5 = VisualRGBColourspace2D()
visual_5.local.position = np.array([3.5, 0, 0])
scene.add(visual_5)
visual_6 = VisualRGBColourspace2D(
method="CIE 1976 UCS",
colour=np.array([0.5, 0.5, 0.5]),
opacity=1,
)
visual_6.local.position = np.array([4.5, 0, 0])
scene.add(visual_6)
visual_7 = VisualRGBColourspace3D(model="RGB")
visual_7.local.position = np.array([5.5, 0, 0])
scene.add(visual_7)
gfx.show(scene, up=np.array([0, 0, 1]))