Coverage for quality/tm3018.py: 53%

1"""

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

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

4

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

6 objects.

7

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

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

10

11 References

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

20 from __future__ import annotations

21

22 import typing

23 from dataclasses import dataclass

24

25 import numpy as np

26

27 if typing.TYPE_CHECKING:

28 from colour.colorimetry import SpectralDistribution

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

30

31 from colour.quality import colour_fidelity_index_CIE2017

32 from colour.quality.cfi2017 import (

33 DataColorimetry_TCS_CIE2017,

34 delta_E_to_R_f,

35)

36 from colour.utilities import as_float_array, as_float_scalar, as_int_array

37

38

39@dataclass

40 class 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

102 def colour_fidelity_index_ANSIIESTM3018(

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

104) -> ColourQuality_Specification_ANSIIESTM3018: ...

105

106

107@typing.overload

108 def colour_fidelity_index_ANSIIESTM3018(

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

110) -> float: ...

111

112

113@typing.overload

114 def colour_fidelity_index_ANSIIESTM3018(

115 sd_test: SpectralDistribution, additional_data: Literal[False]

116) -> float: ...

117

118

119 def 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

235 def 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))