Coverage for characterisation/aces_it.py: 77%

1"""

2 Academy Color Encoding System - Input Transform

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

4

5 Define the *Academy Color Encoding System* (ACES) *Input Transform* utilities

6 for camera RAW data processing and colour space transformations:

7

8- :func:`colour.sd_to_aces_relative_exposure_values`

9- :func:`colour.sd_to_ACES2065_1`

10- :func:`colour.characterisation.read_training_data_rawtoaces_v1`

11- :func:`colour.characterisation.generate_illuminants_rawtoaces_v1`

12- :func:`colour.characterisation.white_balance_multipliers`

13- :func:`colour.characterisation.best_illuminant`

14- :func:`colour.characterisation.normalise_illuminant`

15- :func:`colour.characterisation.training_data_sds_to_RGB`

16- :func:`colour.characterisation.training_data_sds_to_XYZ`

17- :func:`colour.characterisation.optimisation_factory_rawtoaces_v1`

18- :func:`colour.characterisation.optimisation_factory_Jzazbz`

19- :func:`colour.characterisation.optimisation_factory_Oklab_15`

20- :func:`colour.matrix_idt`

21- :func:`colour.camera_RGB_to_ACES2065_1`

22

23 References

24----------

25- :cite:`Dyer2017` : Dyer, S., Forsythe, A., Irons, J., Mansencal, T., & Zhu,

26 M. (2017). RAW to ACES (Version 1.0) [Computer software].

27- :cite:`Forsythe2018` : Borer, T. (2017). Private Discussion with Mansencal,

28 T. and Shaw, N.

29- :cite:`Finlayson2015` : Finlayson, G. D., MacKiewicz, M., & Hurlbert, A.

30 (2015). Color Correction Using Root-Polynomial Regression. IEEE

31 Transactions on Image Processing, 24(5), 1460-1470.

32 doi:10.1109/TIP.2015.2405336

33- :cite:`TheAcademyofMotionPictureArtsandSciences2014q` : The Academy of

34 Motion Picture Arts and Sciences, Science and Technology Council, & Academy

35 Color Encoding System (ACES) Project Subcommittee. (2014). Technical

36 Bulletin TB-2014-004 - Informative Notes on SMPTE ST 2065-1 - Academy Color

37 Encoding Specification (ACES) (pp. 1-40). Retrieved December 19, 2014, from

38 http://j.mp/TB-2014-004

39- :cite:`TheAcademyofMotionPictureArtsandSciences2014r` : The Academy of

40 Motion Picture Arts and Sciences, Science and Technology Council, & Academy

41 Color Encoding System (ACES) Project Subcommittee. (2014). Technical

42 Bulletin TB-2014-012 - Academy Color Encoding System Version 1.0 Component

43 Names (pp. 1-8). Retrieved December 19, 2014, from http://j.mp/TB-2014-012

44- :cite:`TheAcademyofMotionPictureArtsandSciences2015c` : The Academy of

45 Motion Picture Arts and Sciences, Science and Technology Council, & Academy

46 Color Encoding System (ACES) Project Subcommittee. (2015). Procedure

47 P-2013-001 - Recommended Procedures for the Creation and Use of Digital

48 Camera System Input Device Transforms (IDTs) (pp. 1-29). Retrieved April

49 24, 2015, from http://j.mp/P-2013-001

50- :cite:`TheAcademyofMotionPictureArtsandSciencese` : The Academy of Motion

51 Picture Arts and Sciences, Science and Technology Council, & Academy Color

52 Encoding System (ACES) Project Subcommittee. (n.d.). Academy Color Encoding

53 System. Retrieved February 24, 2014, from

54 http://www.oscars.org/science-technology/council/projects/aces.html

55"""

56

57 from __future__ import annotations

58

59 import os

60 import typing

61

62 import numpy as np

63

64 from colour.adaptation import matrix_chromatic_adaptation_VonKries

65 from colour.algebra import euclidean_distance, vecmul

66 from colour.characterisation import (

67 MSDS_ACES_RICD,

68 RGB_CameraSensitivities,

69 polynomial_expansion_Finlayson2015,

70)

71 from colour.colorimetry import (

72 SDS_ILLUMINANTS,

73 MultiSpectralDistributions,

74 SpectralDistribution,

75 SpectralShape,

76 handle_spectral_arguments,

77 reshape_msds,

78 reshape_sd,

79 sd_blackbody,

80 sd_CIE_illuminant_D_series,

81 sd_to_XYZ,

82 sds_and_msds_to_msds,

83)

84

85 if typing.TYPE_CHECKING:

86 from colour.hints import (

87 Any,

88 ArrayLike,

89 Callable,

90 DTypeFloat,

91 Literal,

92 LiteralChromaticAdaptationTransform,

93 Mapping,

94 NDArrayFloat,

95 Range1,

96 Tuple,

97 )

98

99 from colour.hints import cast

100 from colour.io import read_sds_from_csv_file

101 from colour.models import XYZ_to_Jzazbz, XYZ_to_Lab, XYZ_to_Oklab, XYZ_to_xy, xy_to_XYZ

102 from colour.models.rgb import (

103 RGB_COLOURSPACE_ACES2065_1,

104 RGB_Colourspace,

105 RGB_to_XYZ,

106 XYZ_to_RGB,

107)

108 from colour.temperature import CCT_to_xy_CIE_D

109 from colour.utilities import (

110 CanonicalMapping,

111 as_float,

112 as_float_array,

113 as_float_scalar,

114 from_range_1,

115 optional,

116 required,

117 runtime_warning,

118 tsplit,

119 zeros,

120)

121 from colour.utilities.deprecation import handle_arguments_deprecation

122

123 __author__ = "Colour Developers"

124 __copyright__ = "Copyright 2013 Colour Developers"

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

126 __maintainer__ = "Colour Developers"

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

128 __status__ = "Production"

129

130 __all__ = [

131 "FLARE_PERCENTAGE",

132 "S_FLARE_FACTOR",

133 "sd_to_aces_relative_exposure_values",

134 "sd_to_ACES2065_1",

135 "SPECTRAL_SHAPE_RAWTOACES",

136 "ROOT_RESOURCES_RAWTOACES",

137 "read_training_data_rawtoaces_v1",

138 "generate_illuminants_rawtoaces_v1",

139 "white_balance_multipliers",

140 "best_illuminant",

141 "normalise_illuminant",

142 "training_data_sds_to_RGB",

143 "training_data_sds_to_XYZ",

144 "whitepoint_preserving_matrix",

145 "optimisation_factory_rawtoaces_v1",

146 "optimisation_factory_Jzazbz",

147 "optimisation_factory_Oklab_15",

148 "matrix_idt",

149 "camera_RGB_to_ACES2065_1",

150]

151

152 FLARE_PERCENTAGE: float = 0.00500

153"""Flare percentage in the *ACES* system."""

154

155 S_FLARE_FACTOR: float = 0.18000 / (0.18000 + FLARE_PERCENTAGE)

156"""Flare modulation factor in the *ACES* system."""

157

158

159 def sd_to_aces_relative_exposure_values(

160 sd: SpectralDistribution,

161 illuminant: SpectralDistribution | None = None,

162 chromatic_adaptation_transform: (

163 LiteralChromaticAdaptationTransform | str | None

164 ) = "CAT02",

165 **kwargs: Any,

166) -> Range1:

167 """

168 Convert spectral distribution to *ACES2065-1* colourspace relative

169 exposure values.

170

171 Parameters

172 ----------

173 sd

174 Spectral distribution.

175 illuminant

176 *Illuminant* spectral distribution, default to

177 *CIE Standard Illuminant D65*.

178 chromatic_adaptation_transform

179 *Chromatic adaptation* transform.

180

181 Returns

182 -------

183 :class:`numpy.ndarray`

184 *ACES2065-1* colourspace relative exposure values array.

185

186 Notes

187 -----

188 +------------+-----------------------+---------------+

189 | **Range** | **Scale - Reference** | **Scale - 1** |

190 +============+=======================+===============+

191 | ``XYZ`` | 1 | 1 |

192 +------------+-----------------------+---------------+

193

194 - The chromatic adaptation method implemented here is a bit unusual

195 as it involves building a new colourspace based on *ACES2065-1*

196 colourspace primaries but using the whitepoint of the illuminant

197 that the spectral distribution was measured under.

198

199 References

200 ----------

201 :cite:`Forsythe2018`,

202 :cite:`TheAcademyofMotionPictureArtsandSciences2014q`,

203 :cite:`TheAcademyofMotionPictureArtsandSciences2014r`,

204 :cite:`TheAcademyofMotionPictureArtsandSciencese`

205

206 Examples

207 --------

208 >>> from colour import SDS_COLOURCHECKERS

209 >>> sd = SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"]

210 >>> sd_to_aces_relative_exposure_values(

211 ... sd, chromatic_adaptation_transform=None

212 ... ) # doctest: +ELLIPSIS

213 array([ 0.1171814..., 0.0866360..., 0.0589726...])

214 >>> sd_to_aces_relative_exposure_values(sd, apply_chromatic_adaptation=True)

215 ... # doctest: +ELLIPSIS

216 array([ 0.1180779..., 0.0869031..., 0.0589125...])

217 """

218

219 if isinstance(chromatic_adaptation_transform, bool): # pragma: no cover

220 if chromatic_adaptation_transform is True:

221 chromatic_adaptation_transform = "CAT02"

222 elif chromatic_adaptation_transform is False:

223 chromatic_adaptation_transform = None

224

225 kwargs = {"apply_chromatic_adaptation": True}

226

227 handle_arguments_deprecation(

228 {

229 "ArgumentRemoved": ["apply_chromatic_adaptation"],

230 },

231 **kwargs,

232 )

233

234 illuminant = optional(illuminant, SDS_ILLUMINANTS["D65"])

235

236 shape = MSDS_ACES_RICD.shape

237 if sd.shape != MSDS_ACES_RICD.shape:

238 sd = reshape_sd(sd, shape, copy=False)

239

240 if illuminant.shape != MSDS_ACES_RICD.shape:

241 illuminant = reshape_sd(illuminant, shape, copy=False)

242

243 s_v = sd.values

244 i_v = illuminant.values

245

246 r_bar, g_bar, b_bar = tsplit(MSDS_ACES_RICD.values)

247

248 def k(x: NDArrayFloat, y: NDArrayFloat) -> float:

249 """Compute the :math:`K_r`, :math:`K_g` or :math:`K_b` scale factors."""

250

251 return as_float_scalar(1 / np.sum(x * y))

252

253 k_r = k(i_v, r_bar)

254 k_g = k(i_v, g_bar)

255 k_b = k(i_v, b_bar)

256

257 E_r = k_r * np.sum(i_v * s_v * r_bar)

258 E_g = k_g * np.sum(i_v * s_v * g_bar)

259 E_b = k_b * np.sum(i_v * s_v * b_bar)

260

261 E_rgb = np.array([E_r, E_g, E_b])

262

263 # Accounting for flare.

264 E_rgb += FLARE_PERCENTAGE

265 E_rgb *= S_FLARE_FACTOR

266

267 if chromatic_adaptation_transform is not None:

268 XYZ = RGB_to_XYZ(

269 E_rgb,

270 RGB_Colourspace(

271 "~ACES2065-1",

272 RGB_COLOURSPACE_ACES2065_1.primaries,

273 XYZ_to_xy(sd_to_XYZ(illuminant) / 100),

274 illuminant.name,

275 ),

276 RGB_COLOURSPACE_ACES2065_1.whitepoint,

277 chromatic_adaptation_transform,

278 )

279 E_rgb = XYZ_to_RGB(XYZ, RGB_COLOURSPACE_ACES2065_1)

280

281 return from_range_1(E_rgb)

282

283

284 sd_to_ACES2065_1 = sd_to_aces_relative_exposure_values

285

286 SPECTRAL_SHAPE_RAWTOACES: SpectralShape = SpectralShape(380, 780, 5)

287"""Default spectral shape according to *RAW to ACES* v1."""

288

289 ROOT_RESOURCES_RAWTOACES: str = os.path.join(

290 os.path.dirname(__file__), "datasets", "rawtoaces"

291)

292"""

293*RAW to ACES* resources directory.

294

295 Notes

296-----

297- *Colour* only ships a minimal dataset from *RAW to ACES*, please see

298 `Colour - Datasets <https://github.com/colour-science/colour-datasets>`_

299 for the complete *RAW to ACES* v1 dataset, i.e., *3372171*.

300"""

301

302 _TRAINING_DATA_RAWTOACES_V1: MultiSpectralDistributions | None = None

303

304

305 def read_training_data_rawtoaces_v1() -> MultiSpectralDistributions:

306 """

307 Read the *RAW to ACES* v1 training data comprising 190 reflectance

308 patches.

309

310 Returns

311 -------

312 :class:`colour.MultiSpectralDistributions`

313 *RAW to ACES* v1 190 patches multi-spectral distributions.

314

315 References

316 ----------

317 :cite:`Dyer2017`

318

319 Examples

320 --------

321 >>> len(read_training_data_rawtoaces_v1().labels)

322 190

323 """

324

325 global _TRAINING_DATA_RAWTOACES_V1 # noqa: PLW0603

326

327 if _TRAINING_DATA_RAWTOACES_V1 is not None:

328 training_data = _TRAINING_DATA_RAWTOACES_V1

329 else:

330 path = os.path.join(ROOT_RESOURCES_RAWTOACES, "190_Patches.csv")

331 training_data = sds_and_msds_to_msds(

332 list(read_sds_from_csv_file(path).values())

333 )

334

335 _TRAINING_DATA_RAWTOACES_V1 = training_data

336

337 return training_data

338

339

340 _ILLUMINANTS_RAWTOACES_V1: CanonicalMapping | None = None

341

342

343 def generate_illuminants_rawtoaces_v1() -> CanonicalMapping:

344 """

345 Generate a series of illuminants according to *RAW to ACES* v1:

346

347 - *CIE Illuminant D Series* in range [4000, 25000] kelvin degrees.

348 - *Blackbodies* in range [1000, 3500] kelvin degrees.

349 - A.M.P.A.S. variant of *ISO 7589 Studio Tungsten*.

350

351 Returns

352 -------

353 :class:`colour.utilities.CanonicalMapping`

354 Series of illuminants.

355

356 Notes

357 -----

358 - This definition introduces a few differences compared to

359 *RAW to ACES* v1: *CIE Illuminant D Series* are computed in range

360 [4002.15, 7003.77] kelvin degrees and the :math:`C_2` change is not

361 used in *RAW to ACES* v1.

362

363 References

364 ----------

365 :cite:`Dyer2017`

366

367 Examples

368 --------

369 >>> list(sorted(generate_illuminants_rawtoaces_v1().keys()))

370 ['1000K Blackbody', '1500K Blackbody', '2000K Blackbody', \

371'2500K Blackbody', '3000K Blackbody', '3500K Blackbody', 'D100', 'D105', \

372'D110', 'D115', 'D120', 'D125', 'D130', 'D135', 'D140', 'D145', 'D150', \

373'D155', 'D160', 'D165', 'D170', 'D175', 'D180', 'D185', 'D190', 'D195', \

374'D200', 'D205', 'D210', 'D215', 'D220', 'D225', 'D230', 'D235', 'D240', \

375'D245', 'D250', 'D40', 'D45', 'D50', 'D55', 'D60', 'D65', 'D70', 'D75', \

376'D80', 'D85', 'D90', 'D95', 'iso7589']

377 """

378

379 global _ILLUMINANTS_RAWTOACES_V1 # noqa: PLW0603

380

381 if _ILLUMINANTS_RAWTOACES_V1 is not None:

382 illuminants = _ILLUMINANTS_RAWTOACES_V1

383 else:

384 illuminants = CanonicalMapping()

385

386 # CIE Illuminants D Series from 4000K to 25000K.

387 for i in np.arange(4000, 25000 + 500, 500):

388 CCT = i * 1.4388 / 1.4380

389 xy = CCT_to_xy_CIE_D(CCT)

390 sd = sd_CIE_illuminant_D_series(xy)

391 sd.name = f"D{int(CCT / 100):d}"

392 illuminants[sd.name] = sd.align(SPECTRAL_SHAPE_RAWTOACES)

393

394 # Blackbody from 1000K to 4000K.

395 for i in np.arange(1000, 4000, 500):

396 sd = sd_blackbody(cast("float", i), SPECTRAL_SHAPE_RAWTOACES)

397 illuminants[sd.name] = sd

398

399 # A.M.P.A.S. variant of ISO 7589 Studio Tungsten.

400 sd = read_sds_from_csv_file(

401 os.path.join(ROOT_RESOURCES_RAWTOACES, "AMPAS_ISO_7589_Tungsten.csv")

402 )["iso7589"]

403 illuminants.update({sd.name: sd})

404

405 _ILLUMINANTS_RAWTOACES_V1 = illuminants

406

407 return illuminants

408

409

410 def white_balance_multipliers(

411 sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution

412) -> NDArrayFloat:

413 """

414 Compute *RGB* white balance multipliers for camera *RGB* spectral

415 sensitivities and the specified illuminant spectral distribution.

416

417 Parameters

418 ----------

419 sensitivities

420 Camera *RGB* spectral sensitivities.

421 illuminant

422 Illuminant spectral distribution.

423

424 Returns

425 -------

426 :class:`numpy.ndarray`

427 *RGB* white balance multipliers.

428

429 References

430 ----------

431 :cite:`Dyer2017`

432

433 Examples

434 --------

435 >>> path = os.path.join(

436 ... ROOT_RESOURCES_RAWTOACES,

437 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",

438 ... )

439 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())

440 >>> illuminant = SDS_ILLUMINANTS["D55"]

441 >>> white_balance_multipliers(sensitivities, illuminant)

442 ... # doctest: +ELLIPSIS

443 array([ 2.3414154..., 1. , 1.5163375...])

444 """

445

446 shape = sensitivities.shape

447 if illuminant.shape != shape:

448 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')

449 illuminant = reshape_sd(illuminant, shape, copy=False)

450

451 RGB_w = 1 / np.sum(sensitivities.values * illuminant.values[..., None], axis=0)

452 RGB_w *= 1 / np.min(RGB_w)

453

454 return RGB_w

455

456

457 def best_illuminant(

458 RGB_w: ArrayLike,

459 sensitivities: RGB_CameraSensitivities,

460 illuminants: Mapping,

461) -> SpectralDistribution:

462 """

463 Select the best illuminant for the specified *RGB* white balance

464 multipliers from a series of candidate illuminants based on camera

465 sensitivities.

466

467 The best illuminant is determined by finding the illuminant that produces

468 white balance multipliers closest to the specified values, minimizing the

469 sum of squared errors after normalization.

470

471 Parameters

472 ----------

473 RGB_w

474 *RGB* white balance multipliers.

475 sensitivities

476 Camera *RGB* spectral sensitivities.

477 illuminants

478 Illuminant spectral distributions to choose the best illuminant from.

479

480 Returns

481 -------

482 :class:`colour.SpectralDistribution`

483 Best illuminant spectral distribution.

484

485 Examples

486 --------

487 >>> path = os.path.join(

488 ... ROOT_RESOURCES_RAWTOACES,

489 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",

490 ... )

491 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())

492 >>> illuminants = generate_illuminants_rawtoaces_v1()

493 >>> RGB_w = white_balance_multipliers(sensitivities, SDS_ILLUMINANTS["FL2"])

494 >>> best_illuminant(RGB_w, sensitivities, illuminants).name

495 'D40'

496 """

497

498 RGB_w = as_float_array(RGB_w)

499

500 sse = np.inf

501 illuminant_b = None

502 for illuminant in illuminants.values():

503 RGB_wi = white_balance_multipliers(sensitivities, illuminant)

504 sse_c = np.sum((RGB_wi / RGB_w - 1) ** 2)

505 if sse_c < sse:

506 sse = sse_c

507 illuminant_b = illuminant

508

509 return cast("SpectralDistribution", illuminant_b)

510

511

512 def normalise_illuminant(

513 illuminant: SpectralDistribution, sensitivities: RGB_CameraSensitivities

514) -> SpectralDistribution:

515 """

516 Normalise the specified illuminant with camera *RGB* spectral

517 sensitivities.

518

519 The multiplicative inverse scaling factor :math:`k` is computed by

520 multiplying the illuminant by the sensitivities channel with the maximum

521 value.

522

523 Parameters

524 ----------

525 illuminant

526 Illuminant spectral distribution.

527 sensitivities

528 Camera *RGB* spectral sensitivities.

529

530 Returns

531 -------

532 :class:`colour.SpectralDistribution`

533 Normalised illuminant.

534

535 Examples

536 --------

537 >>> path = os.path.join(

538 ... ROOT_RESOURCES_RAWTOACES,

539 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",

540 ... )

541 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())

542 >>> illuminant = SDS_ILLUMINANTS["D55"]

543 >>> np.sum(illuminant.values) # doctest: +ELLIPSIS

544 7276.1490000...

545 >>> np.sum(normalise_illuminant(illuminant, sensitivities).values)

546 ... # doctest: +ELLIPSIS

547 3.4390373...

548 """

549

550 shape = sensitivities.shape

551 if illuminant.shape != shape:

552 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')

553 illuminant = reshape_sd(illuminant, shape)

554

555 c_i = np.argmax(np.max(sensitivities.values, axis=0))

556 k = 1 / np.sum(illuminant.values * sensitivities.values[..., c_i])

557

558 return illuminant * k

559

560

561 def training_data_sds_to_RGB(

562 training_data: MultiSpectralDistributions,

563 sensitivities: RGB_CameraSensitivities,

564 illuminant: SpectralDistribution,

565) -> Tuple[NDArrayFloat, NDArrayFloat]:

566 """

567 Convert training data to *RGB* tristimulus values using the specified

568 illuminant and camera *RGB* spectral sensitivities.

569

570 Parameters

571 ----------

572 training_data

573 Training data multi-spectral distributions.

574 sensitivities

575 Camera *RGB* spectral sensitivities.

576 illuminant

577 Illuminant spectral distribution.

578

579 Returns

580 -------

581 :class:`tuple`

582 Tuple of training data *RGB* tristimulus values and white balance

583 multipliers.

584

585 Examples

586 --------

587 >>> path = os.path.join(

588 ... ROOT_RESOURCES_RAWTOACES,

589 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",

590 ... )

591 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())

592 >>> illuminant = normalise_illuminant(SDS_ILLUMINANTS["D55"], sensitivities)

593 >>> training_data = read_training_data_rawtoaces_v1()

594 >>> RGB, RGB_w = training_data_sds_to_RGB(training_data, sensitivities, illuminant)

595 >>> RGB[:5] # doctest: +ELLIPSIS

596 array([[ 0.0207582..., 0.0196857..., 0.0213935...],

597 [ 0.0895775..., 0.0891922..., 0.0891091...],

598 [ 0.7810230..., 0.7801938..., 0.7764302...],

599 [ 0.1995 ..., 0.1995 ..., 0.1995 ...],

600 [ 0.5898478..., 0.5904015..., 0.5851076...]])

601 >>> RGB_w # doctest: +ELLIPSIS

602 array([ 2.3414154..., 1. , 1.5163375...])

603 """

604

605 shape = sensitivities.shape

606 if illuminant.shape != shape:

607 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')

608 illuminant = reshape_sd(illuminant, shape, copy=False)

609

610 if training_data.shape != shape:

611 runtime_warning(

612 f'Aligning "{training_data.name}" training data shape to "{shape}".'

613 )

614 training_data = reshape_msds(training_data, shape, copy=False)

615

616 RGB_w = white_balance_multipliers(sensitivities, illuminant)

617

618 RGB = np.dot(

619 np.transpose(illuminant.values[..., None] * training_data.values),

620 sensitivities.values,

621 )

622

623 RGB *= RGB_w

624

625 return RGB, RGB_w

626

627

628 def training_data_sds_to_XYZ(

629 training_data: MultiSpectralDistributions,

630 cmfs: MultiSpectralDistributions,

631 illuminant: SpectralDistribution,

632 chromatic_adaptation_transform: (

633 LiteralChromaticAdaptationTransform | str | None

634 ) = "CAT02",

635) -> Range1:

636 """

637 Convert training data to *CIE XYZ* tristimulus values using the specified

638 illuminant and standard observer colour matching functions.

639

640 Parameters

641 ----------

642 training_data

643 Training data multi-spectral distributions.

644 cmfs

645 Standard observer colour matching functions.

646 illuminant

647 Illuminant spectral distribution.

648 chromatic_adaptation_transform

649 *Chromatic adaptation* transform, if *None* no chromatic adaptation

650 is performed.

651

652 Returns

653 -------

654 :class:`numpy.ndarray`

655 Training data *CIE XYZ* tristimulus values.

656

657 Notes

658 -----

659 +------------+-----------------------+---------------+

660 | **Range** | **Scale - Reference** | **Scale - 1** |

661 +============+=======================+===============+

662 | ``XYZ`` | 1 | 1 |

663 +------------+-----------------------+---------------+

664

665 Examples

666 --------

667 >>> from colour import MSDS_CMFS

668 >>> path = os.path.join(

669 ... ROOT_RESOURCES_RAWTOACES,

670 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",

671 ... )

672 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]

673 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())

674 >>> illuminant = normalise_illuminant(SDS_ILLUMINANTS["D55"], sensitivities)

675 >>> training_data = read_training_data_rawtoaces_v1()

676 >>> training_data_sds_to_XYZ(training_data, cmfs, illuminant)[:5]

677 ... # doctest: +ELLIPSIS

678 array([[ 0.0174353..., 0.0179504..., 0.0196109...],

679 [ 0.0855607..., 0.0895735..., 0.0901703...],

680 [ 0.7455880..., 0.7817549..., 0.7834356...],

681 [ 0.1900528..., 0.1995 ..., 0.2012606...],

682 [ 0.5626319..., 0.5914544..., 0.5894500...]])

683 """

684

685 shape = cmfs.shape

686 if illuminant.shape != shape:

687 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')

688 illuminant = reshape_sd(illuminant, shape, copy=False)

689

690 if training_data.shape != shape:

691 runtime_warning(

692 f'Aligning "{training_data.name}" training data shape to "{shape}".'

693 )

694 training_data = reshape_msds(training_data, shape, copy=False)

695

696 XYZ = np.dot(

697 np.transpose(illuminant.values[..., None] * training_data.values),

698 cmfs.values,

699 )

700

701 XYZ *= 1 / np.sum(cmfs.values[..., 1] * illuminant.values)

702

703 XYZ_w = np.dot(np.transpose(cmfs.values), illuminant.values)

704 XYZ_w *= 1 / XYZ_w[1]

705

706 if chromatic_adaptation_transform is not None:

707 M_CAT = matrix_chromatic_adaptation_VonKries(

708 XYZ_w,

709 xy_to_XYZ(RGB_COLOURSPACE_ACES2065_1.whitepoint),

710 chromatic_adaptation_transform,

711 )

712

713 XYZ = vecmul(M_CAT, XYZ)

714

715 return XYZ

716

717

718 def whitepoint_preserving_matrix(

719 M: ArrayLike, RGB_w: ArrayLike = (1, 1, 1)

720) -> NDArrayFloat:

721 """

722 Normalise the specified matrix :math:`M` to preserve the white point

723 :math:`RGB_w`.

724

725 Parameters

726 ----------

727 M

728 Matrix :math:`M` to normalise.

729 RGB_w

730 White point :math:`RGB_w` to normalise the matrix :math:`M` with.

731

732 Returns

733 -------

734 :class:`numpy.ndarray`

735 Normalised matrix :math:`M`.

736

737 Examples

738 --------

739 >>> M = np.reshape(np.arange(9), (3, 3))

740 >>> whitepoint_preserving_matrix(M)

741 array([[ 0., 1., 0.],

742 [ 3., 4., -6.],

743 [ 6., 7., -12.]])

744 """

745

746 M = as_float_array(M)

747 RGB_w = as_float_array(RGB_w)

748

749 M[..., -1] = RGB_w - np.sum(M[..., :-1], axis=-1)

750

751 return M

752

753

754 def optimisation_factory_rawtoaces_v1() -> Tuple[

755 NDArrayFloat, Callable, Callable, Callable

756]:

757 """

758 Generate the objective function and *CIE XYZ* colourspace to optimisation

759 colourspace/colour model function based according to *RAW to ACES* v1.

760

761 The objective function computes the Euclidean distance between the

762 training data *RGB* tristimulus values and the training data *CIE XYZ*

763 tristimulus values in the *CIE L\\*a\\*b\\** colourspace.

764

765 Implement whitepoint preservation as an optimisation constraint.

766

767 Returns

768 -------

769 :class:`tuple`

770 :math:`x_0` initial values, objective function, *CIE XYZ* colourspace

771 to *CIE L\\*a\\*b\\** colourspace function and finaliser function.

772

773 Examples

774 --------

775 >>> optimisation_factory_rawtoaces_v1() # doctest: +SKIP

776 (array([ 1., 0., 0., 1., 0., 0.]), \

777<function optimisation_factory_rawtoaces_v1.<locals> \

778.objective_function at 0x...>, \

779<function optimisation_factory_rawtoaces_v1.<locals>\

780.XYZ_to_optimization_colour_model at 0x...>, \

781<function optimisation_factory_rawtoaces_v1.<locals>\

782.finaliser_function at 0x...>)

783 """

784

785 x_0 = as_float_array([1, 0, 0, 1, 0, 0])

786

787 def objective_function(

788 M: NDArrayFloat, RGB: NDArrayFloat, Lab: NDArrayFloat

789 ) -> DTypeFloat:

790 """Objective function according to *RAW to ACES* v1."""

791

792 M = finaliser_function(M)

793

794 XYZ_t = vecmul(RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, vecmul(M, RGB))

795 Lab_t = XYZ_to_optimization_colour_model(XYZ_t)

796

797 return as_float(np.linalg.norm(Lab_t - Lab))

798

799 def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat:

800 """*CIE XYZ* colourspace to *CIE L\\*a\\*b\\** colourspace function."""

801

802 return XYZ_to_Lab(XYZ, RGB_COLOURSPACE_ACES2065_1.whitepoint)

803

804 def finaliser_function(M: ArrayLike) -> NDArrayFloat:

805 """Finaliser function."""

806

807 return whitepoint_preserving_matrix(

808 np.hstack([np.reshape(M, (3, 2)), zeros((3, 1))])

809 )

810

811 return (

812 x_0,

813 objective_function,

814 XYZ_to_optimization_colour_model,

815 finaliser_function,

816 )

817

818

819 def optimisation_factory_Jzazbz() -> Tuple[NDArrayFloat, Callable, Callable, Callable]:

820 """

821 Generate the objective function and *CIE XYZ* colourspace to optimisation

822 colourspace/colour model function based on the :math:`J_za_zb_z`

823 colourspace.

824

825 The objective function computes the Euclidean distance between the

826 training data *RGB* tristimulus values and the training data *CIE XYZ*

827 tristimulus values in the :math:`J_za_zb_z` colourspace.

828

829 Implement whitepoint preservation as a post-optimisation step.

830

831 Returns

832 -------

833 :class:`tuple`

834 :math:`x_0` initial values, objective function, *CIE XYZ* colourspace

835 to :math:`J_za_zb_z` colourspace function and finaliser function.

836

837 Examples

838 --------

839 >>> optimisation_factory_Jzazbz() # doctest: +SKIP

840 (array([ 1., 0., 0., 1., 0., 0.]), \

841<function optimisation_factory_Jzazbz.<locals>\

842.objective_function at 0x...>, \

843<function optimisation_factory_Jzazbz.<locals>\

844.XYZ_to_optimization_colour_model at 0x...>, \

845<function optimisation_factory_Jzazbz.<locals>.\

846 finaliser_function at 0x...>)

847 """

848

849 x_0 = as_float_array([1, 0, 0, 1, 0, 0])

850

851 def objective_function(M: ArrayLike, RGB: ArrayLike, Jab: ArrayLike) -> DTypeFloat:

852 """:math:`J_za_zb_z` colourspace based objective function."""

853

854 M = finaliser_function(M)

855

856 XYZ_t = vecmul(RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, vecmul(M, RGB))

857 Jab_t = XYZ_to_optimization_colour_model(XYZ_t)

858

859 return as_float(np.sum(euclidean_distance(Jab, Jab_t)))

860

861 def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat:

862 """*CIE XYZ* colourspace to :math:`J_za_zb_z` colourspace function."""

863

864 return XYZ_to_Jzazbz(XYZ)

865

866 def finaliser_function(M: ArrayLike) -> NDArrayFloat:

867 """Finaliser function."""

868

869 return whitepoint_preserving_matrix(

870 np.hstack([np.reshape(M, (3, 2)), zeros((3, 1))])

871 )

872

873 return (

874 x_0,

875 objective_function,

876 XYZ_to_optimization_colour_model,

877 finaliser_function,

878 )

879

880

881 def optimisation_factory_Oklab_15() -> Tuple[

882 NDArrayFloat, Callable, Callable, Callable

883]:

884 """

885 Generate the objective function and *CIE XYZ* colourspace to optimisation

886 colourspace/colour model function based on the *Oklab* colourspace.

887

888 The objective function computes the Euclidean distance between the

889 training data *RGB* tristimulus values and the training data *CIE XYZ*

890 tristimulus values in the *Oklab* colourspace.

891

892 Implement support for *Finlayson et al. (2015)* root-polynomials of

893 degree 2 and produce 15 terms.

894

895 Returns

896 -------

897 :class:`tuple`

898 :math:`x_0` initial values, objective function, *CIE XYZ* colourspace

899 to *Oklab* colourspace function and finaliser function.

900

901 References

902 ----------

903 :cite:`Finlayson2015`

904

905 Examples

906 --------

907 >>> optimisation_factory_Oklab_15() # doctest: +SKIP

908 (array([ 1., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., \

909 0., 1.]), \

910<function optimisation_factory_Oklab_15.<locals>\

911.objective_function at 0x...>, \

912<function optimisation_factory_Oklab_15.<locals>\

913.XYZ_to_optimization_colour_model at 0x...>, \

914<function optimisation_factory_Oklab_15.<locals>.\

915 finaliser_function at 0x...>)

916 """

917

918 x_0 = as_float_array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1])

919

920 def objective_function(M: ArrayLike, RGB: ArrayLike, Jab: ArrayLike) -> DTypeFloat:

921 """*Oklab* colourspace based objective function."""

922

923 M = finaliser_function(M)

924

925 XYZ_t = np.transpose(

926 np.dot(

927 RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ,

928 np.dot(

929 M,

930 np.transpose(polynomial_expansion_Finlayson2015(RGB, 2, True)),

931 ),

932 )

933 )

934

935 Jab_t = XYZ_to_optimization_colour_model(XYZ_t)

936

937 return as_float(np.sum(euclidean_distance(Jab, Jab_t)))

938

939 def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat:

940 """*CIE XYZ* colourspace to *Oklab* colourspace function."""

941

942 return XYZ_to_Oklab(XYZ)

943

944 def finaliser_function(M: ArrayLike) -> NDArrayFloat:

945 """Finaliser function."""

946

947 return whitepoint_preserving_matrix(

948 np.hstack([np.reshape(M, (3, 5)), zeros((3, 1))])

949 )

950

951 return (

952 x_0,

953 objective_function,

954 XYZ_to_optimization_colour_model,

955 finaliser_function,

956 )

957

958

959@typing.overload

960 def matrix_idt(

961 sensitivities: RGB_CameraSensitivities,

962 illuminant: SpectralDistribution,

963 training_data: MultiSpectralDistributions | None = ...,

964 cmfs: MultiSpectralDistributions | None = ...,

965 optimisation_factory: Callable = ...,

966 optimisation_kwargs: dict | None = ...,

967 chromatic_adaptation_transform: (

968 LiteralChromaticAdaptationTransform | str | None

969 ) = ...,

970 additional_data: Literal[True] = True,

971) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat]: ...

972

973

974@typing.overload

975 def matrix_idt(

976 sensitivities: ...,

977 illuminant: ...,

978 training_data: MultiSpectralDistributions | None = ...,

979 cmfs: MultiSpectralDistributions | None = ...,

980 optimisation_factory: Callable = ...,

981 optimisation_kwargs: dict | None = ...,

982 chromatic_adaptation_transform: (

983 LiteralChromaticAdaptationTransform | str | None

984 ) = ...,

985 *,

986 additional_data: Literal[False],

987) -> Tuple[NDArrayFloat, NDArrayFloat]: ...

988

989

990@typing.overload

991 def matrix_idt(

992 sensitivities: RGB_CameraSensitivities,

993 illuminant: SpectralDistribution,

994 training_data: MultiSpectralDistributions | None,

995 cmfs: MultiSpectralDistributions | None,

996 optimisation_factory: Callable,

997 optimisation_kwargs: dict | None,

998 chromatic_adaptation_transform: (LiteralChromaticAdaptationTransform | str | None),

999 additional_data: Literal[False],

1000) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat]: ...

1001

1002

1003@required("SciPy")

1004 def matrix_idt(

1005 sensitivities: RGB_CameraSensitivities,

1006 illuminant: SpectralDistribution,

1007 training_data: MultiSpectralDistributions | None = None,

1008 cmfs: MultiSpectralDistributions | None = None,

1009 optimisation_factory: Callable = optimisation_factory_rawtoaces_v1,

1010 optimisation_kwargs: dict | None = None,

1011 chromatic_adaptation_transform: (

1012 LiteralChromaticAdaptationTransform | str | None

1013 ) = "CAT02",

1014 additional_data: bool = False,

1015) -> (

1016 Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat]

1017 | Tuple[NDArrayFloat, NDArrayFloat]

1018):

1019 """

1020 Compute an *Input Device Transform* (IDT) matrix for camera *RGB*

1021 spectral sensitivities, illuminant, training data, standard observer

1022 colour matching functions and optimisation settings according to

1023 *RAW to ACES* v1 and *P-2013-001* procedures.

1024

1025 Parameters

1026 ----------

1027 sensitivities

1028 Camera *RGB* spectral sensitivities.

1029 illuminant

1030 Illuminant spectral distribution.

1031 training_data

1032 Training data multi-spectral distributions, defaults to using the

1033 *RAW to ACES* v1 190 patches.

1034 cmfs

1035 Standard observer colour matching functions, default to the

1036 *CIE 1931 2 Degree Standard Observer*.

1037 optimisation_factory

1038 Callable producing the objective function and the *CIE XYZ* to

1039 optimisation colour model function.

1040 optimisation_kwargs

1041 Parameters for :func:`scipy.optimize.minimize` definition.

1042 chromatic_adaptation_transform

1043 *Chromatic adaptation* transform, if *None* no chromatic adaptation

1044 is performed.

1045 additional_data

1046 If *True*, the *XYZ* and *RGB* tristimulus values are also

1047 returned.

1048

1049 Returns

1050 -------

1051 :class:`tuple`

1052 Tuple of IDT matrix and white balance multipliers or tuple of IDT

1053 matrix, white balance multipliers, *XYZ* and *RGB* tristimulus

1054 values.

1055

1056 References

1057 ----------

1058 :cite:`Dyer2017`, :cite:`TheAcademyofMotionPictureArtsandSciences2015c`

1059

1060 Examples

1061 --------

1062 Computing the IDT matrix for a *CANON EOS 5DMark II* and

1063 *CIE Illuminant D Series* *D55* using the method specified in *RAW to ACES* v1:

1064

1065 >>> path = os.path.join(

1066 ... ROOT_RESOURCES_RAWTOACES,

1067 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",

1068 ... )

1069 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())

1070 >>> illuminant = SDS_ILLUMINANTS["D55"]

1071 >>> M, RGB_w = matrix_idt(sensitivities, illuminant)

1072 >>> np.around(M, 3)

1073 array([[ 0.865, -0.026, 0.161],

1074 [ 0.057, 1.123, -0.18 ],

1075 [ 0.024, -0.203, 1.179]])

1076 >>> RGB_w # doctest: +ELLIPSIS

1077 array([ 2.3414154..., 1. , 1.5163375...])

1078

1079 The *RAW to ACES* v1 matrix for the same camera and optimized by

1080 `Ceres Solver <http://ceres-solver.org>`__ is as follows::

1081

1082 0.864994 -0.026302 0.161308

1083 0.056527 1.122997 -0.179524

1084 0.023683 -0.202547 1.178864

1085

1086 >>> M, RGB_w = matrix_idt(

1087 ... sensitivities,

1088 ... illuminant,

1089 ... optimisation_factory=optimisation_factory_Jzazbz,

1090 ... )

1091 >>> np.around(M, 3)

1092 array([[ 0.852, -0.009, 0.158],

1093 [ 0.054, 1.122, -0.176],

1094 [ 0.023, -0.224, 1.2 ]])

1095 >>> RGB_w # doctest: +ELLIPSIS

1096 array([ 2.3414154..., 1. , 1.5163375...])

1097

1098 >>> M, RGB_w = matrix_idt(

1099 ... sensitivities,

1100 ... illuminant,

1101 ... optimisation_factory=optimisation_factory_Oklab_15,

1102 ... )

1103 >>> np.around(M, 3)

1104 array([[ 0.645, -0.611, 0.107, 0.736, 0.398, -0.275],

1105 [-0.159, 0.728, -0.091, 0.651, 0.01 , -0.139],

1106 [-0.172, -0.403, 1.394, 0.51 , -0.295, -0.034]])

1107 >>> RGB_w # doctest: +ELLIPSIS

1108 array([ 2.3414154..., 1. , 1.5163375...])

1109 """

1110

1111 from scipy.optimize import minimize # noqa: PLC0415

1112

1113 training_data = optional(training_data, read_training_data_rawtoaces_v1())

1114

1115 cmfs, illuminant = handle_spectral_arguments(

1116 cmfs, illuminant, shape_default=SPECTRAL_SHAPE_RAWTOACES

1117 )

1118

1119 shape = cmfs.shape

1120 if sensitivities.shape != shape:

1121 runtime_warning(

1122 f'Aligning "{sensitivities.name}" sensitivities shape to "{shape}".'

1123 )

1124 sensitivities = reshape_msds(sensitivities, shape, copy=False)

1125

1126 if training_data.shape != shape:

1127 runtime_warning(

1128 f'Aligning "{training_data.name}" training data shape to "{shape}".'

1129 )

1130 training_data = reshape_msds(training_data, shape, copy=False)

1131

1132 illuminant = normalise_illuminant(illuminant, sensitivities)

1133

1134 RGB, RGB_w = training_data_sds_to_RGB(training_data, sensitivities, illuminant)

1135

1136 XYZ = training_data_sds_to_XYZ(

1137 training_data, cmfs, illuminant, chromatic_adaptation_transform

1138 )

1139

1140 (

1141 x_0,

1142 objective_function,

1143 XYZ_to_optimization_colour_model,

1144 finaliser_function,

1145 ) = optimisation_factory()

1146

1147 optimisation_settings: dict[str, Any] = {

1148 "method": "BFGS",

1149 "jac": "2-point",

1150 }

1151 if optimisation_kwargs is not None:

1152 optimisation_settings.update(optimisation_kwargs)

1153

1154 M = minimize(

1155 objective_function,

1156 x_0,

1157 (RGB, XYZ_to_optimization_colour_model(XYZ)),

1158 **optimisation_settings,

1159 ).x

1160

1161 M = finaliser_function(M)

1162

1163 if additional_data:

1164 return M, RGB_w, XYZ, RGB

1165

1166 return M, RGB_w

1167

1168

1169 def camera_RGB_to_ACES2065_1(

1170 RGB: ArrayLike,

1171 B: ArrayLike,

1172 b: ArrayLike,

1173 k: ArrayLike = (1, 1, 1),

1174 clip: bool = False,

1175) -> NDArrayFloat:

1176 """

1177 Convert camera *RGB* colourspace array to *ACES2065-1* colourspace using

1178 the specified *Input Device Transform* (IDT) matrix :math:`B`, white

1179 balance multipliers :math:`b`, and exposure factor :math:`k` according to

1180 the *P-2013-001* procedure.

1181

1182 Parameters

1183 ----------

1184 RGB

1185 Camera *RGB* colourspace array.

1186 B

1187 *Input Device Transform* (IDT) matrix :math:`B`.

1188 b

1189 White balance multipliers :math:`b`.

1190 k

1191 Exposure factor :math:`k` that results in a nominally "18% gray"

1192 object in the scene producing ACES values [0.18, 0.18, 0.18].

1193 clip

1194 Whether to clip the white balanced camera *RGB* colourspace array

1195 between :math:`-\\infty` and 1. The intent is to keep sensor

1196 saturated values achromatic after white balancing.

1197

1198 Returns

1199 -------

1200 :class:`numpy.ndarray`

1201 *ACES2065-1* colourspace relative exposure values array.

1202

1203 References

1204 ----------

1205 :cite:`TheAcademyofMotionPictureArtsandSciences2015c`

1206

1207 Examples

1208 --------

1209 >>> path = os.path.join(

1210 ... ROOT_RESOURCES_RAWTOACES,

1211 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",

1212 ... )

1213 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())

1214 >>> illuminant = SDS_ILLUMINANTS["D55"]

1215 >>> B, b = matrix_idt(sensitivities, illuminant)

1216 >>> camera_RGB_to_ACES2065_1(np.array([0.1, 0.2, 0.3]), B, b)

1217 ... # doctest: +ELLIPSIS

1218 array([ 0.270644 ..., 0.1561487..., 0.5012965...])

1219 """

1220

1221 RGB = as_float_array(RGB)

1222 B = as_float_array(B)

1223 b = as_float_array(b)

1224 k = as_float_array(k)

1225

1226 RGB_r = b * RGB / np.min(b)

1227

1228 RGB_r = np.clip(RGB_r, -np.inf, 1) if clip else RGB_r

1229

1230 return k * vecmul(B, RGB_r)