"""
LUT Operator
============
Defines the *LUT* operator classes:
- :class:`colour.io.AbstractLUTSequenceOperator`
- :class:`colour.LUTOperatorMatrix`
"""
from __future__ import annotations
import numpy as np
from abc import ABC, abstractmethod
from colour.algebra import vector_dot
from colour.hints import (
Any,
ArrayLike,
List,
NDArrayFloat,
Sequence,
cast,
)
from colour.utilities import (
as_float_array,
attest,
is_iterable,
is_string,
ones,
optional,
zeros,
)
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "New BSD License - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"AbstractLUTSequenceOperator",
"LUTOperatorMatrix",
]
[docs]class AbstractLUTSequenceOperator(ABC):
"""
Define the base class for *LUT* sequence operators.
This is an :class:`ABCMeta` abstract class that must be inherited by
sub-classes.
Parameters
----------
name
*LUT* sequence operator name.
comments
Comments to add to the *LUT* sequence operator.
Attributes
----------
- :attr:`~colour.io.AbstractLUTSequenceOperator.name`
- :attr:`~colour.io.AbstractLUTSequenceOperator.comments`
Methods
-------
- :meth:`~colour.io.AbstractLUTSequenceOperator.apply`
"""
[docs] def __init__(
self,
name: str | None = None,
comments: Sequence[str] | None = None,
) -> None:
self._name = f"LUT Sequence Operator {id(self)}"
self.name = optional(name, self._name)
self._comments: List[str] = []
self.comments = optional(comments, self._comments)
@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(
is_string(value),
f'"name" property: "{value}" type is not "str"!',
)
self._name = value
@property
def comments(self) -> List[str]:
"""
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[str]):
"""Setter for the **self.comments** property."""
attest(
is_iterable(value),
f'"comments" property: "{value}" must be a sequence!',
)
self._comments = list(value)
[docs] @abstractmethod
def apply(self, RGB: ArrayLike, *args: Any, **kwargs: Any) -> NDArrayFloat:
"""
Apply the *LUT* sequence operator to given *RGB* colourspace array.
Parameters
----------
RGB
*RGB* colourspace array to apply the *LUT* sequence operator onto.
Other Parameters
----------------
args
Arguments.
kwargs
Keywords arguments.
Returns
-------
:class:`numpy.ndarray`
Processed *RGB* colourspace array.
"""
[docs]class LUTOperatorMatrix(AbstractLUTSequenceOperator):
"""
Define the *LUT* operator supporting a 3x3 or 4x4 matrix and an offset
vector.
Parameters
----------
matrix
3x3 or 4x4 matrix for the operator.
offset
Offset for the operator.
name
*LUT* operator name.
comments
Comments to add to the *LUT* operator.
Attributes
----------
- :meth:`~colour.LUTOperatorMatrix.matrix`
- :meth:`~colour.LUTOperatorMatrix.offset`
Methods
-------
- :meth:`~colour.LUTOperatorMatrix.__str__`
- :meth:`~colour.LUTOperatorMatrix.__repr__`
- :meth:`~colour.LUTOperatorMatrix.__eq__`
- :meth:`~colour.LUTOperatorMatrix.__ne__`
- :meth:`~colour.LUTOperatorMatrix.apply`
Notes
-----
- The internal :attr:`colour.io.Matrix.matrix` and
:attr:`colour.io.Matrix.offset` properties are reshaped to (4, 4) and
(4, ) respectively.
Examples
--------
Instantiating an identity matrix:
>>> print(LUTOperatorMatrix(name="Identity"))
LUTOperatorMatrix - Identity
----------------------------
<BLANKLINE>
Matrix : [[ 1. 0. 0. 0.]
[ 0. 1. 0. 0.]
[ 0. 0. 1. 0.]
[ 0. 0. 0. 1.]]
Offset : [ 0. 0. 0. 0.]
Instantiating a matrix with comments:
>>> matrix = np.array(
... [
... [1.45143932, -0.23651075, -0.21492857],
... [-0.07655377, 1.1762297, -0.09967593],
... [0.00831615, -0.00603245, 0.9977163],
... ]
... )
>>> print(
... LUTOperatorMatrix(
... matrix,
... name="AP0 to AP1",
... comments=["A first comment.", "A second comment."],
... )
... )
LUTOperatorMatrix - AP0 to AP1
------------------------------
<BLANKLINE>
Matrix : [[ 1.45143932 -0.23651075 -0.21492857 0. ]
[-0.07655377 1.1762297 -0.09967593 0. ]
[ 0.00831615 -0.00603245 0.9977163 0. ]
[ 0. 0. 0. 1. ]]
Offset : [ 0. 0. 0. 0.]
<BLANKLINE>
A first comment.
A second comment.
"""
[docs] def __init__(
self,
matrix: ArrayLike | None = None,
offset: ArrayLike | None = None,
*args: Any,
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
self._matrix: NDArrayFloat = np.diag(ones(4))
self.matrix = cast(ArrayLike, optional(matrix, self._matrix))
self._offset: NDArrayFloat = zeros(4)
self.offset = cast(ArrayLike, optional(offset, self._offset))
@property
def matrix(self) -> NDArrayFloat:
"""
Getter and setter property for the *LUT* operator matrix.
Parameters
----------
value
Value to set the *LUT* operator matrix with.
Returns
-------
:class:`numpy.ndarray`
Operator matrix.
"""
return self._matrix
@matrix.setter
def matrix(self, value: ArrayLike):
"""Setter for the **self.matrix** property."""
value = as_float_array(value)
shape_t = value.shape[-1]
value = value.reshape([shape_t, shape_t])
attest(
value.shape in [(3, 3), (4, 4)],
f'"matrix" property: "{value}" shape is not (3, 3) or (4, 4)!',
)
M = np.identity(4)
M[:shape_t, :shape_t] = value
self._matrix = M
@property
def offset(self) -> NDArrayFloat:
"""
Getter and setter property for the *LUT* operator offset.
Parameters
----------
value
Value to set the *LUT* operator offset with.
Returns
-------
:class:`numpy.ndarray`
Operator offset.
"""
return self._offset
@offset.setter
def offset(self, value: ArrayLike):
"""Setter for the **self.offset** property."""
value = as_float_array(value)
shape_t = value.shape[-1]
attest(
value.shape in [(3,), (4,)],
f'"offset" property: "{value}" shape is not (3, ) or (4, )!',
)
offset = zeros(4)
offset[:shape_t] = value
self._offset = offset
[docs] def __str__(self) -> str:
"""
Return a formatted string representation of the *LUT* operator.
Returns
-------
:class:`str`
Formatted string representation.
Examples
--------
>>> print(LUTOperatorMatrix()) # doctest: +ELLIPSIS
LUTOperatorMatrix - LUT Sequence Operator ...
------------------------------------------...
<BLANKLINE>
Matrix : [[ 1. 0. 0. 0.]
[ 0. 1. 0. 0.]
[ 0. 0. 1. 0.]
[ 0. 0. 0. 1.]]
Offset : [ 0. 0. 0. 0.]
"""
def _format(a: ArrayLike) -> str:
"""Format given array string representation."""
return str(a).replace(" [", " " * 14 + "[")
comments = "\n".join(self._comments)
comments = f"\n\n{comments}" if self._comments else ""
underline = "-" * (len(self.__class__.__name__) + 3 + len(self._name))
return "\n".join(
[
f"{self.__class__.__name__} - {self._name}",
f"{underline}",
"",
f"Matrix : {_format(self._matrix)}",
f"Offset : {_format(self._offset)}{comments}",
]
)
[docs] def __repr__(self) -> str:
"""
Return an evaluable string representation of the *LUT* operator.
Returns
-------
:class:`str`
Evaluable string representation.
Examples
--------
>>> LUTOperatorMatrix(
... comments=["A first comment.", "A second comment."]
... )
... # doctest: +ELLIPSIS
LUTOperatorMatrix([[ 1., 0., 0., 0.],
[ 0., 1., 0., 0.],
[ 0., 0., 1., 0.],
[ 0., 0., 0., 1.]],
[ 0., 0., 0., 0.],
name='LUT Sequence Operator ...',
comments=['A first comment.', 'A second comment.'])
"""
representation = repr(self._matrix)
representation = representation.replace(
"array", self.__class__.__name__
)
representation = representation.replace(
" [", f"{' ' * (len(self.__class__.__name__) + 2)}["
)
indentation = " " * (len(self.__class__.__name__) + 1)
comments = (
f",\n{indentation}comments={repr(self._comments)}"
if self._comments
else ""
)
return "\n".join(
[
f"{representation[:-1]},",
f"{indentation}"
f'{repr(self._offset).replace("array(", "").replace(")", "")},',
f"{indentation}name='{self._name}'{comments})",
]
)
[docs] def __eq__(self, other: Any) -> bool:
"""
Return whether the *LUT* operator is equal to given other object.
Parameters
----------
other
Object to test whether it is equal to the *LUT* operator.
Returns
-------
:class:`bool`
Whether given object equal to the *LUT* operator.
Examples
--------
>>> LUTOperatorMatrix() == LUTOperatorMatrix()
True
"""
if isinstance(other, LUTOperatorMatrix) and all(
[
np.array_equal(self._matrix, other._matrix),
np.array_equal(self._offset, other._offset),
]
):
return True
return False
[docs] def __ne__(self, other: Any) -> bool:
"""
Return whether the *LUT* operator is not equal to given other object.
Parameters
----------
other
Object to test whether it is not equal to the *LUT* operator.
Returns
-------
:class:`bool`
Whether given object is not equal to the *LUT* operator.
Examples
--------
>>> LUTOperatorMatrix() != LUTOperatorMatrix(
... np.linspace(0, 1, 16).reshape([4, 4])
... )
True
"""
return not (self == other)
[docs] def apply(
self, RGB: ArrayLike, *args: Any, **kwargs: Any # noqa: ARG002
) -> NDArrayFloat:
"""
Apply the *LUT* operator to given *RGB* array.
Parameters
----------
RGB
*RGB* array to apply the *LUT* operator transform to.
Other Parameters
----------------
apply_offset_first
Whether to apply the offset first and then the matrix.
Returns
-------
:class:`numpy.ndarray`
Transformed *RGB* array.
Examples
--------
>>> matrix = np.array(
... [
... [1.45143932, -0.23651075, -0.21492857],
... [-0.07655377, 1.1762297, -0.09967593],
... [0.00831615, -0.00603245, 0.9977163],
... ]
... )
>>> M = LUTOperatorMatrix(matrix)
>>> RGB = np.array([0.3, 0.4, 0.5])
>>> M.apply(RGB) # doctest: +ELLIPSIS
array([ 0.2333632..., 0.3976877..., 0.4989400...])
"""
RGB = as_float_array(RGB)
apply_offset_first = kwargs.get("apply_offset_first", False)
has_alpha_channel = RGB.shape[-1] == 4
M = self._matrix
offset = self._offset
if not has_alpha_channel:
M = M[:3, :3]
offset = offset[:3]
if apply_offset_first:
RGB += offset
RGB = vector_dot(M, RGB)
if not apply_offset_first:
RGB += offset
return RGB