Source code for lablib.operators.color
from abc import abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Union
import PyOpenColorIO as OCIO
from .base import BaseOperator
[docs]
class ColorOperator(BaseOperator):
"""Base class for color operators.
Currently this is only used for type checking.
"""
[docs]
@classmethod
@abstractmethod
def from_node_data(cls, data) -> "ColorOperator":
"""An abstract classmethod for returning a :obj:`ColorOperator` from node data.
Args:
data (dict): The node data.
Returns:
ColorOperator: The color operator object.
"""
pass
[docs]
@abstractmethod
def to_ocio_obj(self) -> OCIO.Transform:
"""Converts the object to native OCIO object.
Raises:
NotImplementedError: This method must be implemented in the
subclass.
"""
pass
[docs]
@dataclass
class OCIOColorSpace(ColorOperator):
"""Foundry Hiero Timeline soft effect node class.
Attributes:
in_colorspace (str): The input colorspace.
Defaults to "ACES - ACEScg".
out_colorspace (str): The output colorspace.
Defaults to "ACES - ACEScg".
"""
in_colorspace: str = "ACES - ACEScg"
out_colorspace: str = "ACES - ACEScg"
[docs]
def to_ocio_obj(self) -> OCIO.ColorSpaceTransform:
"""Returns native OCIO ColorSpaceTransform object.
Returns:
OCIO.ColorSpaceTransform: The OCIO ColorSpaceTransform object.
"""
return OCIO.ColorSpaceTransform(
src=self.in_colorspace,
dst=self.out_colorspace,
)
[docs]
@classmethod
def from_node_data(cls, data) -> "OCIOColorSpace":
"""Create :obj:`OCIOColorSpace` from node data.
Arguments:
data (dict): The node data.
Returns:
OCIOColorSpace:
"""
return cls(
in_colorspace=data.get("in_colorspace", ""),
out_colorspace=data.get("out_colorspace", ""),
)
[docs]
@dataclass
class OCIOFileTransform(ColorOperator):
"""Class for handling OCIO FileTransform effects.
Note:
Reads Foundry Hiero Timeline soft effect node class.
Attributes:
file (str): Path to the LUT file.
cccid (str): Path to the cccid file.
direction (int): The direction. Defaults to 0.
interpolation (str): The interpolation. Defaults to "linear".
"""
file: str = ""
cccid: str = ""
direction: int = 0
interpolation: str = "linear"
[docs]
def to_ocio_obj(self) -> OCIO.FileTransform:
"""Converts the object to native OCIO object.
Returns:
OCIO.FileTransform: The OCIO FileTransform object in a list.
"""
# define direction
direction = get_direction(self.direction)
# define interpolation
interpolation = get_interpolation(self.interpolation)
return OCIO.FileTransform(
src=Path(self.file).as_posix(),
cccId=self.cccid,
direction=direction,
interpolation=interpolation,
)
[docs]
@classmethod
def from_node_data(cls, data) -> "OCIOFileTransform":
"""Create :obj:`OCIOFileTransform` from node data.
Note:
Reads Foundry Hiero Timeline soft effect node data.
Would it be cool if we'd had a way to interface other DCC node
data? Would they even be so much different?
Args:
data (dict): The node data. List of expected but not required keys:
- file (str): Path to the LUT file.
- cccid (str): Path to the cccid file.
- direction (int): The direction. Defaults to 0.
- interpolation (str): The interpolation. Defaults to "linear".
Returns:
OCIOFileTransform: The OCIOFileTransform object.
"""
return cls(
file=data.get("file", ""),
cccid=data.get("cccid", ""),
direction=data.get("direction", 0),
interpolation=data.get("interpolation", "linear"),
)
[docs]
@dataclass
class OCIOCDLTransform(OCIOFileTransform):
"""Foundry Hiero Timeline soft effect node class.
Note:
Since this node class combines two of OCIO classes (FileTransform and
CDLTransform), we will separate them here within
:obj:`OCIOCDLTransform.to_ocio_obj()`.
Attributes:
file (Optional[str]): Path to the LUT file.
direction (int): The direction. Defaults to 0.
cccid (str): The cccid. Defaults to "".
offset (List[float]): The offset. Defaults to [0.0, 0.0, 0.0].
power (List[float]): The power. Defaults to [1.0, 1.0, 1.0].
slope (List[float]): The slope. Defaults to [0.0, 0.0, 0.0].
saturation (float): The saturation. Defaults to 1.0.
interpolation (str): The interpolation. Defaults to "linear".
"""
file: Optional[str] = None
direction: int = 0
cccid: str = ""
offset: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
power: List[float] = field(default_factory=lambda: [1.0, 1.0, 1.0])
slope: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
saturation: float = 1.0
interpolation: str = "linear"
[docs]
def to_ocio_obj(self) -> Union[OCIO.FileTransform, OCIO.CDLTransform]: # noqa: E501
"""Returns native OCIO FileTransform and CDLTransform object.
Returns:
Union[OCIO.FileTransform, OCIO.CDLTransform]: Either OCIO
CDLTransform or FileTransform object.
If file is not provided, CDLTransform will be returned.
"""
# define direction
direction = get_direction(self.direction)
if self.file:
# define interpolation
interpolation = get_interpolation(self.interpolation)
lut_file = Path(self.file)
return OCIO.FileTransform(
src=lut_file.as_posix(),
cccId=(self.cccid or "0"),
interpolation=interpolation,
direction=direction,
)
return OCIO.CDLTransform(
slope=self.slope,
offset=self.offset,
power=self.power,
sat=self.saturation,
direction=direction,
)
[docs]
@classmethod
def from_node_data(cls, data) -> "OCIOCDLTransform":
"""Create :obj:`OCIOCDLTransform` from node data.
Args:
data (dict): The node data. List of expected but not required keys:
- file (str): Path to the LUT file.
- direction (int): The direction.
Defaults to 0.
- cccid (str): The cccid.
Defaults to "".
- offset (List[float]): The offset.
Defaults to [0.0, 0.0, 0.0].
- power (List[float]): The power.
Defaults to [1.0, 1.0, 1.0].
- slope (List[float]): The slope.
Defaults to [0.0, 0.0, 0.0].
- saturation (float): The saturation.
Defaults to 1.0.
- interpolation (str): The interpolation.
Defaults to "linear".
Returns:
OCIOCDLTransform: The OCIOCDLTransform object.
"""
if data.get("file"):
return cls(
file=data.get("file", ""),
interpolation=data.get("interpolation", "linear"),
direction=data.get("direction", 0),
offset=data.get("offset", [0.0, 0.0, 0.0]),
power=data.get("power", [1.0, 1.0, 1.0]),
slope=data.get("slope", [0.0, 0.0, 0.0]),
saturation=data.get("saturation", 1.0),
cccid=data.get("cccid", ""),
)
else:
return cls(
direction=data.get("direction", 0),
offset=data.get("offset", [0.0, 0.0, 0.0]),
power=data.get("power", [1.0, 1.0, 1.0]),
slope=data.get("slope", [0.0, 0.0, 0.0]),
saturation=data.get("saturation", 1.0),
)
[docs]
def get_direction(direction: Union[str, int]) -> int:
"""Get the direction for OCIO FileTransform.
Attributes:
direction (Union[str, int]): The direction.
Returns:
int: The direction.
"""
if direction == "inverse":
return OCIO.TransformDirection.TRANSFORM_DIR_INVERSE
return OCIO.TransformDirection.TRANSFORM_DIR_FORWARD
def get_interpolation(interpolation: str) -> int:
if interpolation == "linear":
return OCIO.Interpolation.INTERP_LINEAR
elif interpolation == "best":
return OCIO.Interpolation.INTERP_BEST
elif interpolation == "nearest":
return OCIO.Interpolation.INTERP_NEAREST
elif interpolation == "tetrahedral":
return OCIO.Interpolation.INTERP_TETRAHEDRAL
elif interpolation == "cubic":
return OCIO.Interpolation.INTERP_CUBIC
return OCIO.Interpolation.INTERP_DEFAULT