Coverage for characterisation/aces_it.py: 77%

211 statements  

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

1""" 

2Academy Color Encoding System - Input Transform 

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

4 

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

6for 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 

23References 

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 

57from __future__ import annotations 

58 

59import os 

60import typing 

61 

62import numpy as np 

63 

64from colour.adaptation import matrix_chromatic_adaptation_VonKries 

65from colour.algebra import euclidean_distance, vecmul 

66from colour.characterisation import ( 

67 MSDS_ACES_RICD, 

68 RGB_CameraSensitivities, 

69 polynomial_expansion_Finlayson2015, 

70) 

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

85if 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 

99from colour.hints import cast 

100from colour.io import read_sds_from_csv_file 

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

102from colour.models.rgb import ( 

103 RGB_COLOURSPACE_ACES2065_1, 

104 RGB_Colourspace, 

105 RGB_to_XYZ, 

106 XYZ_to_RGB, 

107) 

108from colour.temperature import CCT_to_xy_CIE_D 

109from 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) 

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

152FLARE_PERCENTAGE: float = 0.00500 

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

154 

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

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

157 

158 

159def 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 

284sd_to_ACES2065_1 = sd_to_aces_relative_exposure_values 

285 

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

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

288 

289ROOT_RESOURCES_RAWTOACES: str = os.path.join( 

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

291) 

292""" 

293*RAW to ACES* resources directory. 

294 

295Notes 

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 

305def 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 

343def 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 

410def 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 

457def 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 

512def 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 

561def 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 

628def 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 

718def 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 

754def 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 

819def 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>.\ 

846finaliser_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 

881def 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., \ 

9090., 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>.\ 

915finaliser_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 

960def 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 

975def 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 

991def 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") 

1004def 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 

1169def 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)