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

68 statements  

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

1""" 

2Sony .spi1d LUT Format Input / Output Utilities 

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

4 

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

6objects: 

7 

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

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

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 LUT1D, LUT3x1D, LUTSequence 

22from colour.io.luts.common import path_to_title 

23from colour.utilities import ( 

24 as_float_array, 

25 as_int_scalar, 

26 attest, 

27 format_array_as_row, 

28 usage_warning, 

29) 

30 

31__author__ = "Colour Developers" 

32__copyright__ = "Copyright 2013 Colour Developers" 

33__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" 

34__maintainer__ = "Colour Developers" 

35__email__ = "colour-developers@colour-science.org" 

36__status__ = "Production" 

37 

38__all__ = [ 

39 "read_LUT_SonySPI1D", 

40 "write_LUT_SonySPI1D", 

41] 

42 

43 

44def read_LUT_SonySPI1D(path: str | PathLike) -> LUT1D | LUT3x1D: 

45 """ 

46 Read the specified *Sony* *.spi1d* *LUT* file. 

47 

48 Parameters 

49 ---------- 

50 path 

51 *LUT* file path. 

52 

53 Returns 

54 ------- 

55 :class:`colour.LUT1D` or :class:`colour.LUT3x1D` 

56 :class:`LUT1D` or :class:`LUT3x1D` class instance. 

57 

58 Examples 

59 -------- 

60 Reading a 1D *Sony* *.spi1d* *LUT*: 

61 

62 >>> import os 

63 >>> path = os.path.join( 

64 ... os.path.dirname(__file__), 

65 ... "tests", 

66 ... "resources", 

67 ... "sony_spi1d", 

68 ... "eotf_sRGB_1D.spi1d", 

69 ... ) 

70 >>> print(read_LUT_SonySPI1D(path)) 

71 LUT1D - eotf sRGB 1D 

72 -------------------- 

73 <BLANKLINE> 

74 Dimensions : 1 

75 Domain : [-0.1 1.5] 

76 Size : (16,) 

77 Comment 01 : Generated by "Colour 0.3.11". 

78 Comment 02 : "colour.models.eotf_sRGB". 

79 

80 Reading a 3x1D *Sony* *.spi1d* *LUT*: 

81 

82 >>> path = os.path.join( 

83 ... os.path.dirname(__file__), 

84 ... "tests", 

85 ... "resources", 

86 ... "sony_spi1d", 

87 ... "eotf_sRGB_3x1D.spi1d", 

88 ... ) 

89 >>> print(read_LUT_SonySPI1D(path)) 

90 LUT3x1D - eotf sRGB 3x1D 

91 ------------------------ 

92 <BLANKLINE> 

93 Dimensions : 2 

94 Domain : [[-0.1 -0.1 -0.1] 

95 [ 1.5 1.5 1.5]] 

96 Size : (16, 3) 

97 Comment 01 : Generated by "Colour 0.3.11". 

98 Comment 02 : "colour.models.eotf_sRGB". 

99 """ 

100 

101 title = path_to_title(path) 

102 domain_min, domain_max = np.array([0, 1]) 

103 dimensions = 1 

104 data = [] 

105 

106 comments = [] 

107 

108 with open(path) as spi1d_file: 

109 lines = filter(None, (line.strip() for line in spi1d_file)) 

110 for line in lines: 

111 if line.startswith("#"): 

112 comments.append(line[1:].strip()) 

113 continue 

114 

115 tokens = line.split() 

116 if tokens[0] == "Version": 

117 continue 

118 if tokens[0] == "From": 

119 domain_min, domain_max = as_float_array(tokens[1:]) 

120 elif tokens[0] == "Length": 

121 continue 

122 elif tokens[0] == "Components": 

123 component = as_int_scalar(tokens[1]) 

124 attest( 

125 component in (1, 3), 

126 "Only 1 or 3 components are supported!", 

127 ) 

128 

129 dimensions = 1 if component == 1 else 2 

130 elif tokens[0] in ("{", "}"): 

131 continue 

132 else: 

133 data.append(tokens) 

134 

135 table = as_float_array(data) 

136 

137 LUT: LUT1D | LUT3x1D 

138 if dimensions == 1: 

139 LUT = LUT1D( 

140 np.squeeze(table), 

141 title, 

142 np.array([domain_min, domain_max]), 

143 comments=comments, 

144 ) 

145 elif dimensions == 2: 

146 LUT = LUT3x1D( 

147 table, 

148 title, 

149 np.array( 

150 [ 

151 [domain_min, domain_min, domain_min], 

152 [domain_max, domain_max, domain_max], 

153 ] 

154 ), 

155 comments=comments, 

156 ) 

157 

158 return LUT 

159 

160 

161def write_LUT_SonySPI1D( 

162 LUT: LUT1D | LUT3x1D | LUTSequence, path: str | PathLike, decimals: int = 7 

163) -> bool: 

164 """ 

165 Write the specified *LUT* to the specified *Sony* *.spi1d* *LUT* file. 

166 

167 Parameters 

168 ---------- 

169 LUT 

170 :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUTSequence` class 

171 instance to write at the specified path. 

172 path 

173 *LUT* file path. 

174 decimals 

175 Number of decimal places for formatting numeric values. 

176 

177 Returns 

178 ------- 

179 :class:`bool` 

180 Whether the write operation was successful. 

181 

182 Warnings 

183 -------- 

184 - If a :class:`LUTSequence` class instance is passed as ``LUT``, 

185 the first *LUT* in the *LUT* sequence will be used. 

186 

187 Examples 

188 -------- 

189 Writing a 1D *Sony* *.spi1d* *LUT*: 

190 

191 >>> from colour.algebra import spow 

192 >>> domain = np.array([-0.1, 1.5]) 

193 >>> LUT = LUT1D( 

194 ... spow(LUT1D.linear_table(16), 1 / 2.2), 

195 ... "My LUT", 

196 ... domain, 

197 ... comments=["A first comment.", "A second comment."], 

198 ... ) 

199 >>> write_LUT_SonySPI1D(LUT, "My_LUT.spi1d") # doctest: +SKIP 

200 

201 Writing a 3x1D *Sony* *.spi1d* *LUT*: 

202 

203 >>> domain = np.array([[-0.1, -0.1, -0.1], [1.5, 1.5, 1.5]]) 

204 >>> LUT = LUT3x1D( 

205 ... spow(LUT3x1D.linear_table(16), 1 / 2.2), 

206 ... "My LUT", 

207 ... domain, 

208 ... comments=["A first comment.", "A second comment."], 

209 ... ) 

210 >>> write_LUT_SonySPI1D(LUT, "My_LUT.spi1d") # doctest: +SKIP 

211 """ 

212 

213 path = str(path) 

214 

215 if isinstance(LUT, LUTSequence): 

216 usage_warning( 

217 f'"LUT" is a "LUTSequence" instance was passed, using first ' 

218 f'sequence "LUT":\n{LUT}' 

219 ) 

220 LUTxD = LUT[0] 

221 else: 

222 LUTxD = LUT 

223 

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

225 

226 attest( 

227 isinstance(LUTxD, (LUT1D, LUT3x1D)), 

228 '"LUT" must be either a 1D or 3x1D "LUT"!', 

229 ) 

230 

231 is_1D = isinstance(LUTxD, LUT1D) 

232 

233 if is_1D: 

234 domain = LUTxD.domain 

235 else: 

236 domain = np.unique(LUTxD.domain) 

237 

238 attest(len(domain) == 2, 'Non-uniform "LUT" domain is unsupported!') 

239 

240 with open(path, "w") as spi1d_file: 

241 spi1d_file.write("Version 1\n") 

242 

243 spi1d_file.write(f"From {format_array_as_row(domain, decimals)}\n") 

244 

245 spi1d_file.write( 

246 f"Length {LUTxD.table.size if is_1D else LUTxD.table.shape[0]}\n" 

247 ) 

248 

249 spi1d_file.write(f"Components {1 if is_1D else 3}\n") 

250 

251 spi1d_file.write("{\n") 

252 spi1d_file.writelines( 

253 f" {format_array_as_row(array, decimals)}\n" for array in LUTxD.table 

254 ) 

255 spi1d_file.write("}\n") 

256 

257 if LUTxD.comments: 

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

259 

260 return True