Coverage for io/luts/operator.py: 59%

1"""

2 LUT Operator

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

4

5 Define operator classes for Look-Up Table (LUT) transformations within the

6 colour processing pipeline.

7

8- :class:`colour.io.AbstractLUTSequenceOperator`

9- :class:`colour.LUTOperatorMatrix`

10"""

11

12 from __future__ import annotations

13

14 import typing

15 from abc import ABC, abstractmethod

16

17 import numpy as np

18

19 from colour.algebra import vecmul

20

21 if typing.TYPE_CHECKING:

22 from colour.hints import (

23 Any,

24 ArrayLike,

25 List,

26 NDArrayFloat,

27 Sequence,

28 )

29

30 from colour.utilities import as_float_array, attest, is_iterable, ones, optional, zeros

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 "AbstractLUTSequenceOperator",

41 "LUTOperatorMatrix",

42]

43

44

45 class AbstractLUTSequenceOperator(ABC):

46 """

47 Define the base class for *LUT* sequence operators.

48

49 Provide an abstract base class that establishes the interface for all

50 *LUT* sequence operator implementations. This :class:`ABCMeta` abstract

51 class must be inherited by concrete sub-classes that implement specific

52 operator functionality within *LUT* processing pipelines.

53

54 Parameters

55 ----------

56 name

57 *LUT* sequence operator name.

58 comments

59 Comments to add to the *LUT* sequence operator.

60

61 Attributes

62 ----------

63 - :attr:`~colour.io.AbstractLUTSequenceOperator.name`

64 - :attr:`~colour.io.AbstractLUTSequenceOperator.comments`

65

66 Methods

67 -------

68 - :meth:`~colour.io.AbstractLUTSequenceOperator.apply`

69 """

70

71 def __init__(

72 self,

73 name: str | None = None,

74 comments: Sequence[str] | None = None,

75 ) -> None:

76 self._name = f"LUT Sequence Operator {id(self)}"

77 self.name = optional(name, self._name)

78 self._comments: List[str] = []

79 self.comments = optional(comments, self._comments)

80

81 @property

82 def name(self) -> str:

83 """

84 Getter and setter for the *LUT* name.

85

86 Parameters

87 ----------

88 value

89 Value to set the *LUT* name with.

90

91 Returns

92 -------

93 :class:`str`

94 *LUT* name.

95 """

96

97 return self._name

98

99 @name.setter

100 def name(self, value: str) -> None:

101 """Setter for the **self.name** property."""

102

103 attest(

104 isinstance(value, str),

105 f'"name" property: "{value}" type is not "str"!',

106 )

107

108 self._name = value

109

110 @property

111 def comments(self) -> List[str]:

112 """

113 Getter and setter for the *LUT* comments.

114

115 Parameters

116 ----------

117 value

118 Value to set the *LUT* comments with.

119

120 Returns

121 -------

122 :class:`list`

123 *LUT* comments.

124 """

125

126 return self._comments

127

128 @comments.setter

129 def comments(self, value: Sequence[str]) -> None:

130 """Setter for the **self.comments** property."""

131

132 attest(

133 is_iterable(value),

134 f'"comments" property: "{value}" must be a sequence!',

135 )

136

137 self._comments = list(value)

138

139 @abstractmethod

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

141 """

142 Apply the *LUT* sequence operator to the specified *RGB* colourspace

143 array.

144

145 Parameters

146 ----------

147 RGB

148 *RGB* colourspace array to apply the *LUT* sequence operator onto.

149

150 Other Parameters

151 ----------------

152 args

153 Arguments.

154 kwargs

155 Keywords arguments.

156

157 Returns

158 -------

159 :class:`numpy.ndarray`

160 Processed *RGB* colourspace array.

161 """

162

163

164 class LUTOperatorMatrix(AbstractLUTSequenceOperator):

165 """

166 Define the *LUT* operator that applies matrix transformations and offset

167 vectors for colour space conversions.

168

169 Support 3x3 or 4x4 matrix operations with optional offset vectors to

170 perform affine transformations on *RGB* colourspace data. The operator

171 internally reshapes matrices to 4x4 and offsets to 4-element vectors to

172 maintain computational consistency.

173

174 Parameters

175 ----------

176 matrix

177 3x3 or 4x4 matrix for the operator.

178 offset

179 Offset for the operator.

180 name

181 *LUT* operator name.

182 comments

183 Comments to add to the *LUT* operator.

184

185 Attributes

186 ----------

187 - :meth:`~colour.LUTOperatorMatrix.matrix`

188 - :meth:`~colour.LUTOperatorMatrix.offset`

189

190 Methods

191 -------

192 - :meth:`~colour.LUTOperatorMatrix.__str__`

193 - :meth:`~colour.LUTOperatorMatrix.__repr__`

194 - :meth:`~colour.LUTOperatorMatrix.__eq__`

195 - :meth:`~colour.LUTOperatorMatrix.__ne__`

196 - :meth:`~colour.LUTOperatorMatrix.apply`

197

198 Notes

199 -----

200 - The internal :attr:`colour.io.Matrix.matrix` and

201 :attr:`colour.io.Matrix.offset` properties are reshaped to (4, 4) and

202 (4, ) respectively.

203

204 Examples

205 --------

206 Instantiating an identity matrix:

207

208 >>> print(LUTOperatorMatrix(name="Identity"))

209 LUTOperatorMatrix - Identity

210 ----------------------------

211 <BLANKLINE>

212 Matrix : [[ 1. 0. 0. 0.]

213 [ 0. 1. 0. 0.]

214 [ 0. 0. 1. 0.]

215 [ 0. 0. 0. 1.]]

216 Offset : [ 0. 0. 0. 0.]

217

218 Instantiating a matrix with comments:

219

220 >>> matrix = np.array(

221 ... [

222 ... [1.45143932, -0.23651075, -0.21492857],

223 ... [-0.07655377, 1.1762297, -0.09967593],

224 ... [0.00831615, -0.00603245, 0.9977163],

225 ... ]

226 ... )

227 >>> print(

228 ... LUTOperatorMatrix(

229 ... matrix,

230 ... name="AP0 to AP1",

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

232 ... )

233 ... )

234 LUTOperatorMatrix - AP0 to AP1

235 ------------------------------

236 <BLANKLINE>

237 Matrix : [[ 1.45143932 -0.23651075 -0.21492857 0. ]

238 [-0.07655377 1.1762297 -0.09967593 0. ]

239 [ 0.00831615 -0.00603245 0.9977163 0. ]

240 [ 0. 0. 0. 1. ]]

241 Offset : [ 0. 0. 0. 0.]

242 <BLANKLINE>

243 A first comment.

244 A second comment.

245 """

246

247 def __init__(

248 self,

249 matrix: ArrayLike | None = None,

250 offset: ArrayLike | None = None,

251 *args: Any,

252 **kwargs: Any,

253 ) -> None:

254 super().__init__(*args, **kwargs)

255

256 self._matrix: NDArrayFloat = np.diag(ones(4))

257 self.matrix = optional(matrix, self._matrix)

258 self._offset: NDArrayFloat = zeros(4)

259 self.offset = optional(offset, self._offset)

260

261 @property

262 def matrix(self) -> NDArrayFloat:

263 """

264 Getter and setter for the *LUT* operator matrix.

265

266 Parameters

267 ----------

268 value

269 Value to set the *LUT* operator matrix with.

270

271 Returns

272 -------

273 :class:`numpy.ndarray`

274 Operator matrix.

275 """

276

277 return self._matrix

278

279 @matrix.setter

280 def matrix(self, value: ArrayLike) -> None:

281 """Setter for the **self.matrix** property."""

282

283 value = as_float_array(value)

284

285 shape_t = value.shape[-1]

286

287 value = np.reshape(value, (shape_t, shape_t))

288

289 attest(

290 value.shape in [(3, 3), (4, 4)],

291 f'"matrix" property: "{value}" shape is not (3, 3) or (4, 4)!',

292 )

293

294 M = np.identity(4)

295 M[:shape_t, :shape_t] = value

296

297 self._matrix = M

298

299 @property

300 def offset(self) -> NDArrayFloat:

301 """

302 Getter and setter for the *LUT* operator offset vector.

303

304 The offset vector is applied after the matrix transformation in the LUT

305 operator, enabling translation operations in the colour space.

306

307 Parameters

308 ----------

309 value

310 Value to set the *LUT* operator offset with.

311

312 Returns

313 -------

314 :class:`numpy.ndarray`

315 Operator offset vector.

316 """

317

318 return self._offset

319

320 @offset.setter

321 def offset(self, value: ArrayLike) -> None:

322 """Setter for the **self.offset** property."""

323

324 value = as_float_array(value)

325

326 shape_t = value.shape[-1]

327

328 attest(

329 value.shape in [(3,), (4,)],

330 f'"offset" property: "{value}" shape is not (3, ) or (4, )!',

331 )

332

333 offset = zeros(4)

334 offset[:shape_t] = value

335

336 self._offset = offset

337

338 def __str__(self) -> str:

339 """

340 Return a formatted string representation of the *LUT* operator.

341

342 Returns

343 -------

344 :class:`str`

345 Formatted string representation.

346

347 Examples

348 --------

349 >>> print(LUTOperatorMatrix()) # doctest: +ELLIPSIS

350 LUTOperatorMatrix - LUT Sequence Operator ...

351 ------------------------------------------...

352 <BLANKLINE>

353 Matrix : [[ 1. 0. 0. 0.]

354 [ 0. 1. 0. 0.]

355 [ 0. 0. 1. 0.]

356 [ 0. 0. 0. 1.]]

357 Offset : [ 0. 0. 0. 0.]

358 """

359

360 def _format(a: ArrayLike) -> str:

361 """Format specified array string representation."""

362

363 return str(a).replace(" [", " " * 14 + "[")

364

365 comments = "\n".join(self._comments)

366 comments = f"\n\n{comments}" if self._comments else ""

367

368 underline = "-" * (len(self.__class__.__name__) + 3 + len(self._name))

369

370 return "\n".join(

371 [

372 f"{self.__class__.__name__} - {self._name}",

373 f"{underline}",

374 "",

375 f"Matrix : {_format(self._matrix)}",

376 f"Offset : {_format(self._offset)}{comments}",

377 ]

378 )

379

380 def __repr__(self) -> str:

381 """

382 Return an evaluable string representation of the *LUT* operator.

383

384 Returns

385 -------

386 :class:`str`

387 Evaluable string representation.

388

389 Examples

390 --------

391 >>> LUTOperatorMatrix(comments=["A first comment.", "A second comment."])

392 ... # doctest: +ELLIPSIS

393 LUTOperatorMatrix([[ 1., 0., 0., 0.],

394 [ 0., 1., 0., 0.],

395 [ 0., 0., 1., 0.],

396 [ 0., 0., 0., 1.]],

397 [ 0., 0., 0., 0.],

398 name='LUT Sequence Operator ...',

399 comments=['A first comment.', 'A second comment.'])

400 """

401

402 representation = repr(self._matrix)

403 representation = representation.replace("array", self.__class__.__name__)

404 representation = representation.replace(

405 " [", f"{' ' * (len(self.__class__.__name__) + 2)}["

406 )

407

408 indentation = " " * (len(self.__class__.__name__) + 1)

409

410 comments = (

411 f",\n{indentation}comments={self._comments! r}" if self._comments else ""

412 )

413

414 return "\n".join(

415 [

416 f"{representation[:-1]},",

417 f"{indentation}"

418 f"{repr(self._offset).replace('array(', '').replace(')', '')},",

419 f"{indentation}name='{self._name}'{comments})",

420 ]

421 )

422

423 __hash__ = None # pyright: ignore

424

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

426 """

427 Determine whether the *LUT* operator is equal to the specified other

428 object.

429

430 Parameters

431 ----------

432 other

433 Object to test whether it is equal to the *LUT* operator.

434

435 Returns

436 -------

437 :class:`bool`

438 Whether specified object equal to the *LUT* operator.

439

440 Examples

441 --------

442 >>> LUTOperatorMatrix() == LUTOperatorMatrix()

443 True

444 """

445

446 return isinstance(other, LUTOperatorMatrix) and all(

447 [

448 np.array_equal(self._matrix, other._matrix),

449 np.array_equal(self._offset, other._offset),

450 ]

451 )

452

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

454 """

455 Determine whether the *LUT* operator is not equal to the specified other

456 object.

457

458 Parameters

459 ----------

460 other

461 Object to test whether it is not equal to the *LUT* operator.

462

463 Returns

464 -------

465 :class:`bool`

466 Whether the specified object is not equal to the *LUT* operator.

467

468 Examples

469 --------

470 >>> LUTOperatorMatrix() != LUTOperatorMatrix(

471 ... np.reshape(np.linspace(0, 1, 16), (4, 4))

472 ... )

473 True

474 """

475

476 return not (self == other)

477

478 def apply(

479 self,

480 RGB: ArrayLike,

481 *args: Any, # noqa: ARG002

482 **kwargs: Any,

483 ) -> NDArrayFloat:

484 """

485 Apply the *LUT* operator to the specified *RGB* array.

486

487 Parameters

488 ----------

489 RGB

490 *RGB* array to apply the *LUT* operator transform to.

491

492 Other Parameters

493 ----------------

494 apply_offset_first

495 Whether to apply the offset first and then the matrix.

496

497 Returns

498 -------

499 :class:`numpy.ndarray`

500 Transformed *RGB* array.

501

502 Examples

503 --------

504 >>> matrix = np.array(

505 ... [

506 ... [1.45143932, -0.23651075, -0.21492857],

507 ... [-0.07655377, 1.1762297, -0.09967593],

508 ... [0.00831615, -0.00603245, 0.9977163],

509 ... ]

510 ... )

511 >>> M = LUTOperatorMatrix(matrix)

512 >>> RGB = np.array([0.3, 0.4, 0.5])

513 >>> M.apply(RGB) # doctest: +ELLIPSIS

514 array([ 0.2333632..., 0.3976877..., 0.4989400...])

515 """

516

517 RGB = as_float_array(RGB)

518 apply_offset_first = kwargs.get("apply_offset_first", False)

519

520 has_alpha_channel = RGB.shape[-1] == 4

521 M = self._matrix

522 offset = self._offset

523

524 if not has_alpha_channel:

525 M = M[:3, :3]

526 offset = offset[:3]

527

528 if apply_offset_first:

529 RGB += offset

530

531 RGB = vecmul(M, RGB)

532

533 if not apply_offset_first:

534 RGB += offset

535

536 return RGB