"""
Jiang et al. (2013) - Camera RGB Sensitivities Recovery
=======================================================
Define the objects for camera *RGB* sensitivities recovery using the
*Jiang, Liu, Gu and Süsstrunk (2013)* method.
- :func:`colour.recovery.PCA_Jiang2013`
- :func:`colour.recovery.RGB_to_sd_camera_sensitivity_Jiang2013`
- :func:`colour.recovery.RGB_to_msds_camera_sensitivities_Jiang2013`
References
----------
- :cite:`Jiang2013` : Jiang, J., Liu, D., Gu, J., & Susstrunk, S. (2013).
What is the space of spectral sensitivity functions for digital color
cameras? 2013 IEEE Workshop on Applications of Computer Vision (WACV),
168-179. doi:10.1109/WACV.2013.6475015
"""
from __future__ import annotations
import typing
import numpy as np
from colour.algebra import eigen_decomposition
from colour.characterisation import RGB_CameraSensitivities
from colour.colorimetry import (
MultiSpectralDistributions,
SpectralDistribution,
SpectralShape,
reshape_msds,
reshape_sd,
)
if typing.TYPE_CHECKING:
from colour.hints import (
ArrayLike,
Domain1,
Literal,
Mapping,
NDArrayFloat,
Tuple,
)
from colour.hints import cast
from colour.recovery import BASIS_FUNCTIONS_DYER2017
from colour.utilities import as_float_array, optional, runtime_warning, tsplit
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"PCA_Jiang2013",
"RGB_to_sd_camera_sensitivity_Jiang2013",
"RGB_to_msds_camera_sensitivities_Jiang2013",
]
@typing.overload
def PCA_Jiang2013(
msds_camera_sensitivities: Mapping[str, MultiSpectralDistributions],
eigen_w_v_count: int | None = ...,
additional_data: Literal[True] = True,
) -> Tuple[
Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat],
Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat],
]: ...
@typing.overload
def PCA_Jiang2013(
msds_camera_sensitivities: Mapping[str, MultiSpectralDistributions],
eigen_w_v_count: int | None = ...,
*,
additional_data: Literal[False],
) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]: ...
@typing.overload
def PCA_Jiang2013(
msds_camera_sensitivities: Mapping[str, MultiSpectralDistributions],
eigen_w_v_count: int | None,
additional_data: Literal[False],
) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]: ...
[docs]
def PCA_Jiang2013(
msds_camera_sensitivities: Mapping[str, MultiSpectralDistributions],
eigen_w_v_count: int | None = None,
additional_data: bool = False,
) -> (
Tuple[
Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat],
Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat],
]
| Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]
):
"""
Perform *Principal Component Analysis* (PCA) on specified camera *RGB*
sensitivities.
Parameters
----------
msds_camera_sensitivities
Camera *RGB* sensitivities.
eigen_w_v_count
Eigen-values :math:`w` and eigen-vectors :math:`v` count.
additional_data
Whether to return both the eigen-values :math:`w` and
eigen-vectors :math:`v`.
Returns
-------
:class:`tuple`
Tuple of camera *RGB* sensitivities eigen-values :math:`w` and
eigen-vectors :math:`v` or tuple of camera *RGB* sensitivities
eigen-vectors :math:`v`.
Examples
--------
>>> from colour.colorimetry import SpectralShape
>>> from colour.characterisation import MSDS_CAMERA_SENSITIVITIES
>>> shape = SpectralShape(400, 700, 10)
>>> camera_sensitivities = {
... camera: msds.copy().align(shape)
... for camera, msds in MSDS_CAMERA_SENSITIVITIES.items()
... }
>>> np.array(PCA_Jiang2013(camera_sensitivities)).shape
(3, 31, 31)
"""
R_sensitivities, G_sensitivities, B_sensitivities = [], [], []
def normalised_sensitivity(
msds: MultiSpectralDistributions, channel: str
) -> NDArrayFloat:
"""Generate a normalised camera *RGB* sensitivity."""
sensitivity = cast("SpectralDistribution", msds.signals[channel].copy())
return sensitivity.normalise().values
for msds in msds_camera_sensitivities.values():
R_sensitivities.append(normalised_sensitivity(msds, msds.labels[0]))
G_sensitivities.append(normalised_sensitivity(msds, msds.labels[1]))
B_sensitivities.append(normalised_sensitivity(msds, msds.labels[2]))
R_w_v = eigen_decomposition(
np.vstack(R_sensitivities), eigen_w_v_count, covariance_matrix=True
)
G_w_v = eigen_decomposition(
np.vstack(G_sensitivities), eigen_w_v_count, covariance_matrix=True
)
B_w_v = eigen_decomposition(
np.vstack(B_sensitivities), eigen_w_v_count, covariance_matrix=True
)
if additional_data:
return (
(R_w_v[1], G_w_v[1], B_w_v[1]),
(R_w_v[0], G_w_v[0], B_w_v[0]),
)
return R_w_v[1], G_w_v[1], B_w_v[1]
[docs]
def RGB_to_sd_camera_sensitivity_Jiang2013(
RGB: Domain1,
illuminant: SpectralDistribution,
reflectances: MultiSpectralDistributions,
eigen_w: ArrayLike,
shape: SpectralShape | None = None,
) -> SpectralDistribution:
"""
Recover a single camera *RGB* sensitivity for the specified camera *RGB*
values using *Jiang et al. (2013)* method.
Parameters
----------
RGB
Camera *RGB* values corresponding with ``reflectances``.
illuminant
Illuminant spectral distribution used to produce the camera *RGB*
values.
reflectances
Reflectance spectral distributions used to produce the camera
*RGB* values.
eigen_w
Eigen-vectors :math:`v` for the particular camera *RGB*
sensitivity being recovered.
shape
Spectral shape of the recovered camera *RGB* sensitivity,
``illuminant`` and ``reflectances`` will be aligned to it if
passed, otherwise, ``illuminant`` shape is used.
Returns
-------
:class:`colour.RGB_CameraSensitivities`
Recovered camera *RGB* sensitivities.
Notes
-----
+------------+-----------------------+---------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===============+
| ``RGB`` | 1 | 1 |
+------------+-----------------------+---------------+
Examples
--------
>>> from colour.colorimetry import (
... SDS_ILLUMINANTS,
... msds_to_XYZ,
... sds_and_msds_to_msds,
... )
>>> from colour.characterisation import (
... MSDS_CAMERA_SENSITIVITIES,
... SDS_COLOURCHECKERS,
... )
>>> from colour.recovery import SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017
>>> illuminant = SDS_ILLUMINANTS["D65"]
>>> sensitivities = MSDS_CAMERA_SENSITIVITIES["Nikon 5100 (NPL)"]
>>> reflectances = [
... sd.copy().align(SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017)
... for sd in SDS_COLOURCHECKERS["BabelColor Average"].values()
... ]
>>> reflectances = sds_and_msds_to_msds(reflectances)
>>> R, G, B = (
... tsplit(
... msds_to_XYZ(
... reflectances,
... method="Integration",
... cmfs=sensitivities,
... illuminant=illuminant,
... k=1,
... shape=SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017,
... )
... )
... / 100
... )
>>> R_w, G_w, B_w = tsplit(np.moveaxis(BASIS_FUNCTIONS_DYER2017, 0, 1))
>>> RGB_to_sd_camera_sensitivity_Jiang2013(
... R,
... illuminant,
... reflectances,
... R_w,
... SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017,
... ) # doctest: +ELLIPSIS
SpectralDistribution([[ 4.00000000e+02, 7.20665029e-06],
[ 4.10000000e+02, -8.96986938e-06],
[ 4.20000000e+02, 4.68719619e-05],
[ 4.30000000e+02, 7.76949719e-05],
[ 4.40000000e+02, 6.93355114e-05],
[ 4.50000000e+02, 5.31349471e-05],
[ 4.60000000e+02, 4.48199586e-05],
[ 4.70000000e+02, 4.63937913e-05],
[ 4.80000000e+02, 5.18666681e-05],
[ 4.90000000e+02, 4.38283172e-05],
[ 5.00000000e+02, 4.20012318e-05],
[ 5.10000000e+02, 5.40655441e-05],
[ 5.20000000e+02, 9.64451417e-05],
[ 5.30000000e+02, 1.42771129e-04],
[ 5.40000000e+02, 7.99507187e-05],
[ 5.50000000e+02, 4.64298137e-05],
[ 5.60000000e+02, 5.34238406e-05],
[ 5.70000000e+02, 1.05193839e-04],
[ 5.80000000e+02, 5.28894436e-04],
[ 5.90000000e+02, 9.78511673e-04],
[ 6.00000000e+02, 9.96003826e-04],
[ 6.10000000e+02, 8.38408920e-04],
[ 6.20000000e+02, 6.91808589e-04],
[ 6.30000000e+02, 5.69678548e-04],
[ 6.40000000e+02, 4.29303089e-04],
[ 6.50000000e+02, 3.02412675e-04],
[ 6.60000000e+02, 2.32300470e-04],
[ 6.70000000e+02, 1.37219431e-04],
[ 6.80000000e+02, 4.09448851e-05],
[ 6.90000000e+02, -4.42234757e-06],
[ 7.00000000e+02, -6.14277697e-06]],
SpragueInterpolator,
{},
Extrapolator,
{'method': 'Constant', 'left': None, 'right': None})
"""
RGB = as_float_array(RGB)
shape = optional(shape, illuminant.shape)
if illuminant.shape != shape:
runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
illuminant = reshape_sd(illuminant, shape, copy=False)
if reflectances.shape != shape:
runtime_warning(
f'Aligning "{reflectances.name}" reflectances shape to "{shape}".'
)
reflectances = reshape_msds(reflectances, shape, copy=False)
S = np.diag(illuminant.values)
R = np.transpose(reflectances.values)
A = np.dot(np.dot(R, S), eigen_w)
X = np.linalg.lstsq(A, RGB, rcond=None)[0]
X = np.dot(eigen_w, X)
return SpectralDistribution(X, shape.wavelengths)
[docs]
def RGB_to_msds_camera_sensitivities_Jiang2013(
RGB: Domain1,
illuminant: SpectralDistribution,
reflectances: MultiSpectralDistributions,
basis_functions: ArrayLike = BASIS_FUNCTIONS_DYER2017,
shape: SpectralShape | None = None,
) -> MultiSpectralDistributions:
"""
Recover the camera *RGB* sensitivities for the specified camera *RGB*
values using *Jiang et al. (2013)* method.
Parameters
----------
RGB
Camera *RGB* values corresponding with ``reflectances``.
illuminant
Illuminant spectral distribution used to produce the camera *RGB*
values.
reflectances
Reflectance spectral distributions used to produce the camera
*RGB* values.
basis_functions
Basis functions for the method. The default is to use the
built-in *sRGB* basis functions, i.e.,
:attr:`colour.recovery.BASIS_FUNCTIONS_DYER2017`.
shape
Spectral shape of the recovered camera *RGB* sensitivities.
The ``illuminant`` and ``reflectances`` will be aligned to it if
passed, otherwise, the ``illuminant`` shape is used.
Returns
-------
:class:`colour.RGB_CameraSensitivities`
Recovered camera *RGB* sensitivities.
Notes
-----
+------------+-----------------------+---------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===============+
| ``RGB`` | 1 | 1 |
+------------+-----------------------+---------------+
Examples
--------
>>> from colour.colorimetry import (
... SDS_ILLUMINANTS,
... msds_to_XYZ,
... sds_and_msds_to_msds,
... )
>>> from colour.characterisation import (
... MSDS_CAMERA_SENSITIVITIES,
... SDS_COLOURCHECKERS,
... )
>>> from colour.recovery import SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017
>>> illuminant = SDS_ILLUMINANTS["D65"]
>>> sensitivities = MSDS_CAMERA_SENSITIVITIES["Nikon 5100 (NPL)"]
>>> reflectances = [
... sd.copy().align(SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017)
... for sd in SDS_COLOURCHECKERS["BabelColor Average"].values()
... ]
>>> reflectances = sds_and_msds_to_msds(reflectances)
>>> RGB = (
... msds_to_XYZ(
... reflectances,
... method="Integration",
... cmfs=sensitivities,
... illuminant=illuminant,
... k=1,
... shape=SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017,
... )
... / 100
... )
>>> RGB_to_msds_camera_sensitivities_Jiang2013(
... RGB,
... illuminant,
... reflectances,
... BASIS_FUNCTIONS_DYER2017,
... SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017,
... ).values # doctest: +ELLIPSIS
array([[ 7.04378461e-03, 9.21260449e-03, -7.64080878e-03],
[-8.76715607e-03, 1.12726694e-02, 6.37434190e-03],
[ 4.58126856e-02, 7.18000418e-02, 4.00001696e-01],
[ 7.59391152e-02, 1.15620933e-01, 7.11521550e-01],
[ 6.77685732e-02, 1.53406449e-01, 8.52668310e-01],
[ 5.19341313e-02, 1.88575472e-01, 9.38957846e-01],
[ 4.38070562e-02, 2.61086603e-01, 9.72130729e-01],
[ 4.53453213e-02, 3.75440392e-01, 9.61450686e-01],
[ 5.06945146e-02, 4.47658155e-01, 8.86481146e-01],
[ 4.28378252e-02, 4.50713447e-01, 7.51770770e-01],
[ 4.10520309e-02, 6.16577286e-01, 5.52730730e-01],
[ 5.28436974e-02, 7.80199548e-01, 3.82269175e-01],
[ 9.42655432e-02, 9.17674257e-01, 2.40354614e-01],
[ 1.39544593e-01, 1.00000000e+00, 1.55374812e-01],
[ 7.81438836e-02, 9.27720273e-01, 1.04409358e-01],
[ 4.53805297e-02, 8.56701565e-01, 6.51222854e-02],
[ 5.22164960e-02, 7.52322921e-01, 3.42954473e-02],
[ 1.02816526e-01, 6.25809730e-01, 2.09495104e-02],
[ 5.16941760e-01, 4.92746166e-01, 1.48524616e-02],
[ 9.56397935e-01, 3.43364817e-01, 1.08983186e-02],
[ 9.73494777e-01, 2.08587708e-01, 7.00494396e-03],
[ 8.19461415e-01, 1.11784838e-01, 4.47180002e-03],
[ 6.76174158e-01, 6.59071962e-02, 4.10135388e-03],
[ 5.56804177e-01, 4.46268353e-02, 4.18528982e-03],
[ 4.19601114e-01, 3.33671033e-02, 4.49165886e-03],
[ 2.95578342e-01, 2.39487762e-02, 4.45932739e-03],
[ 2.27050628e-01, 1.87787770e-02, 4.31697313e-03],
[ 1.34118359e-01, 1.06954985e-02, 3.41192651e-03],
[ 4.00195568e-02, 5.55512389e-03, 1.36794925e-03],
[-4.32240535e-03, 2.49731193e-03, 3.80303275e-04],
[-6.00395414e-03, 1.54678227e-03, 5.40394352e-04]])
"""
R, G, B = tsplit(np.reshape(RGB, [-1, 3]))
basis_functions = as_float_array(basis_functions)
shape = optional(shape, illuminant.shape)
R_w, G_w, B_w = tsplit(np.moveaxis(basis_functions, 0, 1))
if illuminant.shape != shape:
runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
illuminant = reshape_sd(illuminant, shape, copy=False)
if reflectances.shape != shape:
runtime_warning(
f'Aligning "{reflectances.name}" reflectances shape to "{shape}".'
)
reflectances = reshape_msds(reflectances, shape, copy=False)
S_R = RGB_to_sd_camera_sensitivity_Jiang2013(
R, illuminant, reflectances, R_w, shape
)
S_G = RGB_to_sd_camera_sensitivity_Jiang2013(
G, illuminant, reflectances, G_w, shape
)
S_B = RGB_to_sd_camera_sensitivity_Jiang2013(
B, illuminant, reflectances, B_w, shape
)
msds_camera_sensitivities = RGB_CameraSensitivities([S_R, S_G, S_B])
msds_camera_sensitivities /= np.max(msds_camera_sensitivities.values)
return msds_camera_sensitivities