Coverage for colour/io/luts/operator.py: 100%
99 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2LUT Operator
3============
5Define operator classes for Look-Up Table (LUT) transformations within the
6colour processing pipeline.
8- :class:`colour.io.AbstractLUTSequenceOperator`
9- :class:`colour.LUTOperatorMatrix`
10"""
12from __future__ import annotations
14import typing
15from abc import ABC, abstractmethod
17import numpy as np
19from colour.algebra import vecmul
21if typing.TYPE_CHECKING:
22 from colour.hints import (
23 Any,
24 ArrayLike,
25 List,
26 NDArrayFloat,
27 Sequence,
28 )
30from colour.utilities import as_float_array, attest, is_iterable, ones, optional, zeros
32__author__ = "Colour Developers"
33__copyright__ = "Copyright 2013 Colour Developers"
34__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
35__maintainer__ = "Colour Developers"
36__email__ = "colour-developers@colour-science.org"
37__status__ = "Production"
39__all__ = [
40 "AbstractLUTSequenceOperator",
41 "LUTOperatorMatrix",
42]
45class AbstractLUTSequenceOperator(ABC):
46 """
47 Define the base class for *LUT* sequence operators.
49 Provide an abstract base class that establishes the interface for all
50 *LUT* sequence operator implementations. This :class:`ABCMeta` abstract
51 class must be inherited by concrete sub-classes that implement specific
52 operator functionality within *LUT* processing pipelines.
54 Parameters
55 ----------
56 name
57 *LUT* sequence operator name.
58 comments
59 Comments to add to the *LUT* sequence operator.
61 Attributes
62 ----------
63 - :attr:`~colour.io.AbstractLUTSequenceOperator.name`
64 - :attr:`~colour.io.AbstractLUTSequenceOperator.comments`
66 Methods
67 -------
68 - :meth:`~colour.io.AbstractLUTSequenceOperator.apply`
69 """
71 def __init__(
72 self,
73 name: str | None = None,
74 comments: Sequence[str] | None = None,
75 ) -> None:
76 self._name = f"LUT Sequence Operator {id(self)}"
77 self.name = optional(name, self._name)
78 self._comments: List[str] = []
79 self.comments = optional(comments, self._comments)
81 @property
82 def name(self) -> str:
83 """
84 Getter and setter for the *LUT* name.
86 Parameters
87 ----------
88 value
89 Value to set the *LUT* name with.
91 Returns
92 -------
93 :class:`str`
94 *LUT* name.
95 """
97 return self._name
99 @name.setter
100 def name(self, value: str) -> None:
101 """Setter for the **self.name** property."""
103 attest(
104 isinstance(value, str),
105 f'"name" property: "{value}" type is not "str"!',
106 )
108 self._name = value
110 @property
111 def comments(self) -> List[str]:
112 """
113 Getter and setter for the *LUT* comments.
115 Parameters
116 ----------
117 value
118 Value to set the *LUT* comments with.
120 Returns
121 -------
122 :class:`list`
123 *LUT* comments.
124 """
126 return self._comments
128 @comments.setter
129 def comments(self, value: Sequence[str]) -> None:
130 """Setter for the **self.comments** property."""
132 attest(
133 is_iterable(value),
134 f'"comments" property: "{value}" must be a sequence!',
135 )
137 self._comments = list(value)
139 @abstractmethod
140 def apply(self, RGB: ArrayLike, *args: Any, **kwargs: Any) -> NDArrayFloat:
141 """
142 Apply the *LUT* sequence operator to the specified *RGB* colourspace
143 array.
145 Parameters
146 ----------
147 RGB
148 *RGB* colourspace array to apply the *LUT* sequence operator onto.
150 Other Parameters
151 ----------------
152 args
153 Arguments.
154 kwargs
155 Keywords arguments.
157 Returns
158 -------
159 :class:`numpy.ndarray`
160 Processed *RGB* colourspace array.
161 """
164class LUTOperatorMatrix(AbstractLUTSequenceOperator):
165 """
166 Define the *LUT* operator that applies matrix transformations and offset
167 vectors for colour space conversions.
169 Support 3x3 or 4x4 matrix operations with optional offset vectors to
170 perform affine transformations on *RGB* colourspace data. The operator
171 internally reshapes matrices to 4x4 and offsets to 4-element vectors to
172 maintain computational consistency.
174 Parameters
175 ----------
176 matrix
177 3x3 or 4x4 matrix for the operator.
178 offset
179 Offset for the operator.
180 name
181 *LUT* operator name.
182 comments
183 Comments to add to the *LUT* operator.
185 Attributes
186 ----------
187 - :meth:`~colour.LUTOperatorMatrix.matrix`
188 - :meth:`~colour.LUTOperatorMatrix.offset`
190 Methods
191 -------
192 - :meth:`~colour.LUTOperatorMatrix.__str__`
193 - :meth:`~colour.LUTOperatorMatrix.__repr__`
194 - :meth:`~colour.LUTOperatorMatrix.__eq__`
195 - :meth:`~colour.LUTOperatorMatrix.__ne__`
196 - :meth:`~colour.LUTOperatorMatrix.apply`
198 Notes
199 -----
200 - The internal :attr:`colour.io.Matrix.matrix` and
201 :attr:`colour.io.Matrix.offset` properties are reshaped to (4, 4) and
202 (4, ) respectively.
204 Examples
205 --------
206 Instantiating an identity matrix:
208 >>> print(LUTOperatorMatrix(name="Identity"))
209 LUTOperatorMatrix - Identity
210 ----------------------------
211 <BLANKLINE>
212 Matrix : [[ 1. 0. 0. 0.]
213 [ 0. 1. 0. 0.]
214 [ 0. 0. 1. 0.]
215 [ 0. 0. 0. 1.]]
216 Offset : [ 0. 0. 0. 0.]
218 Instantiating a matrix with comments:
220 >>> matrix = np.array(
221 ... [
222 ... [1.45143932, -0.23651075, -0.21492857],
223 ... [-0.07655377, 1.1762297, -0.09967593],
224 ... [0.00831615, -0.00603245, 0.9977163],
225 ... ]
226 ... )
227 >>> print(
228 ... LUTOperatorMatrix(
229 ... matrix,
230 ... name="AP0 to AP1",
231 ... comments=["A first comment.", "A second comment."],
232 ... )
233 ... )
234 LUTOperatorMatrix - AP0 to AP1
235 ------------------------------
236 <BLANKLINE>
237 Matrix : [[ 1.45143932 -0.23651075 -0.21492857 0. ]
238 [-0.07655377 1.1762297 -0.09967593 0. ]
239 [ 0.00831615 -0.00603245 0.9977163 0. ]
240 [ 0. 0. 0. 1. ]]
241 Offset : [ 0. 0. 0. 0.]
242 <BLANKLINE>
243 A first comment.
244 A second comment.
245 """
247 def __init__(
248 self,
249 matrix: ArrayLike | None = None,
250 offset: ArrayLike | None = None,
251 *args: Any,
252 **kwargs: Any,
253 ) -> None:
254 super().__init__(*args, **kwargs)
256 self._matrix: NDArrayFloat = np.diag(ones(4))
257 self.matrix = optional(matrix, self._matrix)
258 self._offset: NDArrayFloat = zeros(4)
259 self.offset = optional(offset, self._offset)
261 @property
262 def matrix(self) -> NDArrayFloat:
263 """
264 Getter and setter for the *LUT* operator matrix.
266 Parameters
267 ----------
268 value
269 Value to set the *LUT* operator matrix with.
271 Returns
272 -------
273 :class:`numpy.ndarray`
274 Operator matrix.
275 """
277 return self._matrix
279 @matrix.setter
280 def matrix(self, value: ArrayLike) -> None:
281 """Setter for the **self.matrix** property."""
283 value = as_float_array(value)
285 shape_t = value.shape[-1]
287 value = np.reshape(value, (shape_t, shape_t))
289 attest(
290 value.shape in [(3, 3), (4, 4)],
291 f'"matrix" property: "{value}" shape is not (3, 3) or (4, 4)!',
292 )
294 M = np.identity(4)
295 M[:shape_t, :shape_t] = value
297 self._matrix = M
299 @property
300 def offset(self) -> NDArrayFloat:
301 """
302 Getter and setter for the *LUT* operator offset vector.
304 The offset vector is applied after the matrix transformation in the LUT
305 operator, enabling translation operations in the colour space.
307 Parameters
308 ----------
309 value
310 Value to set the *LUT* operator offset with.
312 Returns
313 -------
314 :class:`numpy.ndarray`
315 Operator offset vector.
316 """
318 return self._offset
320 @offset.setter
321 def offset(self, value: ArrayLike) -> None:
322 """Setter for the **self.offset** property."""
324 value = as_float_array(value)
326 shape_t = value.shape[-1]
328 attest(
329 value.shape in [(3,), (4,)],
330 f'"offset" property: "{value}" shape is not (3, ) or (4, )!',
331 )
333 offset = zeros(4)
334 offset[:shape_t] = value
336 self._offset = offset
338 def __str__(self) -> str:
339 """
340 Return a formatted string representation of the *LUT* operator.
342 Returns
343 -------
344 :class:`str`
345 Formatted string representation.
347 Examples
348 --------
349 >>> print(LUTOperatorMatrix()) # doctest: +ELLIPSIS
350 LUTOperatorMatrix - LUT Sequence Operator ...
351 ------------------------------------------...
352 <BLANKLINE>
353 Matrix : [[ 1. 0. 0. 0.]
354 [ 0. 1. 0. 0.]
355 [ 0. 0. 1. 0.]
356 [ 0. 0. 0. 1.]]
357 Offset : [ 0. 0. 0. 0.]
358 """
360 def _format(a: ArrayLike) -> str:
361 """Format specified array string representation."""
363 return str(a).replace(" [", " " * 14 + "[")
365 comments = "\n".join(self._comments)
366 comments = f"\n\n{comments}" if self._comments else ""
368 underline = "-" * (len(self.__class__.__name__) + 3 + len(self._name))
370 return "\n".join(
371 [
372 f"{self.__class__.__name__} - {self._name}",
373 f"{underline}",
374 "",
375 f"Matrix : {_format(self._matrix)}",
376 f"Offset : {_format(self._offset)}{comments}",
377 ]
378 )
380 def __repr__(self) -> str:
381 """
382 Return an evaluable string representation of the *LUT* operator.
384 Returns
385 -------
386 :class:`str`
387 Evaluable string representation.
389 Examples
390 --------
391 >>> LUTOperatorMatrix(comments=["A first comment.", "A second comment."])
392 ... # doctest: +ELLIPSIS
393 LUTOperatorMatrix([[ 1., 0., 0., 0.],
394 [ 0., 1., 0., 0.],
395 [ 0., 0., 1., 0.],
396 [ 0., 0., 0., 1.]],
397 [ 0., 0., 0., 0.],
398 name='LUT Sequence Operator ...',
399 comments=['A first comment.', 'A second comment.'])
400 """
402 representation = repr(self._matrix)
403 representation = representation.replace("array", self.__class__.__name__)
404 representation = representation.replace(
405 " [", f"{' ' * (len(self.__class__.__name__) + 2)}["
406 )
408 indentation = " " * (len(self.__class__.__name__) + 1)
410 comments = (
411 f",\n{indentation}comments={self._comments!r}" if self._comments else ""
412 )
414 return "\n".join(
415 [
416 f"{representation[:-1]},",
417 f"{indentation}"
418 f"{repr(self._offset).replace('array(', '').replace(')', '')},",
419 f"{indentation}name='{self._name}'{comments})",
420 ]
421 )
423 __hash__ = None # pyright: ignore
425 def __eq__(self, other: object) -> bool:
426 """
427 Determine whether the *LUT* operator is equal to the specified other
428 object.
430 Parameters
431 ----------
432 other
433 Object to test whether it is equal to the *LUT* operator.
435 Returns
436 -------
437 :class:`bool`
438 Whether specified object equal to the *LUT* operator.
440 Examples
441 --------
442 >>> LUTOperatorMatrix() == LUTOperatorMatrix()
443 True
444 """
446 return isinstance(other, LUTOperatorMatrix) and all(
447 [
448 np.array_equal(self._matrix, other._matrix),
449 np.array_equal(self._offset, other._offset),
450 ]
451 )
453 def __ne__(self, other: object) -> bool:
454 """
455 Determine whether the *LUT* operator is not equal to the specified other
456 object.
458 Parameters
459 ----------
460 other
461 Object to test whether it is not equal to the *LUT* operator.
463 Returns
464 -------
465 :class:`bool`
466 Whether the specified object is not equal to the *LUT* operator.
468 Examples
469 --------
470 >>> LUTOperatorMatrix() != LUTOperatorMatrix(
471 ... np.reshape(np.linspace(0, 1, 16), (4, 4))
472 ... )
473 True
474 """
476 return not (self == other)
478 def apply(
479 self,
480 RGB: ArrayLike,
481 *args: Any, # noqa: ARG002
482 **kwargs: Any,
483 ) -> NDArrayFloat:
484 """
485 Apply the *LUT* operator to the specified *RGB* array.
487 Parameters
488 ----------
489 RGB
490 *RGB* array to apply the *LUT* operator transform to.
492 Other Parameters
493 ----------------
494 apply_offset_first
495 Whether to apply the offset first and then the matrix.
497 Returns
498 -------
499 :class:`numpy.ndarray`
500 Transformed *RGB* array.
502 Examples
503 --------
504 >>> matrix = np.array(
505 ... [
506 ... [1.45143932, -0.23651075, -0.21492857],
507 ... [-0.07655377, 1.1762297, -0.09967593],
508 ... [0.00831615, -0.00603245, 0.9977163],
509 ... ]
510 ... )
511 >>> M = LUTOperatorMatrix(matrix)
512 >>> RGB = np.array([0.3, 0.4, 0.5])
513 >>> M.apply(RGB) # doctest: +ELLIPSIS
514 array([ 0.2333632..., 0.3976877..., 0.4989400...])
515 """
517 RGB = as_float_array(RGB)
518 apply_offset_first = kwargs.get("apply_offset_first", False)
520 has_alpha_channel = RGB.shape[-1] == 4
521 M = self._matrix
522 offset = self._offset
524 if not has_alpha_channel:
525 M = M[:3, :3]
526 offset = offset[:3]
528 if apply_offset_first:
529 RGB += offset
531 RGB = vecmul(M, RGB)
533 if not apply_offset_first:
534 RGB += offset
536 return RGB