Skip to content

Commit

Permalink
ocioview: app mode, tests, color space menus, bug fixes (#2006)
Browse files Browse the repository at this point in the history
* black code formatting

Signed-off-by: Michael Dolan <[email protected]>

* Handle empty image in chromaticities inspector

Signed-off-by: Michael Dolan <[email protected]>

* Allow unsetting shared view rule

Signed-off-by: Michael Dolan <[email protected]>

* Qt6 compatibility fixes

Signed-off-by: Michael Dolan <[email protected]>

* Bug fixes, unit test framework setup

Signed-off-by: Michael Dolan <[email protected]>

* Add initial unit tests

Signed-off-by: Michael Dolan <[email protected]>

* Add application mode

Signed-off-by: Michael Dolan <[email protected]>

* Add new color space combo

Signed-off-by: Michael Dolan <[email protected]>

* Use color space menu helper

Signed-off-by: Michael Dolan <[email protected]>

* Move mode select to top of window

Signed-off-by: Michael Dolan <[email protected]>

* Fix mode viewer context switching

Signed-off-by: Michael Dolan <[email protected]>

* Recombine viewers

Signed-off-by: Michael Dolan <[email protected]>

---------

Signed-off-by: Michael Dolan <[email protected]>
Co-authored-by: Thomas Mansencal <[email protected]>
  • Loading branch information
michdolan and KelSolaar committed Aug 10, 2024
1 parent 757e24b commit 5d3e0b3
Show file tree
Hide file tree
Showing 54 changed files with 2,625 additions and 941 deletions.
59 changes: 4 additions & 55 deletions src/apps/ocioview/main.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,17 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright Contributors to the OpenColorIO Project.

import logging
import os
import sys
from pathlib import Path

import PyOpenColorIO as ocio
from PySide6 import QtCore, QtGui, QtWidgets

import ocioview.log_handlers # Import to initialize logging
from ocioview.main_window import OCIOView
from ocioview.style import QSS, DarkPalette


ROOT_DIR = Path(__file__).resolve().parent.parent
FONTS_DIR = ROOT_DIR / "fonts"


def excepthook(exc_type, exc_value, exc_tb):
"""Log uncaught errors"""
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_tb)
return
logging.error(f"{exc_value}", exc_info=exc_value)
from ocioview.setup import setup_app


if __name__ == "__main__":
sys.excepthook = excepthook

# OpenGL core profile needed on macOS to access programmatic pipeline
gl_format = QtGui.QSurfaceFormat()
gl_format.setProfile(QtGui.QSurfaceFormat.CoreProfile)
gl_format.setSwapInterval(1)
gl_format.setVersion(4, 0)
QtGui.QSurfaceFormat.setDefaultFormat(gl_format)

# Create app
app = QtWidgets.QApplication(sys.argv)

# Initialize style
app.setStyle("fusion")
app.setPalette(DarkPalette())
app.setStyleSheet(QSS)
app.setEffectEnabled(QtCore.Qt.UI_AnimateCombo, False)

font = app.font()
font.setPointSize(8)
app.setFont(font)

# Clean OCIO environment to isolate working config
for env_var in (
ocio.OCIO_CONFIG_ENVVAR,
ocio.OCIO_ACTIVE_VIEWS_ENVVAR,
ocio.OCIO_ACTIVE_DISPLAYS_ENVVAR,
ocio.OCIO_INACTIVE_COLORSPACES_ENVVAR,
ocio.OCIO_OPTIMIZATION_FLAGS_ENVVAR,
ocio.OCIO_USER_CATEGORIES_ENVVAR,
):
if env_var in os.environ:
del os.environ[env_var]
app = setup_app()

# Start ocioview
ocioview = OCIOView()
ocioview.show()
ocio_view = OCIOView()
ocio_view.show()

sys.exit(app.exec_())
49 changes: 39 additions & 10 deletions src/apps/ocioview/ocioview/config_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ class ConfigCache:
_active_views: Optional[list[str]] = None
_all_names: Optional[list[str]] = None
_categories: Optional[list[str]] = None
_color_spaces: dict[bool, list[ocio.ColorSpace]] = {}
_color_spaces: dict[
tuple[bool, ocio.SearchReferenceSpaceType, ocio.ColorSpaceVisibility],
Union[list[ocio.ColorSpace], ocio.ColorSpaceSet],
] = {}
_color_space_names: dict[ocio.SearchReferenceSpaceType, list[str]] = {}
_default_color_space_name: Optional[str] = None
_default_view_transform_name: Optional[str] = None
Expand Down Expand Up @@ -117,7 +120,10 @@ def get_active_displays(cls) -> list[str]:
cls._active_displays = list(
filter(
None,
re.split(r" *[,:] *", ocio.GetCurrentConfig().getActiveDisplays()),
re.split(
r" *[,:] *",
ocio.GetCurrentConfig().getActiveDisplays(),
),
)
)

Expand All @@ -132,7 +138,9 @@ def get_active_views(cls) -> list[str]:
cls._active_views = list(
filter(
None,
re.split(r" *[,:] *", ocio.GetCurrentConfig().getActiveViews()),
re.split(
r" *[,:] *", ocio.GetCurrentConfig().getActiveViews()
),
)
)

Expand Down Expand Up @@ -213,23 +221,34 @@ def get_categories(cls) -> list[str]:

@classmethod
def get_color_spaces(
cls, as_set: bool = False
cls,
reference_space_type: Optional[ocio.SearchReferenceSpaceType] = None,
visibility: Optional[ocio.ColorSpaceVisibility] = None,
as_set: bool = False,
) -> Union[list[ocio.ColorSpace], ocio.ColorSpaceSet]:
"""
Get all (all reference space types and visibility states) color
spaces from the current config.
:param reference_space_type: Optionally filter by reference
space type.
:param visibility: Optional filter by visibility
:param as_set: If True, put returned color spaces into a
ColorSpaceSet, which copies the spaces to insulate from config
changes.
:return: list or color space set of color spaces
"""
cache_key = as_set
if reference_space_type is None:
reference_space_type = ocio.SEARCH_REFERENCE_SPACE_ALL
if visibility is None:
visibility = ocio.COLORSPACE_ALL

cache_key = (as_set, reference_space_type, visibility)

if not cls.validate() or cache_key not in cls._color_spaces:
config = ocio.GetCurrentConfig()
color_spaces = config.getColorSpaces(
ocio.SEARCH_REFERENCE_SPACE_ALL, ocio.COLORSPACE_ALL
reference_space_type, visibility
)
if as_set:
color_space_set = ocio.ColorSpaceSet()
Expand All @@ -253,7 +272,10 @@ def get_color_space_names(
"""
cache_key = reference_space_type

if not cls.validate() or reference_space_type not in cls._color_space_names:
if (
not cls.validate()
or reference_space_type not in cls._color_space_names
):
cls._color_space_names[cache_key] = list(
ocio.GetCurrentConfig().getColorSpaceNames(
reference_space_type, ocio.COLORSPACE_ALL
Expand Down Expand Up @@ -402,7 +424,9 @@ def get_named_transforms(cls) -> list[ocio.NamedTransform]:
"""
if not cls.validate() or cls._named_transforms is None:
cls._named_transforms = list(
ocio.GetCurrentConfig().getNamedTransforms(ocio.NAMEDTRANSFORM_ALL)
ocio.GetCurrentConfig().getNamedTransforms(
ocio.NAMEDTRANSFORM_ALL
)
)

return cls._named_transforms
Expand Down Expand Up @@ -471,7 +495,9 @@ def get_view_transforms(cls) -> list[ocio.ViewTransform]:
:return: List of view transforms from the current config
"""
if not cls.validate() or cls._view_transforms is None:
cls._view_transforms = list(ocio.GetCurrentConfig().getViewTransforms())
cls._view_transforms = list(
ocio.GetCurrentConfig().getViewTransforms()
)

return cls._view_transforms

Expand All @@ -496,7 +522,10 @@ def get_viewing_rule_names(cls) -> list[str]:
if not cls.validate() or cls._viewing_rule_names is None:
viewing_rules = ocio.GetCurrentConfig().getViewingRules()
cls._viewing_rule_names = sorted(
[viewing_rules.getName(i) for i in range(viewing_rules.getNumEntries())]
[
viewing_rules.getName(i)
for i in range(viewing_rules.getNumEntries())
]
)

return cls._viewing_rule_names
34 changes: 26 additions & 8 deletions src/apps/ocioview/ocioview/config_dock.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import PyOpenColorIO as ocio
from PySide6 import QtCore, QtWidgets

from .signal_router import SignalRouter
from .items import (
ColorSpaceEdit,
ConfigPropertiesEdit,
Expand All @@ -26,10 +27,21 @@ class ConfigDock(TabbedDockWidget):
Dockable widget for editing the current config.
"""

config_changed = QtCore.Signal()

def __init__(self, parent: Optional[QtCore.QObject] = None):
super().__init__("Config", get_glyph_icon("ph.file-text"), parent=parent)
def __init__(
self,
corner_widget: Optional[QtWidgets.QWidget] = None,
parent: Optional[QtCore.QObject] = None,
):
"""
:param corner_widget: Optional widget to place on the right
side of the dock title bar.
"""
super().__init__(
"Config",
get_glyph_icon("ph.file-text"),
corner_widget=corner_widget,
parent=parent,
)

self._models = []

Expand All @@ -50,9 +62,13 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self._connect_config_item_model(self.rule_edit.viewing_rule_edit.model)

self.display_view_edit = DisplayViewEdit()
self._connect_config_item_model(self.display_view_edit.view_edit.display_model)
self._connect_config_item_model(
self.display_view_edit.view_edit.display_model
)
self._connect_config_item_model(self.display_view_edit.view_edit.model)
self._connect_config_item_model(self.display_view_edit.shared_view_edit.model)
self._connect_config_item_model(
self.display_view_edit.shared_view_edit.model
)
self._connect_config_item_model(
self.display_view_edit.active_display_view_edit.active_display_edit.model
)
Expand Down Expand Up @@ -137,7 +153,9 @@ def update_config_views(self) -> None:
"""
message_queue.put_nowait(ocio.GetCurrentConfig())

def _connect_config_item_model(self, model: QtCore.QAbstractItemModel) -> None:
def _connect_config_item_model(
self, model: QtCore.QAbstractItemModel
) -> None:
"""
Collect model and route all config changes to the
'config_changed' signal.
Expand All @@ -154,7 +172,7 @@ def _on_config_changed(self, *args, **kwargs) -> None:
"""
Broadcast to the wider application that the config has changed.
"""
self.config_changed.emit()
SignalRouter.get_instance().emit_config_changed()
self.update_config_views()

def _on_warning_raised(self, message: str) -> None:
Expand Down
33 changes: 18 additions & 15 deletions src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,9 @@ def _setup_visuals(self) -> None:
)
self._visuals["rgb_color_space_input_3d"].visible = False
self._visuals["rgb_color_space_chromaticities_2d"].visible = False
self._visuals["rgb_color_space_chromaticities_2d"].local.position = (
np.array([0, 0, 0.00005])
)
self._visuals[
"rgb_color_space_chromaticities_2d"
].local.position = np.array([0, 0, 0.00005])
self._visuals["rgb_color_space_chromaticities_3d"].visible = False
self._visuals["rgb_scatter_3d"].visible = False

Expand Down Expand Up @@ -494,9 +494,11 @@ def _update_visuals(self, *args):
conversion_chain = []

image_array = np.copy(self._image_array)
# Don't try to process single or zero pixel images
image_empty = image_array.size <= 3

# 1. Apply current active processor
if self._processor is not None:
if not image_empty and self._processor is not None:
if self._context.transform_item_name is not None:
conversion_chain += [
self._context.input_color_space,
Expand All @@ -508,12 +510,12 @@ def _update_visuals(self, *args):
)

if rgb_colourspace is not None:
self._visuals["rgb_color_space_input_2d"].colourspace = (
rgb_colourspace
)
self._visuals["rgb_color_space_input_3d"].colourspace = (
rgb_colourspace
)
self._visuals[
"rgb_color_space_input_2d"
].colourspace = rgb_colourspace
self._visuals[
"rgb_color_space_input_3d"
].colourspace = rgb_colourspace
self._processor.applyRGB(image_array)

# 2. Convert from chromaticities input space to "CIE-XYZ-D65" interchange
Expand Down Expand Up @@ -559,11 +561,12 @@ def _update_visuals(self, *args):
# 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,
)
if not image_empty:
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)
Expand Down
28 changes: 21 additions & 7 deletions src/apps/ocioview/ocioview/inspect/code_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,22 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self.export_button = QtWidgets.QToolButton()
self.export_button.setIcon(get_glyph_icon("mdi6.file-export-outline"))
self.export_button.setText("Export CTF")
self.export_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
self.export_button.setToolButtonStyle(
QtCore.Qt.ToolButtonTextBesideIcon
)
self.export_button.released.connect(self._on_export_button_released)

self.ctf_view = LogView()
self.ctf_view.document().setDefaultStyleSheet(html_css)
self.ctf_view.append_tool_bar_widget(self.export_button)

self.gpu_language_box = EnumComboBox(ocio.GpuLanguage)
self.gpu_language_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
self.gpu_language_box.set_member(MessageRouter.get_instance().gpu_language)
self.gpu_language_box.setSizeAdjustPolicy(
QtWidgets.QComboBox.AdjustToContents
)
self.gpu_language_box.set_member(
MessageRouter.get_instance().gpu_language
)
self.gpu_language_box.currentIndexChanged[int].connect(
self._on_gpu_language_changed
)
Expand Down Expand Up @@ -136,7 +142,9 @@ def _scroll_preserved(self, log_view: LogView) -> None:
h_scroll_bar = log_view.horizontalScrollBar()

# Get line number from bottom of view
prev_cursor = log_view.cursorForPosition(log_view.html_view.rect().bottomLeft())
prev_cursor = log_view.cursorForPosition(
log_view.html_view.rect().bottomLeft()
)
prev_line_num = prev_cursor.blockNumber()

# Get scroll bar positions
Expand All @@ -149,7 +157,9 @@ def _scroll_preserved(self, log_view: LogView) -> None:
# Restore current line number
cursor = QtGui.QTextCursor(log_view.document())
cursor.movePosition(
QtGui.QTextCursor.Down, QtGui.QTextCursor.MoveAnchor, prev_line_num - 1
QtGui.QTextCursor.Down,
QtGui.QTextCursor.MoveAnchor,
prev_line_num - 1,
)
log_view.setTextCursor(cursor)

Expand All @@ -167,7 +177,9 @@ def _on_config_html_ready(self, record: str) -> None:
self.config_view.setHtml(record)

@QtCore.Slot(str, ocio.GroupTransform)
def _on_ctf_html_ready(self, record: str, group_tf: ocio.GroupTransform) -> None:
def _on_ctf_html_ready(
self, record: str, group_tf: ocio.GroupTransform
) -> None:
"""
Update CTF view with a lossless XML representation of an
OCIO processor.
Expand All @@ -178,7 +190,9 @@ def _on_ctf_html_ready(self, record: str, group_tf: ocio.GroupTransform) -> None
self.ctf_view.setHtml(record)

@QtCore.Slot(str, ocio.GPUProcessor)
def _on_shader_html_ready(self, record: str, gpu_proc: ocio.GPUProcessor) -> None:
def _on_shader_html_ready(
self, record: str, gpu_proc: ocio.GPUProcessor
) -> None:
"""
Update shader view with fragment shader source created
from an OCIO GPU processor.
Expand Down
Loading

0 comments on commit 5d3e0b3

Please sign in to comment.