Coverage for io/luts/iridas_cube.py: 81%

1"""

2 Iridas .cube LUT Format Input / Output Utilities

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

4

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

6 objects:

7

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

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

10

11 References

12----------

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

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

15"""

16

17 from __future__ import annotations

18

19 import typing

20

21 import numpy as np

22

23 if typing.TYPE_CHECKING:

24 from colour.hints import PathLike

25

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

27 from colour.io.luts.common import path_to_title

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

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

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