Coverage for quality/tm3018.py: 53%

57 statements  

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

1""" 

2ANSI/IES TM-30-18 Colour Fidelity Index 

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

4 

5Define the *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI) computation 

6objects. 

7 

8- :class:`colour.quality.ColourQuality_Specification_ANSIIESTM3018` 

9- :func:`colour.quality.colour_fidelity_index_ANSIIESTM3018` 

10 

11References 

12---------- 

13- :cite:`ANSI2018` : ANSI, & IES Color Committee. (2018). ANSI/IES TM-30-18 - 

14 IES Method for Evaluating Light Source Color Rendition. 

15 ISBN:978-0-87995-379-9 

16- :cite:`VincentJ2017` : Vincent J. (2017). Is there any numpy group by 

17 function? Retrieved June 30, 2023, from https://stackoverflow.com/a/43094244 

18""" 

19 

20from __future__ import annotations 

21 

22import typing 

23from dataclasses import dataclass 

24 

25import numpy as np 

26 

27if typing.TYPE_CHECKING: 

28 from colour.colorimetry import SpectralDistribution 

29 from colour.hints import ArrayLike, Literal, NDArrayFloat, NDArrayInt, Tuple 

30 

31from colour.quality import colour_fidelity_index_CIE2017 

32from colour.quality.cfi2017 import ( 

33 DataColorimetry_TCS_CIE2017, 

34 delta_E_to_R_f, 

35) 

36from colour.utilities import as_float_array, as_float_scalar, as_int_array 

37 

38 

39@dataclass 

40class ColourQuality_Specification_ANSIIESTM3018: 

41 """ 

42 Define the *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI) colour 

43 quality specification. 

44 

45 Parameters 

46 ---------- 

47 name 

48 Name of the test spectral distribution. 

49 sd_test 

50 Spectral distribution of the tested illuminant. 

51 sd_reference 

52 Spectral distribution of the reference illuminant. 

53 R_f 

54 *Colour Fidelity Index* (CFI) :math:`R_f`. 

55 R_s 

56 Individual *colour fidelity indexes* data for each sample. 

57 CCT 

58 Correlated colour temperature :math:`T_{cp}`. 

59 D_uv 

60 Distance from the Planckian locus :math:`\\Delta_{uv}`. 

61 colorimetry_data 

62 Colorimetry data for the test and reference computations. 

63 R_g 

64 Gamut index :math:`R_g`. 

65 bins 

66 List of 16 lists, each containing the indexes of colour samples 

67 that lie in the respective hue bin. 

68 averages_test 

69 Averages of *CAM02-UCS* a', b' coordinates for each hue bin for 

70 test samples. 

71 averages_reference 

72 Averages for reference samples. 

73 average_norms 

74 Distance of averages for reference samples from the origin. 

75 R_fs 

76 Local colour fidelities for each hue bin. 

77 R_cs 

78 Local chromaticity shifts for each hue bin, in percents. 

79 R_hs 

80 Local hue shifts for each hue bin. 

81 """ 

82 

83 name: str 

84 sd_test: SpectralDistribution 

85 sd_reference: SpectralDistribution 

86 R_f: float 

87 R_s: NDArrayFloat 

88 CCT: float 

89 D_uv: float 

90 colorimetry_data: Tuple[DataColorimetry_TCS_CIE2017, DataColorimetry_TCS_CIE2017] 

91 R_g: float 

92 bins: NDArrayInt 

93 averages_test: NDArrayFloat 

94 averages_reference: NDArrayFloat 

95 average_norms: NDArrayFloat 

96 R_fs: NDArrayFloat 

97 R_cs: NDArrayFloat 

98 R_hs: NDArrayFloat 

99 

100 

101@typing.overload 

102def colour_fidelity_index_ANSIIESTM3018( 

103 sd_test: SpectralDistribution, additional_data: Literal[True] = True 

104) -> ColourQuality_Specification_ANSIIESTM3018: ... 

105 

106 

107@typing.overload 

108def colour_fidelity_index_ANSIIESTM3018( 

109 sd_test: SpectralDistribution, *, additional_data: Literal[False] 

110) -> float: ... 

111 

112 

113@typing.overload 

114def colour_fidelity_index_ANSIIESTM3018( 

115 sd_test: SpectralDistribution, additional_data: Literal[False] 

116) -> float: ... 

117 

118 

119def colour_fidelity_index_ANSIIESTM3018( 

120 sd_test: SpectralDistribution, additional_data: bool = False 

121) -> float | ColourQuality_Specification_ANSIIESTM3018: 

122 """ 

123 Compute the *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI) :math:`R_f` 

124 for the specified test spectral distribution. 

125 

126 Parameters 

127 ---------- 

128 sd_test 

129 Test spectral distribution. 

130 additional_data 

131 Whether to output additional data. 

132 

133 Returns 

134 ------- 

135 :class:`float` or \ 

136 :class:`colour.quality.ColourQuality_Specification_ANSIIESTM3018` 

137 *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI). 

138 

139 References 

140 ---------- 

141 :cite:`ANSI2018`, :cite:`VincentJ2017` 

142 

143 Examples 

144 -------- 

145 >>> from colour import SDS_ILLUMINANTS 

146 >>> sd = SDS_ILLUMINANTS["FL2"] 

147 >>> colour_fidelity_index_ANSIIESTM3018(sd) # doctest: +ELLIPSIS 

148 70.1208244... 

149 """ 

150 

151 if not additional_data: 

152 return colour_fidelity_index_CIE2017(sd_test, False) 

153 

154 specification = colour_fidelity_index_CIE2017(sd_test, True) 

155 

156 # Setup bins based on where the reference a'b' points are located. 

157 bins = as_int_array(np.floor(specification.colorimetry_data[1].JMh[:, 2] / 22.5)) 

158 

159 bin_mask = bins == np.reshape(np.arange(16), (-1, 1)) 

160 

161 # "bin_mask" is used later with Numpy broadcasting and "np.nanmean" 

162 # to skip a list comprehension and keep all the mean calculation vectorised 

163 # as per :cite:`VincentJ2017`. 

164 bin_mask = np.choose(bin_mask, [np.nan, 1]) 

165 

166 # Per-bin a'b' averages. 

167 test_apbp = as_float_array(specification.colorimetry_data[0].Jpapbp[:, 1:]) 

168 ref_apbp = as_float_array(specification.colorimetry_data[1].Jpapbp[:, 1:]) 

169 

170 # Tile the "apbp" data in the third dimension and use broadcasting to place 

171 # each bin mask along the third dimension. By multiplying these matrices 

172 # together, Numpy automatically expands the apbp data in the third 

173 # dimension and multiplies by the nan-filled bin mask. Finally, 

174 # "np.nanmean" can compute the bin mean apbp positions with the appropriate 

175 # axis argument. 

176 averages_test = np.transpose( 

177 np.nanmean( 

178 np.reshape(np.transpose(bin_mask), (99, 1, 16)) 

179 * np.reshape(test_apbp, (*ref_apbp.shape, 1)), 

180 axis=0, 

181 ) 

182 ) 

183 averages_reference = np.transpose( 

184 np.nanmean( 

185 np.reshape(np.transpose(bin_mask), (99, 1, 16)) 

186 * np.reshape(ref_apbp, (*ref_apbp.shape, 1)), 

187 axis=0, 

188 ) 

189 ) 

190 

191 # Gamut Index. 

192 R_g = 100 * (averages_area(averages_test) / averages_area(averages_reference)) 

193 

194 # Local colour fidelity indexes, i.e., 16 CFIs for each bin. 

195 bin_delta_E_s = np.nanmean( 

196 np.reshape(specification.delta_E_s, (1, -1)) * bin_mask, axis=1 

197 ) 

198 R_fs = as_float_array(delta_E_to_R_f(bin_delta_E_s)) 

199 

200 # Angles bisecting the hue bins. 

201 angles = (22.5 * np.arange(16) + 11.25) / 180 * np.pi 

202 cosines = np.cos(angles) 

203 sines = np.sin(angles) 

204 

205 average_norms = np.linalg.norm(averages_reference, axis=1) 

206 a_deltas = averages_test[:, 0] - averages_reference[:, 0] 

207 b_deltas = averages_test[:, 1] - averages_reference[:, 1] 

208 

209 # Local chromaticity shifts, multiplied by 100 to obtain percentages. 

210 R_cs = 100 * (a_deltas * cosines + b_deltas * sines) / average_norms 

211 

212 # Local hue shifts. 

213 R_hs = (-a_deltas * sines + b_deltas * cosines) / average_norms 

214 

215 return ColourQuality_Specification_ANSIIESTM3018( 

216 specification.name, 

217 sd_test, 

218 specification.sd_reference, 

219 specification.R_f, 

220 specification.R_s, 

221 specification.CCT, 

222 specification.D_uv, 

223 specification.colorimetry_data, 

224 R_g, 

225 bins, 

226 averages_test, 

227 averages_reference, 

228 average_norms, 

229 R_fs, 

230 R_cs, 

231 R_hs, 

232 ) 

233 

234 

235def averages_area(averages: ArrayLike) -> float: 

236 """ 

237 Compute the area of the polygon formed by the hue bin averages. 

238 

239 Parameters 

240 ---------- 

241 averages 

242 Hue bin averages. 

243 

244 Returns 

245 ------- 

246 :class:`float` 

247 Area of the polygon. 

248 """ 

249 

250 averages = as_float_array(averages) 

251 

252 N = averages.shape[0] 

253 

254 triangle_areas = np.empty(N) 

255 for i in range(N): 

256 u = averages[i, :] 

257 v = averages[(i + 1) % N, :] 

258 triangle_areas[i] = (u[0] * v[1] - u[1] * v[0]) / 2 

259 

260 return as_float_scalar(np.sum(triangle_areas))