Coverage for colour/recovery/jiang2013.py: 93%

69 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

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

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

4 

5Define 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 

12References 

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 

20from __future__ import annotations 

21 

22import typing 

23 

24import numpy as np 

25 

26from colour.algebra import eigen_decomposition 

27from colour.characterisation import RGB_CameraSensitivities 

28from colour.colorimetry import ( 

29 MultiSpectralDistributions, 

30 SpectralDistribution, 

31 SpectralShape, 

32 reshape_msds, 

33 reshape_sd, 

34) 

35 

36if typing.TYPE_CHECKING: 

37 from colour.hints import ( 

38 ArrayLike, 

39 Domain1, 

40 Literal, 

41 Mapping, 

42 NDArrayFloat, 

43 Tuple, 

44 ) 

45 

46from colour.hints import cast 

47from colour.recovery import BASIS_FUNCTIONS_DYER2017 

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

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

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

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

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

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

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