From 21a0ef81beb09c2ab5acbdb62ee92f518b4b5ab2 Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 8 Oct 2023 12:06:36 +1300 Subject: [PATCH] Add initial visuals and chromaticity diagram inspector. Signed-off-by: Thomas Mansencal --- src/apps/ocioview/ocioview/constants.py | 5 +- .../ocioview/ocioview/inspect/__init__.py | 2 + .../inspect/chromaticities_inspector.py | 101 ++++++ .../ocioview/inspect/visuals/__init__.py | 8 + .../ocioview/inspect/visuals/common.py | 127 ++++++++ .../ocioview/inspect/visuals/diagrams.py | 296 ++++++++++++++++++ .../ocioview/ocioview/inspect/visuals/grid.py | 209 +++++++++++++ .../inspect/visuals/rgb_colorspace.py | 206 ++++++++++++ .../ocioview/inspect/visuals/rgb_scatter.py | 109 +++++++ src/apps/ocioview/ocioview/inspect_dock.py | 10 +- 10 files changed, 1070 insertions(+), 3 deletions(-) create mode 100644 src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py create mode 100644 src/apps/ocioview/ocioview/inspect/visuals/__init__.py create mode 100644 src/apps/ocioview/ocioview/inspect/visuals/common.py create mode 100644 src/apps/ocioview/ocioview/inspect/visuals/diagrams.py create mode 100644 src/apps/ocioview/ocioview/inspect/visuals/grid.py create mode 100644 src/apps/ocioview/ocioview/inspect/visuals/rgb_colorspace.py create mode 100644 src/apps/ocioview/ocioview/inspect/visuals/rgb_scatter.py diff --git a/src/apps/ocioview/ocioview/constants.py b/src/apps/ocioview/ocioview/constants.py index 18b7f0909e..2414b4414d 100644 --- a/src/apps/ocioview/ocioview/constants.py +++ b/src/apps/ocioview/ocioview/constants.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright Contributors to the OpenColorIO Project. +import numpy as np from pathlib import Path - from PySide6 import QtCore, QtGui @@ -33,3 +33,6 @@ # Value edit array component label sets RGB = ("r", "g", "b") RGBA = tuple(list(RGB) + ["a"]) + +DEFAULT_FLOAT_DTYPE = np.float32 +DEFAULT_INT_DTYPE = np.uint32 diff --git a/src/apps/ocioview/ocioview/inspect/__init__.py b/src/apps/ocioview/ocioview/inspect/__init__.py index 6d5f42f92c..6e021885bb 100644 --- a/src/apps/ocioview/ocioview/inspect/__init__.py +++ b/src/apps/ocioview/ocioview/inspect/__init__.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright Contributors to the OpenColorIO Project. +from .chromaticities_inspector import ChromaticitiesInspector from .code_inspector import CodeInspector +from .curve_inspector import CurveInspector from .log_inspector import LogInspector diff --git a/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py b/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py new file mode 100644 index 0000000000..db3fd81cc9 --- /dev/null +++ b/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. + +import numpy as np +import pygfx as gfx +import OpenImageIO as oiio +import PyOpenColorIO as ocio +from PySide6 import QtCore, QtGui, QtWidgets +from typing import Optional +from wgpu.gui.qt import WgpuWidget + +from ..message_router import MessageRouter +from ..utils import get_glyph_icon +from .visuals import VisualChromaticityDiagramCIE1931, VisualGrid + + +class ChromaticitiesInspector(QtWidgets.QWidget): + @classmethod + def label(cls) -> str: + return "Chromaticities" + + @classmethod + def icon(cls) -> QtGui.QIcon: + return get_glyph_icon("mdi6.grain") + + def __init__(self, parent: Optional[QtCore.QObject] = None): + super().__init__(parent=parent) + + self._cpu_proc = None + self._image_buf = None + + # Scene + self.canvas = WgpuWidget(parent=self) + self.renderer = gfx.WgpuRenderer(self.canvas) + self.scene = gfx.Scene() + self.grid = VisualGrid() + self.grid.local.position = np.array([0, 0, -1e-3]) + self.chromaticity_diagram = VisualChromaticityDiagramCIE1931( + kwargs_visual_chromaticity_diagram={"opacity": 0.25} + ) + self.scene.add(self.grid, self.chromaticity_diagram) + self.camera = gfx.PerspectiveCamera(45) + self.camera.show_object( + self.chromaticity_diagram, up=np.array([0, 0, 1]), scale=1.5 + ) + self.controller = gfx.OrbitController(register_events=self.renderer) + self.controller.add_camera(self.camera) + + self.canvas.request_draw(self.draw) + + # Layout + layout = QtWidgets.QHBoxLayout() + self.setLayout(layout) + layout.addWidget(self.canvas) + + msg_router = MessageRouter.get_instance() + # msg_router.processor_ready.connect(self._on_processor_ready) + # msg_router.image_ready.connect(self._on_image_ready) + + def draw(self): + self.renderer.render(self.scene, self.camera) + + def reset(self) -> None: + raise NotImplementedError() + + def showEvent(self, event: QtGui.QShowEvent) -> None: + """Start listening for processor updates, if visible.""" + super().showEvent(event) + + msg_router = MessageRouter.get_instance() + # msg_router.set_processor_updates_allowed(True) + # msg_router.set_image_updates_allowed(True) + + def hideEvent(self, event: QtGui.QHideEvent) -> None: + """Stop listening for processor updates, if not visible.""" + super().hideEvent(event) + + msg_router = MessageRouter.get_instance() + # msg_router.set_processor_updates_allowed(False) + # msg_router.set_image_updates_allowed(False) + + @QtCore.Slot(ocio.CPUProcessor) + def _on_processor_ready(self, cpu_proc: ocio.CPUProcessor) -> None: + self._cpu_proc = cpu_proc + + print("_on_processor_ready") + + @QtCore.Slot(np.ndarray) + def _on_image_ready(self, image_buf: oiio.ImageBuf) -> None: + self._image_buf = image_buf + + print("_on_image_ready") + + +if __name__ == "__main__": + application = QtWidgets.QApplication([]) + chromaticity_inspector = ChromaticitiesInspector() + chromaticity_inspector.resize(800, 600) + chromaticity_inspector.show() + + application.exec() diff --git a/src/apps/ocioview/ocioview/inspect/visuals/__init__.py b/src/apps/ocioview/ocioview/inspect/visuals/__init__.py new file mode 100644 index 0000000000..1fb9c25aa6 --- /dev/null +++ b/src/apps/ocioview/ocioview/inspect/visuals/__init__.py @@ -0,0 +1,8 @@ +from .diagrams import ( + VisualChromaticityDiagramCIE1931, + VisualChromaticityDiagramCIE1960UCS, + VisualChromaticityDiagramCIE1976UCS, +) +from .grid import VisualGrid +from .rgb_colorspace import VisualRGBColorspace2D, VisualRGBColorspace3D +from .rgb_scatter import VisualRGBScatter3d diff --git a/src/apps/ocioview/ocioview/inspect/visuals/common.py b/src/apps/ocioview/ocioview/inspect/visuals/common.py new file mode 100644 index 0000000000..d655c27a5f --- /dev/null +++ b/src/apps/ocioview/ocioview/inspect/visuals/common.py @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. + +""" +Common Utilities +================ + +Defines the common utilities objects that don't fall in any specific category. +""" + +import numpy as np +from colour import ( + Luv_to_uv, + Luv_uv_to_xy, + UCS_to_uv, + UCS_uv_to_xy, + XYZ_to_Jzazbz, + XYZ_to_Luv, + XYZ_to_OSA_UCS, + XYZ_to_UCS, + XYZ_to_xy, + convert, + xy_to_XYZ, +) +from colour.hints import ArrayLike, NDArray, NDArrayFloat, NDArrayInt, Tuple +from colour.utilities import full + +from ocioview.constants import DEFAULT_FLOAT_DTYPE, DEFAULT_INT_DTYPE + +__all__ = [ + "METHODS_CHROMATICITY_DIAGRAM", + "XYZ_to_colourspace_model", + "as_float_array", + "as_int_array", + "as_contiguous_array", + "conform_primitive_dtype", + "append_alpha_channel", +] + +METHODS_CHROMATICITY_DIAGRAM = { + "CIE 1931": { + "XYZ_to_ij": lambda a, i: XYZ_to_xy(a), + "ij_to_XYZ": lambda a, i: xy_to_XYZ(a), + }, + "CIE 1960 UCS": { + "XYZ_to_ij": lambda a, i: UCS_to_uv(XYZ_to_UCS(a)), + "ij_to_XYZ": lambda a, i: xy_to_XYZ(UCS_uv_to_xy(a)), + }, + "CIE 1976 UCS": { + "XYZ_to_ij": lambda a, i: Luv_to_uv(XYZ_to_Luv(a, i), i), + "ij_to_XYZ": lambda a, i: xy_to_XYZ(Luv_uv_to_xy(a)), + }, +} +""" +Chromaticity diagram specific helper conversion objects. +""" + + +def XYZ_to_colourspace_model( + XYZ: ArrayLike, illuminant: ArrayLike, model: str, **kwargs +) -> NDArray: + """ + Converts from *CIE XYZ* tristimulus values to given colourspace model while + normalising for visual convenience some of the models. + """ + + ijk = convert( + XYZ, + "CIE XYZ", + model, + illuminant=illuminant, + verbose={"mode": "Short"}, + **kwargs + ) + + # TODO: ICtCp? + if model == "JzAzBz": + ijk /= XYZ_to_Jzazbz([1, 1, 1])[0] + elif model == "OSA UCS": + ijk /= XYZ_to_OSA_UCS([1, 1, 1])[0] + + return ijk + + +def as_float_array(a: ArrayLike) -> NDArrayFloat: + from colour.utilities import as_float_array + + return as_float_array(a, DEFAULT_FLOAT_DTYPE) + + +def as_int_array(a: ArrayLike) -> NDArrayInt: + from colour.utilities import as_int_array + + return as_int_array(a, DEFAULT_INT_DTYPE) + + +def as_contiguous_array(a, dtype=DEFAULT_FLOAT_DTYPE): + return np.ascontiguousarray(a.astype(dtype)) + + +def conform_primitive_dtype( + primitive: Tuple[NDArray, NDArray, NDArray] +) -> Tuple[NDArray, NDArray, NDArray]: + """ + Conform the given primitive to the required dtype. + """ + + vertices, faces, outline = primitive + + return ( + vertices.astype( + [ + ("position", DEFAULT_FLOAT_DTYPE, (3,)), + ("uv", DEFAULT_FLOAT_DTYPE, (2,)), + ("normal", DEFAULT_FLOAT_DTYPE, (3,)), + ("colour", DEFAULT_FLOAT_DTYPE, (4,)), + ] + ), + faces.astype(DEFAULT_INT_DTYPE), + outline.astype(DEFAULT_INT_DTYPE), + ) + + +def append_alpha_channel(a: ArrayLike, alpha: float = 1) -> NDArray: + a = np.copy(a) + + return np.hstack([a, full(list(a.shape[:-1]) + [1], alpha, dtype=a.dtype)]) diff --git a/src/apps/ocioview/ocioview/inspect/visuals/diagrams.py b/src/apps/ocioview/ocioview/inspect/visuals/diagrams.py new file mode 100644 index 0000000000..01d7625a0b --- /dev/null +++ b/src/apps/ocioview/ocioview/inspect/visuals/diagrams.py @@ -0,0 +1,296 @@ +# !/usr/bin/env python +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. + +""" +Chromaticity Diagram Visuals +============================ + +Defines the *chromaticity diagram* visuals: + +- :class:`VisualChromaticityDiagramCIE1931` +- :class:`VisualChromaticityDiagramCIE1960UCS` +- :class:`VisualChromaticityDiagramCIE1976UCS` +""" + +import numpy as np +import pygfx as gfx +from colour.algebra import euclidean_distance, normalise_maximum +from colour.hints import ArrayLike +from colour.plotting import ( + CONSTANTS_COLOUR_STYLE, + XYZ_to_plotting_colourspace, + filter_cmfs, +) +from colour.utilities import first_item, optional, tstack +from scipy.spatial import Delaunay +from typing import Optional, Type +from ocioview.inspect.visuals.common import ( + METHODS_CHROMATICITY_DIAGRAM, + append_alpha_channel, + as_contiguous_array, +) +from ocioview.constants import ( + DEFAULT_FLOAT_DTYPE, + DEFAULT_INT_DTYPE, +) + +__all__ = [ + "VisualSpectralLocus", + "VisualChromaticityDiagram", + "VisualChromaticityDiagramCIE1931", + "VisualChromaticityDiagramCIE1960UCS", + "VisualChromaticityDiagramCIE1976UCS", +] + + +class VisualSpectralLocus(gfx.Line): + """ + Create a *spectral locus* visual. + """ + + def __init__( + self, + cmfs: str = "CIE 1931 2 Degree Standard Observer", + method: str = "CIE 1931", + colors: Optional[ArrayLike] = None, + opacity: float = 1, + thickness: float = 1, + ): + cmfs = first_item(filter_cmfs(cmfs).values()) + + illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint + + XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[method]["XYZ_to_ij"] + ij_to_XYZ = METHODS_CHROMATICITY_DIAGRAM[method]["ij_to_XYZ"] + + # CMFS + ij_l = XYZ_to_ij(cmfs.values, illuminant) + ij_l = np.concatenate([ij_l[:-1], ij_l[1:]], axis=1).reshape(-1, 2) + + # Line of Purples + ij_p = tstack( + [ + np.linspace(ij_l[0][0], ij_l[-1][0], 20), + np.linspace(ij_l[0][1], ij_l[-1][1], 20), + ] + ) + ij_p = np.concatenate([ij_p[:-1], ij_p[1:]], axis=1).reshape(-1, 2) + + ij = np.vstack([ij_l, ij_p]) + + positions = np.hstack( + [ij, np.full((ij.shape[0], 1), 0, DEFAULT_FLOAT_DTYPE)] + ) + + if colors is None: + colors = normalise_maximum( + XYZ_to_plotting_colourspace( + ij_to_XYZ(positions[..., :2], illuminant), illuminant + ), + axis=-1, + ) + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + geometry = gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_alpha_channel(colors, opacity)), + ) + + super().__init__( + geometry, + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + + +class VisualChromaticityDiagram(gfx.Mesh): + """ + Create a *chromaticity diagram* visual. + """ + + def __init__( + self, + samples=64, + cmfs: str = "CIE 1931 2 Degree Standard Observer", + method: str = "CIE 1931", + material: Type[gfx.MeshAbstractMaterial] = gfx.MeshBasicMaterial, + colors: Optional[ArrayLike] = None, + opacity: float = 1, + wireframe: bool = False, + ): + cmfs = first_item(filter_cmfs(cmfs).values()) + + illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint + + XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[method]["XYZ_to_ij"] + ij_to_XYZ = METHODS_CHROMATICITY_DIAGRAM[method]["ij_to_XYZ"] + + # CMFS + ij_l = XYZ_to_ij(cmfs.values, illuminant) + + # Line of Purples + d = euclidean_distance(ij_l[0], ij_l[-1]) + ij_p = tstack( + [ + np.linspace(ij_l[0][0], ij_l[-1][0], int(d * samples)), + np.linspace(ij_l[0][1], ij_l[-1][1], int(d * samples)), + ] + ) + + # Grid + triangulation = Delaunay(ij_l, qhull_options="QJ") + samples = np.linspace(0, 1, samples) + ii_g, jj_g = np.meshgrid(samples, samples) + ij_g = tstack([ii_g, jj_g]) + ij_g = ij_g[triangulation.find_simplex(ij_g) > 0] + + ij = np.vstack([ij_l, illuminant, ij_p, ij_g]) + triangulation = Delaunay(ij, qhull_options="QJ") + positions = np.hstack( + [ij, np.full((ij.shape[0], 1), 0, DEFAULT_FLOAT_DTYPE)] + ) + + if colors is None: + colors = normalise_maximum( + XYZ_to_plotting_colourspace( + ij_to_XYZ(positions[..., :2], illuminant), illuminant + ), + axis=-1, + ) + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + geometry = gfx.Geometry( + positions=as_contiguous_array(positions), + indices=as_contiguous_array( + triangulation.simplices, DEFAULT_INT_DTYPE + ), + colors=as_contiguous_array(append_alpha_channel(colors, opacity)), + ) + + super().__init__( + geometry, + material(color_mode="vertex", wireframe=wireframe) + if wireframe + else material(color_mode="vertex"), + ) + + +class VisualChromaticityDiagramCIE1931(gfx.Group): + """ + Create the *CIE 1931* chromaticity diagram visual. + """ + + def __init__( + self, + kwargs_visual_chromaticity_diagram: Optional[dict] = None, + kwargs_visual_spectral_locus: Optional[dict] = None, + ): + super().__init__() + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1931", + **(optional(kwargs_visual_chromaticity_diagram, {})) + ) + self.add(self._chromaticity_diagram) + + self._spectral_locus = VisualSpectralLocus( + method="CIE 1931", **(optional(kwargs_visual_spectral_locus, {})) + ) + self.add(self._spectral_locus) + + +class VisualChromaticityDiagramCIE1960UCS(gfx.Group): + """ + Create the *CIE 1960 UCS* chromaticity diagram visual. + """ + + def __init__( + self, + kwargs_visual_chromaticity_diagram: Optional[dict] = None, + kwargs_visual_spectral_locus: Optional[dict] = None, + ): + super().__init__() + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1960 UCS", + **(optional(kwargs_visual_chromaticity_diagram, {})) + ) + self.add(self._chromaticity_diagram) + + self._spectral_locus = VisualSpectralLocus( + method="CIE 1960 UCS", + **(optional(kwargs_visual_spectral_locus, {})) + ) + self.add(self._spectral_locus) + + +class VisualChromaticityDiagramCIE1976UCS(gfx.Group): + """ + Create the *CIE 1976 UCS* chromaticity diagram visual. + """ + + def __init__( + self, + kwargs_visual_chromaticity_diagram: Optional[dict] = None, + kwargs_visual_spectral_locus: Optional[dict] = None, + ): + super().__init__() + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1976 UCS", + **(optional(kwargs_visual_chromaticity_diagram, {})) + ) + self.add(self._chromaticity_diagram) + + self._spectral_locus = VisualSpectralLocus( + method="CIE 1976 UCS", + **(optional(kwargs_visual_spectral_locus, {})) + ) + self.add(self._spectral_locus) + + +if __name__ == "__main__": + from pygfx import ( + Background, + BackgroundMaterial, + Display, + Scene, + ) + + scene = Scene() + + scene.add( + Background(None, BackgroundMaterial(np.array([0.18, 0.18, 0.18]))) + ) + + mesh_1 = VisualChromaticityDiagramCIE1931() + scene.add(mesh_1) + + mesh_2 = VisualChromaticityDiagramCIE1931( + kwargs_visual_chromaticity_diagram={"wireframe": True, "opacity": 0.5} + ) + mesh_2.local.position = np.array([1, 0, 0]) + scene.add(mesh_2) + + mesh_3 = VisualChromaticityDiagramCIE1931( + kwargs_visual_chromaticity_diagram={"colors": [0.36, 0.36, 0.36]} + ) + mesh_3.local.position = np.array([2, 0, 0]) + scene.add(mesh_3) + + mesh_4 = VisualChromaticityDiagramCIE1960UCS() + mesh_4.local.position = np.array([3, 0, 0]) + scene.add(mesh_4) + + mesh_5 = VisualChromaticityDiagramCIE1976UCS() + mesh_5.local.position = np.array([4, 0, 0]) + scene.add(mesh_5) + + mesh_6 = VisualSpectralLocus(colors=[0.36, 0.36, 0.36]) + mesh_6.local.position = np.array([5, 0, 0]) + scene.add(mesh_6) + + display = Display() + display.show(scene, up=np.array([0, 0, 1])) diff --git a/src/apps/ocioview/ocioview/inspect/visuals/grid.py b/src/apps/ocioview/ocioview/inspect/visuals/grid.py new file mode 100644 index 0000000000..6d755ff43e --- /dev/null +++ b/src/apps/ocioview/ocioview/inspect/visuals/grid.py @@ -0,0 +1,209 @@ +# !/usr/bin/env python +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. + +""" +Grid Visuals +============ + +Defines the grid visuals: + +- :class:`VisualGrid` +""" + +import numpy as np +import pygfx as gfx +from colour.hints import ArrayLike +from colour.geometry import primitive_grid +from ocioview.inspect.visuals.common import ( + DEFAULT_FLOAT_DTYPE, + append_alpha_channel, + as_contiguous_array, + conform_primitive_dtype, +) + +__all__ = ["VisualGrid"] + + +class VisualGrid(gfx.Group): + """ + Create a *RGB* 3D scatter visual. + """ + + def __init__( + self, + size: int = 20, + major_grid_color: ArrayLike = np.array([0.5, 0.5, 0.5]), + minor_grid_color: ArrayLike = np.array([0.25, 0.25, 0.25]), + major_tick_labels=True, + major_tick_label_color: ArrayLike = np.array([0.75, 0.75, 0.75]), + major_tick_label_font_size: float = 1, + minor_tick_labels=True, + minor_tick_label_color: ArrayLike = np.array([0.5, 0.5, 0.5]), + minor_tick_label_font_size: float = 0.75, + ): + super().__init__() + + size = int(size) + + vertices, faces, outline = conform_primitive_dtype( + primitive_grid( + width_segments=size, + height_segments=size, + ) + ) + positions = vertices["position"] + + self._grid_major = gfx.Mesh( + gfx.Geometry( + positions=as_contiguous_array(positions), + indices=outline[..., 1].reshape([-1, 4]), + colors=as_contiguous_array( + append_alpha_channel( + np.tile(major_grid_color, (positions.shape[0], 1)), 1 + ) + ), + ), + gfx.MeshBasicMaterial(color_mode="vertex", wireframe=True), + ) + self._grid_major.local.scale = np.array([size, size, 1]) + self.add(self._grid_major) + + vertices, faces, outline = conform_primitive_dtype( + primitive_grid( + width_segments=size * 10, + height_segments=size * 10, + ) + ) + positions = vertices["position"] + + self._grid_minor = gfx.Mesh( + gfx.Geometry( + positions=as_contiguous_array(positions), + indices=outline[..., 1].reshape([-1, 4]), + colors=as_contiguous_array( + append_alpha_channel( + np.tile(minor_grid_color, (positions.shape[0], 1)), 1 + ) + ), + ), + gfx.MeshBasicMaterial(color_mode="vertex", wireframe=True), + ) + self._grid_minor.local.position = np.array([0, 0, -1e-3]) + self._grid_minor.local.scale = np.array([size, size, 1]) + self.add(self._grid_minor) + + axes_positions = np.array( + [ + [0, 0, 0], + [1, 0, 0], + [0, 0, 0], + [0, 1, 0], + ], + dtype=DEFAULT_FLOAT_DTYPE, + ) + axes_positions *= size / 2 + + axes_colors = np.array( + [ + [1, 0, 0, 1], + [1, 0, 0, 1], + [0, 1, 0, 1], + [0, 1, 0, 1], + ], + dtype=DEFAULT_FLOAT_DTYPE, + ) + + self._axes_helper = gfx.Line( + gfx.Geometry(positions=axes_positions, colors=axes_colors), + gfx.LineSegmentMaterial(color_mode="vertex", thickness=2), + ) + self.add(self._axes_helper) + + if major_tick_labels: + self._ticks_major_x, self._ticks_major_y = [], [] + for i in np.arange(-size // 2, size // 2 + 1, 1): + x_text = gfx.Text( + gfx.TextGeometry( + f"{i} " if i == 0 else str(i), + font_size=major_tick_label_font_size * 10, + screen_space=True, + anchor="Top-Right" if i == 0 else "Top-Center", + ), + gfx.TextMaterial(color=major_tick_label_color), + ) + x_text.local.position = np.array([i, 0, 1e-3]) + self.add(x_text) + self._ticks_major_x.append(x_text) + + if i == 0: + continue + + y_text = gfx.Text( + gfx.TextGeometry( + f"{i} ", + font_size=major_tick_label_font_size * 10, + screen_space=True, + anchor="Center-Right", + ), + gfx.TextMaterial(color=major_tick_label_color), + ) + y_text.local.position = np.array([0, i, 1e-3]) + self.add(y_text) + self._ticks_major_y.append(y_text) + + if minor_tick_labels: + self._ticks_minor_x, self._ticks_minor_y = [], [] + for i in np.arange(-size // 2, size // 2 + 0.1, 0.1): + if np.around(i, 0) == np.around(i, 1): + continue + + i = np.around(i, 1) + + x_text = gfx.Text( + gfx.TextGeometry( + f"{i} " if i == 0 else str(i), + font_size=minor_tick_label_font_size * 10, + screen_space=True, + anchor="Top-Right" if i == 0 else "Top-Center", + ), + gfx.TextMaterial(color=minor_tick_label_color), + ) + x_text.local.position = np.array([i, 0, 1e-3]) + self.add(x_text) + self._ticks_minor_x.append(x_text) + + if i == 0: + continue + + y_text = gfx.Text( + gfx.TextGeometry( + f"{i} ", + font_size=minor_tick_label_font_size * 10, + screen_space=True, + anchor="Center-Right", + ), + gfx.TextMaterial(color=minor_tick_label_color), + ) + y_text.local.position = np.array([0, i, 1e-3]) + self.add(y_text) + self._ticks_minor_y.append(y_text) + + +if __name__ == "__main__": + from pygfx import ( + Background, + Display, + BackgroundMaterial, + Scene, + ) + + scene = Scene() + + scene.add(Background(None, BackgroundMaterial(np.array([0, 0, 0])))) + + grid_1 = VisualGrid() + scene.add(grid_1) + + display = Display() + display.show(scene, up=np.array([0, 0, 1])) diff --git a/src/apps/ocioview/ocioview/inspect/visuals/rgb_colorspace.py b/src/apps/ocioview/ocioview/inspect/visuals/rgb_colorspace.py new file mode 100644 index 0000000000..a62debc4e0 --- /dev/null +++ b/src/apps/ocioview/ocioview/inspect/visuals/rgb_colorspace.py @@ -0,0 +1,206 @@ +# !/usr/bin/env python +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. + +""" +RGB Colorspace Visuals +======================= + +Defines the *RGB colorspace* visuals: + +- :class:`VisualRGBColorspace2D` +- :class:`VisualRGBColorspace3D` +""" + +import numpy as np +import pygfx as gfx +from colour import RGB_to_XYZ, XYZ_to_RGB, xy_to_XYZ +from colour.constants import EPSILON +from colour.geometry import primitive_cube +from colour.hints import ArrayLike +from colour.plotting import ( + CONSTANTS_COLOUR_STYLE, + colourspace_model_axis_reorder, + filter_RGB_colourspaces, +) +from colour.utilities import first_item +from typing import Optional, Type + +from ocioview.inspect.visuals.common import ( + METHODS_CHROMATICITY_DIAGRAM, + XYZ_to_colourspace_model, + append_alpha_channel, + as_contiguous_array, + conform_primitive_dtype, +) + +__all__ = [ + "VisualRGBColorspace2D", + "VisualRGBColorspace3D", +] +class VisualRGBColorspace2D(gfx.Line): + """ + Create a *RGB* colorspace 2D gamut visual. + """ + + def __init__( + self, + colorspace: str = "ITU-R BT.709", + chromaticity_diagram: str = "CIE 1931", + colors: Optional[ArrayLike] = None, + opacity: float = 0.5, + thickness: float = 1, + ): + colorspace = first_item(filter_RGB_colourspaces(colorspace).values()) + + plotting_colorspace = CONSTANTS_COLOUR_STYLE.colour.colourspace + + XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[chromaticity_diagram][ + "XYZ_to_ij" + ] + + ij = XYZ_to_ij( + xy_to_XYZ(colorspace.primaries), plotting_colorspace.whitepoint + ) + ij[np.isnan(ij)] = 0 + + positions = append_alpha_channel( + np.array([ij[0], ij[1], ij[1], ij[2], ij[2], ij[0]]), 0 + ) + + if colors is None: + RGB = XYZ_to_RGB( + xy_to_XYZ(colorspace.primaries), plotting_colorspace + ) + colors = np.array([RGB[0], RGB[1], RGB[1], RGB[2], RGB[2], RGB[0]]) + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + geometry = gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_alpha_channel(colors, opacity)), + ) + + super().__init__( + geometry, + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + + +class VisualRGBColorspace3D(gfx.Mesh): + """ + Create a *RGB* colorspace 3D volume visual. + """ + + def __init__( + self, + colorspace: str = "ITU-R BT.709", + colorspace_model: str = "CIE xyY", + segments: int = 16, + material: Type[gfx.MeshAbstractMaterial] = gfx.MeshBasicMaterial, + colors: Optional[ArrayLike] = None, + opacity: float = 0.5, + wireframe: bool = False, + ): + colorspace = first_item(filter_RGB_colourspaces(colorspace).values()) + + vertices, faces, outline = conform_primitive_dtype( + primitive_cube( + width_segments=segments, + height_segments=segments, + depth_segments=segments, + ) + ) + + positions = vertices["position"] + 0.5 + + if colors is None: + colors = positions + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + positions[positions == 0] = EPSILON + XYZ = RGB_to_XYZ(positions, colorspace) + positions = colourspace_model_axis_reorder( + XYZ_to_colourspace_model( + XYZ, colorspace.whitepoint, colorspace_model + ), + colorspace_model, + ) + + geometry = gfx.Geometry( + positions=as_contiguous_array(positions), + normals=vertices["normal"], + indices=outline[..., 1].reshape([-1, 4]), + colors=as_contiguous_array(append_alpha_channel(colors, opacity)), + ) + + super().__init__( + geometry, + material(color_mode="vertex", wireframe=wireframe) + if wireframe + else material(color_mode="vertex"), + ) + + + + +if __name__ == "__main__": + from pygfx import ( + AmbientLight, + Background, + BackgroundMaterial, + DirectionalLight, + Display, + MeshStandardMaterial, + MeshNormalMaterial, + Scene, + ) + + scene = Scene() + + scene.add( + Background(None, BackgroundMaterial(np.array([0.18, 0.18, 0.18]))) + ) + + light_1 = AmbientLight() + scene.add(light_1) + + light_2 = DirectionalLight() + light_2.local.position = np.array([1, 1, 0]) + scene.add(light_2) + + mesh_1 = VisualRGBColorspace3D() + scene.add(mesh_1) + + mesh_2 = VisualRGBColorspace3D(wireframe=True) + mesh_2.local.position = np.array([0.5, 0, 0]) + scene.add(mesh_2) + + mesh_3 = VisualRGBColorspace3D(material=MeshNormalMaterial) + mesh_3.local.position = np.array([1, 0, 0]) + scene.add(mesh_3) + + mesh_4 = VisualRGBColorspace3D( + colorspace_model="CIE Lab", + colors=np.array([0.36, 0.36, 0.36]), + opacity=1, + material=MeshStandardMaterial, + ) + mesh_4.local.position = np.array([2.5, 0, 0]) + scene.add(mesh_4) + + mesh_5 = VisualRGBColorspace2D() + mesh_5.local.position = np.array([3.5, 0, 0]) + scene.add(mesh_5) + + mesh_6 = VisualRGBColorspace2D( + chromaticity_diagram="CIE 1976 UCS", + colors=np.array([0.36, 0.36, 0.36]), + opacity=1, + ) + mesh_6.local.position = np.array([4.5, 0, 0]) + scene.add(mesh_6) + + display = Display() + display.show(scene, up=np.array([0, 0, 1])) diff --git a/src/apps/ocioview/ocioview/inspect/visuals/rgb_scatter.py b/src/apps/ocioview/ocioview/inspect/visuals/rgb_scatter.py new file mode 100644 index 0000000000..5c27e471b7 --- /dev/null +++ b/src/apps/ocioview/ocioview/inspect/visuals/rgb_scatter.py @@ -0,0 +1,109 @@ +# !/usr/bin/env python +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. + +""" +RGB Scatter Visuals +=================== + +Defines the *RGB* scatter visuals: + +- :class:`VisualRGBScatter3d` +""" + +import numpy as np +import pygfx as gfx +from colour import RGB_to_XYZ +from colour.constants import EPSILON +from colour.hints import ArrayLike +from colour.plotting import ( + colourspace_model_axis_reorder, + filter_RGB_colourspaces, +) +from colour.utilities import first_item +from typing import Optional, Type + +from ocioview.inspect.visuals.common import ( + XYZ_to_colourspace_model, + as_float_array, + as_contiguous_array, + append_alpha_channel, +) + +__all__ = ["VisualRGBScatter3d"] + + +class VisualRGBScatter3d(gfx.Points): + """ + Create a *RGB* 3D scatter visual. + """ + + def __init__( + self, + RGB: ArrayLike, + colourspace: str = "ITU-R BT.709", + colourspace_model: str = "CIE xyY", + size: float = 3.0, + material: Type[gfx.MeshAbstractMaterial] = gfx.PointsMaterial, + colors: Optional[ArrayLike] = None, + opacity: float = 0.5, + ): + colourspace = first_item(filter_RGB_colourspaces(colourspace).values()) + + RGB = as_float_array(RGB).reshape(-1, 3) + + RGB[RGB == 0] = EPSILON + + XYZ = RGB_to_XYZ(RGB, colourspace) + + positions = colourspace_model_axis_reorder( + XYZ_to_colourspace_model( + XYZ, colourspace.whitepoint, colourspace_model + ), + colourspace_model, + ) + + if colors is None: + colors = RGB + else: + colors = np.tile(colors, (RGB.shape[0], 1)) + + geometry = gfx.Geometry( + positions=as_contiguous_array(positions), + sizes=as_contiguous_array(np.full(RGB.shape[0], size)), + colors=as_contiguous_array(append_alpha_channel(colors, opacity)), + ) + + super().__init__( + geometry, + material(color_mode="vertex", vertex_sizes=True) + if material is gfx.PointsMaterial + else material(), + ) + + +if __name__ == "__main__": + from pygfx import ( + Background, + Display, + BackgroundMaterial, + Scene, + ) + + scene = Scene() + + scene.add( + Background(None, BackgroundMaterial(np.array([0.18, 0.18, 0.18]))) + ) + + scatter_1 = VisualRGBScatter3d(np.random.random((64, 64, 3))) + scene.add(scatter_1) + + scatter_2 = VisualRGBScatter3d( + np.random.random((64, 64, 3)), colors=np.array([0.36, 0.36, 0.36]) + ) + scatter_2.local.position = np.array([0.5, 0, 0]) + scene.add(scatter_2) + + display = Display() + display.show(scene, up=np.array([0, 0, 1])) diff --git a/src/apps/ocioview/ocioview/inspect_dock.py b/src/apps/ocioview/ocioview/inspect_dock.py index 39c352cb11..c9494c6dec 100644 --- a/src/apps/ocioview/ocioview/inspect_dock.py +++ b/src/apps/ocioview/ocioview/inspect_dock.py @@ -5,8 +5,7 @@ from PySide6 import QtCore, QtWidgets -from .inspect.curve_inspector import CurveInspector -from .inspect import LogInspector, CodeInspector +from .inspect import ChromaticitiesInspector, CodeInspector, CurveInspector, LogInspector from .utils import get_glyph_icon from .widgets.structure import TabbedDockWidget @@ -26,11 +25,17 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): self.tabs.setTabPosition(QtWidgets.QTabWidget.West) # Widgets + # self.chromaticities_inspector = ChromaticitiesInspector() self.curve_inspector = CurveInspector() self.code_inspector = CodeInspector() self.log_inspector = LogInspector() # Layout + # self.add_tab( + # self.chromaticities_inspector, + # self.chromaticities_inspector.label(), + # self.chromaticities_inspector.icon(), + # ) self.add_tab( self.curve_inspector, self.curve_inspector.label(), @@ -49,6 +54,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): def reset(self) -> None: """Reset data for all inspectors.""" + # self.chromaticities_inspector.reset() self.curve_inspector.reset() self.code_inspector.reset() self.log_inspector.reset()