Coverage for io/luts/__init__.py: 34%

47 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Look-Up Table (LUT) I/O 

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

4 

5Implement support for reading and writing industry-standard Look-Up Table 

6(LUT) formats used in colour pipelines and digital intermediate workflows. 

7 

8- :class:`colour.LUT1D`: 1D LUT for single-channel transformations 

9- :class:`colour.LUT3x1D`: Three separate 1D LUTs for per-channel operations 

10- :class:`colour.LUT3D`: 3D LUT for complex colour transformations 

11- :class:`colour.LUTSequence`: Sequential LUT operations 

12- :class:`colour.LUTOperatorMatrix`: Matrix-based LUT operations 

13- :func:`colour.io.read_LUT`: Auto-detect and read LUT files 

14- :func:`colour.io.write_LUT`: Write LUT files in specified formats 

15 

16References 

17---------- 

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

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

20- :cite:`Chamberlain2015` : Chamberlain, P. (2015). LUT documentation (to 

21 create from another program). Retrieved August 23, 2018, from 

22 https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=40284#p232952 

23- :cite:`RisingSunResearch` : Rising Sun Research. (n.d.). cineSpace LUT 

24 Library. Retrieved November 30, 2018, from 

25 https://sourceforge.net/projects/cinespacelutlib/ 

26""" 

27 

28from __future__ import annotations 

29 

30import os 

31import typing 

32 

33if typing.TYPE_CHECKING: 

34 from colour.hints import Any, LiteralLUTReadMethod, LiteralLUTWriteMethod, PathLike 

35 

36from colour.utilities import ( 

37 CanonicalMapping, 

38 filter_kwargs, 

39 validate_method, 

40) 

41 

42# isort: split 

43 

44from .lut import LUT1D, LUT3D, LUT3x1D, LUT_to_LUT 

45from .operator import AbstractLUTSequenceOperator, LUTOperatorMatrix 

46from .sequence import LUTSequence 

47 

48# isort: split 

49 

50from .cinespace_csp import read_LUT_Cinespace, write_LUT_Cinespace 

51from .iridas_cube import read_LUT_IridasCube, write_LUT_IridasCube 

52from .resolve_cube import read_LUT_ResolveCube, write_LUT_ResolveCube 

53from .sony_spi1d import read_LUT_SonySPI1D, write_LUT_SonySPI1D 

54from .sony_spi3d import read_LUT_SonySPI3D, write_LUT_SonySPI3D 

55from .sony_spimtx import read_LUT_SonySPImtx, write_LUT_SonySPImtx 

56 

57__all__ = [ 

58 "LUT1D", 

59 "LUT3D", 

60 "LUT3x1D", 

61 "LUT_to_LUT", 

62] 

63__all__ += [ 

64 "AbstractLUTSequenceOperator", 

65 "LUTOperatorMatrix", 

66] 

67__all__ += [ 

68 "LUTSequence", 

69] 

70__all__ += [ 

71 "read_LUT_Cinespace", 

72 "write_LUT_Cinespace", 

73] 

74__all__ += [ 

75 "read_LUT_IridasCube", 

76 "write_LUT_IridasCube", 

77] 

78__all__ += [ 

79 "read_LUT_ResolveCube", 

80 "write_LUT_ResolveCube", 

81] 

82__all__ += [ 

83 "read_LUT_SonySPI1D", 

84 "write_LUT_SonySPI1D", 

85] 

86__all__ += [ 

87 "read_LUT_SonySPI3D", 

88 "write_LUT_SonySPI3D", 

89] 

90__all__ += [ 

91 "read_LUT_SonySPImtx", 

92 "write_LUT_SonySPImtx", 

93] 

94 

95MAPPING_EXTENSION_TO_LUT_FORMAT: CanonicalMapping = CanonicalMapping( 

96 { 

97 ".cube": "Iridas Cube", 

98 ".spi1d": "Sony SPI1D", 

99 ".spi3d": "Sony SPI3D", 

100 ".spimtx": "Sony SPImtx", 

101 ".csp": "Cinespace", 

102 } 

103) 

104"""Extension to *LUT* format.""" 

105 

106LUT_READ_METHODS: CanonicalMapping = CanonicalMapping( 

107 { 

108 "Cinespace": read_LUT_Cinespace, 

109 "Iridas Cube": read_LUT_IridasCube, 

110 "Resolve Cube": read_LUT_ResolveCube, 

111 "Sony SPI1D": read_LUT_SonySPI1D, 

112 "Sony SPI3D": read_LUT_SonySPI3D, 

113 "Sony SPImtx": read_LUT_SonySPImtx, 

114 } 

115) 

116LUT_READ_METHODS.__doc__ = """ 

117Supported *LUT* reading methods. 

118 

119References 

120---------- 

121:cite:`AdobeSystems2013b`, :cite:`Chamberlain2015` 

122""" 

123 

124 

125def read_LUT( 

126 path: str | PathLike, 

127 method: LiteralLUTReadMethod | str | None = None, 

128 **kwargs: Any, 

129) -> LUT1D | LUT3x1D | LUT3D | LUTSequence | LUTOperatorMatrix: 

130 """ 

131 Read the specified *LUT* file. 

132 

133 Parameters 

134 ---------- 

135 path 

136 *LUT* path. 

137 method 

138 Reading method, if *None*, the method will be auto-detected 

139 according to extension. 

140 

141 Returns 

142 ------- 

143 :class:`colour.LUT1D` or :class:`colour.LUT3x1D` or \ 

144:class:`colour.LUT3D` or :class:`colour.LUTSequence` or \ 

145:class:`colour.LUTOperatorMatrix` 

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

147 :class:`colour.LUT3D` or :class:`colour.LUTSequence` or 

148 :class:`colour.LUTOperatorMatrix` class instance. 

149 

150 Raises 

151 ------ 

152 ValueError 

153 If the *LUT* file format is not supported or if reading fails. 

154 

155 References 

156 ---------- 

157 :cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`, 

158 :cite:`RisingSunResearch` 

159 

160 Examples 

161 -------- 

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

163 

164 >>> path = os.path.join( 

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

166 ... "tests", 

167 ... "resources", 

168 ... "iridas_cube", 

169 ... "ACES_Proxy_10_to_ACES.cube", 

170 ... ) 

171 >>> print(read_LUT(path)) 

172 LUT3x1D - ACES Proxy 10 to ACES 

173 ------------------------------- 

174 <BLANKLINE> 

175 Dimensions : 2 

176 Domain : [[ 0. 0. 0.] 

177 [ 1. 1. 1.]] 

178 Size : (32, 3) 

179 

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

181 

182 >>> path = os.path.join( 

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

184 ... "tests", 

185 ... "resources", 

186 ... "sony_spi1d", 

187 ... "eotf_sRGB_1D.spi1d", 

188 ... ) 

189 >>> print(read_LUT(path)) 

190 LUT1D - eotf sRGB 1D 

191 -------------------- 

192 <BLANKLINE> 

193 Dimensions : 1 

194 Domain : [-0.1 1.5] 

195 Size : (16,) 

196 Comment 01 : Generated by "Colour 0.3.11". 

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

198 

199 Reading a 3D *Sony* *.spi3d* *LUT*: 

200 

201 >>> path = os.path.join( 

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

203 ... "tests", 

204 ... "resources", 

205 ... "sony_spi3d", 

206 ... "Colour_Correct.spi3d", 

207 ... ) 

208 >>> print(read_LUT(path)) 

209 LUT3D - Colour Correct 

210 ---------------------- 

211 <BLANKLINE> 

212 Dimensions : 3 

213 Domain : [[ 0. 0. 0.] 

214 [ 1. 1. 1.]] 

215 Size : (4, 4, 4, 3) 

216 Comment 01 : Adapted from a LUT generated by Foundry::LUT. 

217 

218 Reading a *Sony* *.spimtx* *LUT*: 

219 

220 >>> path = os.path.join( 

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

222 ... "tests", 

223 ... "resources", 

224 ... "sony_spimtx", 

225 ... "dt.spimtx", 

226 ... ) 

227 >>> print(read_LUT(path)) 

228 LUTOperatorMatrix - dt 

229 ---------------------- 

230 <BLANKLINE> 

231 Matrix : [[ 0.864274 0. 0. 0. ] 

232 [ 0. 0.864274 0. 0. ] 

233 [ 0. 0. 0.864274 0. ] 

234 [ 0. 0. 0. 1. ]] 

235 Offset : [ 0. 0. 0. 0.] 

236 """ 

237 

238 path = str(path) 

239 

240 method = ( 

241 MAPPING_EXTENSION_TO_LUT_FORMAT[os.path.splitext(path)[-1]].lower() 

242 if method is None 

243 else validate_method(method, tuple(LUT_WRITE_METHODS)) 

244 ) 

245 

246 function = LUT_READ_METHODS[method] 

247 

248 try: 

249 return function(path, **filter_kwargs(function, **kwargs)) 

250 except ValueError as error: 

251 # Case where a "Resolve Cube" with "LUT3x1D" shaper was read as an 

252 # "Iridas Cube" "LUT". 

253 if method == "iridas cube": 

254 function = LUT_READ_METHODS["Resolve Cube"] 

255 return function(path, **filter_kwargs(function, **kwargs)) 

256 

257 raise ValueError from error 

258 

259 

260LUT_WRITE_METHODS = CanonicalMapping( 

261 { 

262 "Cinespace": write_LUT_Cinespace, 

263 "Iridas Cube": write_LUT_IridasCube, 

264 "Resolve Cube": write_LUT_ResolveCube, 

265 "Sony SPI1D": write_LUT_SonySPI1D, 

266 "Sony SPI3D": write_LUT_SonySPI3D, 

267 "Sony SPImtx": write_LUT_SonySPImtx, 

268 } 

269) 

270LUT_WRITE_METHODS.__doc__ = """ 

271Supported *LUT* writing methods. 

272 

273References 

274---------- 

275:cite:`AdobeSystems2013b`, :cite:`Chamberlain2015` 

276""" 

277 

278 

279def write_LUT( 

280 LUT: LUT1D | LUT3x1D | LUT3D | LUTSequence | LUTOperatorMatrix, 

281 path: str | PathLike, 

282 decimals: int = 7, 

283 method: LiteralLUTWriteMethod | str | None = None, 

284 **kwargs: Any, 

285) -> bool: 

286 """ 

287 Write the specified *LUT* to the specified file. 

288 

289 Parameters 

290 ---------- 

291 LUT 

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

293 :class:`colour.LUT3D` or :class:`colour.LUTSequence` or 

294 :class:`colour.LUTOperatorMatrix` class instance to write at the 

295 specified path. 

296 path 

297 *LUT* file path. 

298 decimals 

299 Number of decimal places for formatting numeric values. 

300 method 

301 Writing method, if *None*, the method will be auto-detected 

302 according to the file extension. 

303 

304 Returns 

305 ------- 

306 :class:`bool` 

307 Definition success. 

308 

309 References 

310 ---------- 

311 :cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`, 

312 :cite:`RisingSunResearch` 

313 

314 Examples 

315 -------- 

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

317 

318 >>> import numpy as np 

319 >>> from colour.algebra import spow 

320 >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) 

321 >>> LUT = LUT3x1D( 

322 ... spow(LUT3x1D.linear_table(16, domain), 1 / 2.2), 

323 ... "My LUT", 

324 ... domain, 

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

326 ... ) 

327 >>> write_LUT(LUT, "My_LUT.cube") # doctest: +SKIP 

328 

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

330 

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

332 >>> LUT = LUT1D( 

333 ... spow(LUT1D.linear_table(16, domain), 1 / 2.2), 

334 ... "My LUT", 

335 ... domain, 

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

337 ... ) 

338 >>> write_LUT(LUT, "My_LUT.spi1d") # doctest: +SKIP 

339 

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

341 

342 >>> LUT = LUT3D( 

343 ... LUT3D.linear_table(16) ** (1 / 2.2), 

344 ... "My LUT", 

345 ... np.array([[0, 0, 0], [1, 1, 1]]), 

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

347 ... ) 

348 >>> write_LUT(LUT, "My_LUT.cube") # doctest: +SKIP 

349 """ 

350 

351 path = str(path) 

352 

353 method = ( 

354 MAPPING_EXTENSION_TO_LUT_FORMAT[os.path.splitext(path)[-1]].lower() 

355 if method is None 

356 else validate_method(method, tuple(LUT_WRITE_METHODS)) 

357 ) 

358 

359 if method == "iridas cube" and isinstance(LUT, LUTSequence): 

360 method = "resolve cube" 

361 

362 function = LUT_WRITE_METHODS[method] 

363 

364 return function(LUT, path, decimals, **filter_kwargs(function, **kwargs)) 

365 

366 

367__all__ += [ 

368 "LUT_READ_METHODS", 

369 "read_LUT", 

370 "LUT_WRITE_METHODS", 

371 "write_LUT", 

372]