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

112 statements  

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

1""" 

2Resolve .cube LUT Format Input / Output Utilities 

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

4 

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

6objects: 

7 

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

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

10 

11References 

12---------- 

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

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

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

16""" 

17 

18from __future__ import annotations 

19 

20import typing 

21 

22import numpy as np 

23 

24if typing.TYPE_CHECKING: 

25 from colour.hints import PathLike 

26 

27from colour.io.luts import LUT1D, LUT3D, LUT3x1D, LUTSequence 

28from colour.io.luts.common import path_to_title 

29from colour.utilities import ( 

30 as_float_array, 

31 as_int_scalar, 

32 attest, 

33 format_array_as_row, 

34 tstack, 

35) 

36 

37__author__ = "Colour Developers" 

38__copyright__ = "Copyright 2013 Colour Developers" 

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

40__maintainer__ = "Colour Developers" 

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

42__status__ = "Production" 

43 

44__all__ = [ 

45 "read_LUT_ResolveCube", 

46 "write_LUT_ResolveCube", 

47] 

48 

49 

50def read_LUT_ResolveCube(path: str | PathLike) -> LUT3x1D | LUT3D | LUTSequence: 

51 """ 

52 Read the specified *Resolve* *.cube* *LUT* file. 

53 

54 Read and parse a *DaVinci Resolve* *.cube* lookup table file, which may 

55 contain a 1D LUT, a 3D LUT, or a sequence of both. The *.cube* format 

56 supports configurable precision and domain ranges, making it suitable for 

57 colour grading and colour space transformations. 

58 

59 Parameters 

60 ---------- 

61 path 

62 Path to the *.cube* *LUT* file to read. 

63 

64 Returns 

65 ------- 

66 :class:`colour.LUT3x1D` or :class:`colour.LUT3D` or :class:`colour.LUTSequence` 

67 :class:`LUT3x1D` instance for 1D shaper LUTs, :class:`LUT3D` instance 

68 for 3D colour transformation LUTs, or :class:`LUTSequence` instance 

69 when the file contains both shaper and 3D LUT data. 

70 

71 References 

72 ---------- 

73 :cite:`Chamberlain2015` 

74 

75 Examples 

76 -------- 

77 Reading a 3x1D *Resolve* *.cube* *LUT*: 

78 

79 >>> import os 

80 >>> path = os.path.join( 

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

82 ... "tests", 

83 ... "resources", 

84 ... "resolve_cube", 

85 ... "ACES_Proxy_10_to_ACES.cube", 

86 ... ) 

87 >>> print(read_LUT_ResolveCube(path)) 

88 LUT3x1D - ACES Proxy 10 to ACES 

89 ------------------------------- 

90 <BLANKLINE> 

91 Dimensions : 2 

92 Domain : [[ 0. 0. 0.] 

93 [ 1. 1. 1.]] 

94 Size : (32, 3) 

95 

96 Reading a 3D *Resolve* *.cube* *LUT*: 

97 

98 >>> path = os.path.join( 

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

100 ... "tests", 

101 ... "resources", 

102 ... "resolve_cube", 

103 ... "Colour_Correct.cube", 

104 ... ) 

105 >>> print(read_LUT_ResolveCube(path)) 

106 LUT3D - Generated by Foundry::LUT 

107 --------------------------------- 

108 <BLANKLINE> 

109 Dimensions : 3 

110 Domain : [[ 0. 0. 0.] 

111 [ 1. 1. 1.]] 

112 Size : (4, 4, 4, 3) 

113 

114 Reading a 3D *Resolve* *.cube* *LUT* with comments: 

115 

116 >>> path = os.path.join( 

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

118 ... "tests", 

119 ... "resources", 

120 ... "resolve_cube", 

121 ... "Demo.cube", 

122 ... ) 

123 >>> print(read_LUT_ResolveCube(path)) 

124 LUT3x1D - Demo 

125 -------------- 

126 <BLANKLINE> 

127 Dimensions : 2 

128 Domain : [[ 0. 0. 0.] 

129 [ 3. 3. 3.]] 

130 Size : (3, 3) 

131 Comment 01 : Comments can't go anywhere 

132 

133 Reading a 3x1D + 3D *Resolve* *.cube* *LUT*: 

134 

135 >>> path = os.path.join( 

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

137 ... "tests", 

138 ... "resources", 

139 ... "resolve_cube", 

140 ... "Three_Dimensional_Table_With_Shaper.cube", 

141 ... ) 

142 >>> print(read_LUT_ResolveCube(path)) 

143 LUT Sequence 

144 ------------ 

145 <BLANKLINE> 

146 Overview 

147 <BLANKLINE> 

148 LUT3x1D --> LUT3D 

149 <BLANKLINE> 

150 Operations 

151 <BLANKLINE> 

152 LUT3x1D - LUT3D with My Shaper - Shaper 

153 --------------------------------------- 

154 <BLANKLINE> 

155 Dimensions : 2 

156 Domain : [[-0.1 -0.1 -0.1] 

157 [ 3. 3. 3. ]] 

158 Size : (10, 3) 

159 <BLANKLINE> 

160 LUT3D - LUT3D with My Shaper - Cube 

161 ----------------------------------- 

162 <BLANKLINE> 

163 Dimensions : 3 

164 Domain : [[-0.1 -0.1 -0.1] 

165 [ 3. 3. 3. ]] 

166 Size : (3, 3, 3, 3) 

167 Comment 01 : A first "Shaper" comment. 

168 Comment 02 : A second "Shaper" comment. 

169 Comment 03 : A first "LUT3D" comment. 

170 Comment 04 : A second "LUT3D" comment. 

171 """ 

172 

173 path = str(path) 

174 

175 title = path_to_title(path) 

176 domain_3x1D, domain_3D = None, None 

177 size_3x1D: int = 2 

178 size_3D: int = 2 

179 data = [] 

180 comments = [] 

181 has_3x1D, has_3D = False, False 

182 

183 with open(path) as cube_file: 

184 lines = cube_file.readlines() 

185 for line in lines: 

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

187 

188 if len(line) == 0: 

189 continue 

190 

191 if line.startswith("#"): 

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

193 continue 

194 

195 tokens = line.split() 

196 if tokens[0] == "TITLE": 

197 title = " ".join(tokens[1:])[1:-1] 

198 elif tokens[0] == "LUT_1D_INPUT_RANGE": 

199 domain_3x1D = tstack([tokens[1:], tokens[1:], tokens[1:]]) 

200 elif tokens[0] == "LUT_3D_INPUT_RANGE": 

201 domain_3D = tstack([tokens[1:], tokens[1:], tokens[1:]]) 

202 elif tokens[0] == "LUT_1D_SIZE": 

203 has_3x1D = True 

204 size_3x1D = as_int_scalar(tokens[1]) 

205 elif tokens[0] == "LUT_3D_SIZE": 

206 has_3D = True 

207 size_3D = as_int_scalar(tokens[1]) 

208 else: 

209 data.append(tokens) 

210 

211 table = as_float_array(data) 

212 

213 LUT: LUT3x1D | LUT3D | LUTSequence 

214 if has_3x1D and has_3D: 

215 table_1D = table[: int(size_3x1D)] 

216 # The lines of table data shall be in ascending index order, 

217 # with the first component index (Red) changing most rapidly, 

218 # and the last component index (Blue) changing least rapidly. 

219 table_3D = np.reshape( 

220 table[int(size_3x1D) :], (size_3D, size_3D, size_3D, 3), order="F" 

221 ) 

222 LUT = LUTSequence( 

223 LUT3x1D( 

224 table_1D, 

225 f"{title} - Shaper", 

226 domain_3x1D, 

227 ), 

228 LUT3D( 

229 table_3D, 

230 f"{title} - Cube", 

231 domain_3D, 

232 comments=comments, 

233 ), 

234 ) 

235 elif has_3x1D: 

236 LUT = LUT3x1D(table, title, domain_3x1D, comments=comments) 

237 elif has_3D: 

238 # The lines of table data shall be in ascending index order, 

239 # with the first component index (Red) changing most rapidly, 

240 # and the last component index (Blue) changing least rapidly. 

241 table = np.reshape(table, (size_3D, size_3D, size_3D, 3), order="F") 

242 LUT = LUT3D(table, title, domain_3D, comments=comments) 

243 

244 return LUT 

245 

246 

247def write_LUT_ResolveCube( 

248 LUT: LUT1D | LUT3x1D | LUT3D | LUTSequence, 

249 path: str | PathLike, 

250 decimals: int = 7, 

251) -> bool: 

252 """ 

253 Write the specified *LUT* to the specified *Resolve* *.cube* *LUT* file. 

254 

255 Parameters 

256 ---------- 

257 LUT 

258 :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUT3D` or 

259 :class:`LUTSequence` class instance to write at the specified path. 

260 path 

261 *LUT* file path. 

262 decimals 

263 Number of decimal places for formatting numeric values. 

264 

265 Returns 

266 ------- 

267 :class:`bool` 

268 Definition success. 

269 

270 References 

271 ---------- 

272 :cite:`Chamberlain2015` 

273 

274 Examples 

275 -------- 

276 Writing a 3x1D *Resolve* *.cube* *LUT*: 

277 

278 >>> from colour.algebra import spow 

279 >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]]) 

280 >>> LUT = LUT3x1D( 

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

282 ... "My LUT", 

283 ... domain, 

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

285 ... ) 

286 >>> write_LUT_ResolveCube(LUT, "My_LUT.cube") # doctest: +SKIP 

287 

288 Writing a 3D *Resolve* *.cube* *LUT*: 

289 

290 >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]]) 

291 >>> LUT = LUT3D( 

292 ... spow(LUT3D.linear_table(16, domain), 1 / 2.2), 

293 ... "My LUT", 

294 ... domain, 

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

296 ... ) 

297 >>> write_LUT_ResolveCube(LUT, "My_LUT.cube") # doctest: +SKIP 

298 

299 Writing a 3x1D + 3D *Resolve* *.cube* *LUT*: 

300 

301 >>> from colour.models import RGB_to_HSV, HSV_to_RGB 

302 >>> from colour.utilities import tstack 

303 >>> def rotate_hue(a, angle): 

304 ... H, S, V = RGB_to_HSV(a) 

305 ... H += angle / 360 

306 ... H[H > 1] -= 1 

307 ... H[H < 0] += 1 

308 ... return HSV_to_RGB([H, S, V]) 

309 >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]]) 

310 >>> shaper = LUT3x1D( 

311 ... spow(LUT3x1D.linear_table(10, domain), 1 / 2.2), 

312 ... "My Shaper", 

313 ... domain, 

314 ... comments=[ 

315 ... 'A first "Shaper" comment.', 

316 ... 'A second "Shaper" comment.', 

317 ... ], 

318 ... ) 

319 >>> LUT = LUT3D( 

320 ... rotate_hue(LUT3D.linear_table(3, domain), 10), 

321 ... "LUT3D with My Shaper", 

322 ... domain, 

323 ... comments=['A first "LUT3D" comment.', 'A second "LUT3D" comment.'], 

324 ... ) 

325 >>> LUT_sequence = LUTSequence(shaper, LUT) 

326 >>> write_LUT_ResolveCube(LUT_sequence, "My_LUT.cube") # doctest: +SKIP 

327 """ 

328 

329 path = str(path) 

330 

331 has_3D, has_3x1D = False, False 

332 

333 if isinstance(LUT, LUTSequence): 

334 attest( 

335 len(LUT) == 2 

336 and isinstance(LUT[0], (LUT1D, LUT3x1D)) 

337 and isinstance(LUT[1], LUT3D), 

338 "LUTSequence must be 1D + 3D or 3x1D + 3D!", 

339 ) 

340 

341 if isinstance(LUT[0], LUT1D): 

342 LUT[0] = LUT[0].convert(LUT3x1D) 

343 

344 name = f"{LUT[0].name} - {LUT[1].name}" 

345 has_3x1D = True 

346 has_3D = True 

347 elif isinstance(LUT, LUT1D): 

348 name = LUT.name 

349 has_3x1D = True 

350 LUT = LUTSequence(LUT.convert(LUT3x1D), LUT3D()) 

351 elif isinstance(LUT, LUT3x1D): 

352 name = LUT.name 

353 has_3x1D = True 

354 LUT = LUTSequence(LUT, LUT3D()) 

355 elif isinstance(LUT, LUT3D): 

356 name = LUT.name 

357 has_3D = True 

358 LUT = LUTSequence(LUT3x1D(), LUT) 

359 else: 

360 error = "LUT must be 1D, 3x1D, 3D, 1D + 3D or 3x1D + 3D!" 

361 

362 raise TypeError(error) 

363 

364 for i in range(2): 

365 attest(not LUT[i].is_domain_explicit(), '"LUT" domain must be implicit!') 

366 

367 attest( 

368 (len(np.unique(LUT[0].domain)) == 2 and len(np.unique(LUT[1].domain)) == 2), 

369 '"LUT" domain must be 1D!', 

370 ) 

371 

372 if has_3x1D: 

373 attest( 

374 2 <= LUT[0].size <= 65536, 

375 "Shaper size must be in domain [2, 65536]!", 

376 ) 

377 if has_3D: 

378 attest(2 <= LUT[1].size <= 256, "Cube size must be in domain [2, 256]!") 

379 

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

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

382 

383 if LUT[0].comments: 

384 cube_file.writelines(f"# {comment}\n" for comment in LUT[0].comments) 

385 

386 if LUT[1].comments: 

387 cube_file.writelines(f"# {comment}\n" for comment in LUT[1].comments) 

388 

389 default_domain = np.array([[0, 0, 0], [1, 1, 1]]) 

390 

391 if has_3x1D: 

392 cube_file.write(f"LUT_1D_SIZE {LUT[0].table.shape[0]}\n") 

393 if not np.array_equal(LUT[0].domain, default_domain): 

394 input_range = format_array_as_row( 

395 [LUT[0].domain[0][0], LUT[0].domain[1][0]], decimals 

396 ) 

397 cube_file.write(f"LUT_1D_INPUT_RANGE {input_range}\n") 

398 

399 if has_3D: 

400 cube_file.write(f"LUT_3D_SIZE {LUT[1].table.shape[0]}\n") 

401 if not np.array_equal(LUT[1].domain, default_domain): 

402 input_range = format_array_as_row( 

403 [LUT[1].domain[0][0], LUT[1].domain[1][0]], decimals 

404 ) 

405 cube_file.write(f"LUT_3D_INPUT_RANGE {input_range}\n") 

406 

407 if has_3x1D: 

408 table = LUT[0].table 

409 cube_file.writelines( 

410 f"{format_array_as_row(vector, decimals)}\n" for vector in table 

411 ) 

412 cube_file.write("\n") 

413 

414 if has_3D: 

415 table = np.reshape(LUT[1].table, (-1, 3), order="F") 

416 cube_file.writelines( 

417 f"{format_array_as_row(vector, decimals)}\n" for vector in table 

418 ) 

419 

420 return True