Source code for colour.io.luts.lut

"""
LUT Processing
==============

Define the classes and definitions handling *LUT* processing:

-   :class:`colour.LUT1D`
-   :class:`colour.LUT3x1D`
-   :class:`colour.LUT3D`
-   :class:`colour.io.LUT_to_LUT`
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from copy import deepcopy
from operator import (
    add,
    iadd,
    imul,
    ipow,
    isub,
    itruediv,
    mul,
    pow,
    sub,
    truediv,
)

import numpy as np
from scipy.spatial import KDTree

from colour.algebra import (
    Extrapolator,
    LinearInterpolator,
    linear_conversion,
    table_interpolation_trilinear,
)
from colour.hints import (
    Any,
    ArrayLike,
    List,
    Literal,
    NDArrayFloat,
    Sequence,
    Type,
    cast,
)
from colour.utilities import (
    as_array,
    as_float_array,
    as_int,
    as_int_array,
    as_int_scalar,
    attest,
    full,
    is_iterable,
    is_numeric,
    multiline_repr,
    multiline_str,
    optional,
    runtime_warning,
    tsplit,
    tstack,
    usage_warning,
    validate_method,
)

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "AbstractLUT",
    "LUT1D",
    "LUT3x1D",
    "LUT3D",
    "LUT_to_LUT",
]


class AbstractLUT(ABC):
    """
    Define the base class for *LUT*.

    This is an :class:`ABCMeta` abstract class that must be inherited by
    sub-classes.

    Parameters
    ----------
    table
        Underlying *LUT* table.
    name
        *LUT* name.
    dimensions
        *LUT* dimensions, typically, 1 for a 1D *LUT*, 2 for a 3x1D *LUT* and 3
        for a 3D *LUT*.
    domain
        *LUT* domain, also used to define the instantiation time default table
        domain.
    size
        *LUT* size, also used to define the instantiation time default table
        size.
    comments
        Comments to add to the *LUT*.

    Attributes
    ----------
    -   :attr:`~colour.io.luts.lut.AbstractLUT.table`
    -   :attr:`~colour.io.luts.lut.AbstractLUT.name`
    -   :attr:`~colour.io.luts.lut.AbstractLUT.dimensions`
    -   :attr:`~colour.io.luts.lut.AbstractLUT.domain`
    -   :attr:`~colour.io.luts.lut.AbstractLUT.size`
    -   :attr:`~colour.io.luts.lut.AbstractLUT.comments`

    Methods
    -------
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__init__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__str__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__repr__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__eq__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__ne__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__add__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__iadd__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__sub__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__isub__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__mul__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__imul__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__div__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__idiv__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__pow__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.__ipow__`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.arithmetical_operation`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.is_domain_explicit`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.linear_table`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.copy`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.invert`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.apply`
    -   :meth:`~colour.io.luts.lut.AbstractLUT.convert`
    """

    def __init__(
        self,
        table: ArrayLike | None = None,
        name: str | None = None,
        dimensions: int | None = None,
        domain: ArrayLike | None = None,
        size: ArrayLike | None = None,
        comments: Sequence | None = None,
    ) -> None:
        self._name: str = f"Unity {size!r}" if table is None else f"{id(self)}"
        self.name = optional(name, self._name)
        self._dimensions = optional(dimensions, 0)
        self._table: NDArrayFloat = self.linear_table(
            optional(size, 0), optional(domain, np.array([]))
        )
        self.table = optional(table, self._table)
        self._domain: NDArrayFloat = np.array([])
        self.domain = optional(domain, self._domain)
        self._comments: list = []
        self.comments = cast(list, optional(comments, self._comments))

    @property
    def table(self) -> NDArrayFloat:
        """
        Getter and setter property for the underlying *LUT* table.

        Parameters
        ----------
        value
            Value to set the underlying *LUT* table with.

        Returns
        -------
        :class:`numpy.ndarray`
            Underlying *LUT* table.
        """

        return self._table

    @table.setter
    def table(self, value: ArrayLike):
        """Setter for the **self.table** property."""

        self._table = self._validate_table(value)

    @property
    def name(self) -> str:
        """
        Getter and setter property for the *LUT* name.

        Parameters
        ----------
        value
            Value to set the *LUT* name with.

        Returns
        -------
        :class:`str`
            *LUT* name.
        """

        return self._name

    @name.setter
    def name(self, value: str):
        """Setter for the **self.name** property."""

        attest(
            isinstance(value, str),
            f'"name" property: "{value}" type is not "str"!',
        )

        self._name = value

    @property
    def domain(self) -> NDArrayFloat:
        """
        Getter and setter property for the *LUT* domain.

        Parameters
        ----------
        value
            Value to set the *LUT* domain with.

        Returns
        -------
        :class:`numpy.ndarray`
            *LUT* domain.
        """

        return self._domain

    @domain.setter
    def domain(self, value: ArrayLike):
        """Setter for the **self.domain** property."""

        self._domain = self._validate_domain(value)

    @property
    def dimensions(self) -> int:
        """
        Getter property for the *LUT* dimensions.

        Returns
        -------
        :class:`int`
            *LUT* dimensions.
        """

        return self._dimensions

    @property
    def size(self) -> int:
        """
        Getter property for the *LUT* size.

        Returns
        -------
        :class:`int`
            *LUT* size.
        """

        return self._table.shape[0]

    @property
    def comments(self) -> list:
        """
        Getter and setter property for the *LUT* comments.

        Parameters
        ----------
        value
            Value to set the *LUT* comments with.

        Returns
        -------
        :class:`list`
            *LUT* comments.
        """

        return self._comments

    @comments.setter
    def comments(self, value: Sequence):
        """Setter for the **self.comments** property."""

        attest(
            is_iterable(value),
            f'"comments" property: "{value}" must be a sequence!',
        )

        self._comments = list(value)

    def __str__(self) -> str:
        """
        Return a formatted string representation of the *LUT*.

        Returns
        -------
        :class:`str`
            Formatted string representation.
        """

        attributes = [
            {
                "formatter": lambda x: (  # noqa: ARG005
                    f"{self.__class__.__name__} - {self.name}"
                ),
                "section": True,
            },
            {"line_break": True},
            {"name": "dimensions", "label": "Dimensions"},
            {"name": "domain", "label": "Domain"},
            {
                "label": "Size",
                "formatter": lambda x: str(self.table.shape),  # noqa: ARG005
            },
        ]

        if self.comments:
            attributes.append(
                {
                    "formatter": lambda x: "\n".join(  # noqa: ARG005
                        [
                            f"Comment {str(i + 1).zfill(2)} : {comment}"
                            for i, comment in enumerate(self.comments)
                        ]
                    ),
                }
            )

        return multiline_str(self, cast(List[dict], attributes))

    def __repr__(self) -> str:
        """
        Return an evaluable string representation of the *LUT*.

        Returns
        -------
        :class:`str`
            Evaluable string representation.
        """

        attributes = [
            {"name": "table"},
            {"name": "name"},
            {"name": "domain"},
            {"name": "size"},
        ]

        if self.comments:
            attributes.append({"name": "comments"})

        return multiline_repr(self, attributes)

    def __eq__(self, other: Any) -> bool:
        """
        Return whether the *LUT* is equal to given other object.

        Parameters
        ----------
        other
            Object to test whether it is equal to the *LUT*.

        Returns
        -------
        :class:`bool`
            Whether given object is equal to the *LUT*.
        """

        if isinstance(other, AbstractLUT) and all(
            [
                np.array_equal(self.table, other.table),
                np.array_equal(self.domain, other.domain),
            ]
        ):
            return True

        return False

    def __ne__(self, other: Any) -> bool:
        """
        Return whether the *LUT* is not equal to given other object.

        Parameters
        ----------
        other
            Object to test whether it is not equal to the *LUT*.

        Returns
        -------
        :class:`bool`
            Whether given object is not equal to the *LUT*.
        """

        return not (self == other)

    def __add__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for addition.

        Parameters
        ----------
        a
            :math:`a` variable to add.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            Variable added *LUT*.
        """

        return self.arithmetical_operation(a, "+")

    def __iadd__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for in-place addition.

        Parameters
        ----------
        a
            :math:`a` variable to add in-place.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            In-place variable added *LUT*.
        """

        return self.arithmetical_operation(a, "+", True)

    def __sub__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for subtraction.

        Parameters
        ----------
        a
            :math:`a` variable to subtract.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            Variable subtracted *LUT*.
        """

        return self.arithmetical_operation(a, "-")

    def __isub__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for in-place subtraction.

        Parameters
        ----------
        a
            :math:`a` variable to subtract in-place.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            In-place variable subtracted *LUT*.
        """

        return self.arithmetical_operation(a, "-", True)

    def __mul__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for multiplication.

        Parameters
        ----------
        a
            :math:`a` variable to multiply by.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            Variable multiplied *LUT*.
        """

        return self.arithmetical_operation(a, "*")

    def __imul__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for in-place multiplication.

        Parameters
        ----------
        a
            :math:`a` variable to multiply by in-place.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            In-place variable multiplied *LUT*.
        """

        return self.arithmetical_operation(a, "*", True)

    def __div__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for division.

        Parameters
        ----------
        a
            :math:`a` variable to divide by.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            Variable divided *LUT*.
        """

        return self.arithmetical_operation(a, "/")

    def __idiv__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for in-place division.

        Parameters
        ----------
        a
            :math:`a` variable to divide by in-place.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            In-place variable divided *LUT*.
        """

        return self.arithmetical_operation(a, "/", True)

    __itruediv__ = __idiv__
    __truediv__ = __div__

    def __pow__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for exponentiation.

        Parameters
        ----------
        a
            :math:`a` variable to exponentiate by.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            Variable exponentiated *LUT*.
        """

        return self.arithmetical_operation(a, "**")

    def __ipow__(self, a: ArrayLike | AbstractLUT) -> AbstractLUT:
        """
        Implement support for in-place exponentiation.

        Parameters
        ----------
        a
            :math:`a` variable to exponentiate by in-place.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            In-place variable exponentiated *LUT*.
        """

        return self.arithmetical_operation(a, "**", True)

    def arithmetical_operation(
        self,
        a: ArrayLike | AbstractLUT,
        operation: Literal["+", "-", "*", "/", "**"],
        in_place: bool = False,
    ) -> AbstractLUT:
        """
        Perform given arithmetical operation with :math:`a` operand, the
        operation can be either performed on a copy or in-place, must be
        reimplemented by sub-classes.

        Parameters
        ----------
        a
            Operand.
        operation
            Operation to perform.
        in_place
            Operation happens in place.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            *LUT*.
        """

        operator, ioperator = {
            "+": (add, iadd),
            "-": (sub, isub),
            "*": (mul, imul),
            "/": (truediv, itruediv),
            "**": (pow, ipow),
        }[operation]

        if in_place:
            operand = a.table if isinstance(a, AbstractLUT) else as_float_array(a)

            self.table = operator(self.table, operand)

            return self
        else:
            copy = ioperator(self.copy(), a)

            return copy

    @abstractmethod
    def _validate_table(self, table: ArrayLike) -> NDArrayFloat:
        """
        Validate given table according to *LUT* dimensions.

        Parameters
        ----------
        table
            Table to validate.

        Returns
        -------
        :class:`numpy.ndarray`
            Validated table as a :class:`ndarray` instance.
        """

    @abstractmethod
    def _validate_domain(self, domain: ArrayLike) -> NDArrayFloat:
        """
        Validate given domain according to *LUT* dimensions.

        Parameters
        ----------
        domain
            Domain to validate.

        Returns
        -------
        :class:`numpy.ndarray`
            Validated domain as a :class:`ndarray` instance.
        """

    @abstractmethod
    def is_domain_explicit(self) -> bool:
        """
        Return whether the *LUT* domain is explicit (or implicit).

        An implicit domain is defined by its shape only::

            [[0 1]
             [0 1]
             [0 1]]

        While an explicit domain defines every single discrete samples::

            [[0.0 0.0 0.0]
             [0.1 0.1 0.1]
             [0.2 0.2 0.2]
             [0.3 0.3 0.3]
             [0.4 0.4 0.4]
             [0.8 0.8 0.8]
             [1.0 1.0 1.0]]

        Returns
        -------
        :class:`bool`
            Is *LUT* domain explicit.
        """

    @staticmethod
    @abstractmethod
    def linear_table(
        size: ArrayLike | None = None,
        domain: ArrayLike | None = None,
    ) -> NDArrayFloat:
        """
        Return a linear table of given size according to *LUT* dimensions.

        Parameters
        ----------
        size
            Expected table size, for a 1D *LUT*, the number of output samples
            :math:`n` is equal to ``size``, for a 3x1D *LUT* :math:`n` is equal
            to ``size * 3`` or ``size[0] + size[1] + size[2]``, for a 3D *LUT*
            :math:`n` is equal to ``size**3 * 3`` or
            ``size[0] * size[1] * size[2] * 3``.
        domain
            Domain of the table.

        Returns
        -------
        :class:`numpy.ndarray`
            Linear table.
        """

    def copy(self) -> AbstractLUT:
        """
        Return a copy of the sub-class instance.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            *LUT* copy.
        """

        return deepcopy(self)

    @abstractmethod
    def invert(self, **kwargs: Any) -> AbstractLUT:
        """
        Compute and returns an inverse copy of the *LUT*.

        Other Parameters
        ----------------
        kwargs
            Keywords arguments.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            Inverse *LUT* class instance.
        """

    @abstractmethod
    def apply(self, RGB: ArrayLike, **kwargs: Any) -> NDArrayFloat:
        """
        Apply the *LUT* to given *RGB* colourspace array using given method.

        Parameters
        ----------
        RGB
            *RGB* colourspace array to apply the *LUT* onto.

        Other Parameters
        ----------------
        direction
            Whether the *LUT* should be applied in the forward or inverse
            direction.
        extrapolator
            Extrapolator class type or object to use as extrapolating function.
        extrapolator_kwargs
            Arguments to use when instantiating or calling the extrapolating
            function.
        interpolator
            Interpolator class type or object to use as interpolating function.
        interpolator_kwargs
            Arguments to use when instantiating or calling the interpolating
            function.

        Returns
        -------
        :class:`numpy.ndarray`
            Interpolated *RGB* colourspace array.
        """

    def convert(
        self,
        cls: Type[AbstractLUT],
        force_conversion: bool = False,
        **kwargs: Any,
    ) -> AbstractLUT:
        """
        Convert the *LUT* to given ``cls`` class instance.

        Parameters
        ----------
        cls
            *LUT* class instance.
        force_conversion
            Whether to force the conversion as it might be destructive.

        Other Parameters
        ----------------
        interpolator
            Interpolator class type to use as interpolating function.
        interpolator_kwargs
            Arguments to use when instantiating the interpolating function.
        size
            Expected table size in case of an upcast to or a downcast from a
            :class:`LUT3D` class instance.

        Returns
        -------
        :class:`colour.io.luts.lut.AbstractLUT`
            Converted *LUT* class instance.

        Warnings
        --------
        Some conversions are destructive and raise a :class:`ValueError`
        exception by default.

        Raises
        ------
        ValueError
            If the conversion is destructive.
        """

        return LUT_to_LUT(
            self,
            cls,
            force_conversion,
            **kwargs,  # pyright: ignore
        )


[docs] class LUT1D(AbstractLUT): """ Define the base class for a 1D *LUT*. Parameters ---------- table Underlying *LUT* table. name *LUT* name. domain *LUT* domain, also used to define the instantiation time default table domain. size Size of the instantiation time default table, default to 10. comments Comments to add to the *LUT*. Methods ------- - :meth:`~colour.LUT1D.__init__` - :meth:`~colour.LUT1D.is_domain_explicit` - :meth:`~colour.LUT1D.linear_table` - :meth:`~colour.LUT1D.invert` - :meth:`~colour.LUT1D.apply` Examples -------- Instantiating a unity LUT with a table with 16 elements: >>> print(LUT1D(size=16)) LUT1D - Unity 16 ---------------- <BLANKLINE> Dimensions : 1 Domain : [ 0. 1.] Size : (16,) Instantiating a LUT using a custom table with 16 elements: >>> print(LUT1D(LUT1D.linear_table(16) ** (1 / 2.2))) # doctest: +ELLIPSIS LUT1D - ... --------... <BLANKLINE> Dimensions : 1 Domain : [ 0. 1.] Size : (16,) Instantiating a LUT using a custom table with 16 elements, custom name, custom domain and comments: >>> from colour.algebra import spow >>> domain = np.array([-0.1, 1.5]) >>> print( ... LUT1D( ... spow(LUT1D.linear_table(16, domain), 1 / 2.2), ... "My LUT", ... domain, ... comments=["A first comment.", "A second comment."], ... ) ... ) LUT1D - My LUT -------------- <BLANKLINE> Dimensions : 1 Domain : [-0.1 1.5] Size : (16,) Comment 01 : A first comment. Comment 02 : A second comment. """
[docs] def __init__( self, table: ArrayLike | None = None, name: str | None = None, domain: ArrayLike | None = None, size: ArrayLike | None = None, comments: Sequence | None = None, ) -> None: domain = as_float_array(optional(domain, np.array([0, 1]))) size = optional(size, 10) super().__init__(table, name, 1, domain, size, comments)
def _validate_table(self, table: ArrayLike) -> NDArrayFloat: """ Validate given table is a 1D array. Parameters ---------- table Table to validate. Returns ------- :class:`numpy.ndarray` Validated table as a :class:`ndarray` instance. """ table = as_float_array(table) attest(len(table.shape) == 1, "The table must be a 1D array!") return table def _validate_domain(self, domain: ArrayLike) -> NDArrayFloat: """ Validate given domain. Parameters ---------- domain Domain to validate. Returns ------- :class:`numpy.ndarray` Validated domain as a :class:`ndarray` instance. """ domain = as_float_array(domain) attest(len(domain.shape) == 1, "The domain must be a 1D array!") attest( domain.shape[0] >= 2, "The domain column count must be equal or greater than 2!", ) return domain
[docs] def is_domain_explicit(self) -> bool: """ Return whether the *LUT* domain is explicit (or implicit). An implicit domain is defined by its shape only:: [0 1] While an explicit domain defines every single discrete samples:: [0.0 0.1 0.2 0.4 0.8 1.0] Returns ------- :class:`bool` Is *LUT* domain explicit. Examples -------- >>> LUT1D().is_domain_explicit() False >>> table = domain = np.linspace(0, 1, 10) >>> LUT1D(table, domain=domain).is_domain_explicit() True """ return len(self.domain) != 2
[docs] @staticmethod def linear_table( size: ArrayLike | None = None, domain: ArrayLike | None = None, ) -> NDArrayFloat: """ Return a linear table, the number of output samples :math:`n` is equal to ``size``. Parameters ---------- size Expected table size, default to 10. domain Domain of the table. Returns ------- :class:`numpy.ndarray` Linear table with ``size`` samples. Examples -------- >>> LUT1D.linear_table(5, np.array([-0.1, 1.5])) array([-0.1, 0.3, 0.7, 1.1, 1.5]) >>> LUT1D.linear_table(domain=np.linspace(-0.1, 1.5, 5)) array([-0.1, 0.3, 0.7, 1.1, 1.5]) """ size = optional(size, 10) domain = as_float_array(optional(domain, np.array([0, 1]))) if len(domain) != 2: return domain else: attest(is_numeric(size), "Linear table size must be a numeric!") return np.linspace(domain[0], domain[1], as_int_scalar(size))
[docs] def invert(self, **kwargs: Any) -> LUT1D: # noqa: ARG002 """ Compute and returns an inverse copy of the *LUT*. Other Parameters ---------------- kwargs Keywords arguments, only given for signature compatibility with the :meth:`AbstractLUT.invert` method. Returns ------- :class:`colour.LUT1D` Inverse *LUT* class instance. Examples -------- >>> LUT = LUT1D(LUT1D.linear_table() ** (1 / 2.2)) >>> print(LUT.table) # doctest: +ELLIPSIS [ 0. ... 0.3683438... 0.5047603... 0.6069133... \ 0.6916988... 0.7655385... 0.8316843... 0.8920493... 0.9478701... 1. ] >>> print(LUT.invert()) # doctest: +ELLIPSIS LUT1D - ... - Inverse --------...---------- <BLANKLINE> Dimensions : 1 Domain : [ 0. 0.3683438... 0.5047603... 0.6069133... \ 0.6916988... 0.7655385... 0.8316843... 0.8920493... 0.9478701... 1. ] Size : (10,) >>> print(LUT.invert().table) # doctest: +ELLIPSIS [ 0. ... 0.1111111... 0.2222222... 0.3333333... \ 0.4444444... 0.5555555... 0.6666666... 0.7777777... 0.8888888... 1. ] """ if self.is_domain_explicit(): domain = self.domain else: domain_min, domain_max = self.domain domain = np.linspace(domain_min, domain_max, self.size) LUT_i = LUT1D( table=domain, name=f"{self.name} - Inverse", domain=self.table, ) return LUT_i
[docs] def apply(self, RGB: ArrayLike, **kwargs: Any) -> NDArrayFloat: """ Apply the *LUT* to given *RGB* colourspace array using given method. Parameters ---------- RGB *RGB* colourspace array to apply the *LUT* onto. Other Parameters ---------------- direction Whether the *LUT* should be applied in the forward or inverse direction. extrapolator Extrapolator class type or object to use as extrapolating function. extrapolator_kwargs Arguments to use when instantiating or calling the extrapolating function. interpolator Interpolator class type to use as interpolating function. interpolator_kwargs Arguments to use when instantiating the interpolating function. Returns ------- :class:`numpy.ndarray` Interpolated *RGB* colourspace array. Examples -------- >>> LUT = LUT1D(LUT1D.linear_table() ** (1 / 2.2)) >>> RGB = np.array([0.18, 0.18, 0.18]) *LUT* applied to the given *RGB* colourspace in the forward direction: >>> LUT.apply(RGB) # doctest: +ELLIPSIS array([ 0.4529220..., 0.4529220..., 0.4529220...]) *LUT* applied to the modified *RGB* colourspace in the inverse direction: >>> LUT.apply(LUT.apply(RGB), direction="Inverse") ... # doctest: +ELLIPSIS array([ 0.18..., 0.18..., 0.18...]) """ direction = validate_method( kwargs.get("direction", "Forward"), ("Forward", "Inverse") ) interpolator = kwargs.get("interpolator", LinearInterpolator) interpolator_kwargs = kwargs.get("interpolator_kwargs", {}) extrapolator = kwargs.get("extrapolator", Extrapolator) extrapolator_kwargs = kwargs.get("extrapolator_kwargs", {}) LUT = self.invert() if direction == "inverse" else self if LUT.is_domain_explicit(): samples = LUT.domain else: domain_min, domain_max = LUT.domain samples = np.linspace(domain_min, domain_max, LUT.size) RGB_interpolator = extrapolator( interpolator(samples, LUT.table, **interpolator_kwargs), **extrapolator_kwargs, ) return RGB_interpolator(RGB)
[docs] class LUT3x1D(AbstractLUT): """ Define the base class for a 3x1D *LUT*. Parameters ---------- table Underlying *LUT* table. name *LUT* name. domain *LUT* domain, also used to define the instantiation time default table domain. size Size of the instantiation time default table, default to 10. comments Comments to add to the *LUT*. Methods ------- - :meth:`~colour.LUT3x1D.__init__` - :meth:`~colour.LUT3x1D.is_domain_explicit` - :meth:`~colour.LUT3x1D.linear_table` - :meth:`~colour.LUT3x1D.invert` - :meth:`~colour.LUT3x1D.apply` Examples -------- Instantiating a unity LUT with a table with 16x3 elements: >>> print(LUT3x1D(size=16)) LUT3x1D - Unity 16 ------------------ <BLANKLINE> Dimensions : 2 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (16, 3) Instantiating a LUT using a custom table with 16x3 elements: >>> print(LUT3x1D(LUT3x1D.linear_table(16) ** (1 / 2.2))) ... # doctest: +ELLIPSIS LUT3x1D - ... ----------... <BLANKLINE> Dimensions : 2 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (16, 3) Instantiating a LUT using a custom table with 16x3 elements, custom name, custom domain and comments: >>> from colour.algebra import spow >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> print( ... LUT3x1D( ... spow(LUT3x1D.linear_table(16), 1 / 2.2), ... "My LUT", ... domain, ... comments=["A first comment.", "A second comment."], ... ) ... ) LUT3x1D - My LUT ---------------- <BLANKLINE> Dimensions : 2 Domain : [[-0.1 -0.2 -0.4] [ 1.5 3. 6. ]] Size : (16, 3) Comment 01 : A first comment. Comment 02 : A second comment. """
[docs] def __init__( self, table: ArrayLike | None = None, name: str | None = None, domain: ArrayLike | None = None, size: ArrayLike | None = None, comments: Sequence | None = None, ) -> None: domain = as_float_array(optional(domain, [[0, 0, 0], [1, 1, 1]])) size = optional(size, 10) super().__init__(table, name, 2, domain, size, comments)
def _validate_table(self, table: ArrayLike) -> NDArrayFloat: """ Validate given table is a 3x1D array. Parameters ---------- table Table to validate. Returns ------- :class:`numpy.ndarray` Validated table as a :class:`ndarray` instance. """ table = as_float_array(table) attest(len(table.shape) == 2, "The table must be a 2D array!") return table def _validate_domain(self, domain: ArrayLike) -> NDArrayFloat: """ Validate given domain. Parameters ---------- domain Domain to validate. Returns ------- :class:`numpy.ndarray` Validated domain as a :class:`ndarray` instance. """ domain = as_float_array(domain) attest(len(domain.shape) == 2, "The domain must be a 2D array!") attest( domain.shape[0] >= 2, "The domain row count must be equal or greater than 2!", ) attest(domain.shape[1] == 3, "The domain column count must be equal to 3!") return domain
[docs] def is_domain_explicit(self) -> bool: """ Return whether the *LUT* domain is explicit (or implicit). An implicit domain is defined by its shape only:: [[0 1] [0 1] [0 1]] While an explicit domain defines every single discrete samples:: [[0.0 0.0 0.0] [0.1 0.1 0.1] [0.2 0.2 0.2] [0.3 0.3 0.3] [0.4 0.4 0.4] [0.8 0.8 0.8] [1.0 1.0 1.0]] Returns ------- :class:`bool` Is *LUT* domain explicit. Examples -------- >>> LUT3x1D().is_domain_explicit() False >>> samples = np.linspace(0, 1, 10) >>> table = domain = tstack([samples, samples, samples]) >>> LUT3x1D(table, domain=domain).is_domain_explicit() True """ return self.domain.shape != (2, 3)
[docs] @staticmethod def linear_table( size: ArrayLike | None = None, domain: ArrayLike | None = None, ) -> NDArrayFloat: """ Return a linear table, the number of output samples :math:`n` is equal to ``size * 3`` or ``size[0] + size[1] + size[2]``. Parameters ---------- size Expected table size, default to 10. domain Domain of the table. Returns ------- :class:`numpy.ndarray` Linear table with ``size * 3`` or ``size[0] + size[1] + size[2]`` samples. Warnings -------- If ``size`` is non uniform, the linear table will be padded accordingly. Examples -------- >>> LUT3x1D.linear_table(5, np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]])) array([[-0.1, -0.2, -0.4], [ 0.3, 0.6, 1.2], [ 0.7, 1.4, 2.8], [ 1.1, 2.2, 4.4], [ 1.5, 3. , 6. ]]) >>> LUT3x1D.linear_table( ... np.array([5, 3, 2]), ... np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]), ... ) array([[-0.1, -0.2, -0.4], [ 0.3, 1.4, 6. ], [ 0.7, 3. , nan], [ 1.1, nan, nan], [ 1.5, nan, nan]]) >>> domain = np.array( ... [ ... [-0.1, -0.2, -0.4], ... [0.3, 1.4, 6.0], ... [0.7, 3.0, np.nan], ... [1.1, np.nan, np.nan], ... [1.5, np.nan, np.nan], ... ] ... ) >>> LUT3x1D.linear_table(domain=domain) array([[-0.1, -0.2, -0.4], [ 0.3, 1.4, 6. ], [ 0.7, 3. , nan], [ 1.1, nan, nan], [ 1.5, nan, nan]]) """ size = optional(size, 10) domain = as_float_array(optional(domain, [[0, 0, 0], [1, 1, 1]])) if domain.shape != (2, 3): return domain else: size_array = np.tile(size, 3) if is_numeric(size) else as_int_array(size) R, G, B = tsplit(domain) samples = [ np.linspace(a[0], a[1], size_array[i]) for i, a in enumerate([R, G, B]) ] if len(np.unique(size_array)) != 1: runtime_warning( "Table is non uniform, axis will be " 'padded with "NaNs" accordingly!' ) samples = [ np.pad( axis, (0, np.max(size_array) - len(axis)), # pyright: ignore mode="constant", constant_values=np.nan, ) for axis in samples ] return tstack(samples)
[docs] def invert(self, **kwargs: Any) -> LUT3x1D: # noqa: ARG002 """ Compute and returns an inverse copy of the *LUT*. Other Parameters ---------------- kwargs Keywords arguments, only given for signature compatibility with the :meth:`AbstractLUT.invert` method. Returns ------- :class:`colour.LUT3x1D` Inverse *LUT* class instance. Examples -------- >>> LUT = LUT3x1D(LUT3x1D.linear_table() ** (1 / 2.2)) >>> print(LUT.table) [[ 0. 0. 0. ] [ 0.36834383 0.36834383 0.36834383] [ 0.50476034 0.50476034 0.50476034] [ 0.60691337 0.60691337 0.60691337] [ 0.69169882 0.69169882 0.69169882] [ 0.76553851 0.76553851 0.76553851] [ 0.83168433 0.83168433 0.83168433] [ 0.89204934 0.89204934 0.89204934] [ 0.94787016 0.94787016 0.94787016] [ 1. 1. 1. ]] >>> print(LUT.invert()) # doctest: +ELLIPSIS LUT3x1D - ... - Inverse ----------...---------- <BLANKLINE> Dimensions : 2 Domain : [[ 0. ... 0. ... 0. ...] [ 0.3683438... 0.3683438... 0.3683438...] [ 0.5047603... 0.5047603... 0.5047603...] [ 0.6069133... 0.6069133... 0.6069133...] [ 0.6916988... 0.6916988... 0.6916988...] [ 0.7655385... 0.7655385... 0.7655385...] [ 0.8316843... 0.8316843... 0.8316843...] [ 0.8920493... 0.8920493... 0.8920493...] [ 0.9478701... 0.9478701... 0.9478701...] [ 1. ... 1. ... 1. ...]] Size : (10, 3) >>> print(LUT.invert().table) # doctest: +ELLIPSIS [[ 0. ... 0. ... 0. ...] [ 0.1111111... 0.1111111... 0.1111111...] [ 0.2222222... 0.2222222... 0.2222222...] [ 0.3333333... 0.3333333... 0.3333333...] [ 0.4444444... 0.4444444... 0.4444444...] [ 0.5555555... 0.5555555... 0.5555555...] [ 0.6666666... 0.6666666... 0.6666666...] [ 0.7777777... 0.7777777... 0.7777777...] [ 0.8888888... 0.8888888... 0.8888888...] [ 1. ... 1. ... 1. ...]] """ size = self.table.size // 3 if self.is_domain_explicit(): domain = [ axes[: (~np.isnan(axes)).cumsum().argmax() + 1] for axes in np.transpose(self.domain) ] else: domain_min, domain_max = self.domain domain = [np.linspace(domain_min[i], domain_max[i], size) for i in range(3)] LUT_i = LUT3x1D( table=tstack(domain), name=f"{self.name} - Inverse", domain=self.table, ) return LUT_i
[docs] def apply(self, RGB: ArrayLike, **kwargs: Any) -> NDArrayFloat: """ Apply the *LUT* to given *RGB* colourspace array using given method. Parameters ---------- RGB *RGB* colourspace array to apply the *LUT* onto. Other Parameters ---------------- direction Whether the *LUT* should be applied in the forward or inverse direction. extrapolator Extrapolator class type or object to use as extrapolating function. extrapolator_kwargs Arguments to use when instantiating or calling the extrapolating function. interpolator Interpolator class type to use as interpolating function. interpolator_kwargs Arguments to use when instantiating the interpolating function. Returns ------- :class:`numpy.ndarray` Interpolated *RGB* colourspace array. Examples -------- >>> LUT = LUT3x1D(LUT3x1D.linear_table() ** (1 / 2.2)) >>> RGB = np.array([0.18, 0.18, 0.18]) >>> LUT.apply(RGB) # doctest: +ELLIPSIS array([ 0.4529220..., 0.4529220..., 0.4529220...]) >>> LUT.apply(LUT.apply(RGB), direction="Inverse") ... # doctest: +ELLIPSIS array([ 0.18..., 0.18..., 0.18...]) >>> from colour.algebra import spow >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> table = spow(LUT3x1D.linear_table(domain=domain), 1 / 2.2) >>> LUT = LUT3x1D(table, domain=domain) >>> RGB = np.array([0.18, 0.18, 0.18]) >>> LUT.apply(RGB) # doctest: +ELLIPSIS array([ 0.4423903..., 0.4503801..., 0.3581625...]) >>> domain = np.array( ... [ ... [-0.1, -0.2, -0.4], ... [0.3, 1.4, 6.0], ... [0.7, 3.0, np.nan], ... [1.1, np.nan, np.nan], ... [1.5, np.nan, np.nan], ... ] ... ) >>> table = spow(LUT3x1D.linear_table(domain=domain), 1 / 2.2) >>> LUT = LUT3x1D(table, domain=domain) >>> RGB = np.array([0.18, 0.18, 0.18]) >>> LUT.apply(RGB) # doctest: +ELLIPSIS array([ 0.2996370..., -0.0901332..., -0.3949770...]) """ direction = validate_method( kwargs.get("direction", "Forward"), ("Forward", "Inverse") ) interpolator = kwargs.get("interpolator", LinearInterpolator) interpolator_kwargs = kwargs.get("interpolator_kwargs", {}) extrapolator = kwargs.get("extrapolator", Extrapolator) extrapolator_kwargs = kwargs.get("extrapolator_kwargs", {}) R, G, B = tsplit(RGB) LUT = self.invert() if direction == "inverse" else self size = LUT.table.size // 3 if LUT.is_domain_explicit(): samples = [ axes[: (~np.isnan(axes)).cumsum().argmax() + 1] for axes in np.transpose(LUT.domain) ] R_t, G_t, B_t = ( axes[: len(samples[i])] for i, axes in enumerate(np.transpose(LUT.table)) ) else: domain_min, domain_max = LUT.domain samples = [ np.linspace(domain_min[i], domain_max[i], size) for i in range(3) ] R_t, G_t, B_t = tsplit(LUT.table) s_R, s_G, s_B = samples RGB_i = [ extrapolator( interpolator(a[0], a[1], **interpolator_kwargs), **extrapolator_kwargs, )(a[2]) for a in zip((s_R, s_G, s_B), (R_t, G_t, B_t), (R, G, B)) ] return tstack(RGB_i)
[docs] class LUT3D(AbstractLUT): """ Define the base class for a 3D *LUT*. Parameters ---------- table Underlying *LUT* table. name *LUT* name. domain *LUT* domain, also used to define the instantiation time default table domain. size Size of the instantiation time default table, default to 33. comments Comments to add to the *LUT*. Methods ------- - :meth:`~colour.LUT3D.__init__` - :meth:`~colour.LUT3D.is_domain_explicit` - :meth:`~colour.LUT3D.linear_table` - :meth:`~colour.LUT3D.invert` - :meth:`~colour.LUT3D.apply` Examples -------- Instantiating a unity LUT with a table with 16x16x16x3 elements: >>> print(LUT3D(size=16)) LUT3D - Unity 16 ---------------- <BLANKLINE> Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (16, 16, 16, 3) Instantiating a LUT using a custom table with 16x16x16x3 elements: >>> print(LUT3D(LUT3D.linear_table(16) ** (1 / 2.2))) # doctest: +ELLIPSIS LUT3D - ... --------... <BLANKLINE> Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (16, 16, 16, 3) Instantiating a LUT using a custom table with 16x16x16x3 elements, custom name, custom domain and comments: >>> from colour.algebra import spow >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> print( ... LUT3D( ... spow(LUT3D.linear_table(16), 1 / 2.2), ... "My LUT", ... domain, ... comments=["A first comment.", "A second comment."], ... ) ... ) LUT3D - My LUT -------------- <BLANKLINE> Dimensions : 3 Domain : [[-0.1 -0.2 -0.4] [ 1.5 3. 6. ]] Size : (16, 16, 16, 3) Comment 01 : A first comment. Comment 02 : A second comment. """
[docs] def __init__( self, table: ArrayLike | None = None, name: str | None = None, domain: ArrayLike | None = None, size: ArrayLike | None = None, comments: Sequence | None = None, ) -> None: domain = as_float_array(optional(domain, [[0, 0, 0], [1, 1, 1]])) size = optional(size, 33) super().__init__(table, name, 3, domain, size, comments)
def _validate_table(self, table: ArrayLike) -> NDArrayFloat: """ Validate given table is a 4D array and that its dimensions are equal. Parameters ---------- table Table to validate. Returns ------- :class:`numpy.ndarray` Validated table as a :class:`ndarray` instance. """ table = as_float_array(table) attest(len(table.shape) == 4, "The table must be a 4D array!") return table def _validate_domain(self, domain: ArrayLike) -> NDArrayFloat: """ Validate given domain. Parameters ---------- domain Domain to validate. Returns ------- :class:`numpy.ndarray` Validated domain as a :class:`ndarray` instance. Notes ----- - A :class:`LUT3D` class instance must use an implicit domain. """ domain = as_float_array(domain) attest(len(domain.shape) == 2, "The domain must be a 2D array!") attest( domain.shape[0] >= 2, "The domain row count must be equal or greater than 2!", ) attest(domain.shape[1] == 3, "The domain column count must be equal to 3!") return domain
[docs] def is_domain_explicit(self) -> bool: """ Return whether the *LUT* domain is explicit (or implicit). An implicit domain is defined by its shape only:: [[0 0 0] [1 1 1]] While an explicit domain defines every single discrete samples:: [[0.0 0.0 0.0] [0.1 0.1 0.1] [0.2 0.2 0.2] [0.3 0.3 0.3] [0.4 0.4 0.4] [0.8 0.8 0.8] [1.0 1.0 1.0]] Returns ------- :class:`bool` Is *LUT* domain explicit. Examples -------- >>> LUT3D().is_domain_explicit() False >>> domain = np.array([[-0.1, -0.2, -0.4], [0.7, 1.4, 6.0], [1.5, 3.0, np.nan]]) >>> LUT3D(domain=domain).is_domain_explicit() True """ return self.domain.shape != (2, 3)
[docs] @staticmethod def linear_table( size: ArrayLike | None = None, domain: ArrayLike | None = None, ) -> NDArrayFloat: """ Return a linear table, the number of output samples :math:`n` is equal to ``size**3 * 3`` or ``size[0] * size[1] * size[2] * 3``. Parameters ---------- size Expected table size, default to 33. domain Domain of the table. Returns ------- :class:`numpy.ndarray` Linear table with ``size**3 * 3`` or ``size[0] * size[1] * size[2] * 3`` samples. Examples -------- >>> LUT3D.linear_table(3, np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]])) array([[[[-0.1, -0.2, -0.4], [-0.1, -0.2, 2.8], [-0.1, -0.2, 6. ]], <BLANKLINE> [[-0.1, 1.4, -0.4], [-0.1, 1.4, 2.8], [-0.1, 1.4, 6. ]], <BLANKLINE> [[-0.1, 3. , -0.4], [-0.1, 3. , 2.8], [-0.1, 3. , 6. ]]], <BLANKLINE> <BLANKLINE> [[[ 0.7, -0.2, -0.4], [ 0.7, -0.2, 2.8], [ 0.7, -0.2, 6. ]], <BLANKLINE> [[ 0.7, 1.4, -0.4], [ 0.7, 1.4, 2.8], [ 0.7, 1.4, 6. ]], <BLANKLINE> [[ 0.7, 3. , -0.4], [ 0.7, 3. , 2.8], [ 0.7, 3. , 6. ]]], <BLANKLINE> <BLANKLINE> [[[ 1.5, -0.2, -0.4], [ 1.5, -0.2, 2.8], [ 1.5, -0.2, 6. ]], <BLANKLINE> [[ 1.5, 1.4, -0.4], [ 1.5, 1.4, 2.8], [ 1.5, 1.4, 6. ]], <BLANKLINE> [[ 1.5, 3. , -0.4], [ 1.5, 3. , 2.8], [ 1.5, 3. , 6. ]]]]) >>> LUT3D.linear_table( ... np.array([3, 3, 2]), ... np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]), ... ) array([[[[-0.1, -0.2, -0.4], [-0.1, -0.2, 6. ]], <BLANKLINE> [[-0.1, 1.4, -0.4], [-0.1, 1.4, 6. ]], <BLANKLINE> [[-0.1, 3. , -0.4], [-0.1, 3. , 6. ]]], <BLANKLINE> <BLANKLINE> [[[ 0.7, -0.2, -0.4], [ 0.7, -0.2, 6. ]], <BLANKLINE> [[ 0.7, 1.4, -0.4], [ 0.7, 1.4, 6. ]], <BLANKLINE> [[ 0.7, 3. , -0.4], [ 0.7, 3. , 6. ]]], <BLANKLINE> <BLANKLINE> [[[ 1.5, -0.2, -0.4], [ 1.5, -0.2, 6. ]], <BLANKLINE> [[ 1.5, 1.4, -0.4], [ 1.5, 1.4, 6. ]], <BLANKLINE> [[ 1.5, 3. , -0.4], [ 1.5, 3. , 6. ]]]]) >>> domain = np.array([[-0.1, -0.2, -0.4], [0.7, 1.4, 6.0], [1.5, 3.0, np.nan]]) >>> LUT3D.linear_table(domain=domain) array([[[[-0.1, -0.2, -0.4], [-0.1, -0.2, 6. ]], <BLANKLINE> [[-0.1, 1.4, -0.4], [-0.1, 1.4, 6. ]], <BLANKLINE> [[-0.1, 3. , -0.4], [-0.1, 3. , 6. ]]], <BLANKLINE> <BLANKLINE> [[[ 0.7, -0.2, -0.4], [ 0.7, -0.2, 6. ]], <BLANKLINE> [[ 0.7, 1.4, -0.4], [ 0.7, 1.4, 6. ]], <BLANKLINE> [[ 0.7, 3. , -0.4], [ 0.7, 3. , 6. ]]], <BLANKLINE> <BLANKLINE> [[[ 1.5, -0.2, -0.4], [ 1.5, -0.2, 6. ]], <BLANKLINE> [[ 1.5, 1.4, -0.4], [ 1.5, 1.4, 6. ]], <BLANKLINE> [[ 1.5, 3. , -0.4], [ 1.5, 3. , 6. ]]]]) """ size = optional(size, 33) domain = as_float_array(optional(domain, [[0, 0, 0], [1, 1, 1]])) if domain.shape != (2, 3): samples = list( np.flip( # NOTE: "dtype=object" is required for ragged array support # in "Numpy" 1.24.0. as_array( [ axes[: (~np.isnan(axes)).cumsum().argmax() + 1] for axes in np.transpose(domain) ], dtype=object, # pyright: ignore ), -1, ) ) size_array = as_int_array([len(axes) for axes in samples]) else: size_array = np.tile(size, 3) if is_numeric(size) else as_int_array(size) R, G, B = tsplit(domain) size_array = np.flip(size_array, -1) samples = [ np.linspace(a[0], a[1], size_array[i]) for i, a in enumerate([B, G, R]) ] table = np.flip( np.reshape( np.transpose(np.meshgrid(*samples, indexing="ij")), np.hstack([np.flip(size_array, -1), 3]), ), -1, ) return table
[docs] def invert(self, **kwargs: Any) -> LUT3D: """ Compute and returns an inverse copy of the *LUT*. Other Parameters ---------------- extrapolate Whether to extrapolate the *LUT* when computing its inverse. Extrapolation is performed by reflecting the *LUT* cube along its 8 faces. Note that the domain is extended beyond [0, 1], thus the *LUT* might not be handled properly in other software. interpolator Interpolator class type or object to use as interpolating function. query_size Number of points to query in the KDTree, their mean is computed, resulting in a smoother result. size Size of the inverse *LUT*. With the given implementation, it is good practise to double the size of the inverse *LUT* to provide a smoother result. If ``size`` is not given, :math:`2^{\\sqrt{size_{LUT}} + 1} + 1` will be used instead. Returns ------- :class:`colour.LUT3D` Inverse *LUT* class instance. Examples -------- >>> LUT = LUT3D() >>> print(LUT) LUT3D - Unity 33 ---------------- <BLANKLINE> Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (33, 33, 33, 3) >>> print(LUT.invert()) LUT3D - Unity 33 - Inverse -------------------------- <BLANKLINE> Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (108, 108, 108, 3) """ if self.is_domain_explicit(): raise NotImplementedError( 'Inverting a "LUT3D" with an explicit domain is not implemented!' ) interpolator = kwargs.get("interpolator", table_interpolation_trilinear) extrapolate = kwargs.get("extrapolate", False) query_size = kwargs.get("query_size", 3) LUT = self.copy() source_size = LUT.size target_size = kwargs.get("size", (as_int(2 ** (np.sqrt(source_size) + 1) + 1))) if target_size > 129: # pragma: no cover usage_warning("LUT3D inverse computation time could be excessive!") if extrapolate: LUT.table = np.pad( LUT.table, [(1, 1), (1, 1), (1, 1), (0, 0)], "reflect", reflect_type="odd", ) LUT.domain[0] -= 1 / (source_size - 1) LUT.domain[1] += 1 / (source_size - 1) # "LUT_t" is an intermediate LUT with a size equal to that of the # final inverse LUT which is usually larger than the input LUT. # The intent is to smooth the inverse LUT's table by increasing the # resolution of the KDTree. LUT_t = LUT3D(size=target_size, domain=LUT.domain) table = np.reshape(LUT_t.table, (-1, 3)) LUT_t.table = LUT.apply(LUT_t.table, interpolator=interpolator) tree = KDTree(np.reshape(LUT_t.table, (-1, 3))) # "LUT_q" stores the indexes of the KDTree query, i.e., the closest # entry of "LUT_t" for any searched table sample. LUT_q = LUT3D(size=target_size, domain=LUT.domain) query = tree.query(table, query_size)[-1] if query_size == 1: LUT_q.table = np.reshape( table[query], (target_size, target_size, target_size, 3) ) else: LUT_q.table = np.reshape( np.mean(table[query], axis=-2), (target_size, target_size, target_size, 3), ) LUT_q.name = f"{self.name} - Inverse" return LUT_q
[docs] def apply(self, RGB: ArrayLike, **kwargs: Any) -> NDArrayFloat: """ Apply the *LUT* to given *RGB* colourspace array using given method. Parameters ---------- RGB *RGB* colourspace array to apply the *LUT* onto. Other Parameters ---------------- direction Whether the *LUT* should be applied in the forward or inverse direction. extrapolate Whether to extrapolate the *LUT* when computing its inverse. Extrapolation is performed by reflecting the *LUT* cube along its 8 faces. interpolator Interpolator object to use as interpolating function. interpolator_kwargs Arguments to use when calling the interpolating function. query_size Number of points to query in the KDTree, their mean is computed, resulting in a smoother result. size Size of the inverse *LUT*. With the given implementation, it is good practise to double the size of the inverse *LUT* to provide a smoother result. If ``size`` is not given, :math:`2^{\\sqrt{size_{LUT}} + 1} + 1` will be used instead. Returns ------- :class:`numpy.ndarray` Interpolated *RGB* colourspace array. Examples -------- >>> LUT = LUT3D(LUT3D.linear_table() ** (1 / 2.2)) >>> RGB = np.array([0.18, 0.18, 0.18]) >>> LUT.apply(RGB) # doctest: +ELLIPSIS array([ 0.4583277..., 0.4583277..., 0.4583277...]) >>> LUT.apply(LUT.apply(RGB), direction="Inverse") ... # doctest: +ELLIPSIS array([ 0.1781995..., 0.1809414..., 0.1809513...]) >>> from colour.algebra import spow >>> domain = np.array( ... [ ... [-0.1, -0.2, -0.4], ... [0.3, 1.4, 6.0], ... [0.7, 3.0, np.nan], ... [1.1, np.nan, np.nan], ... [1.5, np.nan, np.nan], ... ] ... ) >>> table = spow(LUT3D.linear_table(domain=domain), 1 / 2.2) >>> LUT = LUT3D(table, domain=domain) >>> RGB = np.array([0.18, 0.18, 0.18]) >>> LUT.apply(RGB) # doctest: +ELLIPSIS array([ 0.2996370..., -0.0901332..., -0.3949770...]) """ direction = validate_method( kwargs.get("direction", "Forward"), ("Forward", "Inverse") ) interpolator = kwargs.get("interpolator", table_interpolation_trilinear) interpolator_kwargs = kwargs.get("interpolator_kwargs", {}) R, G, B = tsplit(RGB) settings = {"interpolator": interpolator} settings.update(**kwargs) LUT = self.invert(**settings) if direction == "inverse" else self if LUT.is_domain_explicit(): domain_min = LUT.domain[0, ...] domain_max = [ axes[: (~np.isnan(axes)).cumsum().argmax() + 1][-1] for axes in np.transpose(LUT.domain) ] usage_warning( f'"LUT" was defined with an explicit domain but requires an ' f"implicit domain to be applied. The following domain will be " f"used: {np.vstack([domain_min, domain_max])}" ) else: domain_min, domain_max = LUT.domain RGB_l = [ linear_conversion(j, (domain_min[i], domain_max[i]), (0, 1)) for i, j in enumerate((R, G, B)) ] RGB_i = interpolator(tstack(RGB_l), LUT.table, **interpolator_kwargs) return RGB_i
[docs] def LUT_to_LUT( LUT, cls: Type[AbstractLUT], force_conversion: bool = False, **kwargs: Any, ) -> AbstractLUT: """ Convert given *LUT* to given ``cls`` class instance. Parameters ---------- cls *LUT* class instance. force_conversion Whether to force the conversion if destructive. Other Parameters ---------------- channel_weights Channel weights in case of a downcast from a :class:`LUT3x1D` or :class:`LUT3D` class instance. interpolator Interpolator class type to use as interpolating function. interpolator_kwargs Arguments to use when instantiating the interpolating function. size Expected table size in case of an upcast to or a downcast from a :class:`LUT3D` class instance. Returns ------- :class:`colour.LUT1D` or :class:`colour.LUT3x1D` or :class:`colour.LUT3D` Converted *LUT* class instance. Warnings -------- Some conversions are destructive and raise a :class:`ValueError` exception by default. Raises ------ ValueError If the conversion is destructive. Examples -------- >>> print(LUT_to_LUT(LUT1D(), LUT3D, force_conversion=True)) LUT3D - Unity 10 - Converted 1D to 3D ------------------------------------- <BLANKLINE> Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (33, 33, 33, 3) >>> print(LUT_to_LUT(LUT3x1D(), LUT1D, force_conversion=True)) LUT1D - Unity 10 - Converted 3x1D to 1D --------------------------------------- <BLANKLINE> Dimensions : 1 Domain : [ 0. 1.] Size : (10,) >>> print(LUT_to_LUT(LUT3D(), LUT1D, force_conversion=True)) LUT1D - Unity 33 - Converted 3D to 1D ------------------------------------- <BLANKLINE> Dimensions : 1 Domain : [ 0. 1.] Size : (10,) """ ranks = {LUT1D: 1, LUT3x1D: 2, LUT3D: 3} path = (ranks[LUT.__class__], ranks[cls]) # pyright: ignore path_verbose = [f"{element}D" if element != 2 else "3x1D" for element in path] if path in ((1, 3), (2, 1), (2, 3), (3, 1), (3, 2)) and not force_conversion: raise ValueError( f'Conversion of a "LUT" {path_verbose[0]} to a "LUT" ' f"{path_verbose[1]} is destructive, please use the " f'"force_conversion" argument to proceed!' ) suffix = f" - Converted {path_verbose[0]} to {path_verbose[1]}" name = f"{LUT.name}{suffix}" # Same dimension conversion, returning a copy. if len(set(path)) == 1: LUT = LUT.copy() LUT.name = name else: size = kwargs.get("size", 33 if cls is LUT3D else 10) if "size" in kwargs: del kwargs["size"] channel_weights = as_float_array(kwargs.get("channel_weights", full(3, 1 / 3))) if "channel_weights" in kwargs: del kwargs["channel_weights"] if isinstance(LUT, LUT1D): if cls is LUT3x1D: domain = tstack([LUT.domain, LUT.domain, LUT.domain]) table = tstack([LUT.table, LUT.table, LUT.table]) elif cls is LUT3D: domain = tstack([LUT.domain, LUT.domain, LUT.domain]) table = LUT3D.linear_table(size, domain) table = LUT.apply(table, **kwargs) elif isinstance(LUT, LUT3x1D): if cls is LUT1D: domain = np.sum(LUT.domain * channel_weights, axis=-1) table = np.sum(LUT.table * channel_weights, axis=-1) elif cls is LUT3D: domain = LUT.domain table = LUT3D.linear_table(size, domain) table = LUT.apply(table, **kwargs) elif isinstance(LUT, LUT3D): if cls is LUT1D: domain = np.sum(LUT.domain * channel_weights, axis=-1) table = LUT1D.linear_table(size, domain) table = LUT.apply(tstack([table, table, table]), **kwargs) table = np.sum(table * channel_weights, axis=-1) elif cls is LUT3x1D: domain = LUT.domain table = LUT3x1D.linear_table(size, domain) table = LUT.apply(table, **kwargs) LUT = cls( table=table, name=name, domain=domain, size=table.shape[0], comments=LUT.comments, ) # pyright: ignore return LUT