Coverage for appearance/tests/test_scam.py: 100%

126 statements  

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

1""" 

2Define the unit tests for the :mod:`colour.appearance.scam` module. 

3""" 

4 

5from __future__ import annotations 

6 

7from itertools import product 

8 

9import numpy as np 

10import pytest 

11 

12from colour.appearance import ( 

13 CAM_Specification_sCAM, 

14 VIEWING_CONDITIONS_sCAM, 

15 XYZ_to_sCAM, 

16 sCAM_to_XYZ, 

17) 

18from colour.constants import TOLERANCE_ABSOLUTE_TESTS 

19from colour.utilities import ( 

20 as_float_array, 

21 domain_range_scale, 

22 ignore_numpy_errors, 

23 tsplit, 

24) 

25 

26__author__ = "Colour Developers, UltraMo114(Molin Li)" 

27__copyright__ = "Copyright 2024 Colour Developers" 

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

29__maintainer__ = "Colour Developers" 

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

31__status__ = "Production" 

32 

33__all__ = ["TestXYZ_to_sCAM", "TestsCAM_to_XYZ"] 

34 

35 

36class TestXYZ_to_sCAM: 

37 """ 

38 Define :func:`colour.appearance.scam.XYZ_to_sCAM` definition unit 

39 tests methods. 

40 """ 

41 

42 def test_XYZ_to_sCAM(self) -> None: 

43 """ 

44 Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition. 

45 """ 

46 

47 XYZ = np.array([19.01, 20.00, 21.78]) 

48 XYZ_w = np.array([95.05, 100.00, 108.88]) 

49 L_A = 318.31 

50 Y_b = 20 

51 surround = VIEWING_CONDITIONS_sCAM["Average"] 

52 np.testing.assert_allclose( 

53 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), 

54 np.array( 

55 [ 

56 49.97956680, 

57 0.01405311, 

58 328.27249244, 

59 195.23024234, 

60 0.00502448, 

61 363.60134377, 

62 np.nan, 

63 49.97957273, 

64 50.02042727, 

65 34.97343274, 

66 65.02656726, 

67 ] 

68 ), 

69 atol=TOLERANCE_ABSOLUTE_TESTS, 

70 ) 

71 

72 XYZ = np.array([57.06, 43.06, 31.96]) 

73 L_A = 31.83 

74 np.testing.assert_allclose( 

75 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), 

76 np.array( 

77 [ 

78 71.63079886, 

79 37.33838127, 

80 18.75135858, 

81 259.13174065, 

82 10.66713872, 

83 4.20415978, 

84 np.nan, 

85 96.50614225, 

86 3.49385775, 

87 28.37649889, 

88 71.62350111, 

89 ] 

90 ), 

91 atol=TOLERANCE_ABSOLUTE_TESTS, 

92 ) 

93 

94 XYZ = np.array([3.53, 6.56, 2.14]) 

95 XYZ_w = np.array([109.85, 100, 35.58]) 

96 L_A = 318.31 

97 np.testing.assert_allclose( 

98 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), 

99 np.array( 

100 [ 

101 29.61821869, 

102 25.97461207, 

103 178.56952253, 

104 115.69472052, 

105 10.76901611, 

106 227.46922207, 

107 np.nan, 

108 53.86353400, 

109 46.13646600, 

110 -0.97480767, 

111 100.97480767, 

112 ] 

113 ), 

114 atol=TOLERANCE_ABSOLUTE_TESTS, 

115 ) 

116 

117 def test_n_dimensional_XYZ_to_sCAM(self) -> None: 

118 """ 

119 Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition 

120 n-dimensional support. 

121 """ 

122 

123 XYZ = np.array([19.01, 20.00, 21.78]) 

124 XYZ_w = np.array([95.05, 100.00, 108.88]) 

125 L_A = 318.31 

126 Y_b = 20 

127 surround = VIEWING_CONDITIONS_sCAM["Average"] 

128 specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) 

129 

130 XYZ = np.tile(XYZ, (6, 1)) 

131 specification = np.tile(specification, (6, 1)) 

132 np.testing.assert_allclose( 

133 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), 

134 specification, 

135 atol=TOLERANCE_ABSOLUTE_TESTS, 

136 ) 

137 

138 XYZ_w = np.tile(XYZ_w, (6, 1)) 

139 np.testing.assert_allclose( 

140 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), 

141 specification, 

142 atol=TOLERANCE_ABSOLUTE_TESTS, 

143 ) 

144 

145 XYZ = np.reshape(XYZ, (2, 3, 3)) 

146 XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) 

147 specification = np.reshape(specification, (2, 3, 11)) 

148 np.testing.assert_allclose( 

149 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), 

150 specification, 

151 atol=TOLERANCE_ABSOLUTE_TESTS, 

152 ) 

153 

154 @ignore_numpy_errors 

155 def test_domain_range_scale_XYZ_to_sCAM(self) -> None: 

156 """ 

157 Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition 

158 domain and range scale support. 

159 """ 

160 

161 XYZ = np.array([19.01, 20.00, 21.78]) 

162 XYZ_w = np.array([95.05, 100.00, 108.88]) 

163 L_A = 318.31 

164 Y_b = 20 

165 surround = VIEWING_CONDITIONS_sCAM["Average"] 

166 

167 specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) 

168 

169 d_r = ( 

170 ("reference", 1, 1), 

171 ( 

172 "1", 

173 0.01, 

174 np.array( 

175 [ 

176 1 / 100, 

177 1 / 100, 

178 1 / 360, 

179 1 / 100, 

180 1 / 100, 

181 1 / 400, 

182 1, 

183 1 / 100, 

184 1 / 100, 

185 1 / 100, 

186 1 / 100, 

187 ] 

188 ), 

189 ), 

190 ( 

191 "100", 

192 1, 

193 np.array([1, 1, 100 / 360, 1, 1, 100 / 400, 1, 1, 1, 1, 1]), 

194 ), 

195 ) 

196 

197 for scale, factor_a, factor_b in d_r: 

198 with domain_range_scale(scale): 

199 np.testing.assert_allclose( 

200 XYZ_to_sCAM(XYZ * factor_a, XYZ_w * factor_a, L_A, Y_b, surround), 

201 as_float_array(specification) * factor_b, 

202 atol=TOLERANCE_ABSOLUTE_TESTS, 

203 ) 

204 

205 @ignore_numpy_errors 

206 def test_nan_XYZ_to_sCAM(self) -> None: 

207 """ 

208 Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition 

209 nan support. 

210 """ 

211 

212 cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] 

213 cases = np.array(list(set(product(cases, repeat=3)))) 

214 surround = VIEWING_CONDITIONS_sCAM["Average"] 

215 XYZ_to_sCAM(cases, cases, cases[..., 0], cases[..., 0], surround) 

216 

217 

218class TestsCAM_to_XYZ: 

219 """ 

220 Define :func:`colour.appearance.scam.sCAM_to_XYZ` definition unit 

221 tests methods. 

222 """ 

223 

224 def test_sCAM_to_XYZ(self) -> None: 

225 """ 

226 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition. 

227 """ 

228 

229 specification = CAM_Specification_sCAM(49.97956680, 0.01405311, 328.27249244) 

230 XYZ_w = np.array([95.05, 100.00, 108.88]) 

231 L_A = 318.31 

232 Y_b = 20 

233 surround = VIEWING_CONDITIONS_sCAM["Average"] 

234 np.testing.assert_allclose( 

235 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

236 np.array([19.01, 20.00, 21.78]), 

237 atol=TOLERANCE_ABSOLUTE_TESTS, 

238 ) 

239 

240 specification = CAM_Specification_sCAM(71.63079886, 37.33838127, 18.75135858) 

241 L_A = 31.83 

242 np.testing.assert_allclose( 

243 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

244 np.array([57.06, 43.06, 31.96]), 

245 atol=TOLERANCE_ABSOLUTE_TESTS, 

246 ) 

247 

248 specification = CAM_Specification_sCAM(29.61821869, 25.97461207, 178.56952253) 

249 XYZ_w = np.array([109.85, 100, 35.58]) 

250 L_A = 318.31 

251 np.testing.assert_allclose( 

252 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

253 np.array([3.53256359, 6.56009775, 2.15585716]), 

254 atol=TOLERANCE_ABSOLUTE_TESTS, 

255 ) 

256 

257 def test_n_dimensional_sCAM_to_XYZ(self) -> None: 

258 """ 

259 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition 

260 n-dimensional support. 

261 """ 

262 

263 XYZ = np.array([19.01, 20.00, 21.78]) 

264 XYZ_w = np.array([95.05, 100.00, 108.88]) 

265 L_A = 318.31 

266 Y_b = 20 

267 surround = VIEWING_CONDITIONS_sCAM["Average"] 

268 specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) 

269 XYZ = sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) 

270 

271 specification = CAM_Specification_sCAM( 

272 *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() 

273 ) 

274 XYZ = np.tile(XYZ, (6, 1)) 

275 np.testing.assert_allclose( 

276 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

277 XYZ, 

278 atol=TOLERANCE_ABSOLUTE_TESTS, 

279 ) 

280 

281 XYZ_w = np.tile(XYZ_w, (6, 1)) 

282 np.testing.assert_allclose( 

283 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

284 XYZ, 

285 atol=TOLERANCE_ABSOLUTE_TESTS, 

286 ) 

287 

288 specification = CAM_Specification_sCAM( 

289 *tsplit(np.reshape(specification, (2, 3, 11))).tolist() 

290 ) 

291 XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) 

292 XYZ = np.reshape(XYZ, (2, 3, 3)) 

293 np.testing.assert_allclose( 

294 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

295 XYZ, 

296 atol=TOLERANCE_ABSOLUTE_TESTS, 

297 ) 

298 

299 @ignore_numpy_errors 

300 def test_domain_range_scale_sCAM_to_XYZ(self) -> None: 

301 """ 

302 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition 

303 domain and range scale support. 

304 """ 

305 

306 XYZ = np.array([19.01, 20.00, 21.78]) 

307 XYZ_w = np.array([95.05, 100.00, 108.88]) 

308 L_A = 318.31 

309 Y_b = 20 

310 surround = VIEWING_CONDITIONS_sCAM["Average"] 

311 specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) 

312 XYZ = sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) 

313 

314 d_r = ( 

315 ("reference", 1, 1), 

316 ( 

317 "1", 

318 np.array( 

319 [ 

320 1 / 100, 

321 1 / 100, 

322 1 / 360, 

323 1 / 100, 

324 1 / 100, 

325 1 / 400, 

326 1, 

327 1 / 100, 

328 1 / 100, 

329 1 / 100, 

330 1 / 100, 

331 ] 

332 ), 

333 0.01, 

334 ), 

335 ( 

336 "100", 

337 np.array([1, 1, 100 / 360, 1, 1, 100 / 400, 1, 1, 1, 1, 1]), 

338 1, 

339 ), 

340 ) 

341 for scale, factor_a, factor_b in d_r: 

342 with domain_range_scale(scale): 

343 np.testing.assert_allclose( 

344 sCAM_to_XYZ( 

345 specification * factor_a, 

346 XYZ_w * factor_b, 

347 L_A, 

348 Y_b, 

349 surround, 

350 ), 

351 XYZ * factor_b, 

352 atol=TOLERANCE_ABSOLUTE_TESTS, 

353 ) 

354 

355 @ignore_numpy_errors 

356 def test_raise_exception_sCAM_to_XYZ(self) -> None: 

357 """ 

358 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition 

359 raised exception. 

360 """ 

361 XYZ_w = np.array([95.05, 100.00, 108.88]) 

362 L_A = 318.31 

363 Y_b = 20 

364 surround = VIEWING_CONDITIONS_sCAM["Average"] 

365 

366 with pytest.raises(ValueError): 

367 sCAM_to_XYZ( 

368 CAM_Specification_sCAM(J=None, C=20.0, h=210.0), 

369 XYZ_w, 

370 L_A, 

371 Y_b, 

372 surround, 

373 ) 

374 

375 with pytest.raises(ValueError): 

376 sCAM_to_XYZ( 

377 CAM_Specification_sCAM(J=40.0, C=20.0, h=None), 

378 XYZ_w, 

379 L_A, 

380 Y_b, 

381 surround, 

382 ) 

383 

384 with pytest.raises(ValueError): 

385 sCAM_to_XYZ( 

386 CAM_Specification_sCAM(J=40.0, C=None, h=210.0, M=None), 

387 XYZ_w, 

388 L_A, 

389 Y_b, 

390 surround, 

391 ) 

392 

393 @ignore_numpy_errors 

394 def test_nan_sCAM_to_XYZ(self) -> None: 

395 """ 

396 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition nan 

397 support. 

398 """ 

399 

400 cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] 

401 cases = np.array(list(set(product(cases, repeat=3)))) 

402 surround = VIEWING_CONDITIONS_sCAM["Average"] 

403 sCAM_to_XYZ( 

404 CAM_Specification_sCAM(cases[..., 0], cases[..., 0], cases[..., 0], M=50), 

405 cases, 

406 cases[..., 0], 

407 cases[..., 0], 

408 surround, 

409 )