Coverage for colorimetry/dominant.py: 63%

1"""

2 Dominant Wavelength and Purity

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

4

5 Define the objects to compute the *dominant wavelength* and *purity* of a

6 colour and related quantities:

7

8- :func:`colour.dominant_wavelength`

9- :func:`colour.complementary_wavelength`

10- :func:`colour.excitation_purity`

11- :func:`colour.colorimetric_purity`

12

13 References

14----------

15- :cite:`CIETC1-482004o` : CIE TC 1-48. (2004). 9.1 Dominant wavelength and

16 purity. In CIE 015:2004 Colorimetry, 3rd Edition (pp. 32-33).

17 ISBN:978-3-901906-33-6

18- :cite:`Erdogana` : Erdogan, T. (n.d.). How to Calculate Luminosity,

19 Dominant Wavelength, and Excitation Purity (p. 7).

20 http://www.semrock.com/Data/Sites/1/semrockpdfs/\

21 whitepaper_howtocalculateluminositywavelengthandpurity.pdf

22"""

23

24 from __future__ import annotations

25

26 import typing

27

28 import numpy as np

29

30 from colour.algebra import euclidean_distance, sdiv, sdiv_mode

31 from colour.colorimetry import MultiSpectralDistributions, handle_spectral_arguments

32 from colour.geometry import extend_line_segment, intersect_line_segments

33

34 if typing.TYPE_CHECKING:

35 from colour.hints import ArrayLike, NDArrayFloat, NDArrayInt, Tuple

36

37 from colour.models import XYZ_to_xy

38 from colour.utilities import as_float_array, required

39

40 __author__ = "Colour Developers"

41 __copyright__ = "Copyright 2013 Colour Developers"

42 __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"

43 __maintainer__ = "Colour Developers"

44 __email__ = "colour-developers@colour-science.org"

45 __status__ = "Production"

46

47 __all__ = [

48 "closest_spectral_locus_wavelength",

49 "dominant_wavelength",

50 "complementary_wavelength",

51 "excitation_purity",

52 "colorimetric_purity",

53]

54

55

56@required("SciPy")

57 def closest_spectral_locus_wavelength(

58 xy: ArrayLike, xy_n: ArrayLike, xy_s: ArrayLike, inverse: bool = False

59) -> Tuple[NDArrayInt, NDArrayFloat]:

60 """

61 Compute the coordinates and closest spectral locus wavelength index to the

62 point where the line defined by the achromatic stimulus :math:`xy_n` to

63 colour stimulus :math:`xy` *CIE xy* chromaticity coordinates intersects

64 the spectral locus.

65

66 Parameters

67 ----------

68 xy

69 Colour stimulus *CIE xy* chromaticity coordinates.

70 xy_n

71 Achromatic stimulus *CIE xy* chromaticity coordinates.

72 xy_s

73 Spectral locus *CIE xy* chromaticity coordinates.

74 inverse

75 The intersection will be computed using the colour stimulus :math:`xy`

76 to achromatic stimulus :math:`xy_n` inverse direction.

77

78 Returns

79 -------

80 :class:`tuple`

81 Closest wavelength index, intersection point *CIE xy* chromaticity

82 coordinates.

83

84 Raises

85 ------

86 ValueError

87 If no closest spectral locus wavelength index and coordinates found.

88

89 Examples

90 --------

91 >>> from colour.colorimetry import MSDS_CMFS

92 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]

93 >>> xy = np.array([0.54369557, 0.32107944])

94 >>> xy_n = np.array([0.31270000, 0.32900000])

95 >>> xy_s = XYZ_to_xy(cmfs.values)

96 >>> ix, intersect = closest_spectral_locus_wavelength(xy, xy_n, xy_s)

97 >>> print(ix) #

98 256

99 >>> print(intersect) # doctest: +ELLIPSIS

100 [ 0.6835474... 0.3162840...]

101 """

102

103 import scipy.spatial.distance # noqa: PLC0415

104

105 xy = as_float_array(xy)

106 xy_n = np.resize(xy_n, xy.shape)

107 xy_s = as_float_array(xy_s)

108

109 xy_e = extend_line_segment(xy, xy_n) if inverse else extend_line_segment(xy_n, xy)

110

111 # Closing horse-shoe shape to handle line of purples intersections.

112 xy_s = np.vstack([xy_s, xy_s[0, :]])

113

114 xy_wl = intersect_line_segments(

115 np.concatenate((xy_n, xy_e), -1),

116 np.hstack([xy_s, np.roll(xy_s, 1, axis=0)]),

117 ).xy

118 # Extracting the first intersection per-wavelength.

119 xy_wl = np.sort(xy_wl, 1)[:, 0, :]

120

121 i_wl = np.argmin(scipy.spatial.distance.cdist(xy_wl, xy_s), axis=-1)

122

123 i_wl = np.reshape(i_wl, xy.shape[0:-1])

124 xy_wl = np.reshape(xy_wl, xy.shape)

125

126 return i_wl, xy_wl

127

128

129 def dominant_wavelength(

130 xy: ArrayLike,

131 xy_n: ArrayLike,

132 cmfs: MultiSpectralDistributions | None = None,

133 inverse: bool = False,

134) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]:

135 """

136 Compute the *dominant wavelength* :math:`\\lambda_d` for colour stimulus

137 :math:`xy` and the related :math:`xy_wl` first and :math:`xy_{cw}` second

138 intersection coordinates with the spectral locus.

139

140 In the eventuality where the :math:`xy_wl` first intersection coordinates

141 are on the line of purples, the *complementary wavelength* will be

142 computed in lieu.

143

144 The *complementary wavelength* is indicated by a negative sign and the

145 :math:`xy_{cw}` second intersection coordinates which are set by default

146 to the same value as :math:`xy_wl` first intersection coordinates will be

147 set to the *complementary dominant wavelength* intersection coordinates

148 with the spectral locus.

149

150 Parameters

151 ----------

152 xy

153 Colour stimulus *CIE xy* chromaticity coordinates.

154 xy_n

155 Achromatic stimulus *CIE xy* chromaticity coordinates.

156 cmfs

157 Standard observer colour matching functions, default to the

158 *CIE 1931 2 Degree Standard Observer*.

159 inverse

160 Inverse the computation direction to retrieve the

161 *complementary wavelength*.

162

163 Returns

164 -------

165 :class:`tuple`

166 *Dominant wavelength*, first intersection point *CIE xy* chromaticity

167 coordinates, second intersection point *CIE xy* chromaticity

168 coordinates.

169

170 References

171 ----------

172 :cite:`CIETC1-482004o`, :cite:`Erdogana`

173

174 Examples

175 --------

176 *Dominant wavelength* computation:

177

178 >>> from colour.colorimetry import MSDS_CMFS

179 >>> from pprint import pprint

180 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]

181 >>> xy = np.array([0.54369557, 0.32107944])

182 >>> xy_n = np.array([0.31270000, 0.32900000])

183 >>> pprint(dominant_wavelength(xy, xy_n, cmfs)) # doctest: +ELLIPSIS

184 (array(616...),

185 array([ 0.6835474..., 0.3162840...]),

186 array([ 0.6835474..., 0.3162840...]))

187

188 *Complementary dominant wavelength* is returned if the first intersection

189 is located on the line of purples:

190

191 >>> xy = np.array([0.37605506, 0.24452225])

192 >>> pprint(dominant_wavelength(xy, xy_n)) # doctest: +ELLIPSIS

193 (array(-509.0),

194 array([ 0.4572314..., 0.1362814...]),

195 array([ 0.0104096..., 0.7320745...]))

196 """

197

198 cmfs, _illuminant = handle_spectral_arguments(cmfs)

199

200 xy = as_float_array(xy)

201 xy_n = np.resize(xy_n, xy.shape)

202

203 xy_s = XYZ_to_xy(cmfs.values)

204

205 i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, xy_s, inverse)

206 xy_cwl = xy_wl

207 wl = cmfs.wavelengths[i_wl]

208

209 xy_e = extend_line_segment(xy, xy_n) if inverse else extend_line_segment(xy_n, xy)

210 intersect = intersect_line_segments(

211 np.concatenate((xy_n, xy_e), -1), np.hstack([xy_s[0], xy_s[-1]])

212 ).intersect

213 intersect = np.reshape(intersect, wl.shape)

214

215 i_wl_r, xy_cwl_r = closest_spectral_locus_wavelength(xy, xy_n, xy_s, not inverse)

216 wl_r = -cmfs.wavelengths[i_wl_r]

217

218 wl = np.where(intersect, wl_r, wl)

219 xy_cwl = np.where(intersect[..., None], xy_cwl_r, xy_cwl)

220

221 return wl, np.squeeze(xy_wl), np.squeeze(xy_cwl)

222

223

224 def complementary_wavelength(

225 xy: ArrayLike,

226 xy_n: ArrayLike,

227 cmfs: MultiSpectralDistributions | None = None,

228) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]:

229 """

230 Compute the *complementary wavelength* :math:`\\lambda_c` for the

231 specified colour stimulus :math:`xy` and the related :math:`xy_wl` first

232 and :math:`xy_{cw}` second intersection coordinates with the spectral

233 locus.

234

235 In the eventuality where the :math:`xy_wl` first intersection coordinates

236 are on the line of purples, the *dominant wavelength* will be computed in

237 lieu.

238

239 The *dominant wavelength* is indicated by a negative sign and the

240 :math:`xy_{cw}` second intersection coordinates which are set by default

241 to the same value as :math:`xy_wl` first intersection coordinates will be

242 set to the *dominant wavelength* intersection coordinates with the

243 spectral locus.

244

245 Parameters

246 ----------

247 xy

248 Colour stimulus *CIE xy* chromaticity coordinates.

249 xy_n

250 Achromatic stimulus *CIE xy* chromaticity coordinates.

251 cmfs

252 Standard observer colour matching functions, default to the

253 *CIE 1931 2 Degree Standard Observer*.

254

255 Returns

256 -------

257 :class:`tuple`

258 *Complementary wavelength*, first intersection point *CIE xy*

259 chromaticity coordinates, second intersection point *CIE xy*

260 chromaticity coordinates.

261

262 References

263 ----------

264 :cite:`CIETC1-482004o`, :cite:`Erdogana`

265

266 Examples

267 --------

268 *Complementary wavelength* computation:

269

270 >>> from colour.colorimetry import MSDS_CMFS

271 >>> from pprint import pprint

272 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]

273 >>> xy = np.array([0.37605506, 0.24452225])

274 >>> xy_n = np.array([0.31270000, 0.32900000])

275 >>> pprint(complementary_wavelength(xy, xy_n, cmfs)) # doctest: +ELLIPSIS

276 (array(509.0),

277 array([ 0.0104096..., 0.7320745...]),

278 array([ 0.0104096..., 0.7320745...]))

279

280 *Dominant wavelength* is returned if the first intersection is located on

281 the line of purples:

282

283 >>> xy = np.array([0.54369557, 0.32107944])

284 >>> pprint(complementary_wavelength(xy, xy_n)) # doctest: +ELLIPSIS

285 (array(492.0),

286 array([ 0.0364795 , 0.3384712...]),

287 array([ 0.0364795 , 0.3384712...]))

288 """

289

290 return dominant_wavelength(xy, xy_n, cmfs, True)

291

292

293 def excitation_purity(

294 xy: ArrayLike,

295 xy_n: ArrayLike,

296 cmfs: MultiSpectralDistributions | None = None,

297) -> NDArrayFloat:

298 """

299 Compute the *excitation purity* :math:`P_e` for the specified colour

300 stimulus :math:`xy`.

301

302 Parameters

303 ----------

304 xy

305 Colour stimulus *CIE xy* chromaticity coordinates.

306 xy_n

307 Achromatic stimulus *CIE xy* chromaticity coordinates.

308 cmfs

309 Standard observer colour matching functions, default to the

310 *CIE 1931 2 Degree Standard Observer*.

311

312 Returns

313 -------

314 :class:`np.float` or :class:`numpy.ndarray`

315 *Excitation purity* :math:`P_e`.

316

317 References

318 ----------

319 :cite:`CIETC1-482004o`, :cite:`Erdogana`

320

321 Examples

322 --------

323 >>> from colour.colorimetry import MSDS_CMFS

324 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]

325 >>> xy = np.array([0.54369557, 0.32107944])

326 >>> xy_n = np.array([0.31270000, 0.32900000])

327 >>> excitation_purity(xy, xy_n, cmfs) # doctest: +ELLIPSIS

328 0.6228856...

329 """

330

331 _wl, xy_wl, _xy_cwl = dominant_wavelength(xy, xy_n, cmfs)

332

333 with sdiv_mode():

334 return sdiv(

335 euclidean_distance(xy_n, xy),

336 euclidean_distance(xy_n, xy_wl),

337 )

338

339

340 def colorimetric_purity(

341 xy: ArrayLike,

342 xy_n: ArrayLike,

343 cmfs: MultiSpectralDistributions | None = None,

344) -> NDArrayFloat:

345 """

346 Compute the *colorimetric purity* :math:`P_c` for the specified

347 colour stimulus :math:`xy`.

348

349 Parameters

350 ----------

351 xy

352 Colour stimulus *CIE xy* chromaticity coordinates.

353 xy_n

354 Achromatic stimulus *CIE xy* chromaticity coordinates.

355 cmfs

356 Standard observer colour matching functions, default to the

357 *CIE 1931 2 Degree Standard Observer*.

358

359 Returns

360 -------

361 :class:`np.float` or :class:`numpy.ndarray`

362 *Colorimetric purity* :math:`P_c`.

363

364 References

365 ----------

366 :cite:`CIETC1-482004o`, :cite:`Erdogana`

367

368 Examples

369 --------

370 >>> from colour.colorimetry import MSDS_CMFS

371 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]

372 >>> xy = np.array([0.54369557, 0.32107944])

373 >>> xy_n = np.array([0.31270000, 0.32900000])

374 >>> colorimetric_purity(xy, xy_n, cmfs) # doctest: +ELLIPSIS

375 0.6135828...

376 """

377

378 xy = as_float_array(xy)

379

380 _wl, xy_wl, _xy_cwl = dominant_wavelength(xy, xy_n, cmfs)

381 P_e = excitation_purity(xy, xy_n, cmfs)

382

383 with sdiv_mode():

384 return P_e * sdiv(xy_wl[..., 1], xy[..., 1])