Coverage for io/luts/sony_spi3d.py: 75%

1"""

2 Sony .spi3d LUT Format Input / Output Utilities

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

4

5 Define the *Sony* *.spi3d* *LUT* format related input / output utilities

6 objects:

7

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

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

10"""

11

12 from __future__ import annotations

13

14 import typing

15

16 import numpy as np

17

18 if typing.TYPE_CHECKING:

19 from colour.hints import PathLike

20

21 from colour.io.luts import LUT3D, LUTSequence

22 from colour.io.luts.common import path_to_title

23 from colour.utilities import (

24 as_float_array,

25 as_int_array,

26 as_int_scalar,

27 attest,

28 format_array_as_row,

29 usage_warning,

30)

31

32 __author__ = "Colour Developers"

33 __copyright__ = "Copyright 2013 Colour Developers"

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

35 __maintainer__ = "Colour Developers"

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

37 __status__ = "Production"

38

39 __all__ = [

40 "read_LUT_SonySPI3D",

41 "write_LUT_SonySPI3D",

42]

43

44

45 def read_LUT_SonySPI3D(path: str | PathLike) -> LUT3D:

46 """

47 Read the specified *Sony* *.spi3d* *LUT* file.

48

49 Parameters

50 ----------

51 path

52 *LUT* file path.

53

54 Returns

55 -------

56 :class:`colour.LUT3D`

57 :class:`LUT3D` class instance.

58

59 Examples

60 --------

61 Reading an ordered and an unordered 3D *Sony* *.spi3d* *LUT*:

62

63 >>> import os

64 >>> path = os.path.join(

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

66 ... "tests",

67 ... "resources",

68 ... "sony_spi3d",

69 ... "Colour_Correct.spi3d",

70 ... )

71 >>> print(read_LUT_SonySPI3D(path))

72 LUT3D - Colour Correct

73 ----------------------

74 <BLANKLINE>

75 Dimensions : 3

76 Domain : [[ 0. 0. 0.]

77 [ 1. 1. 1.]]

78 Size : (4, 4, 4, 3)

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

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

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

82 ... "tests",

83 ... "resources",

84 ... "sony_spi3d",

85 ... "Colour_Correct_Unordered.spi3d",

86 ... )

87 >>> print(read_LUT_SonySPI3D(path))

88 LUT3D - Colour Correct Unordered

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

90 <BLANKLINE>

91 Dimensions : 3

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

93 [ 1. 1. 1.]]

94 Size : (4, 4, 4, 3)

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

96 """

97

98 path = str(path)

99

100 title = path_to_title(path)

101 domain_min, domain_max = np.array([0, 0, 0]), np.array([1, 1, 1])

102 size: int = 2

103 data_table = []

104 data_indexes = []

105 comments = []

106

107 with open(path) as spi3d_file:

108 lines = filter(None, (line.strip() for line in spi3d_file))

109 for line in lines:

110 if line.startswith("#"):

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

112 continue

113

114 tokens = line.split()

115 if len(tokens) == 3:

116 attest(

117 len(set(tokens)) == 1,

118 'Non-uniform "LUT" shape is unsupported!',

119 )

120

121 size = as_int_scalar(tokens[0])

122 if len(tokens) == 6:

123 data_table.append(as_float_array(tokens[3:]))

124 data_indexes.append(as_int_array(tokens[:3]))

125

126 indexes = as_int_array(data_indexes)

127 sorting_indexes = np.lexsort((indexes[:, 2], indexes[:, 1], indexes[:, 0]))

128

129 attest(

130 np.array_equal(

131 indexes[sorting_indexes],

132 np.reshape(

133 as_int_array(np.around(LUT3D.linear_table(size) * (size - 1))), (-1, 3)

134 ),

135 ),

136 'Indexes do not match expected "LUT3D" indexes!',

137 )

138

139 table = np.reshape(

140 as_float_array(data_table)[sorting_indexes], (size, size, size, 3)

141 )

142

143 return LUT3D(table, title, np.vstack([domain_min, domain_max]), comments=comments)

144

145

146 def write_LUT_SonySPI3D(

147 LUT: LUT3D | LUTSequence, path: str | PathLike, decimals: int = 7

148) -> bool:

149 """

150 Write the specified *LUT* to the specified *Sony* *.spi3d* *LUT* file.

151

152 Parameters

153 ----------

154 LUT

155 :class:`LUT3D` or :class:`LUTSequence` class instance to write at

156 the specified path.

157 path

158 *LUT* file path.

159 decimals

160 Number of decimal places for formatting numeric values.

161

162 Returns

163 -------

164 :class:`bool`

165 Definition success.

166

167 Warnings

168 --------

169 - If a :class:`LUTSequence` class instance is passed as ``LUT``,

170 the first *LUT* in the *LUT* sequence will be used.

171

172 Examples

173 --------

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

175

176 >>> LUT = LUT3D(

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

178 ... "My LUT",

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

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

181 ... )

182 >>> write_LUT_SonySPI3D(LUT, "My_LUT.cube") # doctest: +SKIP

183 """

184

185 path = str(path)

186

187 if isinstance(LUT, LUTSequence):

188 usage_warning(

189 f'"LUT" is a "LUTSequence" instance was passed, using first '

190 f'sequence "LUT":\n{LUT}'

191 )

192 LUTxD = LUT[0]

193 else:

194 LUTxD = LUT

195

196 attest(not LUTxD.is_domain_explicit(), '"LUT" domain must be implicit!')

197

198 attest(isinstance(LUTxD, LUT3D), '"LUT" must be either a 3D "LUT"!')

199

200 attest(

201 np.array_equal(

202 LUTxD.domain,

203 np.array(

204 [

205 [0, 0, 0],

206 [1, 1, 1],

207 ]

208 ),

209 ),

210 '"LUT" domain must be [[0, 0, 0], [1, 1, 1]]!',

211 )

212

213 with open(path, "w") as spi3d_file:

214 spi3d_file.write("SPILUT 1.0\n")

215

216 spi3d_file.write("3 3\n")

217

218 spi3d_file.write(f"{LUTxD.size} {LUTxD.size} {LUTxD.size}\n")

219

220 indexes = np.reshape(

221 as_int_array(np.around(LUTxD.linear_table(LUTxD.size) * (LUTxD.size - 1))),

222 (-1, 3),

223 )

224 table = np.reshape(LUTxD.table, (-1, 3))

225

226 for i, array in enumerate(indexes):

227 spi3d_file.write("{:d} {:d} {:d}".format(*array))

228 spi3d_file.write(f" {format_array_as_row(table[i], decimals)}\n")

229

230 if LUTxD.comments:

231 spi3d_file.writelines(f"# {comment}\n" for comment in LUTxD.comments)

232

233 return True