Coverage for colour/io/luts/sequence.py: 100%
65 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 Sequence
3============
5Define the *LUT* sequence container for Look-Up Table (LUT) processing
6pipelines:
8- :class:`colour.LUTSequence`
9"""
11from __future__ import annotations
13import re
14import typing
15from collections.abc import MutableSequence
16from copy import deepcopy
18if typing.TYPE_CHECKING:
19 from colour.hints import (
20 Any,
21 ArrayLike,
22 List,
23 NDArrayFloat,
24 Sequence,
25 )
27from colour.hints import ProtocolLUTSequenceItem
28from colour.utilities import as_float_array, attest, is_iterable
30__author__ = "Colour Developers"
31__copyright__ = "Copyright 2013 Colour Developers"
32__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
33__maintainer__ = "Colour Developers"
34__email__ = "colour-developers@colour-science.org"
35__status__ = "Production"
37__all__ = [
38 "LUTSequence",
39]
42class LUTSequence(MutableSequence):
43 """
44 Define the base class for a *LUT* sequence.
46 A *LUT* sequence represents a series of *LUTs*, *LUT* operators or
47 objects implementing the :class:`colour.hints.ProtocolLUTSequenceItem`
48 protocol.
50 The :class:`colour.LUTSequence` class can be used to model series of
51 *LUTs* such as when a shaper *LUT* is combined with a 3D *LUT*.
53 Other Parameters
54 ----------------
55 args
56 Sequence of objects implementing the
57 :class:`colour.hints.ProtocolLUTSequenceItem` protocol.
59 Attributes
60 ----------
61 - :attr:`~colour.LUTSequence.sequence`
63 Methods
64 -------
65 - :meth:`~colour.LUTSequence.__init__`
66 - :meth:`~colour.LUTSequence.__getitem__`
67 - :meth:`~colour.LUTSequence.__setitem__`
68 - :meth:`~colour.LUTSequence.__delitem__`
69 - :meth:`~colour.LUTSequence.__len__`
70 - :meth:`~colour.LUTSequence.__str__`
71 - :meth:`~colour.LUTSequence.__repr__`
72 - :meth:`~colour.LUTSequence.__eq__`
73 - :meth:`~colour.LUTSequence.__ne__`
74 - :meth:`~colour.LUTSequence.insert`
75 - :meth:`~colour.LUTSequence.apply`
76 - :meth:`~colour.LUTSequence.copy`
78 Examples
79 --------
80 >>> from colour.io.luts import LUT1D, LUT3x1D, LUT3D
81 >>> LUT_1 = LUT1D()
82 >>> LUT_2 = LUT3D(size=3)
83 >>> LUT_3 = LUT3x1D()
84 >>> print(LUTSequence(LUT_1, LUT_2, LUT_3))
85 LUT Sequence
86 ------------
87 <BLANKLINE>
88 Overview
89 <BLANKLINE>
90 LUT1D --> LUT3D --> LUT3x1D
91 <BLANKLINE>
92 Operations
93 <BLANKLINE>
94 LUT1D - Unity 10
95 ----------------
96 <BLANKLINE>
97 Dimensions : 1
98 Domain : [ 0. 1.]
99 Size : (10,)
100 <BLANKLINE>
101 LUT3D - Unity 3
102 ---------------
103 <BLANKLINE>
104 Dimensions : 3
105 Domain : [[ 0. 0. 0.]
106 [ 1. 1. 1.]]
107 Size : (3, 3, 3, 3)
108 <BLANKLINE>
109 LUT3x1D - Unity 10
110 ------------------
111 <BLANKLINE>
112 Dimensions : 2
113 Domain : [[ 0. 0. 0.]
114 [ 1. 1. 1.]]
115 Size : (10, 3)
116 """
118 def __init__(self, *args: ProtocolLUTSequenceItem) -> None:
119 self._sequence: List[ProtocolLUTSequenceItem] = []
120 self.sequence = args
122 @property
123 def sequence(self) -> List[ProtocolLUTSequenceItem]:
124 """
125 Getter and setter for the underlying *LUT* sequence.
127 Access and modify the sequence of lookup table operations that
128 define the transformation pipeline.
130 Parameters
131 ----------
132 value
133 Value to set the underlying *LUT* sequence with.
135 Returns
136 -------
137 :class:`list`
138 Underlying *LUT* sequence.
139 """
141 return self._sequence
143 @sequence.setter
144 def sequence(self, value: Sequence[ProtocolLUTSequenceItem]) -> None:
145 """Setter for the **self.sequence** property."""
147 for item in value:
148 attest(
149 isinstance(item, ProtocolLUTSequenceItem),
150 '"value" items must implement the "ProtocolLUTSequenceItem" protocol!',
151 )
153 self._sequence = list(value)
155 def __getitem__(self, index: int | slice) -> Any:
156 """
157 Return *LUT* sequence item(s) at specified index or slice.
159 Parameters
160 ----------
161 index
162 Index or slice to return *LUT* sequence item(s) at.
164 Returns
165 -------
166 ProtocolLUTSequenceItem
167 *LUT* sequence item(s) at specified index or slice.
168 """
170 return self._sequence[index]
172 def __setitem__(self, index: int | slice, value: Any) -> None:
173 """
174 Set the *LUT* sequence at the specified index or slice with the
175 specified value.
177 Parameters
178 ----------
179 index
180 Index or slice to set the *LUT* sequence value at.
181 value
182 Value to set the *LUT* sequence with.
183 """
185 for item in value if is_iterable(value) else [value]:
186 attest(
187 isinstance(item, ProtocolLUTSequenceItem),
188 '"value" items must implement the "ProtocolLUTSequenceItem" protocol!',
189 )
191 self._sequence[index] = value
193 def __delitem__(self, index: int | slice) -> None:
194 """
195 Delete the *LUT* sequence item(s) at the specified index (or slice).
197 Parameters
198 ----------
199 index
200 Index (or slice) to delete the *LUT* sequence items at.
201 """
203 del self._sequence[index]
205 def __len__(self) -> int:
206 """
207 Return the *LUT* sequence items count.
209 Returns
210 -------
211 :class:`int`
212 *LUT* sequence items count.
213 """
215 return len(self._sequence)
217 def __str__(self) -> str:
218 """
219 Return a formatted string representation of the *LUT* sequence.
221 Returns
222 -------
223 :class:`str`
224 Formatted string representation.
225 """
227 sequence = " --> ".join([a.__class__.__name__ for a in self._sequence])
229 operations = re.sub(
230 "^",
231 " " * 4,
232 "\n\n".join([str(a) for a in self._sequence]),
233 flags=re.MULTILINE,
234 )
235 operations = re.sub("^\\s+$", "", operations, flags=re.MULTILINE)
237 return "\n".join(
238 [
239 "LUT Sequence",
240 "------------",
241 "",
242 "Overview",
243 "",
244 f" {sequence}",
245 "",
246 "Operations",
247 "",
248 f"{operations}",
249 ]
250 )
252 def __repr__(self) -> str:
253 """
254 Return an evaluable string representation of the *LUT* sequence.
256 Generate a string representation that can be evaluated to recreate
257 the *LUT* sequence with its current state.
259 Returns
260 -------
261 :class:`str`
262 Evaluable string representation.
263 """
265 operations = re.sub(
266 "^",
267 " " * 4,
268 ",\n".join([repr(a) for a in self._sequence]),
269 flags=re.MULTILINE,
270 )
271 operations = re.sub("^\\s+$", "", operations, flags=re.MULTILINE)
273 return f"{self.__class__.__name__}(\n{operations}\n)"
275 __hash__ = None # pyright: ignore
277 def __eq__(self, other: object) -> bool:
278 """
279 Test whether the *LUT* sequence is equal to the specified other object.
281 Compare this *LUT* sequence with another object for equality. The
282 comparison evaluates structural and content equivalence.
284 Parameters
285 ----------
286 other
287 Object to test whether it is equal to the *LUT* sequence.
289 Returns
290 -------
291 :class:`bool`
292 Whether specified object is equal to the *LUT* sequence.
293 """
295 if not isinstance(other, LUTSequence):
296 return False
298 if len(self) != len(other):
299 return False
301 return all(self[i] == other[i] for i in range(len(self)))
303 def __ne__(self, other: object) -> bool:
304 """
305 Return whether the *LUT* sequence is not equal to the specified other
306 object.
308 Parameters
309 ----------
310 other
311 Object to test whether it is not equal to the *LUT* sequence.
313 Returns
314 -------
315 :class:`bool`
316 Whether the specified object is not equal to the *LUT* sequence.
317 """
319 return not (self == other)
321 def insert(self, index: int, value: ProtocolLUTSequenceItem) -> None:
322 """
323 Insert the specified *LUT* at the specified index in the *LUT*
324 sequence.
326 Parameters
327 ----------
328 index
329 Index at which to insert the item in the *LUT* sequence.
330 value
331 *LUT* to insert into the *LUT* sequence.
332 """
334 attest(
335 isinstance(value, ProtocolLUTSequenceItem),
336 '"value" items must implement the "ProtocolLUTSequenceItem" protocol!',
337 )
339 self._sequence.insert(index, value)
341 def apply(self, RGB: ArrayLike, **kwargs: Any) -> NDArrayFloat:
342 """
343 Apply the *LUT* sequence sequentially to the specified *RGB* colourspace
344 array.
346 Parameters
347 ----------
348 RGB
349 *RGB* colourspace array to apply the *LUT* sequence sequentially
350 onto.
352 Other Parameters
353 ----------------
354 kwargs
355 Keywords arguments. The keys must be the class type names for
356 which they are intended to be used with. There is no implemented
357 way to discriminate which class instance the keyword arguments
358 should be used with, thus if many class instances of the same
359 type are members of the sequence, any matching keyword arguments
360 will be used with all the class instances.
362 Returns
363 -------
364 :class:`numpy.ndarray`
365 Processed *RGB* colourspace array.
367 Examples
368 --------
369 >>> import numpy as np
370 >>> from colour.io.luts import LUT1D, LUT3x1D, LUT3D
371 >>> from colour.utilities import tstack
372 >>> LUT_1 = LUT1D(LUT1D.linear_table(16) + 0.125)
373 >>> LUT_2 = LUT3D(LUT3D.linear_table(16) ** (1 / 2.2))
374 >>> LUT_3 = LUT3x1D(LUT3x1D.linear_table(16) * 0.750)
375 >>> LUT_sequence = LUTSequence(LUT_1, LUT_2, LUT_3)
376 >>> samples = np.linspace(0, 1, 5)
377 >>> RGB = tstack([samples, samples, samples])
378 >>> LUT_sequence.apply(RGB, LUT1D={"direction": "Inverse"})
379 ... # doctest: +ELLIPSIS
380 array([[ 0. ..., 0. ..., 0. ...],
381 [ 0.2899886..., 0.2899886..., 0.2899886...],
382 [ 0.4797662..., 0.4797662..., 0.4797662...],
383 [ 0.6055328..., 0.6055328..., 0.6055328...],
384 [ 0.7057779..., 0.7057779..., 0.7057779...]])
385 """
387 RGB = as_float_array(RGB)
389 RGB_o = RGB
390 for operator in self:
391 RGB_o = operator.apply(RGB_o, **kwargs.get(operator.__class__.__name__, {}))
393 return RGB_o
395 def copy(self) -> LUTSequence:
396 """
397 Return a copy of the *LUT* sequence.
399 Returns
400 -------
401 :class:`colour.LUTSequence`
402 *LUT* sequence copy.
403 """
405 return deepcopy(self)