Coverage for recovery/jiang2013.py: 60%

1"""

2 Jiang et al. (2013) - Camera RGB Sensitivities Recovery

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

4

5 Define the objects for camera *RGB* sensitivities recovery using the

6*Jiang, Liu, Gu and Süsstrunk (2013)* method.

7

8- :func:`colour.recovery.PCA_Jiang2013`

9- :func:`colour.recovery.RGB_to_sd_camera_sensitivity_Jiang2013`

10- :func:`colour.recovery.RGB_to_msds_camera_sensitivities_Jiang2013`

11

12 References

13----------

14- :cite:`Jiang2013` : Jiang, J., Liu, D., Gu, J., & Susstrunk, S. (2013).

15 What is the space of spectral sensitivity functions for digital color

16 cameras? 2013 IEEE Workshop on Applications of Computer Vision (WACV),

17 168-179. doi:10.1109/WACV.2013.6475015

18"""

19

20 from __future__ import annotations

21

22 import typing

23

24 import numpy as np

25

26 from colour.algebra import eigen_decomposition

27 from colour.characterisation import RGB_CameraSensitivities

28 from colour.colorimetry import (

29 MultiSpectralDistributions,

30 SpectralDistribution,

31 SpectralShape,

32 reshape_msds,

33 reshape_sd,

34)

35

36 if typing.TYPE_CHECKING:

37 from colour.hints import (

38 ArrayLike,

39 Domain1,

40 Literal,

41 Mapping,

42 NDArrayFloat,

43 Tuple,

44 )

45

46 from colour.hints import cast

47 from colour.recovery import BASIS_FUNCTIONS_DYER2017

48 from colour.utilities import as_float_array, optional, runtime_warning, tsplit

49

50 __author__ = "Colour Developers"

51 __copyright__ = "Copyright 2013 Colour Developers"

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

53 __maintainer__ = "Colour Developers"

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

55 __status__ = "Production"

56

57 __all__ = [

58 "PCA_Jiang2013",

59 "RGB_to_sd_camera_sensitivity_Jiang2013",

60 "RGB_to_msds_camera_sensitivities_Jiang2013",

61]

62

63

64@typing.overload

65 def PCA_Jiang2013(

66 msds_camera_sensitivities: Mapping[str, MultiSpectralDistributions],

67 eigen_w_v_count: int | None = ...,

68 additional_data: Literal[True] = True,

69) -> Tuple[

70 Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat],

71 Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat],

72]: ...

73

74

75@typing.overload

76 def PCA_Jiang2013(

77 msds_camera_sensitivities: Mapping[str, MultiSpectralDistributions],

78 eigen_w_v_count: int | None = ...,

79 *,

80 additional_data: Literal[False],

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

82

83

84@typing.overload

85 def PCA_Jiang2013(

86 msds_camera_sensitivities: Mapping[str, MultiSpectralDistributions],

87 eigen_w_v_count: int | None,

88 additional_data: Literal[False],

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

90

91

92 def PCA_Jiang2013(

93 msds_camera_sensitivities: Mapping[str, MultiSpectralDistributions],

94 eigen_w_v_count: int | None = None,

95 additional_data: bool = False,

96) -> (

97 Tuple[

98 Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat],

99 Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat],

100 ]

101 | Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]

102):

103 """

104 Perform *Principal Component Analysis* (PCA) on specified camera *RGB*

105 sensitivities.

106

107 Parameters

108 ----------

109 msds_camera_sensitivities

110 Camera *RGB* sensitivities.

111 eigen_w_v_count

112 Eigen-values :math:`w` and eigen-vectors :math:`v` count.

113 additional_data

114 Whether to return both the eigen-values :math:`w` and

115 eigen-vectors :math:`v`.

116

117 Returns

118 -------

119 :class:`tuple`

120 Tuple of camera *RGB* sensitivities eigen-values :math:`w` and

121 eigen-vectors :math:`v` or tuple of camera *RGB* sensitivities

122 eigen-vectors :math:`v`.

123

124 Examples

125 --------

126 >>> from colour.colorimetry import SpectralShape

127 >>> from colour.characterisation import MSDS_CAMERA_SENSITIVITIES

128 >>> shape = SpectralShape(400, 700, 10)

129 >>> camera_sensitivities = {

130 ... camera: msds.copy().align(shape)

131 ... for camera, msds in MSDS_CAMERA_SENSITIVITIES.items()

132 ... }

133 >>> np.array(PCA_Jiang2013(camera_sensitivities)).shape

134 (3, 31, 31)

135 """

136

137 R_sensitivities, G_sensitivities, B_sensitivities = [], [], []

138

139 def normalised_sensitivity(

140 msds: MultiSpectralDistributions, channel: str

141 ) -> NDArrayFloat:

142 """Generate a normalised camera *RGB* sensitivity."""

143

144 sensitivity = cast("SpectralDistribution", msds.signals[channel].copy())

145

146 return sensitivity.normalise().values

147

148 for msds in msds_camera_sensitivities.values():

149 R_sensitivities.append(normalised_sensitivity(msds, msds.labels[0]))

150 G_sensitivities.append(normalised_sensitivity(msds, msds.labels[1]))

151 B_sensitivities.append(normalised_sensitivity(msds, msds.labels[2]))

152

153 R_w_v = eigen_decomposition(

154 np.vstack(R_sensitivities), eigen_w_v_count, covariance_matrix=True

155 )

156 G_w_v = eigen_decomposition(

157 np.vstack(G_sensitivities), eigen_w_v_count, covariance_matrix=True

158 )

159 B_w_v = eigen_decomposition(

160 np.vstack(B_sensitivities), eigen_w_v_count, covariance_matrix=True

161 )

162

163 if additional_data:

164 return (

165 (R_w_v[1], G_w_v[1], B_w_v[1]),

166 (R_w_v[0], G_w_v[0], B_w_v[0]),

167 )

168

169 return R_w_v[1], G_w_v[1], B_w_v[1]

170

171

172 def RGB_to_sd_camera_sensitivity_Jiang2013(

173 RGB: Domain1,

174 illuminant: SpectralDistribution,

175 reflectances: MultiSpectralDistributions,

176 eigen_w: ArrayLike,

177 shape: SpectralShape | None = None,

178) -> SpectralDistribution:

179 """

180 Recover a single camera *RGB* sensitivity for the specified camera *RGB*

181 values using *Jiang et al. (2013)* method.

182

183 Parameters

184 ----------

185 RGB

186 Camera *RGB* values corresponding with ``reflectances``.

187 illuminant

188 Illuminant spectral distribution used to produce the camera *RGB*

189 values.

190 reflectances

191 Reflectance spectral distributions used to produce the camera

192 *RGB* values.

193 eigen_w

194 Eigen-vectors :math:`v` for the particular camera *RGB*

195 sensitivity being recovered.

196 shape

197 Spectral shape of the recovered camera *RGB* sensitivity,

198 ``illuminant`` and ``reflectances`` will be aligned to it if

199 passed, otherwise, ``illuminant`` shape is used.

200

201 Returns

202 -------

203 :class:`colour.RGB_CameraSensitivities`

204 Recovered camera *RGB* sensitivities.

205

206 Notes

207 -----

208 +------------+-----------------------+---------------+

209 | **Domain** | **Scale - Reference** | **Scale - 1** |

210 +============+=======================+===============+

211 | ``RGB`` | 1 | 1 |

212 +------------+-----------------------+---------------+

213

214 Examples

215 --------

216 >>> from colour.colorimetry import (

217 ... SDS_ILLUMINANTS,

218 ... msds_to_XYZ,

219 ... sds_and_msds_to_msds,

220 ... )

221 >>> from colour.characterisation import (

222 ... MSDS_CAMERA_SENSITIVITIES,

223 ... SDS_COLOURCHECKERS,

224 ... )

225 >>> from colour.recovery import SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017

226 >>> illuminant = SDS_ILLUMINANTS["D65"]

227 >>> sensitivities = MSDS_CAMERA_SENSITIVITIES["Nikon 5100 (NPL)"]

228 >>> reflectances = [

229 ... sd.copy().align(SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017)

230 ... for sd in SDS_COLOURCHECKERS["BabelColor Average"].values()

231 ... ]

232 >>> reflectances = sds_and_msds_to_msds(reflectances)

233 >>> R, G, B = (

234 ... tsplit(

235 ... msds_to_XYZ(

236 ... reflectances,

237 ... method="Integration",

238 ... cmfs=sensitivities,

239 ... illuminant=illuminant,

240 ... k=1,

241 ... shape=SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017,

242 ... )

243 ... )

244 ... / 100

245 ... )

246 >>> R_w, G_w, B_w = tsplit(np.moveaxis(BASIS_FUNCTIONS_DYER2017, 0, 1))

247 >>> RGB_to_sd_camera_sensitivity_Jiang2013(

248 ... R,

249 ... illuminant,

250 ... reflectances,

251 ... R_w,

252 ... SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017,

253 ... ) # doctest: +ELLIPSIS

254 SpectralDistribution([[ 4.00000000e+02, 7.2066502...e-06],

255 [ 4.10000000e+02, -8.9698693...e-06],

256 [ 4.20000000e+02, 4.6871961...e-05],

257 [ 4.30000000e+02, 7.7694971...e-05],

258 [ 4.40000000e+02, 6.9335511...e-05],

259 [ 4.50000000e+02, 5.3134947...e-05],

260 [ 4.60000000e+02, 4.4819958...e-05],

261 [ 4.70000000e+02, 4.6393791...e-05],

262 [ 4.80000000e+02, 5.1866668...e-05],

263 [ 4.90000000e+02, 4.3828317...e-05],

264 [ 5.00000000e+02, 4.2001231...e-05],

265 [ 5.10000000e+02, 5.4065544...e-05],

266 [ 5.20000000e+02, 9.6445141...e-05],

267 [ 5.30000000e+02, 1.4277112...e-04],

268 [ 5.40000000e+02, 7.9950718...e-05],

269 [ 5.50000000e+02, 4.6429813...e-05],

270 [ 5.60000000e+02, 5.3423840...e-05],

271 [ 5.70000000e+02, 1.0519383...e-04],

272 [ 5.80000000e+02, 5.2889443...e-04],

273 [ 5.90000000e+02, 9.7851167...e-04],

274 [ 6.00000000e+02, 9.9600382...e-04],

275 [ 6.10000000e+02, 8.3840892...e-04],

276 [ 6.20000000e+02, 6.9180858...e-04],

277 [ 6.30000000e+02, 5.6967854...e-04],

278 [ 6.40000000e+02, 4.2930308...e-04],

279 [ 6.50000000e+02, 3.0241267...e-04],

280 [ 6.60000000e+02, 2.3230047...e-04],

281 [ 6.70000000e+02, 1.3721943...e-04],

282 [ 6.80000000e+02, 4.0944885...e-05],

283 [ 6.90000000e+02, -4.4223475...e-06],

284 [ 7.00000000e+02, -6.1427769...e-06]],

285 SpragueInterpolator,

286 {},

287 Extrapolator,

288 {'method': 'Constant', 'left': None, 'right': None})

289 """

290

291 RGB = as_float_array(RGB)

292 shape = optional(shape, illuminant.shape)

293

294 if illuminant.shape != shape:

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

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

297

298 if reflectances.shape != shape:

299 runtime_warning(

300 f'Aligning "{reflectances.name}" reflectances shape to "{shape}".'

301 )

302 reflectances = reshape_msds(reflectances, shape, copy=False)

303

304 S = np.diag(illuminant.values)

305 R = np.transpose(reflectances.values)

306

307 A = np.dot(np.dot(R, S), eigen_w)

308

309 X = np.linalg.lstsq(A, RGB, rcond=None)[0]

310 X = np.dot(eigen_w, X)

311

312 return SpectralDistribution(X, shape.wavelengths)

313

314

315 def RGB_to_msds_camera_sensitivities_Jiang2013(

316 RGB: Domain1,

317 illuminant: SpectralDistribution,

318 reflectances: MultiSpectralDistributions,

319 basis_functions: ArrayLike = BASIS_FUNCTIONS_DYER2017,

320 shape: SpectralShape | None = None,

321) -> MultiSpectralDistributions:

322 """

323 Recover the camera *RGB* sensitivities for the specified camera *RGB*

324 values using *Jiang et al. (2013)* method.

325

326 Parameters

327 ----------

328 RGB

329 Camera *RGB* values corresponding with ``reflectances``.

330 illuminant

331 Illuminant spectral distribution used to produce the camera *RGB*

332 values.

333 reflectances

334 Reflectance spectral distributions used to produce the camera

335 *RGB* values.

336 basis_functions

337 Basis functions for the method. The default is to use the

338 built-in *sRGB* basis functions, i.e.,

339 :attr:`colour.recovery.BASIS_FUNCTIONS_DYER2017`.

340 shape

341 Spectral shape of the recovered camera *RGB* sensitivities.

342 The ``illuminant`` and ``reflectances`` will be aligned to it if

343 passed, otherwise, the ``illuminant`` shape is used.

344

345 Returns

346 -------

347 :class:`colour.RGB_CameraSensitivities`

348 Recovered camera *RGB* sensitivities.

349

350 Notes

351 -----

352 +------------+-----------------------+---------------+

353 | **Domain** | **Scale - Reference** | **Scale - 1** |

354 +============+=======================+===============+

355 | ``RGB`` | 1 | 1 |

356 +------------+-----------------------+---------------+

357

358 Examples

359 --------

360 >>> from colour.colorimetry import (

361 ... SDS_ILLUMINANTS,

362 ... msds_to_XYZ,

363 ... sds_and_msds_to_msds,

364 ... )

365 >>> from colour.characterisation import (

366 ... MSDS_CAMERA_SENSITIVITIES,

367 ... SDS_COLOURCHECKERS,

368 ... )

369 >>> from colour.recovery import SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017

370 >>> illuminant = SDS_ILLUMINANTS["D65"]

371 >>> sensitivities = MSDS_CAMERA_SENSITIVITIES["Nikon 5100 (NPL)"]

372 >>> reflectances = [

373 ... sd.copy().align(SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017)

374 ... for sd in SDS_COLOURCHECKERS["BabelColor Average"].values()

375 ... ]

376 >>> reflectances = sds_and_msds_to_msds(reflectances)

377 >>> RGB = (

378 ... msds_to_XYZ(

379 ... reflectances,

380 ... method="Integration",

381 ... cmfs=sensitivities,

382 ... illuminant=illuminant,

383 ... k=1,

384 ... shape=SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017,

385 ... )

386 ... / 100

387 ... )

388 >>> RGB_to_msds_camera_sensitivities_Jiang2013(

389 ... RGB,

390 ... illuminant,

391 ... reflectances,

392 ... BASIS_FUNCTIONS_DYER2017,

393 ... SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017,

394 ... ).values # doctest: +ELLIPSIS

395 array([[ 7.0437846...e-03, 9.2126044...e-03, -7.6408087...e-03],

396 [ -8.7671560...e-03, 1.1272669...e-02, 6.3743419...e-03],

397 [ 4.5812685...e-02, 7.1800041...e-02, 4.0000169...e-01],

398 [ 7.5939115...e-02, 1.1562093...e-01, 7.1152155...e-01],

399 [ 6.7768573...e-02, 1.5340644...e-01, 8.5266831...e-01],

400 [ 5.1934131...e-02, 1.8857547...e-01, 9.3895784...e-01],

401 [ 4.3807056...e-02, 2.6108660...e-01, 9.7213072...e-01],

402 [ 4.5345321...e-02, 3.7544039...e-01, 9.6145068...e-01],

403 [ 5.0694514...e-02, 4.4765815...e-01, 8.8648114...e-01],

404 [ 4.2837825...e-02, 4.5071344...e-01, 7.5177077...e-01],

405 [ 4.1052030...e-02, 6.1657728...e-01, 5.5273073...e-01],

406 [ 5.2843697...e-02, 7.8019954...e-01, 3.8226917...e-01],

407 [ 9.4265543...e-02, 9.1767425...e-01, 2.4035461...e-01],

408 [ 1.3954459...e-01, 1.0000000...e+00, 1.5537481...e-01],

409 [ 7.8143883...e-02, 9.2772027...e-01, 1.0440935...e-01],

410 [ 4.5380529...e-02, 8.5670156...e-01, 6.5122285...e-02],

411 [ 5.2216496...e-02, 7.5232292...e-01, 3.4295447...e-02],

412 [ 1.0281652...e-01, 6.2580973...e-01, 2.0949510...e-02],

413 [ 5.1694176...e-01, 4.9274616...e-01, 1.4852461...e-02],

414 [ 9.5639793...e-01, 3.4336481...e-01, 1.0898318...e-02],

415 [ 9.7349477...e-01, 2.0858770...e-01, 7.0049439...e-03],

416 [ 8.1946141...e-01, 1.1178483...e-01, 4.4718000...e-03],

417 [ 6.7617415...e-01, 6.5907196...e-02, 4.1013538...e-03],

418 [ 5.5680417...e-01, 4.4626835...e-02, 4.1852898...e-03],

419 [ 4.1960111...e-01, 3.3367103...e-02, 4.4916588...e-03],

420 [ 2.9557834...e-01, 2.3948776...e-02, 4.4593273...e-03],

421 [ 2.2705062...e-01, 1.8778777...e-02, 4.3169731...e-03],

422 [ 1.3411835...e-01, 1.0695498...e-02, 3.4119265...e-03],

423 [ 4.0019556...e-02, 5.5551238...e-03, 1.3679492...e-03],

424 [ -4.3224053...e-03, 2.4973119...e-03, 3.8030327...e-04],

425 [ -6.0039541...e-03, 1.5467822...e-03, 5.4039435...e-04]])

426 """

427

428 R, G, B = tsplit(np.reshape(RGB, [-1, 3]))

429 basis_functions = as_float_array(basis_functions)

430 shape = optional(shape, illuminant.shape)

431

432 R_w, G_w, B_w = tsplit(np.moveaxis(basis_functions, 0, 1))

433

434 if illuminant.shape != shape:

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

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

437

438 if reflectances.shape != shape:

439 runtime_warning(

440 f'Aligning "{reflectances.name}" reflectances shape to "{shape}".'

441 )

442 reflectances = reshape_msds(reflectances, shape, copy=False)

443

444 S_R = RGB_to_sd_camera_sensitivity_Jiang2013(

445 R, illuminant, reflectances, R_w, shape

446 )

447 S_G = RGB_to_sd_camera_sensitivity_Jiang2013(

448 G, illuminant, reflectances, G_w, shape

449 )

450 S_B = RGB_to_sd_camera_sensitivity_Jiang2013(

451 B, illuminant, reflectances, B_w, shape

452 )

453

454 msds_camera_sensitivities = RGB_CameraSensitivities([S_R, S_G, S_B])

455

456 msds_camera_sensitivities /= np.max(msds_camera_sensitivities.values)

457

458 return msds_camera_sensitivities