Coverage for colour/io/luts/iridas_cube.py: 100%
77 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"""
2Iridas .cube LUT Format Input / Output Utilities
3================================================
5Define the *Iridas* *.cube* *LUT* format related input / output utilities
6objects:
8- :func:`colour.io.read_LUT_IridasCube`
9- :func:`colour.io.write_LUT_IridasCube`
11References
12----------
13- :cite:`AdobeSystems2013b` : Adobe Systems. (2013). Cube LUT Specification.
14 https://drive.google.com/open?id=143Eh08ZYncCAMwJ1q4gWxVOqR_OSWYvs
15"""
17from __future__ import annotations
19import typing
21import numpy as np
23if typing.TYPE_CHECKING:
24 from colour.hints import PathLike
26from colour.io.luts import LUT1D, LUT3D, LUT3x1D, LUTSequence
27from colour.io.luts.common import path_to_title
28from colour.utilities import (
29 as_float_array,
30 as_int_scalar,
31 attest,
32 format_array_as_row,
33 usage_warning,
34)
36__author__ = "Colour Developers"
37__copyright__ = "Copyright 2013 Colour Developers"
38__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
39__maintainer__ = "Colour Developers"
40__email__ = "colour-developers@colour-science.org"
41__status__ = "Production"
43__all__ = [
44 "read_LUT_IridasCube",
45 "write_LUT_IridasCube",
46]
49def read_LUT_IridasCube(path: str | PathLike) -> LUT3x1D | LUT3D:
50 """
51 Read the specified *Iridas* *.cube* *LUT* file.
53 Parse an *Iridas* *.cube* Look-Up Table file and return the
54 corresponding *LUT* object. The function automatically detects
55 whether the file contains a 3x1D or 3D *LUT* based on the
56 presence of *LUT_1D_SIZE* or *LUT_3D_SIZE* declarations.
58 Parameters
59 ----------
60 path
61 *LUT* file path.
63 Returns
64 -------
65 :class:`LUT3x1D` or :class:`LUT3D`
66 :class:`LUT3x1D` or :class:`LUT3D` class instance.
68 References
69 ----------
70 :cite:`AdobeSystems2013b`
72 Examples
73 --------
74 Reading a 3x1D *Iridas* *.cube* *LUT*:
76 >>> import os
77 >>> path = os.path.join(
78 ... os.path.dirname(__file__),
79 ... "tests",
80 ... "resources",
81 ... "iridas_cube",
82 ... "ACES_Proxy_10_to_ACES.cube",
83 ... )
84 >>> print(read_LUT_IridasCube(path))
85 LUT3x1D - ACES Proxy 10 to ACES
86 -------------------------------
87 <BLANKLINE>
88 Dimensions : 2
89 Domain : [[ 0. 0. 0.]
90 [ 1. 1. 1.]]
91 Size : (32, 3)
93 Reading a 3D *Iridas* *.cube* *LUT*:
95 >>> path = os.path.join(
96 ... os.path.dirname(__file__),
97 ... "tests",
98 ... "resources",
99 ... "iridas_cube",
100 ... "Colour_Correct.cube",
101 ... )
102 >>> print(read_LUT_IridasCube(path))
103 LUT3D - Generated by Foundry::LUT
104 ---------------------------------
105 <BLANKLINE>
106 Dimensions : 3
107 Domain : [[ 0. 0. 0.]
108 [ 1. 1. 1.]]
109 Size : (4, 4, 4, 3)
111 Reading a 3D *Iridas* *.cube* *LUT* with comments:
113 >>> path = os.path.join(
114 ... os.path.dirname(__file__),
115 ... "tests",
116 ... "resources",
117 ... "iridas_cube",
118 ... "Demo.cube",
119 ... )
120 >>> print(read_LUT_IridasCube(path))
121 LUT3x1D - Demo
122 --------------
123 <BLANKLINE>
124 Dimensions : 2
125 Domain : [[ 0. 0. 0.]
126 [ 1. 2. 3.]]
127 Size : (3, 3)
128 Comment 01 : Comments can go anywhere
129 """
131 path = str(path)
133 title = path_to_title(path)
134 domain_min, domain_max = np.array([0, 0, 0]), np.array([1, 1, 1])
135 dimensions: int = 3
136 size: int = 2
137 data = []
138 comments = []
140 with open(path) as cube_file:
141 lines = cube_file.readlines()
142 for line in lines:
143 line = line.strip() # noqa: PLW2901
145 if len(line) == 0:
146 continue
148 if line.startswith("#"):
149 comments.append(line[1:].strip())
150 continue
152 tokens = line.split()
153 if tokens[0] == "TITLE":
154 title = " ".join(tokens[1:])[1:-1]
155 elif tokens[0] == "DOMAIN_MIN":
156 domain_min = as_float_array(tokens[1:])
157 elif tokens[0] == "DOMAIN_MAX":
158 domain_max = as_float_array(tokens[1:])
159 elif tokens[0] == "LUT_1D_SIZE":
160 dimensions = 2
161 size = as_int_scalar(tokens[1])
162 elif tokens[0] == "LUT_3D_SIZE":
163 dimensions = 3
164 size = as_int_scalar(tokens[1])
165 else:
166 data.append(tokens)
168 table = as_float_array(data)
170 LUT: LUT3x1D | LUT3D
171 if dimensions == 2:
172 LUT = LUT3x1D(
173 table,
174 title,
175 np.vstack([domain_min, domain_max]),
176 comments=comments,
177 )
178 elif dimensions == 3:
179 # The lines of table data shall be in ascending index order,
180 # with the first component index (Red) changing most rapidly,
181 # and the last component index (Blue) changing least rapidly.
182 table = np.reshape(table, (size, size, size, 3), order="F")
184 LUT = LUT3D(
185 table,
186 title,
187 np.vstack([domain_min, domain_max]),
188 comments=comments,
189 )
191 return LUT
194def write_LUT_IridasCube(
195 LUT: LUT1D | LUT3x1D | LUT3D | LUTSequence, path: str | PathLike, decimals: int = 7
196) -> bool:
197 """
198 Write the specified *LUT* to the specified *Iridas* *.cube* *LUT* file.
200 Parameters
201 ----------
202 LUT
203 :class:`LUT1D`, :class:`LUT3x1D`, :class:`LUT3D` or
204 :class:`LUTSequence` class instance to write at the specified path.
205 path
206 *LUT* file path.
207 decimals
208 Number of decimal places for formatting numeric values.
210 Returns
211 -------
212 :class:`bool`
213 Definition success.
215 Warnings
216 --------
217 - If a :class:`LUTSequence` class instance is passed as ``LUT``, the
218 first *LUT* in the *LUT* sequence will be used.
220 References
221 ----------
222 :cite:`AdobeSystems2013b`
224 Examples
225 --------
226 Writing a 3x1D *Iridas* *.cube* *LUT*:
228 >>> from colour.algebra import spow
229 >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]])
230 >>> LUT = LUT3x1D(
231 ... spow(LUT3x1D.linear_table(16, domain), 1 / 2.2),
232 ... "My LUT",
233 ... domain,
234 ... comments=["A first comment.", "A second comment."],
235 ... )
236 >>> write_LUT_IridasCube(LUT, "My_LUT.cube") # doctest: +SKIP
238 Writing a 3D *Iridas* *.cube* *LUT*:
240 >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]])
241 >>> LUT = LUT3D(
242 ... spow(LUT3D.linear_table(16, domain), 1 / 2.2),
243 ... "My LUT",
244 ... np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]),
245 ... comments=["A first comment.", "A second comment."],
246 ... )
247 >>> write_LUT_IridasCube(LUT, "My_LUT.cube") # doctest: +SKIP
248 """
250 path = str(path)
252 if isinstance(LUT, LUTSequence):
253 usage_warning(
254 f'"LUT" is a "LUTSequence" instance was passed, '
255 f'using first sequence "LUT":\n{LUT}'
256 )
257 LUTxD = LUT[0]
258 elif isinstance(LUT, LUT1D):
259 LUTxD = LUT.convert(LUT3x1D)
260 else:
261 LUTxD = LUT
263 attest(
264 isinstance(LUTxD, (LUT3x1D, LUT3D)),
265 '"LUT" must be a 1D, 3x1D or 3D "LUT"!',
266 )
268 attest(not LUTxD.is_domain_explicit(), '"LUT" domain must be implicit!')
270 is_3x1D = isinstance(LUTxD, LUT3x1D)
272 size = LUTxD.size
273 if is_3x1D:
274 attest(2 <= size <= 65536, '"LUT" size must be in domain [2, 65536]!')
275 else:
276 attest(2 <= size <= 256, '"LUT" size must be in domain [2, 256]!')
278 with open(path, "w") as cube_file:
279 cube_file.write(f'TITLE "{LUTxD.name}"\n')
281 if LUTxD.comments:
282 cube_file.writelines(f"# {comment}\n" for comment in LUTxD.comments)
284 cube_file.write(
285 f"{'LUT_1D_SIZE' if is_3x1D else 'LUT_3D_SIZE'} {LUTxD.table.shape[0]}\n"
286 )
288 default_domain = np.array([[0, 0, 0], [1, 1, 1]])
289 if not np.array_equal(LUTxD.domain, default_domain):
290 cube_file.write(
291 f"DOMAIN_MIN {format_array_as_row(LUTxD.domain[0], decimals)}\n"
292 )
293 cube_file.write(
294 f"DOMAIN_MAX {format_array_as_row(LUTxD.domain[1], decimals)}\n"
295 )
297 table = (
298 np.reshape(LUTxD.table, (-1, 3), order="F") if not is_3x1D else LUTxD.table
299 )
301 cube_file.writelines(
302 f"{format_array_as_row(array, decimals)}\n" for array in table
303 )
305 return True