Coverage for colour/io/luts/resolve_cube.py: 100%
112 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"""
2Resolve .cube LUT Format Input / Output Utilities
3=================================================
5Define the *Resolve* *.cube* *LUT* format related input / output utilities
6objects:
8- :func:`colour.io.read_LUT_ResolveCube`
9- :func:`colour.io.write_LUT_ResolveCube`
11References
12----------
13- :cite:`Chamberlain2015` : Chamberlain, P. (2015). LUT documentation (to
14 create from another program). Retrieved August 23, 2018, from
15 https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=40284#p232952
16"""
18from __future__ import annotations
20import typing
22import numpy as np
24if typing.TYPE_CHECKING:
25 from colour.hints import PathLike
27from colour.io.luts import LUT1D, LUT3D, LUT3x1D, LUTSequence
28from colour.io.luts.common import path_to_title
29from colour.utilities import (
30 as_float_array,
31 as_int_scalar,
32 attest,
33 format_array_as_row,
34 tstack,
35)
37__author__ = "Colour Developers"
38__copyright__ = "Copyright 2013 Colour Developers"
39__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
40__maintainer__ = "Colour Developers"
41__email__ = "colour-developers@colour-science.org"
42__status__ = "Production"
44__all__ = [
45 "read_LUT_ResolveCube",
46 "write_LUT_ResolveCube",
47]
50def read_LUT_ResolveCube(path: str | PathLike) -> LUT3x1D | LUT3D | LUTSequence:
51 """
52 Read the specified *Resolve* *.cube* *LUT* file.
54 Read and parse a *DaVinci Resolve* *.cube* lookup table file, which may
55 contain a 1D LUT, a 3D LUT, or a sequence of both. The *.cube* format
56 supports configurable precision and domain ranges, making it suitable for
57 colour grading and colour space transformations.
59 Parameters
60 ----------
61 path
62 Path to the *.cube* *LUT* file to read.
64 Returns
65 -------
66 :class:`colour.LUT3x1D` or :class:`colour.LUT3D` or :class:`colour.LUTSequence`
67 :class:`LUT3x1D` instance for 1D shaper LUTs, :class:`LUT3D` instance
68 for 3D colour transformation LUTs, or :class:`LUTSequence` instance
69 when the file contains both shaper and 3D LUT data.
71 References
72 ----------
73 :cite:`Chamberlain2015`
75 Examples
76 --------
77 Reading a 3x1D *Resolve* *.cube* *LUT*:
79 >>> import os
80 >>> path = os.path.join(
81 ... os.path.dirname(__file__),
82 ... "tests",
83 ... "resources",
84 ... "resolve_cube",
85 ... "ACES_Proxy_10_to_ACES.cube",
86 ... )
87 >>> print(read_LUT_ResolveCube(path))
88 LUT3x1D - ACES Proxy 10 to ACES
89 -------------------------------
90 <BLANKLINE>
91 Dimensions : 2
92 Domain : [[ 0. 0. 0.]
93 [ 1. 1. 1.]]
94 Size : (32, 3)
96 Reading a 3D *Resolve* *.cube* *LUT*:
98 >>> path = os.path.join(
99 ... os.path.dirname(__file__),
100 ... "tests",
101 ... "resources",
102 ... "resolve_cube",
103 ... "Colour_Correct.cube",
104 ... )
105 >>> print(read_LUT_ResolveCube(path))
106 LUT3D - Generated by Foundry::LUT
107 ---------------------------------
108 <BLANKLINE>
109 Dimensions : 3
110 Domain : [[ 0. 0. 0.]
111 [ 1. 1. 1.]]
112 Size : (4, 4, 4, 3)
114 Reading a 3D *Resolve* *.cube* *LUT* with comments:
116 >>> path = os.path.join(
117 ... os.path.dirname(__file__),
118 ... "tests",
119 ... "resources",
120 ... "resolve_cube",
121 ... "Demo.cube",
122 ... )
123 >>> print(read_LUT_ResolveCube(path))
124 LUT3x1D - Demo
125 --------------
126 <BLANKLINE>
127 Dimensions : 2
128 Domain : [[ 0. 0. 0.]
129 [ 3. 3. 3.]]
130 Size : (3, 3)
131 Comment 01 : Comments can't go anywhere
133 Reading a 3x1D + 3D *Resolve* *.cube* *LUT*:
135 >>> path = os.path.join(
136 ... os.path.dirname(__file__),
137 ... "tests",
138 ... "resources",
139 ... "resolve_cube",
140 ... "Three_Dimensional_Table_With_Shaper.cube",
141 ... )
142 >>> print(read_LUT_ResolveCube(path))
143 LUT Sequence
144 ------------
145 <BLANKLINE>
146 Overview
147 <BLANKLINE>
148 LUT3x1D --> LUT3D
149 <BLANKLINE>
150 Operations
151 <BLANKLINE>
152 LUT3x1D - LUT3D with My Shaper - Shaper
153 ---------------------------------------
154 <BLANKLINE>
155 Dimensions : 2
156 Domain : [[-0.1 -0.1 -0.1]
157 [ 3. 3. 3. ]]
158 Size : (10, 3)
159 <BLANKLINE>
160 LUT3D - LUT3D with My Shaper - Cube
161 -----------------------------------
162 <BLANKLINE>
163 Dimensions : 3
164 Domain : [[-0.1 -0.1 -0.1]
165 [ 3. 3. 3. ]]
166 Size : (3, 3, 3, 3)
167 Comment 01 : A first "Shaper" comment.
168 Comment 02 : A second "Shaper" comment.
169 Comment 03 : A first "LUT3D" comment.
170 Comment 04 : A second "LUT3D" comment.
171 """
173 path = str(path)
175 title = path_to_title(path)
176 domain_3x1D, domain_3D = None, None
177 size_3x1D: int = 2
178 size_3D: int = 2
179 data = []
180 comments = []
181 has_3x1D, has_3D = False, False
183 with open(path) as cube_file:
184 lines = cube_file.readlines()
185 for line in lines:
186 line = line.strip() # noqa: PLW2901
188 if len(line) == 0:
189 continue
191 if line.startswith("#"):
192 comments.append(line[1:].strip())
193 continue
195 tokens = line.split()
196 if tokens[0] == "TITLE":
197 title = " ".join(tokens[1:])[1:-1]
198 elif tokens[0] == "LUT_1D_INPUT_RANGE":
199 domain_3x1D = tstack([tokens[1:], tokens[1:], tokens[1:]])
200 elif tokens[0] == "LUT_3D_INPUT_RANGE":
201 domain_3D = tstack([tokens[1:], tokens[1:], tokens[1:]])
202 elif tokens[0] == "LUT_1D_SIZE":
203 has_3x1D = True
204 size_3x1D = as_int_scalar(tokens[1])
205 elif tokens[0] == "LUT_3D_SIZE":
206 has_3D = True
207 size_3D = as_int_scalar(tokens[1])
208 else:
209 data.append(tokens)
211 table = as_float_array(data)
213 LUT: LUT3x1D | LUT3D | LUTSequence
214 if has_3x1D and has_3D:
215 table_1D = table[: int(size_3x1D)]
216 # The lines of table data shall be in ascending index order,
217 # with the first component index (Red) changing most rapidly,
218 # and the last component index (Blue) changing least rapidly.
219 table_3D = np.reshape(
220 table[int(size_3x1D) :], (size_3D, size_3D, size_3D, 3), order="F"
221 )
222 LUT = LUTSequence(
223 LUT3x1D(
224 table_1D,
225 f"{title} - Shaper",
226 domain_3x1D,
227 ),
228 LUT3D(
229 table_3D,
230 f"{title} - Cube",
231 domain_3D,
232 comments=comments,
233 ),
234 )
235 elif has_3x1D:
236 LUT = LUT3x1D(table, title, domain_3x1D, comments=comments)
237 elif has_3D:
238 # The lines of table data shall be in ascending index order,
239 # with the first component index (Red) changing most rapidly,
240 # and the last component index (Blue) changing least rapidly.
241 table = np.reshape(table, (size_3D, size_3D, size_3D, 3), order="F")
242 LUT = LUT3D(table, title, domain_3D, comments=comments)
244 return LUT
247def write_LUT_ResolveCube(
248 LUT: LUT1D | LUT3x1D | LUT3D | LUTSequence,
249 path: str | PathLike,
250 decimals: int = 7,
251) -> bool:
252 """
253 Write the specified *LUT* to the specified *Resolve* *.cube* *LUT* file.
255 Parameters
256 ----------
257 LUT
258 :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUT3D` or
259 :class:`LUTSequence` class instance to write at the specified path.
260 path
261 *LUT* file path.
262 decimals
263 Number of decimal places for formatting numeric values.
265 Returns
266 -------
267 :class:`bool`
268 Definition success.
270 References
271 ----------
272 :cite:`Chamberlain2015`
274 Examples
275 --------
276 Writing a 3x1D *Resolve* *.cube* *LUT*:
278 >>> from colour.algebra import spow
279 >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]])
280 >>> LUT = LUT3x1D(
281 ... spow(LUT3x1D.linear_table(16, domain), 1 / 2.2),
282 ... "My LUT",
283 ... domain,
284 ... comments=["A first comment.", "A second comment."],
285 ... )
286 >>> write_LUT_ResolveCube(LUT, "My_LUT.cube") # doctest: +SKIP
288 Writing a 3D *Resolve* *.cube* *LUT*:
290 >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]])
291 >>> LUT = LUT3D(
292 ... spow(LUT3D.linear_table(16, domain), 1 / 2.2),
293 ... "My LUT",
294 ... domain,
295 ... comments=["A first comment.", "A second comment."],
296 ... )
297 >>> write_LUT_ResolveCube(LUT, "My_LUT.cube") # doctest: +SKIP
299 Writing a 3x1D + 3D *Resolve* *.cube* *LUT*:
301 >>> from colour.models import RGB_to_HSV, HSV_to_RGB
302 >>> from colour.utilities import tstack
303 >>> def rotate_hue(a, angle):
304 ... H, S, V = RGB_to_HSV(a)
305 ... H += angle / 360
306 ... H[H > 1] -= 1
307 ... H[H < 0] += 1
308 ... return HSV_to_RGB([H, S, V])
309 >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]])
310 >>> shaper = LUT3x1D(
311 ... spow(LUT3x1D.linear_table(10, domain), 1 / 2.2),
312 ... "My Shaper",
313 ... domain,
314 ... comments=[
315 ... 'A first "Shaper" comment.',
316 ... 'A second "Shaper" comment.',
317 ... ],
318 ... )
319 >>> LUT = LUT3D(
320 ... rotate_hue(LUT3D.linear_table(3, domain), 10),
321 ... "LUT3D with My Shaper",
322 ... domain,
323 ... comments=['A first "LUT3D" comment.', 'A second "LUT3D" comment.'],
324 ... )
325 >>> LUT_sequence = LUTSequence(shaper, LUT)
326 >>> write_LUT_ResolveCube(LUT_sequence, "My_LUT.cube") # doctest: +SKIP
327 """
329 path = str(path)
331 has_3D, has_3x1D = False, False
333 if isinstance(LUT, LUTSequence):
334 attest(
335 len(LUT) == 2
336 and isinstance(LUT[0], (LUT1D, LUT3x1D))
337 and isinstance(LUT[1], LUT3D),
338 "LUTSequence must be 1D + 3D or 3x1D + 3D!",
339 )
341 if isinstance(LUT[0], LUT1D):
342 LUT[0] = LUT[0].convert(LUT3x1D)
344 name = f"{LUT[0].name} - {LUT[1].name}"
345 has_3x1D = True
346 has_3D = True
347 elif isinstance(LUT, LUT1D):
348 name = LUT.name
349 has_3x1D = True
350 LUT = LUTSequence(LUT.convert(LUT3x1D), LUT3D())
351 elif isinstance(LUT, LUT3x1D):
352 name = LUT.name
353 has_3x1D = True
354 LUT = LUTSequence(LUT, LUT3D())
355 elif isinstance(LUT, LUT3D):
356 name = LUT.name
357 has_3D = True
358 LUT = LUTSequence(LUT3x1D(), LUT)
359 else:
360 error = "LUT must be 1D, 3x1D, 3D, 1D + 3D or 3x1D + 3D!"
362 raise TypeError(error)
364 for i in range(2):
365 attest(not LUT[i].is_domain_explicit(), '"LUT" domain must be implicit!')
367 attest(
368 (len(np.unique(LUT[0].domain)) == 2 and len(np.unique(LUT[1].domain)) == 2),
369 '"LUT" domain must be 1D!',
370 )
372 if has_3x1D:
373 attest(
374 2 <= LUT[0].size <= 65536,
375 "Shaper size must be in domain [2, 65536]!",
376 )
377 if has_3D:
378 attest(2 <= LUT[1].size <= 256, "Cube size must be in domain [2, 256]!")
380 with open(path, "w") as cube_file:
381 cube_file.write(f'TITLE "{name}"\n')
383 if LUT[0].comments:
384 cube_file.writelines(f"# {comment}\n" for comment in LUT[0].comments)
386 if LUT[1].comments:
387 cube_file.writelines(f"# {comment}\n" for comment in LUT[1].comments)
389 default_domain = np.array([[0, 0, 0], [1, 1, 1]])
391 if has_3x1D:
392 cube_file.write(f"LUT_1D_SIZE {LUT[0].table.shape[0]}\n")
393 if not np.array_equal(LUT[0].domain, default_domain):
394 input_range = format_array_as_row(
395 [LUT[0].domain[0][0], LUT[0].domain[1][0]], decimals
396 )
397 cube_file.write(f"LUT_1D_INPUT_RANGE {input_range}\n")
399 if has_3D:
400 cube_file.write(f"LUT_3D_SIZE {LUT[1].table.shape[0]}\n")
401 if not np.array_equal(LUT[1].domain, default_domain):
402 input_range = format_array_as_row(
403 [LUT[1].domain[0][0], LUT[1].domain[1][0]], decimals
404 )
405 cube_file.write(f"LUT_3D_INPUT_RANGE {input_range}\n")
407 if has_3x1D:
408 table = LUT[0].table
409 cube_file.writelines(
410 f"{format_array_as_row(vector, decimals)}\n" for vector in table
411 )
412 cube_file.write("\n")
414 if has_3D:
415 table = np.reshape(LUT[1].table, (-1, 3), order="F")
416 cube_file.writelines(
417 f"{format_array_as_row(vector, decimals)}\n" for vector in table
418 )
420 return True