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

1"""

2 LUT Sequence

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

4

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

6 pipelines:

7

8- :class:`colour.LUTSequence`

9"""

10

11 from __future__ import annotations

12

13 import re

14 import typing

15 from collections.abc import MutableSequence

16 from copy import deepcopy

17

18 if typing.TYPE_CHECKING:

19 from colour.hints import (

20 Any,

21 ArrayLike,

22 List,

23 NDArrayFloat,

24 Sequence,

25 )

26

27 from colour.hints import ProtocolLUTSequenceItem

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

42 class 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)