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

146 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.zcam` module. 

3""" 

4 

5from __future__ import annotations 

6 

7from itertools import permutations 

8 

9import numpy as np 

10import pytest 

11 

12from colour.appearance import ( 

13 VIEWING_CONDITIONS_ZCAM, 

14 CAM_Specification_ZCAM, 

15 InductionFactors_ZCAM, 

16 XYZ_to_ZCAM, 

17 ZCAM_to_XYZ, 

18) 

19from colour.utilities import ( 

20 as_float_array, 

21 domain_range_scale, 

22 ignore_numpy_errors, 

23 tsplit, 

24) 

25 

26__author__ = "Colour Developers" 

27__copyright__ = "Copyright 2013 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_ZCAM", "TestZCAM_to_XYZ"] 

34 

35 

36class TestXYZ_to_ZCAM: 

37 """ 

38 Defines :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition unit tests 

39 methods. 

40 """ 

41 

42 def test_XYZ_to_ZCAM(self) -> None: 

43 """ 

44 Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition. 

45 """ 

46 

47 XYZ = np.array([185, 206, 163]) 

48 XYZ_w = np.array([256, 264, 202]) 

49 L_a = 264 

50 Y_b = 100 

51 surround = VIEWING_CONDITIONS_ZCAM["Average"] 

52 np.testing.assert_allclose( 

53 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), 

54 np.array( 

55 [ 

56 92.2520, 

57 3.0216, 

58 196.3524, 

59 19.1314, 

60 321.3464, 

61 10.5252, 

62 237.6401, 

63 np.nan, 

64 34.7022, 

65 25.2994, 

66 91.6837, 

67 ] 

68 ), 

69 rtol=0.025, 

70 atol=0.025, 

71 ) 

72 

73 XYZ = np.array([89, 96, 120]) 

74 np.testing.assert_allclose( 

75 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), 

76 np.array( 

77 [ 

78 71.2071, 

79 6.8539, 

80 250.6422, 

81 32.7963, 

82 248.0394, 

83 23.8744, 

84 307.0595, 

85 np.nan, 

86 18.2796, 

87 40.4621, 

88 70.4026, 

89 ] 

90 ), 

91 rtol=0.025, 

92 atol=0.025, 

93 ) 

94 

95 # NOTE: Hue quadrature :math:`H_z` is significantly different for this 

96 # test, i.e., 47.748252 vs 43.8258. 

97 # NOTE: :math:`F_L` as reported in the supplemental document has the 

98 # same value as for :math:`L_a` = 264 instead of 150. The values seem 

99 # to be computed for :math:`L_a` = 264 and :math:`Y_b` = 100. 

100 XYZ = np.array([79, 81, 62]) 

101 # L_a = 150 

102 # Y_b = 60 

103 surround = VIEWING_CONDITIONS_ZCAM["Dim"] 

104 np.testing.assert_allclose( 

105 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), 

106 np.array( 

107 [ 

108 68.8890, 

109 0.9774, 

110 58.7532, 

111 12.5916, 

112 196.7686, 

113 2.7918, 

114 43.8258, 

115 np.nan, 

116 11.0371, 

117 44.4143, 

118 68.8737, 

119 ] 

120 ), 

121 rtol=0.025, 

122 atol=4, 

123 ) 

124 

125 XYZ = np.array([910, 1114, 500]) 

126 XYZ_w = np.array([2103, 2259, 1401]) 

127 L_a = 359 

128 Y_b = 16 

129 surround = VIEWING_CONDITIONS_ZCAM["Dark"] 

130 np.testing.assert_allclose( 

131 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), 

132 np.array( 

133 [ 

134 82.6445, 

135 13.0838, 

136 123.9464, 

137 44.7277, 

138 114.7431, 

139 18.1655, 

140 178.6422, 

141 np.nan, 

142 34.4874, 

143 26.8778, 

144 78.2653, 

145 ] 

146 ), 

147 rtol=0.025, 

148 atol=0.025, 

149 ) 

150 

151 XYZ = np.array([96, 67, 28]) 

152 np.testing.assert_allclose( 

153 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), 

154 np.array( 

155 [ 

156 33.0139, 

157 19.4070, 

158 389.7720 % 360, 

159 86.1882, 

160 45.8363, 

161 26.9446, 

162 397.3301, 

163 np.nan, 

164 43.6447, 

165 47.9942, 

166 30.2593, 

167 ] 

168 ), 

169 rtol=0.025, 

170 atol=0.025, 

171 ) 

172 

173 def test_n_dimensional_XYZ_to_ZCAM(self) -> None: 

174 """ 

175 Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition 

176 n-dimensional support. 

177 """ 

178 

179 XYZ = np.array([185, 206, 163]) 

180 XYZ_w = np.array([256, 264, 202]) 

181 L_a = 264 

182 Y_b = 100 

183 surround = VIEWING_CONDITIONS_ZCAM["Average"] 

184 specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround) 

185 

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

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

188 np.testing.assert_almost_equal( 

189 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7 

190 ) 

191 

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

193 np.testing.assert_almost_equal( 

194 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7 

195 ) 

196 

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

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

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

200 np.testing.assert_almost_equal( 

201 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7 

202 ) 

203 

204 @ignore_numpy_errors 

205 def test_domain_range_scale_XYZ_to_ZCAM(self) -> None: 

206 """ 

207 Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition 

208 domain and range scale support. 

209 """ 

210 

211 XYZ = np.array([185, 206, 163]) 

212 XYZ_w = np.array([256, 264, 202]) 

213 L_a = 264 

214 Y_b = 100 

215 surround = VIEWING_CONDITIONS_ZCAM["Average"] 

216 specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround) 

217 

218 d_r = ( 

219 ("reference", 1, 1), 

220 ("1", 1, np.array([1, 1, 1 / 360, 1, 1, 1, 1 / 400, np.nan, 1, 1, 1])), 

221 ( 

222 "100", 

223 100, 

224 np.array( 

225 [ 

226 100, 

227 100, 

228 100 / 360, 

229 100, 

230 100, 

231 100, 

232 100 / 400, 

233 np.nan, 

234 100, 

235 100, 

236 100, 

237 ] 

238 ), 

239 ), 

240 ) 

241 for scale, factor_a, factor_b in d_r: 

242 with domain_range_scale(scale): 

243 np.testing.assert_almost_equal( 

244 XYZ_to_ZCAM(XYZ * factor_a, XYZ_w * factor_a, L_a, Y_b, surround), 

245 as_float_array(specification) * factor_b, 

246 decimal=7, 

247 ) 

248 

249 @ignore_numpy_errors 

250 def test_nan_XYZ_to_ZCAM(self) -> None: 

251 """ 

252 Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition 

253 nan support. 

254 """ 

255 

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

257 cases = set(permutations(cases * 3, r=3)) 

258 for case in cases: 

259 XYZ = np.array(case) 

260 XYZ_w = np.array(case) 

261 L_a = case[0] 

262 Y_b = 100 

263 surround = InductionFactors_ZCAM(case[0], case[0], case[0], case[0]) 

264 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround) 

265 

266 

267class TestZCAM_to_XYZ: 

268 """ 

269 Defines :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition unit 

270 tests methods. 

271 """ 

272 

273 def test_ZCAM_to_XYZ(self) -> None: 

274 """ 

275 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition. 

276 """ 

277 

278 specification = CAM_Specification_ZCAM( 

279 92.2520, 

280 3.0216, 

281 196.3524, 

282 19.1314, 

283 321.3464, 

284 10.5252, 

285 237.6401, 

286 np.nan, 

287 34.7022, 

288 25.2994, 

289 91.6837, 

290 ) 

291 XYZ_w = np.array([256, 264, 202]) 

292 L_a = 264 

293 Y_b = 100 

294 surround = VIEWING_CONDITIONS_ZCAM["Average"] 

295 np.testing.assert_allclose( 

296 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), 

297 np.array([185, 206, 163]), 

298 atol=0.01, 

299 rtol=0.01, 

300 ) 

301 

302 specification = CAM_Specification_ZCAM( 

303 71.2071, 

304 6.8539, 

305 250.6422, 

306 32.7963, 

307 248.0394, 

308 23.8744, 

309 307.0595, 

310 np.nan, 

311 18.2796, 

312 40.4621, 

313 70.4026, 

314 ) 

315 np.testing.assert_allclose( 

316 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), 

317 np.array([89, 96, 120]), 

318 atol=0.01, 

319 rtol=0.01, 

320 ) 

321 

322 specification = CAM_Specification_ZCAM( 

323 68.8890, 

324 0.9774, 

325 58.7532, 

326 12.5916, 

327 196.7686, 

328 2.7918, 

329 43.8258, 

330 np.nan, 

331 11.0371, 

332 44.4143, 

333 68.8737, 

334 ) 

335 surround = VIEWING_CONDITIONS_ZCAM["Dim"] 

336 np.testing.assert_allclose( 

337 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), 

338 np.array([79, 81, 62]), 

339 atol=0.01, 

340 rtol=0.01, 

341 ) 

342 

343 specification = CAM_Specification_ZCAM( 

344 82.6445, 

345 13.0838, 

346 123.9464, 

347 44.7277, 

348 114.7431, 

349 18.1655, 

350 178.6422, 

351 np.nan, 

352 34.4874, 

353 26.8778, 

354 78.2653, 

355 ) 

356 XYZ_w = np.array([2103, 2259, 1401]) 

357 L_a = 359 

358 Y_b = 16 

359 surround = VIEWING_CONDITIONS_ZCAM["Dark"] 

360 np.testing.assert_allclose( 

361 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), 

362 np.array([910, 1114, 500]), 

363 atol=0.01, 

364 rtol=0.01, 

365 ) 

366 

367 specification = CAM_Specification_ZCAM( 

368 33.0139, 

369 19.4070, 

370 389.7720 % 360, 

371 86.1882, 

372 45.8363, 

373 26.9446, 

374 397.3301, 

375 np.nan, 

376 43.6447, 

377 47.9942, 

378 30.2593, 

379 ) 

380 np.testing.assert_allclose( 

381 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), 

382 np.array([96, 67, 28]), 

383 atol=0.01, 

384 rtol=0.01, 

385 ) 

386 

387 # Test using C instead of M 

388 specification = CAM_Specification_ZCAM( 

389 J=82.61980483202505, C=13.194790413382647, h=123.77987744640157 

390 ) 

391 XYZ_w = np.array([2103, 2259, 1401]) 

392 L_a = 359 

393 Y_b = 16 

394 surround = VIEWING_CONDITIONS_ZCAM["Dark"] 

395 np.testing.assert_allclose( 

396 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), 

397 np.array([910, 1114, 500]), 

398 atol=0.01, 

399 rtol=0.01, 

400 ) 

401 

402 def test_n_dimensional_ZCAM_to_XYZ(self) -> None: 

403 """ 

404 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition 

405 n-dimensional support. 

406 """ 

407 

408 XYZ = np.array([185, 206, 163]) 

409 XYZ_w = np.array([256, 264, 202]) 

410 L_a = 264 

411 Y_b = 100 

412 surround = VIEWING_CONDITIONS_ZCAM["Average"] 

413 specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround) 

414 XYZ = ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround) 

415 

416 specification = CAM_Specification_ZCAM( 

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

418 ) 

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

420 np.testing.assert_almost_equal( 

421 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7 

422 ) 

423 

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

425 np.testing.assert_almost_equal( 

426 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7 

427 ) 

428 

429 specification = CAM_Specification_ZCAM( 

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

431 ) 

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

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

434 np.testing.assert_almost_equal( 

435 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7 

436 ) 

437 

438 @ignore_numpy_errors 

439 def test_domain_range_scale_ZCAM_to_XYZ(self) -> None: 

440 """ 

441 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition 

442 domain and range scale support. 

443 """ 

444 

445 XYZ_i = np.array([185, 206, 163]) 

446 XYZ_w = np.array([256, 264, 202]) 

447 L_a = 264 

448 Y_b = 100 

449 surround = VIEWING_CONDITIONS_ZCAM["Average"] 

450 specification = XYZ_to_ZCAM(XYZ_i, XYZ_w, L_a, Y_b, surround) 

451 XYZ = ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround) 

452 

453 d_r = ( 

454 ("reference", 1, 1), 

455 ("1", np.array([1, 1, 1 / 360, 1, 1, 1, 1 / 400, np.nan, 1, 1, 1]), 1), 

456 ( 

457 "100", 

458 np.array( 

459 [ 

460 100, 

461 100, 

462 100 / 360, 

463 100, 

464 100, 

465 100, 

466 100 / 400, 

467 np.nan, 

468 100, 

469 100, 

470 100, 

471 ] 

472 ), 

473 100, 

474 ), 

475 ) 

476 for scale, factor_a, factor_b in d_r: 

477 with domain_range_scale(scale): 

478 np.testing.assert_almost_equal( 

479 ZCAM_to_XYZ( 

480 specification * factor_a, XYZ_w * factor_b, L_a, Y_b, surround 

481 ), 

482 XYZ * factor_b, 

483 decimal=7, 

484 ) 

485 

486 @ignore_numpy_errors 

487 def test_raise_exception_ZCAM_to_XYZ(self) -> None: 

488 """ 

489 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition 

490 raised exception. 

491 """ 

492 

493 pytest.raises( 

494 ValueError, 

495 ZCAM_to_XYZ, 

496 CAM_Specification_ZCAM( 

497 41.731091132513917, 

498 None, 

499 219.04843265831178, 

500 ), 

501 np.array([256, 264, 202]), 

502 318.31, 

503 20.0, 

504 VIEWING_CONDITIONS_ZCAM["Average"], 

505 ) 

506 

507 @ignore_numpy_errors 

508 def test_nan_ZCAM_to_XYZ(self) -> None: 

509 """ 

510 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition nan 

511 support. 

512 """ 

513 

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

515 cases = set(permutations(cases * 3, r=3)) 

516 for case in cases: 

517 J = case[0] 

518 C = case[0] 

519 h = case[0] 

520 XYZ_w = np.array(case) 

521 L_a = case[0] 

522 Y_b = 100 

523 surround = InductionFactors_ZCAM(case[0], case[0], case[0], case[0]) 

524 ZCAM_to_XYZ( 

525 CAM_Specification_ZCAM(J, C, h, M=50), XYZ_w, L_a, Y_b, surround 

526 )