How to enable/disable OCIO and set OCIO colorspace for group using Python?

I’m trying programmatically set, enable or disable the OCIO colorspace for added media but have been unable to make it work.

In pseudocode this is what I’d like to do:

loaded_node = rv.commands.addSourceVerbose([filepath])
group = rv.commands.nodeGroup(loaded_node)

# Below are psuedocode functions of what I'd like to do
# (not necessarily in this order or together)
enable_group_ocio(group)
set_group_ocio_colorspace(group, "acescg")
disable_group_ocio(group)

My set_group_ocio_colorspace prototype

I’ve been able to get set_group_ocio_colorspace working but ONLY if the group already has OCIO active. I have been unable to activate/deactivate the OCIO pipeline like the OCIO top level menu in RV allows you to do.

import rv


def group_member_of_type(group_node, member_type):
    for node in rv.commands.nodesInGroup(group_node):
        if rv.commands.nodeType(node) == member_type:
            return node


def get_group_ocio_file_node(group):
    """Return OCIOFile node from source group"""
    pipeline = group_member_of_type(group, "RVLinearizePipelineGroup")
    if pipeline:
        return group_member_of_type(pipeline, "OCIOFile")


def set_group_ocio_colorspace(group, colorspace):
    """Set the group's OCIOFile node ocio.inColorSpace property."""
    import ocio_source_setup    # noqa, RV OCIO package
    node = get_group_ocio_file_node(group)
    if not node:
        raise RuntimeError("Unable to find OCIOFile node for {}".format(group))

    rv.commands.setStringProperty(
        f"{node}.ocio.inColorSpace", [colorspace], True
    )

# Example usage
loaded_node = rv.commands.addSourceVerbose([filepath])
group = rv.commands.nodeGroup(loaded_node)
set_group_ocio_colorspace(group, "ACES - ACEScg")

How to enable/disable OCIO for a group using Python?

It seems the ocio_source_setup.py implementation has a OCIOSourceSetupMode.useDisplayOCIO and OCIOSourceSetupMode.disableDisplayOCIO method which is basically exactly what I’d like to call. However, I would need access to the initialized mode class to be able to call that.

If I were to rewrite this logic in my own library (which I’d then need to maintain to stay sync in behavior to ocio_source_setup.py so I’d rather not) then I’ll run into the issue that OCIOSourceSetupMode instances internally cache the state of whether a group is OCIO enabled or not. As such, that cache would then turn invalid as soon as I start adjusting that on my own without going to the OCIOSourceSetupMode instance.

Additional context

  • I’ve read the Getting started with OCIO in RV topic.
  • The question I’m asking here seems to be the same question as asked here in “Need help enabling the OCIO node through python” but aside of pointing to other topics that topic does not seem to present a solution.
  • I also posted this question on the ASFW slack in the #open-review-initiative here
  • I’m aware that I can define custom ocio_config_from_media and ocio_node_from_media functions in a rv_ocio_setup.py file however as far as I’m aware that’s only used on adding media sources and there’s still no way to explicitly set a colorspace at another point in time from Python.

Some other approaches I’ve tried:

Finding the initialized MinorMode class instance

If we were able to retrieve the right initialized OCIOSourceSetupMode instance for the current session window we could call its relevant methods. Once we have the correct instance this should be relatively reliable.

Using RV’s State object :red_square: (no luck)

When trying to access the State object to find the minor modes it seems empty.

state = rv.commands.data()
print(state.minorModes)

Using Python’s gc package :orange_square: (almost there)

We could try and get the minor mode from the Python gc, for example:

import rv
import gc

for obj in gc.get_objects():
    if isinstance(obj, rv.rvtypes.MinorMode):
        if obj.__class__.__name__ == "OCIOSourceSetupMode":
            print(obj)

Which does work, yay! However, an instance would be initialized per RV session window.
And we’d need to find the right one for the current session we’re working with.
I haven’t found yet how to reliably detect which is the right RV session for that. If only we could find which rv.commands.sessionName() it’s related to.

Force the active state through the menu action programmatically :white_check_mark:

This works, but is incredibly hacky.

We could also try and retrieve the relevant active callback through the initialized Qt menu entries from the session window.
However, this would require us to KNOW the defined menu structure of a minor mode.
Anyway, let’s give it a shot.

import rv.qtutils
import rv.commands


def set_current_ocio_active_state(state):
    """Set the OCIO state for the 'currently active source'.

    This is a hacky workaround to enable/disable the OCIO active state for
    a source since it appears to be that there's no way to explicitly trigger
    this callback from the `ocio_source_setup.OCIOSourceSetupMode` instance
    which does these changes.

    """

    group = rv.commands.viewNode()
    ocio_node = get_group_ocio_file_node(group)
    if state == bool(ocio_node):
        # Already in correct state
        return

    window = rv.qtutils.sessionWindow()
    menu_bar = window.menuBar()
    for action in menu_bar.actions():
        if action.text() != "OCIO" or action.toolTip() != "OCIO":
            continue

        ocio_menu = action.menu()

        for ocio_action in ocio_menu.actions():
            if ocio_action.toolTip() == "File Color Space":
                # The first entry is for "current source" instead
                # of all sources so we need to break the for loop
                # The first action of the file color space menu
                # is the "Active" action. So lets take that one
                active_action = ocio_action.menu().actions()[0]

                active_action.trigger()
                return

    raise RuntimeError(
        "Unable to set active state for current source. Make "
        "sure the OCIO package is installed and loaded."
    )


# Example usage
loaded_node = rv.commands.addSourceVerbose([filepath])
group = rv.commands.nodeGroup(loaded_node)
set_current_ocio_active_state(True)

This works, but only on if it’s the currently active source directly after creation - like when it’s the first added footage. However, it wouldn’t work if you run it twice since the second footage is appended at the end and not the active frame.

import contextlib
import rv.commands


@contextlib.contextmanager
def active_view(node):
    """Set active view during contet"""
    original = rv.commands.viewNode()
    try:
        rv.commands.setViewNode(node)
        yield
    finally:
        rv.commands.setViewNode(original)


# Example usage
loaded_node = rv.commands.addSourceVerbose([filepath])
group = rv.commands.nodeGroup(loaded_node)
with active_view(group):
    set_current_ocio_active_state(True)

Then this would set it forced to the group we need. It works, but oh boy does that look like a hack.


Note that some of these examples use functions from the code from above and the previous post, like set_current_ocio_active_state and get_group_ocio_file_node.