Skip to content

Allow review/transcoding of more channels, like "Z", "Y", "XYZ", "AR", "AG" "AB" #1271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
148ce21
Allow ExtractOIIOTranscode to pass with a warning if it can't find th…
BigRoy Nov 6, 2024
d072da8
Stop processing directly on unknown RGBA channel for a representation
BigRoy Nov 6, 2024
363824d
Move logic to make it clearer that we always process the same input f…
BigRoy Nov 6, 2024
5f82473
Merge branch 'develop' of https://github.com/ynput/ayon-core into bug…
BigRoy Mar 17, 2025
2d7bd48
Allow review/transcoding of more channels, like "Y", "XYZ", "AR", "AG…
BigRoy May 13, 2025
82b6837
Merge branch 'develop' into 989-ay-7315_extract-review-and-oiio-trans…
BigRoy May 13, 2025
90070bc
Merge remote-tracking branch 'origin/bugfix/transcode_ignore_conversi…
BigRoy May 17, 2025
a093e1e
Merge branch '989-ay-7315_extract-review-and-oiio-transcode-failing-t…
BigRoy May 17, 2025
afbf2c8
Refactor `UnknownRGBAChannelsError` -> `MissingRGBAChannelsError`
BigRoy May 17, 2025
44dc1ea
Include message of the original raised error
BigRoy May 17, 2025
7fa1922
Improve docstring
BigRoy May 17, 2025
9f3faa0
Merge branch 'develop' into 989-ay-7315_extract-review-and-oiio-trans…
BigRoy May 17, 2025
72895df
Match variable name more with captured exception
BigRoy May 19, 2025
b8ea018
Clarify exception
BigRoy May 19, 2025
fa1820a
Merge branch '989-ay-7315_extract-review-and-oiio-transcode-failing-t…
BigRoy May 19, 2025
526e5bf
Add unittest
BigRoy May 19, 2025
5917671
Add AR, AG, AB test case and fix behavior
BigRoy May 19, 2025
00921e7
Update client/ayon_core/lib/transcoding.py
BigRoy May 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 67 additions & 20 deletions client/ayon_core/lib/transcoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@
}


class MissingRGBAChannelsError(ValueError):
"""Raised when we can't find channels to use as RGBA for conversion in
input media.

This may be other channels than solely RGBA, like Z-channel. The error is
raised when no matching 'reviewable' channel was found.
"""


def get_transcode_temp_directory():
"""Creates temporary folder for transcoding.

Expand Down Expand Up @@ -345,6 +354,10 @@ def get_review_info_by_layer_name(channel_names):
...
]

This tries to find suitable outputs good for review purposes, by
searching for channel names like RGBA, but also XYZ, Z, N, AR, AG, AB
channels.

Args:
channel_names (list[str]): List of channel names.

Expand All @@ -353,7 +366,6 @@ def get_review_info_by_layer_name(channel_names):
"""

layer_names_order = []
rgba_by_layer_name = collections.defaultdict(dict)
channels_by_layer_name = collections.defaultdict(dict)

for channel_name in channel_names:
Expand All @@ -362,42 +374,76 @@ def get_review_info_by_layer_name(channel_names):
if "." in channel_name:
layer_name, last_part = channel_name.rsplit(".", 1)

channels_by_layer_name[layer_name][channel_name] = last_part
if last_part.lower() not in {
"r", "red",
"g", "green",
"b", "blue",
"a", "alpha"
# Detect RGBA channels
"r", "g", "b", "a",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You removed "red", "green", "blue" and "alpha".

Copy link
Collaborator Author

@BigRoy BigRoy May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct - but if you check the old code.. it'd have never been used. Because at line 389-391 it would only ever have used R, G, B. And since we stored them as .upper() it should've then also checked RED, GREEN, BLUE keys, right?

So I'm just removing it because it was never used... and I couldn't find any example files that actually had those channel names

Copy link
Member

@iLLiCiTiT iLLiCiTiT May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is was using first character to store it for mapping, but this condition is to skip the channel if does not match the items in set -> 2 different things. Put it back please. Because you've changed the logic you broke it, so also resolve that plase...

(e.g. Beauty.red would be skipped)

# Allow detecting of x, y and z channels, and normal channels
"x", "y", "z", "n",
# red, green and blue alpha/opacity, for colored mattes
"ar", "ag", "ab"
}:
continue

if layer_name not in layer_names_order:
layer_names_order.append(layer_name)
# R, G, B or A
channel = last_part[0].upper()
rgba_by_layer_name[layer_name][channel] = channel_name

# R, G, B, A or X, Y, Z, N, AR, AG, AB
channel = last_part.upper()
channels_by_layer_name[layer_name][channel] = channel_name

# Put empty layer to the beginning of the list
# - if input has R, G, B, A channels they should be used for review
if "" in layer_names_order:
layer_names_order.remove("")
layer_names_order.insert(0, "")
def _sort(_layer_name: str) -> int:
# Prioritize "" layer name
# Prioritize layers with RGB channels
order = 0
if _layer_name == "":
order -= 10

channels = channels_by_layer_name[_layer_name]
if all(channel in channels for channel in "RGB"):
order -= 1
return order
layer_names_order.sort(key=_sort)

output = []
for layer_name in layer_names_order:
rgba_layer_info = rgba_by_layer_name[layer_name]
red = rgba_layer_info.get("R")
green = rgba_layer_info.get("G")
blue = rgba_layer_info.get("B")
if not red or not green or not blue:
channel_info = channels_by_layer_name[layer_name]

# RGB channels
if all(channel in channel_info for channel in "RGB"):
rgb = "R", "G", "B"

# XYZ channels (position pass)
elif all(channel in channel_info for channel in "XYZ"):
rgb = "X", "Y", "Z"

# Colored mattes (as defined in OpenEXR Channel Name standards)
elif all(channel in channel_info for channel in ("AR", "AG", "AB")):
rgb = "AR", "AG", "AB"

# Luminance channel (as defined in OpenEXR Channel Name standards)
elif "Y" in channel_info:
rgb = "Y", "Y", "Y"

# Has only Z channel (Z-depth layer)
elif "Z" in channel_info:
rgb = "Z", "Z", "Z"

else:
# No reviewable channels found
continue

red = channel_info[rgb[0]]
green = channel_info[rgb[1]]
blue = channel_info[rgb[2]]
output.append({
"name": layer_name,
"review_channels": {
"R": red,
"G": green,
"B": blue,
"A": rgba_layer_info.get("A"),
"A": channel_info.get("A"),
}
})
return output
Expand Down Expand Up @@ -1296,8 +1342,9 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None):
review_channels = get_convert_rgb_channels(channel_names)

if review_channels is None:
raise ValueError(
"Couldn't find channels that can be used for conversion."
raise MissingRGBAChannelsError(
"Couldn't find channels that can be used for conversion "
f"among channels: {channel_names}."
)

red, green, blue, alpha = review_channels
Expand Down
55 changes: 38 additions & 17 deletions client/ayon_core/plugins/publish/extract_color_transcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
is_oiio_supported,
)
from ayon_core.lib.transcoding import (
MissingRGBAChannelsError,
convert_colorspace,
)

Expand Down Expand Up @@ -99,7 +100,19 @@ def process(self, instance):
self.log.warning("Config file doesn't exist, skipping")
continue

# Get representation files to convert
if isinstance(repre["files"], list):
repre_files_to_convert = copy.deepcopy(repre["files"])
else:
repre_files_to_convert = [repre["files"]]
repre_files_to_convert = self._translate_to_sequence(
repre_files_to_convert)

# Process each output definition
for output_def in profile_output_defs:
# Local copy to avoid accidental mutable changes
files_to_convert = list(repre_files_to_convert)

output_name = output_def["name"]
new_repre = copy.deepcopy(repre)

Expand All @@ -110,11 +123,6 @@ def process(self, instance):
)
new_repre["stagingDir"] = new_staging_dir

if isinstance(new_repre["files"], list):
files_to_convert = copy.deepcopy(new_repre["files"])
else:
files_to_convert = [new_repre["files"]]

output_extension = output_def["extension"]
output_extension = output_extension.replace('.', '')
self._rename_in_representation(new_repre,
Expand Down Expand Up @@ -158,25 +166,38 @@ def process(self, instance):
files_to_convert = self._translate_to_sequence(
files_to_convert)
self.log.debug("Files to convert: {}".format(files_to_convert))
missing_rgba_review_channels = False
for file_name in files_to_convert:
self.log.debug("Transcoding file: `{}`".format(file_name))
input_path = os.path.join(original_staging_dir,
file_name)
output_path = self._get_output_file_path(input_path,
new_staging_dir,
output_extension)

convert_colorspace(
input_path,
output_path,
config_path,
source_colorspace,
target_colorspace,
view,
display,
additional_command_args,
self.log
)
try:
convert_colorspace(
input_path,
output_path,
config_path,
source_colorspace,
target_colorspace,
view,
display,
additional_command_args,
self.log
)
except MissingRGBAChannelsError as exc:
missing_rgba_review_channels = True
self.log.error(exc)
self.log.error(
"Skipping OIIO Transcode. Unknown RGBA channels"
f" for colorspace conversion in file: {input_path}"
)
break

if missing_rgba_review_channels:
# Stop processing this representation
break

# cleanup temporary transcoded files
for file_name in new_repre["files"]:
Expand Down
158 changes: 158 additions & 0 deletions tests/client/ayon_core/lib/test_transcoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import unittest

from ayon_core.lib.transcoding import (
get_review_info_by_layer_name
)


class GetReviewInfoByLayerName(unittest.TestCase):
"""Test responses from `get_review_info_by_layer_name`"""
def test_rgba_channels(self):

# RGB is supported
info = get_review_info_by_layer_name(["R", "G", "B"])
self.assertEqual(info, [{
"name": "",
"review_channels": {
"R": "R",
"G": "G",
"B": "B",
"A": None,
}
}])

# rgb is supported
info = get_review_info_by_layer_name(["r", "g", "b"])
self.assertEqual(info, [{
"name": "",
"review_channels": {
"R": "r",
"G": "g",
"B": "b",
"A": None,
}
}])

# diffuse.[RGB] is supported
info = get_review_info_by_layer_name(
["diffuse.R", "diffuse.G", "diffuse.B"]
)
self.assertEqual(info, [{
"name": "diffuse",
"review_channels": {
"R": "diffuse.R",
"G": "diffuse.G",
"B": "diffuse.B",
"A": None,
}
}])

info = get_review_info_by_layer_name(["R", "G", "B", "A"])
self.assertEqual(info, [{
"name": "",
"review_channels": {
"R": "R",
"G": "G",
"B": "B",
"A": "A",
}
}])

def test_z_channel(self):

info = get_review_info_by_layer_name(["Z"])
self.assertEqual(info, [{
"name": "",
"review_channels": {
"R": "Z",
"G": "Z",
"B": "Z",
"A": None,
}
}])

info = get_review_info_by_layer_name(["Z", "A"])
self.assertEqual(info, [{
"name": "",
"review_channels": {
"R": "Z",
"G": "Z",
"B": "Z",
"A": "A",
}
}])

def test_ar_ag_ab_channels(self):

info = get_review_info_by_layer_name(["AR", "AG", "AB"])
self.assertEqual(info, [{
"name": "",
"review_channels": {
"R": "AR",
"G": "AG",
"B": "AB",
"A": None,
}
}])

info = get_review_info_by_layer_name(["AR", "AG", "AB", "A"])
self.assertEqual(info, [{
"name": "",
"review_channels": {
"R": "AR",
"G": "AG",
"B": "AB",
"A": "A",
}
}])

def test_unknown_channels(self):
info = get_review_info_by_layer_name(["hello", "world"])
self.assertEqual(info, [])

def test_rgba_priority(self):
"""Ensure main layer, and RGB channels are prioritized

If both Z and RGB channels are present for a layer name, then RGB
should be prioritized and the Z channel should be ignored.

Also, the alpha channel from another "layer name" is not used. Note
how the diffuse response does not take A channel from the main layer.

"""

info = get_review_info_by_layer_name([
"Z",
"diffuse.R", "diffuse.G", "diffuse.B",
"R", "G", "B", "A",
"specular.R", "specular.G", "specular.B", "specular.A",
])
self.assertEqual(info, [
{
"name": "",
"review_channels": {
"R": "R",
"G": "G",
"B": "B",
"A": "A",
},
},
{
"name": "diffuse",
"review_channels": {
"R": "diffuse.R",
"G": "diffuse.G",
"B": "diffuse.B",
"A": None,
},
},
{
"name": "specular",
"review_channels": {
"R": "specular.R",
"G": "specular.G",
"B": "specular.B",
"A": "specular.A",
},
},
])
Loading