Skip to content

Commit

Permalink
Implement initial chromaticity inspector.
Browse files Browse the repository at this point in the history
  • Loading branch information
KelSolaar committed May 12, 2024
1 parent 5e9182c commit ccd788d
Show file tree
Hide file tree
Showing 9 changed files with 608 additions and 14 deletions.
1 change: 1 addition & 0 deletions src/apps/ocioview/ocioview/inspect/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +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
390 changes: 390 additions & 0 deletions src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright Contributors to the OpenColorIO Project.

from itertools import groupby
from typing import Optional

import colour
import pygfx as gfx
import numpy as np
import PyOpenColorIO as ocio
from colour_visuals import (
VisualChromaticityDiagramCIE1931,
VisualGrid,
VisualRGBColourspace2D,
VisualRGBScatter3D,
)
from colour import CCS_ILLUMINANTS, XYZ_to_RGB
from colour.utilities import set_default_float_dtype
from PySide6 import QtCore, QtGui, QtWidgets

from ..config_cache import ConfigCache
from ..message_router import MessageRouter
from ..processor_context import ProcessorContext
from ..utils import get_glyph_icon, subsampling_factor
from ..viewer import WgpuCanvasOffScreenViewer


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)

# Setting "Colour" float processing precision to Float32.
set_default_float_dtype(np.float32)
colour.plotting.CONSTANTS_COLOUR_STYLE.font.size = 20

self._context = None
self._processor = None
self._image_array = np.atleast_3d([0, 0, 0]).astype(np.float32)

# Chromaticity Diagram Working Space
self._working_whitepoint = CCS_ILLUMINANTS[
"CIE 1931 2 Degree Standard Observer"
]["D65"]
working_space = colour.RGB_Colourspace(
"CIE-XYZ-D65",
colour.XYZ_to_xy(np.identity(3)),
self._working_whitepoint,
"D65",
use_derived_matrix_RGB_to_XYZ=True,
use_derived_matrix_XYZ_to_RGB=True,
)
colour.RGB_COLOURSPACES[working_space.name] = working_space
self._working_space = working_space.name

self._root = None
self._visuals = {}

# Widgets
self._wgpu_viewer = WgpuCanvasOffScreenViewer()
self._conversion_chain_label = QtWidgets.QLabel()
self._conversion_chain_label.setStyleSheet(
".QLabel { font-size: 10pt;qproperty-alignment: AlignCenter;}"
)

self._chromaticities_color_spaces_label = QtWidgets.QLabel(
"Chromaticities Color Space"
)
self._chromaticities_color_spaces_combobox = QtWidgets.QComboBox()

self._draw_input_color_space_checkbox = QtWidgets.QCheckBox()
self._draw_input_color_space_checkbox.setChecked(True)
self._draw_chromaticities_color_space_checkbox = QtWidgets.QCheckBox()
self._draw_chromaticities_color_space_checkbox.setChecked(True)

# Layout
wgpu_layout = QtWidgets.QVBoxLayout()
spacer = QtWidgets.QSpacerItem(
20,
20,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding,
)
wgpu_layout.addItem(spacer)
wgpu_layout.addWidget(self._conversion_chain_label)
self._wgpu_viewer.setLayout(wgpu_layout)

hbox_layout = QtWidgets.QHBoxLayout()
hbox_layout.addWidget(self._chromaticities_color_spaces_label)
hbox_layout.addWidget(self._chromaticities_color_spaces_combobox)

hbox_layout.addWidget(self._draw_input_color_space_checkbox)
hbox_layout.addWidget(self._draw_chromaticities_color_space_checkbox)

vbox_layout = QtWidgets.QVBoxLayout()
vbox_layout.addLayout(hbox_layout)
vbox_layout.addWidget(self._wgpu_viewer)
self.setLayout(vbox_layout)

msg_router = MessageRouter.get_instance()
msg_router.config_html_ready.connect(self._on_config_html_ready)
msg_router.processor_ready.connect(self._on_processor_ready)
msg_router.image_ready.connect(self._on_image_ready)

self._setup_visuals()

# Signals / Slots
self._chromaticities_color_spaces_combobox.textActivated.connect(
self._update_visuals
)
self._draw_input_color_space_checkbox.checkStateChanged.connect(
self._on_draw_input_color_space_checkbox_checkstatechanged
)
self._draw_chromaticities_color_space_checkbox.checkStateChanged.connect(
self._on_draw_chromaticities_color_space_checkbox_checkstatechanged
)

@property
def wgpu_viewer(self):
return self._wgpu_viewer

def _on_draw_input_color_space_checkbox_checkstatechanged(self, state):
self._visuals["rgb_color_space_input"].visible = (
state == QtGui.Qt.CheckState.Checked
)

self._wgpu_viewer.render()

def _on_draw_chromaticities_color_space_checkbox_checkstatechanged(
self, state
):
self._visuals["rgb_color_space_chromaticities"].visible = (
state == QtGui.Qt.CheckState.Checked
)

self._wgpu_viewer.render()

def _setup_visuals(self):
self._wgpu_viewer.wgpu_scene.add(
gfx.Background(
None, gfx.BackgroundMaterial(np.array([0, 0, 0]))
)
)

self._visuals = {
"grid": VisualGrid(size=2),
"chromaticity_diagram": VisualChromaticityDiagramCIE1931(
kwargs_visual_chromaticity_diagram={"opacity": 0.25}
),
"rgb_color_space_input": VisualRGBColourspace2D(
self._working_space,
colour=np.array([1, 0.5, 0.25]),
thickness=2,
),
"rgb_color_space_chromaticities": VisualRGBColourspace2D(
self._working_space,
colour=np.array([0.25, 1, 0.5]),
thickness=2,
),
"rgb_scatter_3d": VisualRGBScatter3D(
np.zeros(3), self._working_space, size=4
),
}

self._root = gfx.Group()
for visual in self._visuals.values():
self._root.add(visual)

self._visuals["rgb_color_space_input"].visible = False
self._visuals["rgb_color_space_input"].local.position = np.array(
[0, 0, 0.000025])
self._visuals["rgb_color_space_chromaticities"].visible = False
self._visuals[
"rgb_color_space_chromaticities"].local.position = np.array(
[0, 0, 0.00005])
self._wgpu_viewer.wgpu_scene.add(self._root)

self._wgpu_viewer.wgpu_camera.local.position = np.array([0.5, 0.5, 2])
self._wgpu_viewer.wgpu_camera.show_pos(np.array([0.5, 0.5, 0.5]))

def reset(self) -> None:
pass

def showEvent(self, event: QtGui.QShowEvent) -> None:
"""Start listening for processor updates, if visible."""
super().showEvent(event)

msg_router = MessageRouter.get_instance()
# NOTE: We need to be able to receive notifications about config changes
# and this is currently the only way to do that without connecting
# to the `ConfigDock.config_changed` signal.
msg_router.config_updates_allowed = True
msg_router.processor_updates_allowed = True
msg_router.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.config_updates_allowed = False
msg_router.processor_updates_allowed = False
msg_router.image_updates_allowed = False

@QtCore.Slot(str)
def _on_config_html_ready(self, record: str) -> None:
color_space_names = ConfigCache.get_color_space_names()

items = [
self._chromaticities_color_spaces_combobox.itemText(i)
for i in range(self._chromaticities_color_spaces_combobox.count())
]

if items != color_space_names:
self._chromaticities_color_spaces_combobox.clear()
self._chromaticities_color_spaces_combobox.addItems(
color_space_names
)

config = ocio.GetCurrentConfig()
has_role_interchange_display = config.hasRole(
ocio.ROLE_INTERCHANGE_DISPLAY
)
self._chromaticities_color_spaces_combobox.setEnabled(
has_role_interchange_display
)

self._draw_input_color_space_checkbox.setEnabled(
has_role_interchange_display
)
self._visuals["rgb_color_space_input"].visible = (
has_role_interchange_display
)
self._draw_chromaticities_color_space_checkbox.setEnabled(
has_role_interchange_display
)
self._visuals["rgb_color_space_chromaticities"].visible = (
has_role_interchange_display
)

@QtCore.Slot(ocio.CPUProcessor)
def _on_processor_ready(
self, proc_context: ProcessorContext, cpu_proc: ocio.CPUProcessor
) -> None:
self._context = proc_context
self._processor = cpu_proc

self._update_visuals()

@QtCore.Slot(np.ndarray)
def _on_image_ready(self, image_array: np.ndarray) -> None:
sub_sampling_factor = int(
np.sqrt(subsampling_factor(image_array, 1e6))
)
self._image_array = image_array[
::sub_sampling_factor, ::sub_sampling_factor
]

self._update_visuals()

def _update_visuals(self, *args):
conversion_chain = []

image_array = np.copy(self._image_array)

# 1. Apply current active processor
if self._processor is not None:
if self._context.transform_item_name is not None:
conversion_chain += [
self._context.input_color_space,
self._context.transform_item_name,
]

rgb_colourspace = color_space_to_RGB_Colourspace(
self._context.input_color_space
)

if rgb_colourspace is not None:
self._visuals["rgb_color_space_input"].colourspace = (
rgb_colourspace
)

self._processor.applyRGB(image_array)

# 2. Convert from chromaticities input space to "CIE-XYZ-D65" interchange
config = ocio.GetCurrentConfig()
input_color_space = (
self._chromaticities_color_spaces_combobox.currentText()
)
if (
config.hasRole(ocio.ROLE_INTERCHANGE_DISPLAY)
and input_color_space in ConfigCache.get_color_space_names()
):
chromaticities_colorspace = (
self._chromaticities_color_spaces_combobox.currentText()
)
conversion_chain += [
chromaticities_colorspace,
ocio.ROLE_INTERCHANGE_DISPLAY.replace(
"cie_xyz_d65_interchange", "CIE-XYZ-D65"
),
]

rgb_colourspace = color_space_to_RGB_Colourspace(
chromaticities_colorspace
)

if rgb_colourspace is not None:
self._visuals["rgb_color_space_chromaticities"].colourspace = (
rgb_colourspace
)

colorspace_transform = ocio.ColorSpaceTransform(
src=chromaticities_colorspace,
dst=ocio.ROLE_INTERCHANGE_DISPLAY,
)
processor = config.getProcessor(
colorspace_transform, ocio.TRANSFORM_DIR_FORWARD
).getDefaultCPUProcessor()
processor.applyRGB(image_array)

# 3. Convert from "CIE-XYZ-D65" to "VisualRGBScatter3D" working space
conversion_chain += ["CIE-XYZ-D65", self._working_space]
image_array = XYZ_to_RGB(
image_array,
self._working_space,
illuminant=self._working_whitepoint,
)

conversion_chain = [
color_space for color_space, _group in groupby(conversion_chain)
]

if len(conversion_chain) == 1:
conversion_chain = []

self._conversion_chain_label.setText(" → ".join(conversion_chain))

self._visuals["rgb_scatter_3d"].RGB = image_array

self._wgpu_viewer.render()


def color_space_to_RGB_Colourspace(
color_space: str,
) -> colour.RGB_Colourspace | None:
config = ocio.GetCurrentConfig()
if (not config.hasRole(ocio.ROLE_INTERCHANGE_DISPLAY)) or (
color_space not in ConfigCache.get_color_space_names()
):
return None

colorspace_transform = ocio.ColorSpaceTransform(
src=color_space,
dst=ocio.ROLE_INTERCHANGE_DISPLAY,
)
processor = config.getProcessor(
colorspace_transform, ocio.TRANSFORM_DIR_FORWARD
).getDefaultCPUProcessor()

XYZ = np.identity(3, dtype=np.float32)
processor.applyRGB(XYZ)

XYZ_w = np.ones(3, dtype=np.float32)
processor.applyRGB(XYZ_w)

return colour.RGB_Colourspace(
color_space,
colour.XYZ_to_xy(XYZ),
colour.XYZ_to_xy(XYZ_w),
f"{color_space}",
use_derived_matrix_RGB_to_XYZ=True,
use_derived_matrix_XYZ_to_RGB=True,
)


if __name__ == "__main__":
application = QtWidgets.QApplication([])
chromaticity_inspector = ChromaticitiesInspector()
chromaticity_inspector.resize(800, 600)
chromaticity_inspector.show()

application.exec()
Loading

0 comments on commit ccd788d

Please sign in to comment.