Coverage for quality/ssi.py: 59%
46 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Academy Spectral Similarity Index (SSI)
3========================================
5Define the *Academy Spectral Similarity Index* (SSI) computation objects.
7- :func:`colour.spectral_similarity_index`
9References
10----------
11- :cite:`TheAcademyofMotionPictureArtsandSciences2020a` : The Academy of
12 Motion Picture Arts and Sciences. (2020). Academy Spectral Similarity
13 Index (SSI): Overview (pp. 1-7). Retrieved June 5, 2023, from
14 https://www.oscars.org/sites/oscars/files/ssi_overview_2020-09-16.pdf
15"""
17from __future__ import annotations
19import typing
21import numpy as np
23from colour.algebra import LinearInterpolator, sdiv, sdiv_mode
24from colour.colorimetry import (
25 MultiSpectralDistributions,
26 SpectralDistribution,
27 SpectralShape,
28 reshape_msds,
29 reshape_sd,
30)
32if typing.TYPE_CHECKING:
33 from colour.hints import NDArrayFloat
36from colour.utilities import required, zeros
38__author__ = "Colour Developers"
39__copyright__ = "Copyright 2013 Colour Developers"
40__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
41__maintainer__ = "Colour Developers"
42__email__ = "colour-developers@colour-science.org"
43__status__ = "Production"
45__all__ = [
46 "SPECTRAL_SHAPE_SSI",
47 "spectral_similarity_index",
48]
50SPECTRAL_SHAPE_SSI: SpectralShape = SpectralShape(375, 675, 1)
51"""*Academy Spectral Similarity Index* (SSI) spectral shape."""
53_SPECTRAL_SHAPE_SSI_LARGE: SpectralShape = SpectralShape(380, 670, 10)
55_MATRIX_INTEGRATION: NDArrayFloat | None = None
58@required("SciPy")
59def spectral_similarity_index(
60 sd_test: SpectralDistribution | MultiSpectralDistributions,
61 sd_reference: SpectralDistribution | MultiSpectralDistributions,
62 round_result: bool = True,
63) -> NDArrayFloat:
64 """
65 Compute the *Academy Spectral Similarity Index* (SSI) of the specified
66 test spectral distribution or multi-spectral distributions with the
67 specified reference spectral distribution or multi-spectral distributions.
69 Parameters
70 ----------
71 sd_test
72 Test spectral distribution or multi-spectral distributions.
73 sd_reference
74 Reference spectral distribution or multi-spectral distributions.
75 round_result
76 Whether to round the result/output. This is particularly useful when
77 using SSI in an optimisation routine. Default is *True*.
79 Returns
80 -------
81 :class:`numpy.ndarray`
82 *Academy Spectral Similarity Index* (SSI). When both inputs are
83 :class:`colour.SpectralDistribution` objects, returns a scalar.
84 When either input is a :class:`colour.MultiSpectralDistributions`
85 object, returns an array with one SSI value per spectral distribution.
87 References
88 ----------
89 :cite:`TheAcademyofMotionPictureArtsandSciences2020a`
91 Examples
92 --------
93 >>> from colour import SDS_ILLUMINANTS
94 >>> sd_test = SDS_ILLUMINANTS["C"]
95 >>> sd_reference = SDS_ILLUMINANTS["D65"]
96 >>> spectral_similarity_index(sd_test, sd_reference)
97 94.0
99 Computing SSI for multi-spectral distributions:
101 >>> from colour.colorimetry import sd_single_led, sds_and_msds_to_msds
102 >>> sd_led_1 = sd_single_led(520, half_spectral_width=45)
103 >>> sd_led_2 = sd_single_led(540, half_spectral_width=55)
104 >>> sd_led_3 = sd_single_led(560, half_spectral_width=50)
105 >>> msds = sds_and_msds_to_msds([sd_led_1, sd_led_2, sd_led_3])
106 >>> sd_reference = sd_single_led(535, half_spectral_width=48)
107 >>> spectral_similarity_index(msds, sd_reference)
108 array([ 52., 82., 18.])
109 """
111 from scipy.ndimage import convolve1d # noqa: PLC0415
113 global _MATRIX_INTEGRATION # noqa: PLW0603
115 if _MATRIX_INTEGRATION is None:
116 _MATRIX_INTEGRATION = zeros(
117 (
118 len(_SPECTRAL_SHAPE_SSI_LARGE.wavelengths),
119 len(SPECTRAL_SHAPE_SSI.wavelengths),
120 )
121 )
123 weights = np.array([0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5])
125 for i in range(_MATRIX_INTEGRATION.shape[0]):
126 _MATRIX_INTEGRATION[i, (10 * i) : (10 * i + 11)] = weights
128 settings = {
129 "interpolator": LinearInterpolator,
130 "extrapolator_kwargs": {"left": 0, "right": 0},
131 }
133 sd_test = (
134 reshape_msds(sd_test, SPECTRAL_SHAPE_SSI, "Align", copy=False, **settings)
135 if isinstance(sd_test, MultiSpectralDistributions)
136 else reshape_sd(sd_test, SPECTRAL_SHAPE_SSI, "Align", copy=False, **settings)
137 )
138 sd_reference = (
139 reshape_msds(sd_reference, SPECTRAL_SHAPE_SSI, "Align", copy=False, **settings)
140 if isinstance(sd_reference, MultiSpectralDistributions)
141 else reshape_sd(
142 sd_reference, SPECTRAL_SHAPE_SSI, "Align", copy=False, **settings
143 )
144 )
146 test_i = np.dot(_MATRIX_INTEGRATION, sd_test.values)
147 reference_i = np.dot(_MATRIX_INTEGRATION, sd_reference.values)
149 if test_i.ndim == 1 and reference_i.ndim == 2:
150 test_i = np.tile(test_i[:, np.newaxis], (1, reference_i.shape[1]))
151 elif test_i.ndim == 2 and reference_i.ndim == 1:
152 reference_i = np.tile(reference_i[:, np.newaxis], (1, test_i.shape[1]))
154 with sdiv_mode():
155 test_i = sdiv(test_i, np.sum(test_i, axis=0, keepdims=True))
156 reference_i = sdiv(reference_i, np.sum(reference_i, axis=0, keepdims=True))
157 dr_i = sdiv(test_i - reference_i, reference_i + 1 / 30)
159 weights = np.array(
160 [
161 4 / 15,
162 22 / 45,
163 32 / 45,
164 40 / 45,
165 44 / 45,
166 1,
167 1,
168 1,
169 1,
170 1,
171 1,
172 1,
173 1,
174 1,
175 1,
176 1,
177 1,
178 1,
179 1,
180 1,
181 1,
182 1,
183 1,
184 1,
185 1,
186 1,
187 1,
188 1,
189 11 / 15,
190 3 / 15,
191 ]
192 )
194 if dr_i.ndim == 2:
195 weights = weights[:, np.newaxis]
197 wdr_i = dr_i * weights
198 c_wdr_i = convolve1d(wdr_i, [0.22, 0.56, 0.22], axis=0, mode="constant", cval=0)
199 m_v = np.sum(np.square(c_wdr_i), axis=0)
201 SSI = 100 - 32 * np.sqrt(m_v)
203 return np.around(SSI) if round_result else SSI