Coverage for colour/quality/tm3018.py: 100%
63 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
1"""
2ANSI/IES TM-30-18 Colour Fidelity Index
3=======================================
5Define the *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI) computation
6objects.
8- :class:`colour.quality.ColourQuality_Specification_ANSIIESTM3018`
9- :func:`colour.quality.colour_fidelity_index_ANSIIESTM3018`
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"""
20from __future__ import annotations
22import typing
23from dataclasses import dataclass
25import numpy as np
27if typing.TYPE_CHECKING:
28 from colour.colorimetry import SpectralDistribution
29 from colour.hints import ArrayLike, Literal, NDArrayFloat, NDArrayInt, Tuple
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
39@dataclass
40class ColourQuality_Specification_ANSIIESTM3018:
41 """
42 Define the *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI) colour
43 quality specification.
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 """
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
101@typing.overload
102def colour_fidelity_index_ANSIIESTM3018(
103 sd_test: SpectralDistribution, additional_data: Literal[True] = True
104) -> ColourQuality_Specification_ANSIIESTM3018: ...
107@typing.overload
108def colour_fidelity_index_ANSIIESTM3018(
109 sd_test: SpectralDistribution, *, additional_data: Literal[False]
110) -> float: ...
113@typing.overload
114def colour_fidelity_index_ANSIIESTM3018(
115 sd_test: SpectralDistribution, additional_data: Literal[False]
116) -> float: ...
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.
126 Parameters
127 ----------
128 sd_test
129 Test spectral distribution.
130 additional_data
131 Whether to output additional data.
133 Returns
134 -------
135 :class:`float` or \
136 :class:`colour.quality.ColourQuality_Specification_ANSIIESTM3018`
137 *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI).
139 References
140 ----------
141 :cite:`ANSI2018`, :cite:`VincentJ2017`
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 """
151 if not additional_data:
152 return colour_fidelity_index_CIE2017(sd_test, False)
154 specification = colour_fidelity_index_CIE2017(sd_test, True)
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))
159 bin_mask = bins == np.reshape(np.arange(16), (-1, 1))
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])
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:])
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 )
191 # Gamut Index.
192 R_g = 100 * (averages_area(averages_test) / averages_area(averages_reference))
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))
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)
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]
209 # Local chromaticity shifts, multiplied by 100 to obtain percentages.
210 R_cs = 100 * (a_deltas * cosines + b_deltas * sines) / average_norms
212 # Local hue shifts.
213 R_hs = (-a_deltas * sines + b_deltas * cosines) / average_norms
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 )
235def averages_area(averages: ArrayLike) -> float:
236 """
237 Compute the area of the polygon formed by the hue bin averages.
239 Parameters
240 ----------
241 averages
242 Hue bin averages.
244 Returns
245 -------
246 :class:`float`
247 Area of the polygon.
248 """
250 averages = as_float_array(averages)
252 N = averages.shape[0]
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
260 return as_float_scalar(np.sum(triangle_areas))