Coverage for io/luts/sony_spi3d.py: 75%
60 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Sony .spi3d LUT Format Input / Output Utilities
3===============================================
5Define the *Sony* *.spi3d* *LUT* format related input / output utilities
6objects:
8- :func:`colour.io.read_LUT_SonySPI3D`
9- :func:`colour.io.write_LUT_SonySPI3D`
10"""
12from __future__ import annotations
14import typing
16import numpy as np
18if typing.TYPE_CHECKING:
19 from colour.hints import PathLike
21from colour.io.luts import LUT3D, LUTSequence
22from colour.io.luts.common import path_to_title
23from colour.utilities import (
24 as_float_array,
25 as_int_array,
26 as_int_scalar,
27 attest,
28 format_array_as_row,
29 usage_warning,
30)
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 "read_LUT_SonySPI3D",
41 "write_LUT_SonySPI3D",
42]
45def read_LUT_SonySPI3D(path: str | PathLike) -> LUT3D:
46 """
47 Read the specified *Sony* *.spi3d* *LUT* file.
49 Parameters
50 ----------
51 path
52 *LUT* file path.
54 Returns
55 -------
56 :class:`colour.LUT3D`
57 :class:`LUT3D` class instance.
59 Examples
60 --------
61 Reading an ordered and an unordered 3D *Sony* *.spi3d* *LUT*:
63 >>> import os
64 >>> path = os.path.join(
65 ... os.path.dirname(__file__),
66 ... "tests",
67 ... "resources",
68 ... "sony_spi3d",
69 ... "Colour_Correct.spi3d",
70 ... )
71 >>> print(read_LUT_SonySPI3D(path))
72 LUT3D - Colour Correct
73 ----------------------
74 <BLANKLINE>
75 Dimensions : 3
76 Domain : [[ 0. 0. 0.]
77 [ 1. 1. 1.]]
78 Size : (4, 4, 4, 3)
79 Comment 01 : Adapted from a LUT generated by Foundry::LUT.
80 >>> path = os.path.join(
81 ... os.path.dirname(__file__),
82 ... "tests",
83 ... "resources",
84 ... "sony_spi3d",
85 ... "Colour_Correct_Unordered.spi3d",
86 ... )
87 >>> print(read_LUT_SonySPI3D(path))
88 LUT3D - Colour Correct Unordered
89 --------------------------------
90 <BLANKLINE>
91 Dimensions : 3
92 Domain : [[ 0. 0. 0.]
93 [ 1. 1. 1.]]
94 Size : (4, 4, 4, 3)
95 Comment 01 : Adapted from a LUT generated by Foundry::LUT.
96 """
98 path = str(path)
100 title = path_to_title(path)
101 domain_min, domain_max = np.array([0, 0, 0]), np.array([1, 1, 1])
102 size: int = 2
103 data_table = []
104 data_indexes = []
105 comments = []
107 with open(path) as spi3d_file:
108 lines = filter(None, (line.strip() for line in spi3d_file))
109 for line in lines:
110 if line.startswith("#"):
111 comments.append(line[1:].strip())
112 continue
114 tokens = line.split()
115 if len(tokens) == 3:
116 attest(
117 len(set(tokens)) == 1,
118 'Non-uniform "LUT" shape is unsupported!',
119 )
121 size = as_int_scalar(tokens[0])
122 if len(tokens) == 6:
123 data_table.append(as_float_array(tokens[3:]))
124 data_indexes.append(as_int_array(tokens[:3]))
126 indexes = as_int_array(data_indexes)
127 sorting_indexes = np.lexsort((indexes[:, 2], indexes[:, 1], indexes[:, 0]))
129 attest(
130 np.array_equal(
131 indexes[sorting_indexes],
132 np.reshape(
133 as_int_array(np.around(LUT3D.linear_table(size) * (size - 1))), (-1, 3)
134 ),
135 ),
136 'Indexes do not match expected "LUT3D" indexes!',
137 )
139 table = np.reshape(
140 as_float_array(data_table)[sorting_indexes], (size, size, size, 3)
141 )
143 return LUT3D(table, title, np.vstack([domain_min, domain_max]), comments=comments)
146def write_LUT_SonySPI3D(
147 LUT: LUT3D | LUTSequence, path: str | PathLike, decimals: int = 7
148) -> bool:
149 """
150 Write the specified *LUT* to the specified *Sony* *.spi3d* *LUT* file.
152 Parameters
153 ----------
154 LUT
155 :class:`LUT3D` or :class:`LUTSequence` class instance to write at
156 the specified path.
157 path
158 *LUT* file path.
159 decimals
160 Number of decimal places for formatting numeric values.
162 Returns
163 -------
164 :class:`bool`
165 Definition success.
167 Warnings
168 --------
169 - If a :class:`LUTSequence` class instance is passed as ``LUT``,
170 the first *LUT* in the *LUT* sequence will be used.
172 Examples
173 --------
174 Writing a 3D *Sony* *.spi3d* *LUT*:
176 >>> LUT = LUT3D(
177 ... LUT3D.linear_table(16) ** (1 / 2.2),
178 ... "My LUT",
179 ... np.array([[0, 0, 0], [1, 1, 1]]),
180 ... comments=["A first comment.", "A second comment."],
181 ... )
182 >>> write_LUT_SonySPI3D(LUT, "My_LUT.cube") # doctest: +SKIP
183 """
185 path = str(path)
187 if isinstance(LUT, LUTSequence):
188 usage_warning(
189 f'"LUT" is a "LUTSequence" instance was passed, using first '
190 f'sequence "LUT":\n{LUT}'
191 )
192 LUTxD = LUT[0]
193 else:
194 LUTxD = LUT
196 attest(not LUTxD.is_domain_explicit(), '"LUT" domain must be implicit!')
198 attest(isinstance(LUTxD, LUT3D), '"LUT" must be either a 3D "LUT"!')
200 attest(
201 np.array_equal(
202 LUTxD.domain,
203 np.array(
204 [
205 [0, 0, 0],
206 [1, 1, 1],
207 ]
208 ),
209 ),
210 '"LUT" domain must be [[0, 0, 0], [1, 1, 1]]!',
211 )
213 with open(path, "w") as spi3d_file:
214 spi3d_file.write("SPILUT 1.0\n")
216 spi3d_file.write("3 3\n")
218 spi3d_file.write(f"{LUTxD.size} {LUTxD.size} {LUTxD.size}\n")
220 indexes = np.reshape(
221 as_int_array(np.around(LUTxD.linear_table(LUTxD.size) * (LUTxD.size - 1))),
222 (-1, 3),
223 )
224 table = np.reshape(LUTxD.table, (-1, 3))
226 for i, array in enumerate(indexes):
227 spi3d_file.write("{:d} {:d} {:d}".format(*array))
228 spi3d_file.write(f" {format_array_as_row(table[i], decimals)}\n")
230 if LUTxD.comments:
231 spi3d_file.writelines(f"# {comment}\n" for comment in LUTxD.comments)
233 return True