Coverage for colour/io/luts/sony_spi3d.py: 100%

59 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

2Sony .spi3d LUT Format Input / Output Utilities 

3=============================================== 

4 

5Define the *Sony* *.spi3d* *LUT* format related input / output utilities 

6objects: 

7 

8- :func:`colour.io.read_LUT_SonySPI3D` 

9- :func:`colour.io.write_LUT_SonySPI3D` 

10""" 

11 

12from __future__ import annotations 

13 

14import typing 

15 

16import numpy as np 

17 

18if typing.TYPE_CHECKING: 

19 from colour.hints import PathLike 

20 

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) 

31 

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" 

38 

39__all__ = [ 

40 "read_LUT_SonySPI3D", 

41 "write_LUT_SonySPI3D", 

42] 

43 

44 

45def read_LUT_SonySPI3D(path: str | PathLike) -> LUT3D: 

46 """ 

47 Read the specified *Sony* *.spi3d* *LUT* file. 

48 

49 Parameters 

50 ---------- 

51 path 

52 *LUT* file path. 

53 

54 Returns 

55 ------- 

56 :class:`colour.LUT3D` 

57 :class:`LUT3D` class instance. 

58 

59 Examples 

60 -------- 

61 Reading an ordered and an unordered 3D *Sony* *.spi3d* *LUT*: 

62 

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 """ 

97 

98 path = str(path) 

99 

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 = [] 

106 

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 

113 

114 tokens = line.split() 

115 if len(tokens) == 3: 

116 attest( 

117 len(set(tokens)) == 1, 

118 'Non-uniform "LUT" shape is unsupported!', 

119 ) 

120 

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])) 

125 

126 indexes = as_int_array(data_indexes) 

127 sorting_indexes = np.lexsort((indexes[:, 2], indexes[:, 1], indexes[:, 0])) 

128 

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 ) 

138 

139 table = np.reshape( 

140 as_float_array(data_table)[sorting_indexes], (size, size, size, 3) 

141 ) 

142 

143 return LUT3D(table, title, np.vstack([domain_min, domain_max]), comments=comments) 

144 

145 

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. 

151 

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. 

161 

162 Returns 

163 ------- 

164 :class:`bool` 

165 Definition success. 

166 

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. 

171 

172 Examples 

173 -------- 

174 Writing a 3D *Sony* *.spi3d* *LUT*: 

175 

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 """ 

184 

185 path = str(path) 

186 

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 

195 

196 attest(not LUTxD.is_domain_explicit(), '"LUT" domain must be implicit!') 

197 

198 attest(isinstance(LUTxD, LUT3D), '"LUT" must be either a 3D "LUT"!') 

199 

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 ) 

212 

213 with open(path, "w") as spi3d_file: 

214 spi3d_file.write("SPILUT 1.0\n") 

215 

216 spi3d_file.write("3 3\n") 

217 

218 spi3d_file.write(f"{LUTxD.size} {LUTxD.size} {LUTxD.size}\n") 

219 

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)) 

225 

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") 

229 

230 if LUTxD.comments: 

231 spi3d_file.writelines(f"# {comment}\n" for comment in LUTxD.comments) 

232 

233 return True