Coverage for colour/appearance/tests/test_cam16.py: 100%

131 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.appearance.cam16` module.""" 

2 

3from __future__ import annotations 

4 

5from itertools import product 

6 

7import numpy as np 

8import pytest 

9 

10from colour.appearance import ( 

11 VIEWING_CONDITIONS_CAM16, 

12 CAM16_to_XYZ, 

13 CAM_Specification_CAM16, 

14 InductionFactors_CAM16, 

15 XYZ_to_CAM16, 

16) 

17from colour.constants import TOLERANCE_ABSOLUTE_TESTS 

18from colour.utilities import ( 

19 as_float_array, 

20 domain_range_scale, 

21 ignore_numpy_errors, 

22 tsplit, 

23) 

24 

25__author__ = "Colour Developers" 

26__copyright__ = "Copyright 2013 Colour Developers" 

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

28__maintainer__ = "Colour Developers" 

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

30__status__ = "Production" 

31 

32__all__ = [ 

33 "TestXYZ_to_CAM16", 

34 "TestCAM16_to_XYZ", 

35] 

36 

37 

38class TestXYZ_to_CAM16: 

39 """ 

40 Define :func:`colour.appearance.cam16.XYZ_to_CAM16` definition unit 

41 tests methods. 

42 """ 

43 

44 def test_XYZ_to_CAM16(self) -> None: 

45 """Test :func:`colour.appearance.cam16.XYZ_to_CAM16` definition.""" 

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_CAM16["Average"] 

52 np.testing.assert_allclose( 

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

54 np.array( 

55 [ 

56 41.73120791, 

57 0.10335574, 

58 217.06795977, 

59 2.34501507, 

60 195.37170899, 

61 0.10743677, 

62 275.59498615, 

63 np.nan, 

64 ] 

65 ), 

66 atol=TOLERANCE_ABSOLUTE_TESTS, 

67 ) 

68 

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

70 L_A = 31.83 

71 np.testing.assert_allclose( 

72 XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), 

73 np.array( 

74 [ 

75 65.42828069, 

76 49.67956420, 

77 17.48659243, 

78 52.94308868, 

79 152.06985268, 

80 42.62473321, 

81 398.03047943, 

82 np.nan, 

83 ] 

84 ), 

85 atol=TOLERANCE_ABSOLUTE_TESTS, 

86 ) 

87 

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

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

90 L_A = 318.31 

91 np.testing.assert_allclose( 

92 XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), 

93 np.array( 

94 [ 

95 21.36052893, 

96 50.99381895, 

97 178.86724266, 

98 61.57953092, 

99 139.78582768, 

100 53.00732582, 

101 223.01823806, 

102 np.nan, 

103 ] 

104 ), 

105 atol=TOLERANCE_ABSOLUTE_TESTS, 

106 ) 

107 

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

109 L_A = 318.31 

110 np.testing.assert_allclose( 

111 XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), 

112 np.array( 

113 [ 

114 41.36326063, 

115 52.81154022, 

116 258.88676291, 

117 53.12406914, 

118 194.52011798, 

119 54.89682038, 

120 311.24768647, 

121 np.nan, 

122 ] 

123 ), 

124 atol=TOLERANCE_ABSOLUTE_TESTS, 

125 ) 

126 

127 XYZ = np.array([61.45276998, 7.00421901, 82.2406738]) 

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

129 L_A = 4.074366543152521 

130 np.testing.assert_allclose( 

131 XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), 

132 np.array( 

133 [ 

134 21.03801957, 

135 457.78881613, 

136 350.06445098, 

137 241.50642846, 

138 56.74143988, 

139 330.94646237, 

140 376.43915877, 

141 np.nan, 

142 ] 

143 ), 

144 atol=TOLERANCE_ABSOLUTE_TESTS, 

145 ) 

146 

147 def test_n_dimensional_XYZ_to_CAM16(self) -> None: 

148 """ 

149 Test :func:`colour.appearance.cam16.XYZ_to_CAM16` definition 

150 n-dimensional support. 

151 """ 

152 

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

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

155 L_A = 318.31 

156 Y_b = 20 

157 surround = VIEWING_CONDITIONS_CAM16["Average"] 

158 specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) 

159 

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

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

162 np.testing.assert_allclose( 

163 XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), 

164 specification, 

165 atol=TOLERANCE_ABSOLUTE_TESTS, 

166 ) 

167 

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

169 np.testing.assert_allclose( 

170 XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), 

171 specification, 

172 atol=TOLERANCE_ABSOLUTE_TESTS, 

173 ) 

174 

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

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

177 specification = np.reshape(specification, (2, 3, 8)) 

178 np.testing.assert_allclose( 

179 XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), 

180 specification, 

181 atol=TOLERANCE_ABSOLUTE_TESTS, 

182 ) 

183 

184 @ignore_numpy_errors 

185 def test_domain_range_scale_XYZ_to_CAM16(self) -> None: 

186 """ 

187 Test :func:`colour.appearance.cam16.XYZ_to_CAM16` definition domain 

188 and range scale support. 

189 """ 

190 

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

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

193 L_A = 318.31 

194 Y_b = 20 

195 surround = VIEWING_CONDITIONS_CAM16["Average"] 

196 specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) 

197 

198 d_r = ( 

199 ("reference", 1, 1), 

200 ( 

201 "1", 

202 0.01, 

203 np.array( 

204 [ 

205 1 / 100, 

206 1 / 100, 

207 1 / 360, 

208 1 / 100, 

209 1 / 100, 

210 1 / 100, 

211 1 / 400, 

212 np.nan, 

213 ] 

214 ), 

215 ), 

216 ( 

217 "100", 

218 1, 

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

220 ), 

221 ) 

222 for scale, factor_a, factor_b in d_r: 

223 with domain_range_scale(scale): 

224 np.testing.assert_allclose( 

225 XYZ_to_CAM16(XYZ * factor_a, XYZ_w * factor_a, L_A, Y_b, surround), 

226 as_float_array(specification) * factor_b, 

227 atol=TOLERANCE_ABSOLUTE_TESTS, 

228 ) 

229 

230 @ignore_numpy_errors 

231 def test_nan_XYZ_to_CAM16(self) -> None: 

232 """ 

233 Test :func:`colour.appearance.cam16.XYZ_to_CAM16` definition 

234 nan support. 

235 """ 

236 

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

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

239 surround = InductionFactors_CAM16(cases[0, 0], cases[0, 0], cases[0, 0]) 

240 XYZ_to_CAM16(cases, cases, cases[..., 0], cases[..., 0], surround) 

241 

242 

243class TestCAM16_to_XYZ: 

244 """ 

245 Define :func:`colour.appearance.cam16.CAM16_to_XYZ` definition unit tests 

246 methods. 

247 """ 

248 

249 def test_CAM16_to_XYZ(self) -> None: 

250 """Test :func:`colour.appearance.cam16.CAM16_to_XYZ` definition.""" 

251 

252 specification = CAM_Specification_CAM16(41.73120791, 0.10335574, 217.06795977) 

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

254 L_A = 318.31 

255 Y_b = 20 

256 surround = VIEWING_CONDITIONS_CAM16["Average"] 

257 np.testing.assert_allclose( 

258 CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

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

260 atol=TOLERANCE_ABSOLUTE_TESTS, 

261 ) 

262 

263 specification = CAM_Specification_CAM16(65.42828069, 49.67956420, 17.48659243) 

264 L_A = 31.83 

265 np.testing.assert_allclose( 

266 CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

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

268 atol=TOLERANCE_ABSOLUTE_TESTS, 

269 ) 

270 

271 specification = CAM_Specification_CAM16(21.36052893, 50.99381895, 178.86724266) 

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

273 L_A = 318.31 

274 np.testing.assert_allclose( 

275 CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

276 np.array([3.53, 6.56, 2.14]), 

277 atol=TOLERANCE_ABSOLUTE_TESTS, 

278 ) 

279 

280 specification = CAM_Specification_CAM16(41.36326063, 52.81154022, 258.88676291) 

281 L_A = 318.31 

282 np.testing.assert_allclose( 

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

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

285 atol=TOLERANCE_ABSOLUTE_TESTS, 

286 ) 

287 

288 specification = CAM_Specification_CAM16(21.03801957, 457.78881613, 350.06445098) 

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

290 L_A = 4.074366543152521 

291 np.testing.assert_allclose( 

292 CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

293 np.array([61.45276998, 7.00421901, 82.2406738]), 

294 atol=TOLERANCE_ABSOLUTE_TESTS, 

295 ) 

296 

297 def test_n_dimensional_CAM16_to_XYZ(self) -> None: 

298 """ 

299 Test :func:`colour.appearance.cam16.CAM16_to_XYZ` definition 

300 n-dimensional support. 

301 """ 

302 

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

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

305 L_A = 318.31 

306 Y_b = 20 

307 surround = VIEWING_CONDITIONS_CAM16["Average"] 

308 specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) 

309 XYZ = CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) 

310 

311 specification = CAM_Specification_CAM16( 

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

313 ) 

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

315 np.testing.assert_allclose( 

316 CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

317 XYZ, 

318 atol=TOLERANCE_ABSOLUTE_TESTS, 

319 ) 

320 

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

322 np.testing.assert_allclose( 

323 CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

324 XYZ, 

325 atol=TOLERANCE_ABSOLUTE_TESTS, 

326 ) 

327 

328 specification = CAM_Specification_CAM16( 

329 *tsplit(np.reshape(specification, (2, 3, 8))).tolist() 

330 ) 

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

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

333 np.testing.assert_allclose( 

334 CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), 

335 XYZ, 

336 atol=TOLERANCE_ABSOLUTE_TESTS, 

337 ) 

338 

339 @ignore_numpy_errors 

340 def test_domain_range_scale_CAM16_to_XYZ(self) -> None: 

341 """ 

342 Test :func:`colour.appearance.cam16.CAM16_to_XYZ` definition domain 

343 and range scale support. 

344 """ 

345 

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

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

348 L_A = 318.31 

349 Y_b = 20 

350 surround = VIEWING_CONDITIONS_CAM16["Average"] 

351 specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) 

352 XYZ = CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) 

353 

354 d_r = ( 

355 ("reference", 1, 1), 

356 ( 

357 "1", 

358 np.array( 

359 [ 

360 1 / 100, 

361 1 / 100, 

362 1 / 360, 

363 1 / 100, 

364 1 / 100, 

365 1 / 100, 

366 1 / 400, 

367 np.nan, 

368 ] 

369 ), 

370 0.01, 

371 ), 

372 ( 

373 "100", 

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

375 1, 

376 ), 

377 ) 

378 for scale, factor_a, factor_b in d_r: 

379 with domain_range_scale(scale): 

380 np.testing.assert_allclose( 

381 CAM16_to_XYZ( 

382 specification * factor_a, 

383 XYZ_w * factor_b, 

384 L_A, 

385 Y_b, 

386 surround, 

387 ), 

388 XYZ * factor_b, 

389 atol=TOLERANCE_ABSOLUTE_TESTS, 

390 ) 

391 

392 @ignore_numpy_errors 

393 def test_raise_exception_CAM16_to_XYZ(self) -> None: 

394 """ 

395 Test :func:`colour.appearance.cam16.CAM16_to_XYZ` definition raised 

396 exception. 

397 """ 

398 

399 pytest.raises( 

400 ValueError, 

401 CAM16_to_XYZ, 

402 CAM_Specification_CAM16(41.731207905126638, None, 217.06795976739301), 

403 np.array([95.05, 100.00, 108.88]), 

404 318.31, 

405 20.0, 

406 VIEWING_CONDITIONS_CAM16["Average"], 

407 ) 

408 

409 @ignore_numpy_errors 

410 def test_nan_CAM16_to_XYZ(self) -> None: 

411 """ 

412 Test :func:`colour.appearance.cam16.CAM16_to_XYZ` definition nan 

413 support. 

414 """ 

415 

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

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

418 surround = InductionFactors_CAM16(cases[0, 0], cases[0, 0], cases[0, 0]) 

419 CAM16_to_XYZ( 

420 CAM_Specification_CAM16(cases[..., 0], cases[..., 0], cases[..., 0], M=50), 

421 cases, 

422 cases[..., 0], 

423 cases[..., 0], 

424 surround, 

425 )