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

1""" 

2Iridas .cube LUT Format Input / Output Utilities 

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

4 

5Define the *Iridas* *.cube* *LUT* format related input / output utilities 

6objects: 

7 

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

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

10 

11References 

12---------- 

13- :cite:`AdobeSystems2013b` : Adobe Systems. (2013). Cube LUT Specification. 

14 https://drive.google.com/open?id=143Eh08ZYncCAMwJ1q4gWxVOqR_OSWYvs 

15""" 

16 

17from __future__ import annotations 

18 

19import typing 

20 

21import numpy as np 

22 

23if typing.TYPE_CHECKING: 

24 from colour.hints import PathLike 

25 

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) 

35 

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" 

42 

43__all__ = [ 

44 "read_LUT_IridasCube", 

45 "write_LUT_IridasCube", 

46] 

47 

48 

49def read_LUT_IridasCube(path: str | PathLike) -> LUT3x1D | LUT3D: 

50 """ 

51 Read the specified *Iridas* *.cube* *LUT* file. 

52 

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. 

57 

58 Parameters 

59 ---------- 

60 path 

61 *LUT* file path. 

62 

63 Returns 

64 ------- 

65 :class:`LUT3x1D` or :class:`LUT3D` 

66 :class:`LUT3x1D` or :class:`LUT3D` class instance. 

67 

68 References 

69 ---------- 

70 :cite:`AdobeSystems2013b` 

71 

72 Examples 

73 -------- 

74 Reading a 3x1D *Iridas* *.cube* *LUT*: 

75 

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) 

92 

93 Reading a 3D *Iridas* *.cube* *LUT*: 

94 

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) 

110 

111 Reading a 3D *Iridas* *.cube* *LUT* with comments: 

112 

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

130 

131 path = str(path) 

132 

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

139 

140 with open(path) as cube_file: 

141 lines = cube_file.readlines() 

142 for line in lines: 

143 line = line.strip() # noqa: PLW2901 

144 

145 if len(line) == 0: 

146 continue 

147 

148 if line.startswith("#"): 

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

150 continue 

151 

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) 

167 

168 table = as_float_array(data) 

169 

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

183 

184 LUT = LUT3D( 

185 table, 

186 title, 

187 np.vstack([domain_min, domain_max]), 

188 comments=comments, 

189 ) 

190 

191 return LUT 

192 

193 

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. 

199 

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. 

209 

210 Returns 

211 ------- 

212 :class:`bool` 

213 Definition success. 

214 

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. 

219 

220 References 

221 ---------- 

222 :cite:`AdobeSystems2013b` 

223 

224 Examples 

225 -------- 

226 Writing a 3x1D *Iridas* *.cube* *LUT*: 

227 

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 

237 

238 Writing a 3D *Iridas* *.cube* *LUT*: 

239 

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

249 

250 path = str(path) 

251 

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 

262 

263 attest( 

264 isinstance(LUTxD, (LUT3x1D, LUT3D)), 

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

266 ) 

267 

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

269 

270 is_3x1D = isinstance(LUTxD, LUT3x1D) 

271 

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

277 

278 with open(path, "w") as cube_file: 

279 cube_file.write(f'TITLE "{LUTxD.name}"\n') 

280 

281 if LUTxD.comments: 

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

283 

284 cube_file.write( 

285 f"{'LUT_1D_SIZE' if is_3x1D else 'LUT_3D_SIZE'} {LUTxD.table.shape[0]}\n" 

286 ) 

287 

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 ) 

296 

297 table = ( 

298 np.reshape(LUTxD.table, (-1, 3), order="F") if not is_3x1D else LUTxD.table 

299 ) 

300 

301 cube_file.writelines( 

302 f"{format_array_as_row(array, decimals)}\n" for array in table 

303 ) 

304 

305 return True