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

157 statements  

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

1""" 

2Cinespace .csp LUT Format Input / Output Utilities 

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

4 

5Define the *Cinespace* *.csp* *LUT* format related input / output utilities 

6objects: 

7 

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

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

10 

11References 

12---------- 

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

14 Library. Retrieved November 30, 2018, from 

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

16""" 

17 

18from __future__ import annotations 

19 

20import typing 

21 

22import numpy as np 

23 

24if typing.TYPE_CHECKING: 

25 from colour.hints import ArrayLike, List, NDArrayFloat, NDArrayInt, PathLike 

26 

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

28from colour.utilities import ( 

29 as_float_array, 

30 as_int_array, 

31 attest, 

32 format_array_as_row, 

33 tsplit, 

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

46 "write_LUT_Cinespace", 

47] 

48 

49 

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

51 """ 

52 Read the specified *Cinespace* *.csp* *LUT* file. 

53 

54 Parameters 

55 ---------- 

56 path 

57 *LUT* file path. 

58 

59 Returns 

60 ------- 

61 :class:`colour.LUT3x1D` or :class:`colour.LUT3D` or \ 

62 :class:`colour.LUTSequence` 

63 :class:`LUT3x1D`, :class:`LUT3D`, or :class:`LUTSequence` 

64 class instance. 

65 

66 References 

67 ---------- 

68 :cite:`RisingSunResearch` 

69 

70 Examples 

71 -------- 

72 Reading a 3x1D *Cinespace* *.csp* *LUT*: 

73 

74 >>> import os 

75 >>> path = os.path.join( 

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

77 ... "tests", 

78 ... "resources", 

79 ... "cinespace", 

80 ... "ACES_Proxy_10_to_ACES.csp", 

81 ... ) 

82 >>> print(read_LUT_Cinespace(path)) 

83 LUT3x1D - ACES Proxy 10 to ACES 

84 ------------------------------- 

85 <BLANKLINE> 

86 Dimensions : 2 

87 Domain : [[ 0. 0. 0.] 

88 [ 1. 1. 1.]] 

89 Size : (32, 3) 

90 

91 Reading a 3D *Cinespace* *.csp* *LUT*: 

92 

93 >>> path = os.path.join( 

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

95 ... "tests", 

96 ... "resources", 

97 ... "cinespace", 

98 ... "Colour_Correct.csp", 

99 ... ) 

100 >>> print(read_LUT_Cinespace(path)) 

101 LUT3D - Generated by Foundry::LUT 

102 --------------------------------- 

103 <BLANKLINE> 

104 Dimensions : 3 

105 Domain : [[ 0. 0. 0.] 

106 [ 1. 1. 1.]] 

107 Size : (4, 4, 4, 3) 

108 """ 

109 

110 unity_range = np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]) 

111 

112 def _parse_metadata_section(metadata: list) -> tuple: 

113 """Parse the metadata at specified lines.""" 

114 

115 return (metadata[0], metadata[1:]) if len(metadata) > 0 else ("", []) 

116 

117 def _parse_domain_section(lines: List[str]) -> NDArrayFloat: 

118 """Parse the domain at specified lines.""" 

119 

120 pre_LUT_size = max(int(lines[i]) for i in [0, 3, 6]) 

121 pre_LUT = [as_float_array(lines[i].split()) for i in [1, 2, 4, 5, 7, 8]] 

122 

123 pre_LUT_padded = [] 

124 for array in pre_LUT: 

125 if len(array) != pre_LUT_size: 

126 pre_LUT_padded.append( 

127 np.pad( 

128 array, 

129 (0, pre_LUT_size - array.shape[0]), 

130 mode="constant", 

131 constant_values=np.nan, 

132 ) 

133 ) 

134 else: 

135 pre_LUT_padded.append(array) 

136 

137 return np.asarray(pre_LUT_padded) 

138 

139 def _parse_table_section(lines: list[str]) -> tuple[NDArrayInt, NDArrayFloat]: 

140 """Parse the table at specified lines.""" 

141 

142 size = as_int_array(lines[0].split()) 

143 table = as_float_array([line.split() for line in lines[1:]]) 

144 

145 return size, table 

146 

147 with open(path) as csp_file: 

148 lines = csp_file.readlines() 

149 attest(len(lines) > 0, '"LUT" is empty!') 

150 lines = [line.strip() for line in lines if line.strip()] 

151 

152 header = lines[0] 

153 attest(header == "CSPLUTV100", '"LUT" header is invalid!') 

154 

155 kind = lines[1] 

156 attest(kind in ("1D", "3D"), '"LUT" type must be "1D" or "3D"!') 

157 

158 is_3D = kind == "3D" 

159 

160 seek = 2 

161 metadata = [] 

162 is_metadata = False 

163 for i, line in enumerate(lines[2:]): 

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

165 if line == "BEGIN METADATA": 

166 is_metadata = True 

167 continue 

168 if line == "END METADATA": 

169 seek += i 

170 break 

171 

172 if is_metadata: 

173 metadata.append(line) 

174 

175 title, comments = _parse_metadata_section(metadata) 

176 

177 seek += 1 

178 pre_LUT = _parse_domain_section(lines[seek : seek + 9]) 

179 

180 seek += 9 

181 size, table = _parse_table_section(lines[seek:]) 

182 

183 attest(np.prod(size) == len(table), '"LUT" table size is invalid!') 

184 

185 LUT: LUT3x1D | LUT3D | LUTSequence 

186 if ( 

187 is_3D 

188 and pre_LUT.shape == (6, 2) 

189 and np.array_equal(np.transpose(np.reshape(pre_LUT, (3, 4)))[2:4], unity_range) 

190 ): 

191 table = np.reshape(table, (size[0], size[1], size[2], 3), order="F") 

192 LUT = LUT3D( 

193 domain=np.transpose(np.reshape(pre_LUT, (3, 4)))[0:2], 

194 name=title, 

195 comments=comments, 

196 table=table, 

197 ) 

198 

199 elif ( 

200 not is_3D 

201 and pre_LUT.shape == (6, 2) 

202 and np.array_equal(np.transpose(np.reshape(pre_LUT, (3, 4)))[2:4], unity_range) 

203 ): 

204 LUT = LUT3x1D( 

205 domain=np.reshape(pre_LUT, (3, 4)).transpose()[0:2], 

206 name=title, 

207 comments=comments, 

208 table=table, 

209 ) 

210 

211 elif is_3D: 

212 pre_domain = tstack((pre_LUT[0], pre_LUT[2], pre_LUT[4])) 

213 pre_table = tstack((pre_LUT[1], pre_LUT[3], pre_LUT[5])) 

214 shaper_name = f"{title} - Shaper" 

215 cube_name = f"{title} - Cube" 

216 table = np.reshape(table, (size[0], size[1], size[2], 3), order="F") 

217 

218 LUT = LUTSequence( 

219 LUT3x1D(pre_table, shaper_name, pre_domain), 

220 LUT3D(table, cube_name, comments=comments), 

221 ) 

222 

223 elif not is_3D: 

224 pre_domain = tstack((pre_LUT[0], pre_LUT[2], pre_LUT[4])) 

225 pre_table = tstack((pre_LUT[1], pre_LUT[3], pre_LUT[5])) 

226 

227 if table.shape == (2, 3): 

228 table_max = table[1] 

229 table_min = table[0] 

230 pre_table *= table_max - table_min 

231 pre_table += table_min 

232 

233 LUT = LUT3x1D(pre_table, title, pre_domain, comments=comments) 

234 else: 

235 pre_name = f"{title} - PreLUT" 

236 table_name = f"{title} - Table" 

237 

238 LUT = LUTSequence( 

239 LUT3x1D(pre_table, pre_name, pre_domain), 

240 LUT3x1D(table, table_name, comments=comments), 

241 ) 

242 

243 return LUT 

244 

245 

246def write_LUT_Cinespace( 

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

248) -> bool: 

249 """ 

250 Write the specified *LUT* to the specified *Cinespace* *.csp* *LUT* file. 

251 

252 Parameters 

253 ---------- 

254 LUT 

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

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

257 path 

258 *LUT* file path. 

259 decimals 

260 Number of decimal places for formatting numeric values. 

261 

262 Returns 

263 ------- 

264 :class:`bool` 

265 Definition success. 

266 

267 References 

268 ---------- 

269 :cite:`RisingSunResearch` 

270 

271 Examples 

272 -------- 

273 Writing a 3x1D *Cinespace* *.csp* *LUT*: 

274 

275 >>> from colour.algebra import spow 

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

277 >>> LUT = LUT3x1D( 

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

279 ... "My LUT", 

280 ... domain, 

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

282 ... ) 

283 >>> write_LUT_Cinespace(LUT, "My_LUT.cube") # doctest: +SKIP 

284 

285 Writing a 3D *Cinespace* *.csp* *LUT*: 

286 

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

288 >>> LUT = LUT3D( 

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

290 ... "My LUT", 

291 ... domain, 

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

293 ... ) 

294 >>> write_LUT_Cinespace(LUT, "My_LUT.cube") # doctest: +SKIP 

295 """ 

296 

297 path = str(path) 

298 

299 has_3D, has_3x1D = False, False 

300 

301 if isinstance(LUT, LUTSequence): 

302 attest( 

303 len(LUT) == 2 

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

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

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

307 ) 

308 LUT[0] = LUT[0].convert(LUT3x1D) if isinstance(LUT[0], LUT1D) else LUT[0] 

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

310 has_3x1D = True 

311 has_3D = True 

312 

313 elif isinstance(LUT, LUT1D): 

314 name = LUT.name 

315 has_3x1D = True 

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

317 

318 elif isinstance(LUT, LUT3x1D): 

319 name = LUT.name 

320 has_3x1D = True 

321 LUT = LUTSequence(LUT, LUT3D()) 

322 

323 elif isinstance(LUT, LUT3D): 

324 name = LUT.name 

325 has_3D = True 

326 LUT = LUTSequence(LUT3x1D(), LUT) 

327 

328 else: 

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

330 

331 raise TypeError(error) 

332 

333 if has_3x1D: 

334 attest( 

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

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

337 ) 

338 if has_3D: 

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

340 

341 def _ragged_size(table: ArrayLike) -> list: 

342 """Return the ragged size of the specified table.""" 

343 

344 R, G, B = tsplit(table) 

345 

346 R_len = R.shape[-1] - np.sum(np.isnan(R)) 

347 G_len = G.shape[-1] - np.sum(np.isnan(G)) 

348 B_len = B.shape[-1] - np.sum(np.isnan(B)) 

349 

350 return [R_len, G_len, B_len] 

351 

352 with open(path, "w") as csp_file: 

353 csp_file.write("CSPLUTV100\n") 

354 

355 if has_3D: 

356 csp_file.write("3D\n\n") 

357 else: 

358 csp_file.write("1D\n\n") 

359 

360 csp_file.write("BEGIN METADATA\n") 

361 csp_file.write(f"{name}\n") 

362 

363 if LUT[0].comments: 

364 csp_file.writelines(f"{comment}\n" for comment in LUT[0].comments) 

365 

366 if LUT[1].comments: 

367 csp_file.writelines(f"{comment}\n" for comment in LUT[1].comments) 

368 

369 csp_file.write("END METADATA\n\n") 

370 

371 if has_3D: 

372 if has_3x1D: 

373 for i in range(3): 

374 size = ( 

375 _ragged_size(LUT[0].domain)[i] 

376 if LUT[0].is_domain_explicit() 

377 else LUT[0].size 

378 ) 

379 

380 csp_file.write(f"{size}\n") 

381 

382 for j in range(size): 

383 entry = ( 

384 LUT[0].domain[j][i] 

385 if LUT[0].is_domain_explicit() 

386 else ( 

387 LUT[0].domain[0][i] 

388 + j 

389 * (LUT[0].domain[1][i] - LUT[0].domain[0][i]) 

390 / (LUT[0].size - 1) 

391 ) 

392 ) 

393 

394 csp_file.write(f"{format_array_as_row(entry, decimals)} ") 

395 

396 csp_file.write("\n") 

397 

398 for j in range(size): 

399 entry = LUT[0].table[j][i] 

400 csp_file.write(f"{format_array_as_row(entry, decimals)} ") 

401 

402 csp_file.write("\n") 

403 else: 

404 for i in range(3): 

405 csp_file.write("2\n") 

406 domain = format_array_as_row( 

407 [LUT[1].domain[0][i], LUT[1].domain[1][i]], decimals 

408 ) 

409 csp_file.write(f"{domain}\n") 

410 csp_file.write(f"{format_array_as_row([0, 1], decimals)}\n") 

411 

412 csp_file.write( 

413 f"\n{LUT[1].table.shape[0]} " 

414 f"{LUT[1].table.shape[1]} " 

415 f"{LUT[1].table.shape[2]}\n" 

416 ) 

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

418 

419 csp_file.writelines( 

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

421 ) 

422 else: 

423 for i in range(3): 

424 csp_file.write("2\n") 

425 domain = format_array_as_row( 

426 [LUT[0].domain[0][i], LUT[0].domain[1][i]], decimals 

427 ) 

428 csp_file.write(f"{domain}\n") 

429 csp_file.write("0.0 1.0\n") 

430 csp_file.write(f"\n{LUT[0].size}\n") 

431 table = LUT[0].table 

432 

433 csp_file.writelines( 

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

435 ) 

436 

437 return True