Coverage for colour/io/luts/__init__.py: 100%
47 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"""
2Look-Up Table (LUT) I/O
3=======================
5Implement support for reading and writing industry-standard Look-Up Table
6(LUT) formats used in colour pipelines and digital intermediate workflows.
8- :class:`colour.LUT1D`: 1D LUT for single-channel transformations
9- :class:`colour.LUT3x1D`: Three separate 1D LUTs for per-channel operations
10- :class:`colour.LUT3D`: 3D LUT for complex colour transformations
11- :class:`colour.LUTSequence`: Sequential LUT operations
12- :class:`colour.LUTOperatorMatrix`: Matrix-based LUT operations
13- :func:`colour.io.read_LUT`: Auto-detect and read LUT files
14- :func:`colour.io.write_LUT`: Write LUT files in specified formats
16References
17----------
18- :cite:`AdobeSystems2013b` : Adobe Systems. (2013). Cube LUT Specification.
19 https://drive.google.com/open?id=143Eh08ZYncCAMwJ1q4gWxVOqR_OSWYvs
20- :cite:`Chamberlain2015` : Chamberlain, P. (2015). LUT documentation (to
21 create from another program). Retrieved August 23, 2018, from
22 https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=40284#p232952
23- :cite:`RisingSunResearch` : Rising Sun Research. (n.d.). cineSpace LUT
24 Library. Retrieved November 30, 2018, from
25 https://sourceforge.net/projects/cinespacelutlib/
26"""
28from __future__ import annotations
30import os
31import typing
33if typing.TYPE_CHECKING:
34 from colour.hints import Any, LiteralLUTReadMethod, LiteralLUTWriteMethod, PathLike
36from colour.utilities import (
37 CanonicalMapping,
38 filter_kwargs,
39 validate_method,
40)
42# isort: split
44from .lut import LUT1D, LUT3D, LUT3x1D, LUT_to_LUT
45from .operator import AbstractLUTSequenceOperator, LUTOperatorMatrix
46from .sequence import LUTSequence
48# isort: split
50from .cinespace_csp import read_LUT_Cinespace, write_LUT_Cinespace
51from .iridas_cube import read_LUT_IridasCube, write_LUT_IridasCube
52from .resolve_cube import read_LUT_ResolveCube, write_LUT_ResolveCube
53from .sony_spi1d import read_LUT_SonySPI1D, write_LUT_SonySPI1D
54from .sony_spi3d import read_LUT_SonySPI3D, write_LUT_SonySPI3D
55from .sony_spimtx import read_LUT_SonySPImtx, write_LUT_SonySPImtx
57__all__ = [
58 "LUT1D",
59 "LUT3D",
60 "LUT3x1D",
61 "LUT_to_LUT",
62]
63__all__ += [
64 "AbstractLUTSequenceOperator",
65 "LUTOperatorMatrix",
66]
67__all__ += [
68 "LUTSequence",
69]
70__all__ += [
71 "read_LUT_Cinespace",
72 "write_LUT_Cinespace",
73]
74__all__ += [
75 "read_LUT_IridasCube",
76 "write_LUT_IridasCube",
77]
78__all__ += [
79 "read_LUT_ResolveCube",
80 "write_LUT_ResolveCube",
81]
82__all__ += [
83 "read_LUT_SonySPI1D",
84 "write_LUT_SonySPI1D",
85]
86__all__ += [
87 "read_LUT_SonySPI3D",
88 "write_LUT_SonySPI3D",
89]
90__all__ += [
91 "read_LUT_SonySPImtx",
92 "write_LUT_SonySPImtx",
93]
95MAPPING_EXTENSION_TO_LUT_FORMAT: CanonicalMapping = CanonicalMapping(
96 {
97 ".cube": "Iridas Cube",
98 ".spi1d": "Sony SPI1D",
99 ".spi3d": "Sony SPI3D",
100 ".spimtx": "Sony SPImtx",
101 ".csp": "Cinespace",
102 }
103)
104"""Extension to *LUT* format."""
106LUT_READ_METHODS: CanonicalMapping = CanonicalMapping(
107 {
108 "Cinespace": read_LUT_Cinespace,
109 "Iridas Cube": read_LUT_IridasCube,
110 "Resolve Cube": read_LUT_ResolveCube,
111 "Sony SPI1D": read_LUT_SonySPI1D,
112 "Sony SPI3D": read_LUT_SonySPI3D,
113 "Sony SPImtx": read_LUT_SonySPImtx,
114 }
115)
116LUT_READ_METHODS.__doc__ = """
117Supported *LUT* reading methods.
119References
120----------
121:cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`
122"""
125def read_LUT(
126 path: str | PathLike,
127 method: LiteralLUTReadMethod | str | None = None,
128 **kwargs: Any,
129) -> LUT1D | LUT3x1D | LUT3D | LUTSequence | LUTOperatorMatrix:
130 """
131 Read the specified *LUT* file.
133 Parameters
134 ----------
135 path
136 *LUT* path.
137 method
138 Reading method, if *None*, the method will be auto-detected
139 according to extension.
141 Returns
142 -------
143 :class:`colour.LUT1D` or :class:`colour.LUT3x1D` or \
144:class:`colour.LUT3D` or :class:`colour.LUTSequence` or \
145:class:`colour.LUTOperatorMatrix`
146 :class:`colour.LUT1D` or :class:`colour.LUT3x1D` or
147 :class:`colour.LUT3D` or :class:`colour.LUTSequence` or
148 :class:`colour.LUTOperatorMatrix` class instance.
150 Raises
151 ------
152 ValueError
153 If the *LUT* file format is not supported or if reading fails.
155 References
156 ----------
157 :cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`,
158 :cite:`RisingSunResearch`
160 Examples
161 --------
162 Reading a 3x1D *Iridas* *.cube* *LUT*:
164 >>> path = os.path.join(
165 ... os.path.dirname(__file__),
166 ... "tests",
167 ... "resources",
168 ... "iridas_cube",
169 ... "ACES_Proxy_10_to_ACES.cube",
170 ... )
171 >>> print(read_LUT(path))
172 LUT3x1D - ACES Proxy 10 to ACES
173 -------------------------------
174 <BLANKLINE>
175 Dimensions : 2
176 Domain : [[ 0. 0. 0.]
177 [ 1. 1. 1.]]
178 Size : (32, 3)
180 Reading a 1D *Sony* *.spi1d* *LUT*:
182 >>> path = os.path.join(
183 ... os.path.dirname(__file__),
184 ... "tests",
185 ... "resources",
186 ... "sony_spi1d",
187 ... "eotf_sRGB_1D.spi1d",
188 ... )
189 >>> print(read_LUT(path))
190 LUT1D - eotf sRGB 1D
191 --------------------
192 <BLANKLINE>
193 Dimensions : 1
194 Domain : [-0.1 1.5]
195 Size : (16,)
196 Comment 01 : Generated by "Colour 0.3.11".
197 Comment 02 : "colour.models.eotf_sRGB".
199 Reading a 3D *Sony* *.spi3d* *LUT*:
201 >>> path = os.path.join(
202 ... os.path.dirname(__file__),
203 ... "tests",
204 ... "resources",
205 ... "sony_spi3d",
206 ... "Colour_Correct.spi3d",
207 ... )
208 >>> print(read_LUT(path))
209 LUT3D - Colour Correct
210 ----------------------
211 <BLANKLINE>
212 Dimensions : 3
213 Domain : [[ 0. 0. 0.]
214 [ 1. 1. 1.]]
215 Size : (4, 4, 4, 3)
216 Comment 01 : Adapted from a LUT generated by Foundry::LUT.
218 Reading a *Sony* *.spimtx* *LUT*:
220 >>> path = os.path.join(
221 ... os.path.dirname(__file__),
222 ... "tests",
223 ... "resources",
224 ... "sony_spimtx",
225 ... "dt.spimtx",
226 ... )
227 >>> print(read_LUT(path))
228 LUTOperatorMatrix - dt
229 ----------------------
230 <BLANKLINE>
231 Matrix : [[ 0.864274 0. 0. 0. ]
232 [ 0. 0.864274 0. 0. ]
233 [ 0. 0. 0.864274 0. ]
234 [ 0. 0. 0. 1. ]]
235 Offset : [ 0. 0. 0. 0.]
236 """
238 path = str(path)
240 method = (
241 MAPPING_EXTENSION_TO_LUT_FORMAT[os.path.splitext(path)[-1]].lower()
242 if method is None
243 else validate_method(method, tuple(LUT_WRITE_METHODS))
244 )
246 function = LUT_READ_METHODS[method]
248 try:
249 return function(path, **filter_kwargs(function, **kwargs))
250 except ValueError as error:
251 # Case where a "Resolve Cube" with "LUT3x1D" shaper was read as an
252 # "Iridas Cube" "LUT".
253 if method == "iridas cube":
254 function = LUT_READ_METHODS["Resolve Cube"]
255 return function(path, **filter_kwargs(function, **kwargs))
257 raise ValueError from error
260LUT_WRITE_METHODS = CanonicalMapping(
261 {
262 "Cinespace": write_LUT_Cinespace,
263 "Iridas Cube": write_LUT_IridasCube,
264 "Resolve Cube": write_LUT_ResolveCube,
265 "Sony SPI1D": write_LUT_SonySPI1D,
266 "Sony SPI3D": write_LUT_SonySPI3D,
267 "Sony SPImtx": write_LUT_SonySPImtx,
268 }
269)
270LUT_WRITE_METHODS.__doc__ = """
271Supported *LUT* writing methods.
273References
274----------
275:cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`
276"""
279def write_LUT(
280 LUT: LUT1D | LUT3x1D | LUT3D | LUTSequence | LUTOperatorMatrix,
281 path: str | PathLike,
282 decimals: int = 7,
283 method: LiteralLUTWriteMethod | str | None = None,
284 **kwargs: Any,
285) -> bool:
286 """
287 Write the specified *LUT* to the specified file.
289 Parameters
290 ----------
291 LUT
292 :class:`colour.LUT1D` or :class:`colour.LUT3x1D` or
293 :class:`colour.LUT3D` or :class:`colour.LUTSequence` or
294 :class:`colour.LUTOperatorMatrix` class instance to write at the
295 specified path.
296 path
297 *LUT* file path.
298 decimals
299 Number of decimal places for formatting numeric values.
300 method
301 Writing method, if *None*, the method will be auto-detected
302 according to the file extension.
304 Returns
305 -------
306 :class:`bool`
307 Definition success.
309 References
310 ----------
311 :cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`,
312 :cite:`RisingSunResearch`
314 Examples
315 --------
316 Writing a 3x1D *Iridas* *.cube* *LUT*:
318 >>> import numpy as np
319 >>> from colour.algebra import spow
320 >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]])
321 >>> LUT = LUT3x1D(
322 ... spow(LUT3x1D.linear_table(16, domain), 1 / 2.2),
323 ... "My LUT",
324 ... domain,
325 ... comments=["A first comment.", "A second comment."],
326 ... )
327 >>> write_LUT(LUT, "My_LUT.cube") # doctest: +SKIP
329 Writing a 1D *Sony* *.spi1d* *LUT*:
331 >>> domain = np.array([-0.1, 1.5])
332 >>> LUT = LUT1D(
333 ... spow(LUT1D.linear_table(16, domain), 1 / 2.2),
334 ... "My LUT",
335 ... domain,
336 ... comments=["A first comment.", "A second comment."],
337 ... )
338 >>> write_LUT(LUT, "My_LUT.spi1d") # doctest: +SKIP
340 Writing a 3D *Sony* *.spi3d* *LUT*:
342 >>> LUT = LUT3D(
343 ... LUT3D.linear_table(16) ** (1 / 2.2),
344 ... "My LUT",
345 ... np.array([[0, 0, 0], [1, 1, 1]]),
346 ... comments=["A first comment.", "A second comment."],
347 ... )
348 >>> write_LUT(LUT, "My_LUT.cube") # doctest: +SKIP
349 """
351 path = str(path)
353 method = (
354 MAPPING_EXTENSION_TO_LUT_FORMAT[os.path.splitext(path)[-1]].lower()
355 if method is None
356 else validate_method(method, tuple(LUT_WRITE_METHODS))
357 )
359 if method == "iridas cube" and isinstance(LUT, LUTSequence):
360 method = "resolve cube"
362 function = LUT_WRITE_METHODS[method]
364 return function(LUT, path, decimals, **filter_kwargs(function, **kwargs))
367__all__ += [
368 "LUT_READ_METHODS",
369 "read_LUT",
370 "LUT_WRITE_METHODS",
371 "write_LUT",
372]