Coverage for io/luts/cinespace_csp.py: 91%

1"""

2 Cinespace .csp LUT Format Input / Output Utilities

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

4

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

6 objects:

7

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

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

10

11 References

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

18 from __future__ import annotations

19

20 import typing

21

22 import numpy as np

23

24 if typing.TYPE_CHECKING:

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

26

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

28 from 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

50 def 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

246 def 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