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
« 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=======================================================
5Define the objects for camera *RGB* sensitivities recovery using the
6*Jiang, Liu, Gu and Süsstrunk (2013)* method.
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`
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"""
20from __future__ import annotations
22import typing
24import numpy as np
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)
36if typing.TYPE_CHECKING:
37 from colour.hints import (
38 ArrayLike,
39 Domain1,
40 Literal,
41 Mapping,
42 NDArrayFloat,
43 Tuple,
44 )
46from colour.hints import cast
47from colour.recovery import BASIS_FUNCTIONS_DYER2017
48from colour.utilities import as_float_array, optional, runtime_warning, tsplit
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"
57__all__ = [
58 "PCA_Jiang2013",
59 "RGB_to_sd_camera_sensitivity_Jiang2013",
60 "RGB_to_msds_camera_sensitivities_Jiang2013",
61]
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]: ...
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]: ...
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]: ...
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.
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`.
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`.
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 """
137 R_sensitivities, G_sensitivities, B_sensitivities = [], [], []
139 def normalised_sensitivity(
140 msds: MultiSpectralDistributions, channel: str
141 ) -> NDArrayFloat:
142 """Generate a normalised camera *RGB* sensitivity."""
144 sensitivity = cast("SpectralDistribution", msds.signals[channel].copy())
146 return sensitivity.normalise().values
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]))
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 )
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 )
169 return R_w_v[1], G_w_v[1], B_w_v[1]
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.
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.
201 Returns
202 -------
203 :class:`colour.RGB_CameraSensitivities`
204 Recovered camera *RGB* sensitivities.
206 Notes
207 -----
208 +------------+-----------------------+---------------+
209 | **Domain** | **Scale - Reference** | **Scale - 1** |
210 +============+=======================+===============+
211 | ``RGB`` | 1 | 1 |
212 +------------+-----------------------+---------------+
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 """
291 RGB = as_float_array(RGB)
292 shape = optional(shape, illuminant.shape)
294 if illuminant.shape != shape:
295 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
296 illuminant = reshape_sd(illuminant, shape, copy=False)
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)
304 S = np.diag(illuminant.values)
305 R = np.transpose(reflectances.values)
307 A = np.dot(np.dot(R, S), eigen_w)
309 X = np.linalg.lstsq(A, RGB, rcond=None)[0]
310 X = np.dot(eigen_w, X)
312 return SpectralDistribution(X, shape.wavelengths)
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.
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.
345 Returns
346 -------
347 :class:`colour.RGB_CameraSensitivities`
348 Recovered camera *RGB* sensitivities.
350 Notes
351 -----
352 +------------+-----------------------+---------------+
353 | **Domain** | **Scale - Reference** | **Scale - 1** |
354 +============+=======================+===============+
355 | ``RGB`` | 1 | 1 |
356 +------------+-----------------------+---------------+
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 """
428 R, G, B = tsplit(np.reshape(RGB, [-1, 3]))
429 basis_functions = as_float_array(basis_functions)
430 shape = optional(shape, illuminant.shape)
432 R_w, G_w, B_w = tsplit(np.moveaxis(basis_functions, 0, 1))
434 if illuminant.shape != shape:
435 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
436 illuminant = reshape_sd(illuminant, shape, copy=False)
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)
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 )
454 msds_camera_sensitivities = RGB_CameraSensitivities([S_R, S_G, S_B])
456 msds_camera_sensitivities /= np.max(msds_camera_sensitivities.values)
458 return msds_camera_sensitivities