Coverage for colour/graph/tests/test_conversion.py: 100%

61 statements  

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

1"""Define the unit tests for the :mod:`colour.graph.conversion` module.""" 

2 

3from __future__ import annotations 

4 

5import numpy as np 

6import pytest 

7 

8from colour.characterisation import SDS_COLOURCHECKERS 

9from colour.colorimetry import CCS_ILLUMINANTS, SDS_ILLUMINANTS 

10from colour.constants import TOLERANCE_ABSOLUTE_TESTS 

11from colour.graph import convert, describe_conversion_path 

12from colour.models import COLOURSPACE_MODELS, RGB_COLOURSPACE_ACES2065_1, XYZ_to_Lab 

13from colour.utilities import get_domain_range_scale_metadata 

14 

15__author__ = "Colour Developers" 

16__copyright__ = "Copyright 2013 Colour Developers" 

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

18__maintainer__ = "Colour Developers" 

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

20__status__ = "Production" 

21 

22__all__ = [ 

23 "TestDescribeConversionPath", 

24 "TestConvert", 

25] 

26 

27 

28class TestDescribeConversionPath: 

29 """ 

30 Define :func:`colour.graph.conversion.describe_conversion_path` definition 

31 unit tests methods. 

32 """ 

33 

34 def test_describe_conversion_path(self) -> None: 

35 """ 

36 Test :func:`colour.graph.conversion.describe_conversion_path` 

37 definition. 

38 """ 

39 

40 describe_conversion_path("Spectral Distribution", "sRGB") 

41 

42 describe_conversion_path("Spectral Distribution", "sRGB", mode="Long") 

43 

44 describe_conversion_path( 

45 "Spectral Distribution", 

46 "sRGB", 

47 mode="Extended", 

48 sd_to_XYZ={ 

49 "illuminant": SDS_ILLUMINANTS["FL2"], 

50 "return": np.array([0.47924575, 0.31676968, 0.17362725]), 

51 }, 

52 ) 

53 

54 

55class TestConvert: 

56 """ 

57 Define :func:`colour.graph.conversion.convert` definition unit tests 

58 methods. 

59 """ 

60 

61 def test_convert(self) -> None: 

62 """Test :func:`colour.graph.conversion.convert` definition.""" 

63 

64 RGB_a = convert( 

65 SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"], 

66 "Spectral Distribution", 

67 "sRGB", 

68 ) 

69 np.testing.assert_allclose( 

70 RGB_a, 

71 np.array([0.49034776, 0.30185875, 0.23587685]), 

72 atol=TOLERANCE_ABSOLUTE_TESTS, 

73 ) 

74 

75 Jpapbp = convert(RGB_a, "Output-Referred RGB", "CAM16UCS") 

76 np.testing.assert_allclose( 

77 Jpapbp, 

78 np.array([0.40738741, 0.12046560, 0.09284385]), 

79 atol=TOLERANCE_ABSOLUTE_TESTS, 

80 ) 

81 

82 RGB_b = convert(Jpapbp, "CAM16UCS", "sRGB", verbose={"mode": "Extended"}) 

83 # NOTE: The "CIE XYZ" tristimulus values to "sRGB" matrix is given 

84 # rounded at 4 decimals as per "IEC 61966-2-1:1999" and thus preventing 

85 # exact roundtrip. 

86 np.testing.assert_allclose(RGB_a, RGB_b, atol=1e-4) 

87 

88 np.testing.assert_allclose( 

89 convert("#808080", "Hexadecimal", "Scene-Referred RGB"), 

90 np.array([0.21586050, 0.21586050, 0.21586050]), 

91 atol=TOLERANCE_ABSOLUTE_TESTS, 

92 ) 

93 

94 np.testing.assert_allclose( 

95 convert("#808080", "Hexadecimal", "RGB Luminance"), 

96 0.21586050, 

97 atol=TOLERANCE_ABSOLUTE_TESTS, 

98 ) 

99 

100 np.testing.assert_allclose( 

101 convert( 

102 convert( 

103 np.array([0.5, 0.5, 0.5]), 

104 "Output-Referred RGB", 

105 "Scene-Referred RGB", 

106 ), 

107 "RGB", 

108 "YCbCr", 

109 ), 

110 np.array([0.49215686, 0.50196078, 0.50196078]), 

111 atol=TOLERANCE_ABSOLUTE_TESTS, 

112 ) 

113 

114 np.testing.assert_allclose( 

115 convert( 

116 RGB_a, 

117 "RGB", 

118 "Scene-Referred RGB", 

119 RGB_to_RGB={"output_colourspace": RGB_COLOURSPACE_ACES2065_1}, 

120 ), 

121 np.array([0.37308227, 0.31241444, 0.24746366]), 

122 atol=TOLERANCE_ABSOLUTE_TESTS, 

123 ) 

124 

125 # Consistency check to verify that all the colour models are properly 

126 # named in the graph: 

127 for model in COLOURSPACE_MODELS: 

128 convert( 

129 np.array([0.20654008, 0.12197225, 0.05136952]), 

130 "CIE XYZ", 

131 model, 

132 ) 

133 

134 def test_convert_direct_keyword_argument_passing(self) -> None: 

135 """ 

136 Test :func:`colour.graph.conversion.convert` definition behaviour when 

137 direct keyword arguments are passed. 

138 """ 

139 

140 a = np.array([0.20654008, 0.12197225, 0.05136952]) 

141 illuminant = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D50"] 

142 np.testing.assert_allclose( 

143 convert(a, "CIE XYZ", "CIE UVW", XYZ_to_UVW={"illuminant": illuminant}), 

144 convert(a, "CIE XYZ", "CIE UVW", illuminant=illuminant), 

145 atol=TOLERANCE_ABSOLUTE_TESTS, 

146 ) 

147 

148 # Illuminant "ndarray" is converted to tuple here so that it can 

149 # be hashed by the "sd_to_XYZ" definition, this should never occur 

150 # in practical application. 

151 pytest.raises( 

152 AttributeError, 

153 lambda: convert( 

154 SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"], 

155 "Spectral Distribution", 

156 "sRGB", 

157 illuminant=tuple(illuminant), 

158 ), 

159 ) 

160 

161 def test_convert_reference_scale(self) -> None: 

162 """ 

163 Test :func:`colour.graph.conversion.convert` definition behaviour with 

164 `from_reference_scale` and `to_reference_scale` parameters. 

165 """ 

166 

167 XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) 

168 

169 Lab_auto = convert(XYZ, "CIE XYZ", "CIE Lab", to_reference_scale=True) 

170 

171 Lab_manual = convert(XYZ, "CIE XYZ", "CIE Lab") 

172 metadata = get_domain_range_scale_metadata(XYZ_to_Lab) 

173 range_scale = metadata["range"] 

174 Lab_manual_scaled = Lab_manual * range_scale 

175 

176 np.testing.assert_allclose( 

177 Lab_auto, 

178 Lab_manual_scaled, 

179 atol=TOLERANCE_ABSOLUTE_TESTS, 

180 ) 

181 

182 Lab_native = Lab_auto 

183 

184 XYZ_auto = convert( 

185 Lab_native, 

186 "CIE Lab", 

187 "CIE XYZ", 

188 from_reference_scale=True, 

189 ) 

190 

191 Lab_manual_normalized = Lab_native / range_scale 

192 XYZ_manual = convert(Lab_manual_normalized, "CIE Lab", "CIE XYZ") 

193 

194 np.testing.assert_allclose( 

195 XYZ_auto, 

196 XYZ_manual, 

197 atol=TOLERANCE_ABSOLUTE_TESTS, 

198 ) 

199 

200 XYZ_roundtrip = convert( 

201 convert( 

202 XYZ, 

203 "CIE XYZ", 

204 "CIE Lab", 

205 to_reference_scale=True, 

206 ), 

207 "CIE Lab", 

208 "CIE XYZ", 

209 from_reference_scale=True, 

210 ) 

211 

212 np.testing.assert_allclose( 

213 XYZ_roundtrip, 

214 XYZ, 

215 atol=TOLERANCE_ABSOLUTE_TESTS, 

216 ) 

217 

218 XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) 

219 

220 # Test CIE Lab and CIE LCHab consistency 

221 Lab = convert(XYZ, "CIE XYZ", "CIE Lab", to_reference_scale=True) 

222 LCHab = convert(XYZ, "CIE XYZ", "CIE LCHab", to_reference_scale=True) 

223 

224 # L component should be identical 

225 np.testing.assert_allclose( 

226 Lab[0], 

227 LCHab[0], 

228 atol=TOLERANCE_ABSOLUTE_TESTS, 

229 ) 

230 

231 # C should equal sqrt(a^2 + b^2) 

232 expected_C = np.sqrt(Lab[1] ** 2 + Lab[2] ** 2) 

233 np.testing.assert_allclose( 

234 LCHab[1], 

235 expected_C, 

236 atol=TOLERANCE_ABSOLUTE_TESTS, 

237 ) 

238 

239 # Test CIE Luv and CIE LCHuv consistency 

240 Luv = convert(XYZ, "CIE XYZ", "CIE Luv", to_reference_scale=True) 

241 LCHuv = convert(XYZ, "CIE XYZ", "CIE LCHuv", to_reference_scale=True) 

242 

243 # L component should be identical 

244 np.testing.assert_allclose( 

245 Luv[0], 

246 LCHuv[0], 

247 atol=TOLERANCE_ABSOLUTE_TESTS, 

248 ) 

249 

250 # C should equal sqrt(u^2 + v^2) 

251 expected_C = np.sqrt(Luv[1] ** 2 + Luv[2] ** 2) 

252 np.testing.assert_allclose( 

253 LCHuv[1], 

254 expected_C, 

255 atol=TOLERANCE_ABSOLUTE_TESTS, 

256 )