Coverage for colour/recovery/tests/test_mallett2019.py: 100%

55 statements  

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

1"""Define the unit tests for the :mod:`colour.recovery.mallett2019` module.""" 

2 

3from __future__ import annotations 

4 

5import numpy as np 

6import pytest 

7 

8from colour.characterisation import SDS_COLOURCHECKERS 

9from colour.colorimetry import ( 

10 CCS_ILLUMINANTS, 

11 MSDS_CMFS, 

12 SDS_ILLUMINANTS, 

13 SpectralShape, 

14 reshape_msds, 

15 reshape_sd, 

16 sd_to_XYZ, 

17) 

18from colour.difference import JND_CIE1976, delta_E_CIE1976 

19from colour.models import ( 

20 RGB_COLOURSPACE_PAL_SECAM, 

21 RGB_COLOURSPACE_sRGB, 

22 XYZ_to_Lab, 

23 XYZ_to_RGB, 

24) 

25from colour.recovery import ( 

26 MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019, 

27 RGB_to_sd_Mallett2019, 

28 spectral_primary_decomposition_Mallett2019, 

29) 

30 

31__author__ = "Colour Developers" 

32__copyright__ = "Copyright 2013 Colour Developers" 

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

34__maintainer__ = "Colour Developers" 

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

36__status__ = "Production" 

37 

38__all__ = [ 

39 "FixtureMallett2019", 

40 "TestSpectralPrimaryDecompositionMallett2019", 

41 "TestRGB_to_sd_Mallett2019", 

42] 

43 

44 

45class FixtureMallett2019: 

46 """A fixture for testing the :mod:`colour.recovery.mallett2019` module.""" 

47 

48 @pytest.fixture(autouse=True) 

49 def setup_fixture_Mallett2019(self) -> None: 

50 """Configure the class instance.""" 

51 

52 self._basis = MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019 

53 self._RGB_colourspace = RGB_COLOURSPACE_sRGB 

54 self._cmfs = reshape_msds( 

55 MSDS_CMFS["CIE 1931 2 Degree Standard Observer"], 

56 SpectralShape(360, 780, 10), 

57 ) 

58 self._sd_D65 = reshape_sd(SDS_ILLUMINANTS["D65"], self._cmfs.shape) 

59 self._xy_D65 = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] 

60 

61 def check_basis_functions(self) -> None: 

62 """ 

63 Test :func:`colour.recovery.RGB_to_sd_Mallett2019` definition or the 

64 more specialised :func:`colour.recovery.RGB_to_sd_Mallett2019` 

65 definition. 

66 """ 

67 

68 # Make sure the white point is reconstructed as a perfectly flat 

69 # spectrum. 

70 RGB = np.full(3, 1.0) 

71 sd = RGB_to_sd_Mallett2019(RGB, self._basis) 

72 assert np.var(sd.values) < 1e-5 

73 

74 # Check if the primaries or their combination exceeds the [0, 1] range. 

75 lower = np.zeros_like(sd.values) - 1e-12 

76 upper = np.ones_like(sd.values) + 1e12 

77 for RGB in [[1, 1, 1], [1, 0, 0], [0, 1, 0], [0, 0, 1]]: 

78 sd = RGB_to_sd_Mallett2019(RGB, self._basis) 

79 np.testing.assert_array_less(sd.values, upper) 

80 np.testing.assert_array_less(lower, sd.values) 

81 

82 # Check Delta E's using a colour checker. 

83 for name, sd in SDS_COLOURCHECKERS["ColorChecker N Ohta"].items(): 

84 XYZ = sd_to_XYZ(sd, self._cmfs, self._sd_D65) / 100 

85 Lab = XYZ_to_Lab(XYZ, self._xy_D65) 

86 RGB = XYZ_to_RGB(XYZ, self._RGB_colourspace, self._xy_D65) 

87 

88 recovered_sd = RGB_to_sd_Mallett2019(RGB, self._basis) 

89 recovered_XYZ = sd_to_XYZ(recovered_sd, self._cmfs, self._sd_D65) / 100 

90 recovered_Lab = XYZ_to_Lab(recovered_XYZ, self._xy_D65) 

91 

92 error = delta_E_CIE1976(Lab, recovered_Lab) 

93 

94 if error > 4 * JND_CIE1976 / 100: # pragma: no cover 

95 pytest.fail(f'Delta E for "{name}" is {error}!') 

96 

97 

98class TestSpectralPrimaryDecompositionMallett2019(FixtureMallett2019): 

99 """ 

100 Define :func:`colour.recovery.mallett2019.\ 

101spectral_primary_decomposition_Mallett2019` definition unit tests methods. 

102 """ 

103 

104 def setup_method(self) -> None: 

105 """Initialise the common tests attributes.""" 

106 

107 self._RGB_colourspace = RGB_COLOURSPACE_PAL_SECAM 

108 

109 def test_spectral_primary_decomposition_Mallett2019(self) -> None: 

110 """ 

111 Test :func:`colour.recovery.mallett2019.\ 

112test_spectral_primary_decomposition_Mallett2019` definition. 

113 """ 

114 

115 self._basis = spectral_primary_decomposition_Mallett2019( 

116 self._RGB_colourspace, self._cmfs, self._sd_D65 

117 ) 

118 

119 self.check_basis_functions() 

120 

121 self._basis = spectral_primary_decomposition_Mallett2019( 

122 self._RGB_colourspace, 

123 self._cmfs, 

124 self._sd_D65, 

125 optimisation_kwargs={"options": {"maxiter": 10}}, 

126 ) 

127 

128 self.check_basis_functions() 

129 

130 

131class TestRGB_to_sd_Mallett2019(FixtureMallett2019): 

132 """ 

133 Define :func:`colour.recovery.mallett2019.RGB_to_sd_Mallett2019` definition 

134 unit tests methods. 

135 """ 

136 

137 def setup_method(self) -> None: 

138 """Initialise the common tests attributes.""" 

139 

140 self._RGB_colourspace = RGB_COLOURSPACE_sRGB 

141 self._basis = MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019 

142 

143 def test_RGB_to_sd_Mallett2019(self) -> None: 

144 """ 

145 Test :func:`colour.recovery.mallett2019.RGB_to_sd_Mallett2019` 

146 definition. 

147 """ 

148 

149 self.check_basis_functions()