Coverage for quality/ssi.py: 59%

46 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Academy Spectral Similarity Index (SSI) 

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

4 

5Define the *Academy Spectral Similarity Index* (SSI) computation objects. 

6 

7- :func:`colour.spectral_similarity_index` 

8 

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""" 

16 

17from __future__ import annotations 

18 

19import typing 

20 

21import numpy as np 

22 

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) 

31 

32if typing.TYPE_CHECKING: 

33 from colour.hints import NDArrayFloat 

34 

35 

36from colour.utilities import required, zeros 

37 

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" 

44 

45__all__ = [ 

46 "SPECTRAL_SHAPE_SSI", 

47 "spectral_similarity_index", 

48] 

49 

50SPECTRAL_SHAPE_SSI: SpectralShape = SpectralShape(375, 675, 1) 

51"""*Academy Spectral Similarity Index* (SSI) spectral shape.""" 

52 

53_SPECTRAL_SHAPE_SSI_LARGE: SpectralShape = SpectralShape(380, 670, 10) 

54 

55_MATRIX_INTEGRATION: NDArrayFloat | None = None 

56 

57 

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. 

68 

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*. 

78 

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. 

86 

87 References 

88 ---------- 

89 :cite:`TheAcademyofMotionPictureArtsandSciences2020a` 

90 

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 

98 

99 Computing SSI for multi-spectral distributions: 

100 

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 """ 

110 

111 from scipy.ndimage import convolve1d # noqa: PLC0415 

112 

113 global _MATRIX_INTEGRATION # noqa: PLW0603 

114 

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 ) 

122 

123 weights = np.array([0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5]) 

124 

125 for i in range(_MATRIX_INTEGRATION.shape[0]): 

126 _MATRIX_INTEGRATION[i, (10 * i) : (10 * i + 11)] = weights 

127 

128 settings = { 

129 "interpolator": LinearInterpolator, 

130 "extrapolator_kwargs": {"left": 0, "right": 0}, 

131 } 

132 

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 ) 

145 

146 test_i = np.dot(_MATRIX_INTEGRATION, sd_test.values) 

147 reference_i = np.dot(_MATRIX_INTEGRATION, sd_reference.values) 

148 

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])) 

153 

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) 

158 

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 ) 

193 

194 if dr_i.ndim == 2: 

195 weights = weights[:, np.newaxis] 

196 

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) 

200 

201 SSI = 100 - 32 * np.sqrt(m_v) 

202 

203 return np.around(SSI) if round_result else SSI