Coverage for io/luts/sequence.py: 51%

65 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2LUT Sequence 

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

4 

5Define the *LUT* sequence container for Look-Up Table (LUT) processing 

6pipelines: 

7 

8- :class:`colour.LUTSequence` 

9""" 

10 

11from __future__ import annotations 

12 

13import re 

14import typing 

15from collections.abc import MutableSequence 

16from copy import deepcopy 

17 

18if typing.TYPE_CHECKING: 

19 from colour.hints import ( 

20 Any, 

21 ArrayLike, 

22 List, 

23 NDArrayFloat, 

24 Sequence, 

25 ) 

26 

27from colour.hints import ProtocolLUTSequenceItem 

28from colour.utilities import as_float_array, attest, is_iterable 

29 

30__author__ = "Colour Developers" 

31__copyright__ = "Copyright 2013 Colour Developers" 

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

33__maintainer__ = "Colour Developers" 

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

35__status__ = "Production" 

36 

37__all__ = [ 

38 "LUTSequence", 

39] 

40 

41 

42class LUTSequence(MutableSequence): 

43 """ 

44 Define the base class for a *LUT* sequence. 

45 

46 A *LUT* sequence represents a series of *LUTs*, *LUT* operators or 

47 objects implementing the :class:`colour.hints.ProtocolLUTSequenceItem` 

48 protocol. 

49 

50 The :class:`colour.LUTSequence` class can be used to model series of 

51 *LUTs* such as when a shaper *LUT* is combined with a 3D *LUT*. 

52 

53 Other Parameters 

54 ---------------- 

55 args 

56 Sequence of objects implementing the 

57 :class:`colour.hints.ProtocolLUTSequenceItem` protocol. 

58 

59 Attributes 

60 ---------- 

61 - :attr:`~colour.LUTSequence.sequence` 

62 

63 Methods 

64 ------- 

65 - :meth:`~colour.LUTSequence.__init__` 

66 - :meth:`~colour.LUTSequence.__getitem__` 

67 - :meth:`~colour.LUTSequence.__setitem__` 

68 - :meth:`~colour.LUTSequence.__delitem__` 

69 - :meth:`~colour.LUTSequence.__len__` 

70 - :meth:`~colour.LUTSequence.__str__` 

71 - :meth:`~colour.LUTSequence.__repr__` 

72 - :meth:`~colour.LUTSequence.__eq__` 

73 - :meth:`~colour.LUTSequence.__ne__` 

74 - :meth:`~colour.LUTSequence.insert` 

75 - :meth:`~colour.LUTSequence.apply` 

76 - :meth:`~colour.LUTSequence.copy` 

77 

78 Examples 

79 -------- 

80 >>> from colour.io.luts import LUT1D, LUT3x1D, LUT3D 

81 >>> LUT_1 = LUT1D() 

82 >>> LUT_2 = LUT3D(size=3) 

83 >>> LUT_3 = LUT3x1D() 

84 >>> print(LUTSequence(LUT_1, LUT_2, LUT_3)) 

85 LUT Sequence 

86 ------------ 

87 <BLANKLINE> 

88 Overview 

89 <BLANKLINE> 

90 LUT1D --> LUT3D --> LUT3x1D 

91 <BLANKLINE> 

92 Operations 

93 <BLANKLINE> 

94 LUT1D - Unity 10 

95 ---------------- 

96 <BLANKLINE> 

97 Dimensions : 1 

98 Domain : [ 0. 1.] 

99 Size : (10,) 

100 <BLANKLINE> 

101 LUT3D - Unity 3 

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

103 <BLANKLINE> 

104 Dimensions : 3 

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

106 [ 1. 1. 1.]] 

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

108 <BLANKLINE> 

109 LUT3x1D - Unity 10 

110 ------------------ 

111 <BLANKLINE> 

112 Dimensions : 2 

113 Domain : [[ 0. 0. 0.] 

114 [ 1. 1. 1.]] 

115 Size : (10, 3) 

116 """ 

117 

118 def __init__(self, *args: ProtocolLUTSequenceItem) -> None: 

119 self._sequence: List[ProtocolLUTSequenceItem] = [] 

120 self.sequence = args 

121 

122 @property 

123 def sequence(self) -> List[ProtocolLUTSequenceItem]: 

124 """ 

125 Getter and setter for the underlying *LUT* sequence. 

126 

127 Access and modify the sequence of lookup table operations that 

128 define the transformation pipeline. 

129 

130 Parameters 

131 ---------- 

132 value 

133 Value to set the underlying *LUT* sequence with. 

134 

135 Returns 

136 ------- 

137 :class:`list` 

138 Underlying *LUT* sequence. 

139 """ 

140 

141 return self._sequence 

142 

143 @sequence.setter 

144 def sequence(self, value: Sequence[ProtocolLUTSequenceItem]) -> None: 

145 """Setter for the **self.sequence** property.""" 

146 

147 for item in value: 

148 attest( 

149 isinstance(item, ProtocolLUTSequenceItem), 

150 '"value" items must implement the "ProtocolLUTSequenceItem" protocol!', 

151 ) 

152 

153 self._sequence = list(value) 

154 

155 def __getitem__(self, index: int | slice) -> Any: 

156 """ 

157 Return *LUT* sequence item(s) at specified index or slice. 

158 

159 Parameters 

160 ---------- 

161 index 

162 Index or slice to return *LUT* sequence item(s) at. 

163 

164 Returns 

165 ------- 

166 ProtocolLUTSequenceItem 

167 *LUT* sequence item(s) at specified index or slice. 

168 """ 

169 

170 return self._sequence[index] 

171 

172 def __setitem__(self, index: int | slice, value: Any) -> None: 

173 """ 

174 Set the *LUT* sequence at the specified index or slice with the 

175 specified value. 

176 

177 Parameters 

178 ---------- 

179 index 

180 Index or slice to set the *LUT* sequence value at. 

181 value 

182 Value to set the *LUT* sequence with. 

183 """ 

184 

185 for item in value if is_iterable(value) else [value]: 

186 attest( 

187 isinstance(item, ProtocolLUTSequenceItem), 

188 '"value" items must implement the "ProtocolLUTSequenceItem" protocol!', 

189 ) 

190 

191 self._sequence[index] = value 

192 

193 def __delitem__(self, index: int | slice) -> None: 

194 """ 

195 Delete the *LUT* sequence item(s) at the specified index (or slice). 

196 

197 Parameters 

198 ---------- 

199 index 

200 Index (or slice) to delete the *LUT* sequence items at. 

201 """ 

202 

203 del self._sequence[index] 

204 

205 def __len__(self) -> int: 

206 """ 

207 Return the *LUT* sequence items count. 

208 

209 Returns 

210 ------- 

211 :class:`int` 

212 *LUT* sequence items count. 

213 """ 

214 

215 return len(self._sequence) 

216 

217 def __str__(self) -> str: 

218 """ 

219 Return a formatted string representation of the *LUT* sequence. 

220 

221 Returns 

222 ------- 

223 :class:`str` 

224 Formatted string representation. 

225 """ 

226 

227 sequence = " --> ".join([a.__class__.__name__ for a in self._sequence]) 

228 

229 operations = re.sub( 

230 "^", 

231 " " * 4, 

232 "\n\n".join([str(a) for a in self._sequence]), 

233 flags=re.MULTILINE, 

234 ) 

235 operations = re.sub("^\\s+$", "", operations, flags=re.MULTILINE) 

236 

237 return "\n".join( 

238 [ 

239 "LUT Sequence", 

240 "------------", 

241 "", 

242 "Overview", 

243 "", 

244 f" {sequence}", 

245 "", 

246 "Operations", 

247 "", 

248 f"{operations}", 

249 ] 

250 ) 

251 

252 def __repr__(self) -> str: 

253 """ 

254 Return an evaluable string representation of the *LUT* sequence. 

255 

256 Generate a string representation that can be evaluated to recreate 

257 the *LUT* sequence with its current state. 

258 

259 Returns 

260 ------- 

261 :class:`str` 

262 Evaluable string representation. 

263 """ 

264 

265 operations = re.sub( 

266 "^", 

267 " " * 4, 

268 ",\n".join([repr(a) for a in self._sequence]), 

269 flags=re.MULTILINE, 

270 ) 

271 operations = re.sub("^\\s+$", "", operations, flags=re.MULTILINE) 

272 

273 return f"{self.__class__.__name__}(\n{operations}\n)" 

274 

275 __hash__ = None # pyright: ignore 

276 

277 def __eq__(self, other: object) -> bool: 

278 """ 

279 Test whether the *LUT* sequence is equal to the specified other object. 

280 

281 Compare this *LUT* sequence with another object for equality. The 

282 comparison evaluates structural and content equivalence. 

283 

284 Parameters 

285 ---------- 

286 other 

287 Object to test whether it is equal to the *LUT* sequence. 

288 

289 Returns 

290 ------- 

291 :class:`bool` 

292 Whether specified object is equal to the *LUT* sequence. 

293 """ 

294 

295 if not isinstance(other, LUTSequence): 

296 return False 

297 

298 if len(self) != len(other): 

299 return False 

300 

301 return all(self[i] == other[i] for i in range(len(self))) 

302 

303 def __ne__(self, other: object) -> bool: 

304 """ 

305 Return whether the *LUT* sequence is not equal to the specified other 

306 object. 

307 

308 Parameters 

309 ---------- 

310 other 

311 Object to test whether it is not equal to the *LUT* sequence. 

312 

313 Returns 

314 ------- 

315 :class:`bool` 

316 Whether the specified object is not equal to the *LUT* sequence. 

317 """ 

318 

319 return not (self == other) 

320 

321 def insert(self, index: int, value: ProtocolLUTSequenceItem) -> None: 

322 """ 

323 Insert the specified *LUT* at the specified index in the *LUT* 

324 sequence. 

325 

326 Parameters 

327 ---------- 

328 index 

329 Index at which to insert the item in the *LUT* sequence. 

330 value 

331 *LUT* to insert into the *LUT* sequence. 

332 """ 

333 

334 attest( 

335 isinstance(value, ProtocolLUTSequenceItem), 

336 '"value" items must implement the "ProtocolLUTSequenceItem" protocol!', 

337 ) 

338 

339 self._sequence.insert(index, value) 

340 

341 def apply(self, RGB: ArrayLike, **kwargs: Any) -> NDArrayFloat: 

342 """ 

343 Apply the *LUT* sequence sequentially to the specified *RGB* colourspace 

344 array. 

345 

346 Parameters 

347 ---------- 

348 RGB 

349 *RGB* colourspace array to apply the *LUT* sequence sequentially 

350 onto. 

351 

352 Other Parameters 

353 ---------------- 

354 kwargs 

355 Keywords arguments. The keys must be the class type names for 

356 which they are intended to be used with. There is no implemented 

357 way to discriminate which class instance the keyword arguments 

358 should be used with, thus if many class instances of the same 

359 type are members of the sequence, any matching keyword arguments 

360 will be used with all the class instances. 

361 

362 Returns 

363 ------- 

364 :class:`numpy.ndarray` 

365 Processed *RGB* colourspace array. 

366 

367 Examples 

368 -------- 

369 >>> import numpy as np 

370 >>> from colour.io.luts import LUT1D, LUT3x1D, LUT3D 

371 >>> from colour.utilities import tstack 

372 >>> LUT_1 = LUT1D(LUT1D.linear_table(16) + 0.125) 

373 >>> LUT_2 = LUT3D(LUT3D.linear_table(16) ** (1 / 2.2)) 

374 >>> LUT_3 = LUT3x1D(LUT3x1D.linear_table(16) * 0.750) 

375 >>> LUT_sequence = LUTSequence(LUT_1, LUT_2, LUT_3) 

376 >>> samples = np.linspace(0, 1, 5) 

377 >>> RGB = tstack([samples, samples, samples]) 

378 >>> LUT_sequence.apply(RGB, LUT1D={"direction": "Inverse"}) 

379 ... # doctest: +ELLIPSIS 

380 array([[ 0. ..., 0. ..., 0. ...], 

381 [ 0.2899886..., 0.2899886..., 0.2899886...], 

382 [ 0.4797662..., 0.4797662..., 0.4797662...], 

383 [ 0.6055328..., 0.6055328..., 0.6055328...], 

384 [ 0.7057779..., 0.7057779..., 0.7057779...]]) 

385 """ 

386 

387 RGB = as_float_array(RGB) 

388 

389 RGB_o = RGB 

390 for operator in self: 

391 RGB_o = operator.apply(RGB_o, **kwargs.get(operator.__class__.__name__, {})) 

392 

393 return RGB_o 

394 

395 def copy(self) -> LUTSequence: 

396 """ 

397 Return a copy of the *LUT* sequence. 

398 

399 Returns 

400 ------- 

401 :class:`colour.LUTSequence` 

402 *LUT* sequence copy. 

403 """ 

404 

405 return deepcopy(self)