Coverage for io/tests/test_image.py: 100%

1"""Define the unit tests for the :mod:`colour.io.image` module."""

2

3 from __future__ import annotations

4

5 import os

6 import platform

7 import shutil

8 import tempfile

9

10 import numpy as np

11 import pytest

12

13 from colour.constants import TOLERANCE_ABSOLUTE_TESTS

14 from colour.io import (

15 Image_Specification_Attribute,

16 as_3_channels_image,

17 convert_bit_depth,

18 image_specification_OpenImageIO,

19 read_image,

20 read_image_Imageio,

21 read_image_OpenImageIO,

22 write_image,

23 write_image_Imageio,

24 write_image_OpenImageIO,

25)

26 from colour.utilities import attest, full

27

28 __author__ = "Colour Developers"

29 __copyright__ = "Copyright 2013 Colour Developers"

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

31 __maintainer__ = "Colour Developers"

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

33 __status__ = "Production"

34

35 __all__ = [

36 "ROOT_RESOURCES",

37 "TestImageSpecificationOpenImageIO",

38 "TestConvertBitDepth",

39 "TestReadImageOpenImageIO",

40 "TestWriteImageOpenImageIO",

41 "TestReadImageImageio",

42 "TestWriteImageImageio",

43 "TestReadImage",

44 "TestWriteImage",

45 "TestAs3ChannelsImage",

46]

47

48 ROOT_RESOURCES: str = os.path.join(os.path.dirname(__file__), "resources")

49

50

51@pytest.mark.skipif(

52 platform.system() == "Windows",

53 reason="OpenImageIO crashes on Windows due to thread-safety issues",

54)

55 class TestImageSpecificationOpenImageIO:

56 """

57 Define :func:`colour.io.image.image_specification_OpenImageIO` definition

58 unit tests methods.

59 """

60

61 def test_image_specification_OpenImageIO(self) -> None: # pragma: no cover

62 """

63 Test :func:`colour.io.image.image_specification_OpenImageIO`

64 definition.

65 """

66

67 from OpenImageIO import HALF # noqa: PLC0415

68

69 compression = Image_Specification_Attribute("Compression", "none")

70 specification = image_specification_OpenImageIO(

71 1920, 1080, 3, "float16", [compression]

72 )

73

74 assert specification.width == 1920 # pyright: ignore

75 assert specification.height == 1080 # pyright: ignore

76 assert specification.nchannels == 3 # pyright: ignore

77 assert specification.format == HALF # pyright: ignore

78 assert specification.extra_attribs[0].name == "Compression" # pyright: ignore

79

80

81 class TestConvertBitDepth:

82 """

83 Define :func:`colour.io.image.convert_bit_depth` definition unit tests

84 methods.

85 """

86

87 def test_convert_bit_depth(self) -> None:

88 """Test :func:`colour.io.image.convert_bit_depth` definition."""

89

90 a = np.around(np.linspace(0, 1, 10) * 255).astype("uint8")

91 assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8")

92 np.testing.assert_equal(convert_bit_depth(a, "uint8"), a)

93

94 assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16")

95 np.testing.assert_equal(

96 convert_bit_depth(a, "uint16"),

97 np.array(

98 [

99 0,

100 7196,

101 14649,

102 21845,

103 29041,

104 36494,

105 43690,

106 50886,

107 58339,

108 65535,

109 ]

110 ),

111 )

112

113 assert convert_bit_depth(a, "float16").dtype is np.dtype("float16")

114 np.testing.assert_allclose(

115 convert_bit_depth(a, "float16"),

116 np.array(

117 [

118 0.0000,

119 0.1098,

120 0.2235,

121 0.3333,

122 0.443,

123 0.5566,

124 0.6665,

125 0.7764,

126 0.8900,

127 1.0000,

128 ]

129 ),

130 atol=5e-4,

131 )

132

133 assert convert_bit_depth(a, "float32").dtype is np.dtype("float32")

134 np.testing.assert_allclose(

135 convert_bit_depth(a, "float32"),

136 np.array(

137 [

138 0.00000000,

139 0.10980392,

140 0.22352941,

141 0.33333334,

142 0.44313726,

143 0.55686277,

144 0.66666669,

145 0.77647060,

146 0.89019608,

147 1.00000000,

148 ]

149 ),

150 atol=TOLERANCE_ABSOLUTE_TESTS,

151 )

152

153 assert convert_bit_depth(a, "float64").dtype is np.dtype("float64")

154

155 if hasattr(np, "float128"): # pragma: no cover

156 assert convert_bit_depth(a, "float128").dtype is np.dtype("float128")

157

158 a = np.around(np.linspace(0, 1, 10) * 65535).astype("uint16")

159 assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8")

160 np.testing.assert_equal(

161 convert_bit_depth(a, "uint8"),

162 np.array([0, 28, 56, 85, 113, 141, 170, 198, 226, 255]),

163 )

164

165 assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16")

166 np.testing.assert_equal(convert_bit_depth(a, "uint16"), a)

167

168 assert convert_bit_depth(a, "float16").dtype is np.dtype("float16")

169 np.testing.assert_allclose(

170 convert_bit_depth(a, "float16"),

171 np.array(

172 [

173 0.0000,

174 0.1098,

175 0.2235,

176 0.3333,

177 0.443,

178 0.5566,

179 0.6665,

180 0.7764,

181 0.8900,

182 1.0000,

183 ]

184 ),

185 atol=5e-2,

186 )

187

188 assert convert_bit_depth(a, "float32").dtype is np.dtype("float32")

189 np.testing.assert_allclose(

190 convert_bit_depth(a, "float32"),

191 np.array(

192 [

193 0.00000000,

194 0.11111620,

195 0.22221714,

196 0.33333334,

197 0.44444954,

198 0.55555046,

199 0.66666669,

200 0.77778286,

201 0.88888383,

202 1.00000000,

203 ]

204 ),

205 atol=TOLERANCE_ABSOLUTE_TESTS,

206 )

207

208 assert convert_bit_depth(a, "float64").dtype is np.dtype("float64")

209

210 if hasattr(np, "float128"): # pragma: no cover

211 assert convert_bit_depth(a, "float128").dtype is np.dtype("float128")

212

213 a = np.linspace(0, 1, 10, dtype=np.float64)

214 assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8")

215 np.testing.assert_equal(

216 convert_bit_depth(a, "uint8"),

217 np.array([0, 28, 57, 85, 113, 142, 170, 198, 227, 255]),

218 )

219

220 assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16")

221 np.testing.assert_equal(

222 convert_bit_depth(a, "uint16"),

223 np.array(

224 [

225 0,

226 7282,

227 14563,

228 21845,

229 29127,

230 36408,

231 43690,

232 50972,

233 58253,

234 65535,

235 ]

236 ),

237 )

238

239 assert convert_bit_depth(a, "float16").dtype is np.dtype("float16")

240 np.testing.assert_allclose(

241 convert_bit_depth(a, "float16"),

242 np.array(

243 [

244 0.0000,

245 0.1111,

246 0.2222,

247 0.3333,

248 0.4443,

249 0.5557,

250 0.6665,

251 0.7780,

252 0.8887,

253 1.0000,

254 ]

255 ),

256 atol=5e-4,

257 )

258

259 assert convert_bit_depth(a, "float32").dtype is np.dtype("float32")

260 np.testing.assert_allclose(

261 convert_bit_depth(a, "float32"), a, atol=TOLERANCE_ABSOLUTE_TESTS

262 )

263

264 assert convert_bit_depth(a, "float64").dtype is np.dtype("float64")

265

266 if hasattr(np, "float128"): # pragma: no cover

267 assert convert_bit_depth(a, "float128").dtype is np.dtype("float128")

268

269

270@pytest.mark.skipif(

271 platform.system() == "Windows",

272 reason="OpenImageIO crashes on Windows due to thread-safety issues",

273)

274 class TestReadImageOpenImageIO:

275 """

276 Define :func:`colour.io.image.read_image_OpenImageIO` definition unit

277 tests methods.

278 """

279

280 def test_read_image_OpenImageIO(self) -> None: # pragma: no cover

281 """Test :func:`colour.io.image.read_image_OpenImageIO` definition."""

282

283 image = read_image_OpenImageIO(

284 os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),

285 additional_data=False,

286 )

287 assert image.shape == (1267, 1274, 3)

288 assert image.dtype is np.dtype("float32")

289

290 image = read_image_OpenImageIO(

291 os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),

292 "float16",

293 additional_data=False,

294 )

295 assert image.dtype is np.dtype("float16")

296

297 image, attributes = read_image_OpenImageIO(

298 os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),

299 additional_data=True,

300 )

301 assert image.shape == (1267, 1274, 3)

302 assert len(attributes) > 0

303 compression_attribute = next(

304 (attribute for attribute in attributes if attribute.name == "compression"),

305 None,

306 )

307 assert compression_attribute is not None

308

309 image = read_image_OpenImageIO(

310 os.path.join(ROOT_RESOURCES, "Single_Channel.exr"),

311 additional_data=False,

312 )

313 assert image.shape == (256, 256)

314

315 image = read_image_OpenImageIO(

316 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"),

317 "uint8",

318 additional_data=False,

319 )

320 assert image.shape == (128, 256, 4)

321 assert image.dtype is np.dtype("uint8")

322 assert np.min(image) == 0

323 assert np.max(image) == 255

324

325 image = read_image_OpenImageIO(

326 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"),

327 "uint16",

328 additional_data=False,

329 )

330 assert image.shape == (128, 256, 4)

331 assert image.dtype is np.dtype("uint16")

332 assert np.min(image) == 0

333 assert np.max(image) == 65535

334

335 # TODO: Investigate "OIIO" behaviour here: 1.0 != 15360.0

336 # image = read_image_OpenImageIO(

337 # os.path.join(ROOT_RESOURCES, 'Colour_Logo.png'), 'float16')

338 # self.assertIs(image.dtype, np.dtype('float16'))

339 # self.assertEqual(np.min(image), 0.0)

340 # self.assertEqual(np.max(image), 1.0)

341

342 image = read_image_OpenImageIO(

343 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"),

344 "float32",

345 additional_data=False,

346 )

347 assert image.dtype is np.dtype("float32")

348 assert np.min(image) == 0.0

349 assert np.max(image) == 1.0

350

351

352@pytest.mark.skipif(

353 platform.system() == "Windows",

354 reason="OpenImageIO crashes on Windows due to thread-safety issues",

355)

356 class TestWriteImageOpenImageIO:

357 """

358 Define :func:`colour.io.image.write_image_OpenImageIO` definition unit

359 tests methods.

360 """

361

362 def setup_method(self) -> None:

363 """Initialise the common tests attributes."""

364

365 self._temporary_directory = tempfile.mkdtemp()

366

367 def teardown_method(self) -> None:

368 """After tests actions."""

369

370 shutil.rmtree(self._temporary_directory)

371

372 def test_write_image_OpenImageIO(self) -> None: # pragma: no cover

373 """Test :func:`colour.io.image.write_image_OpenImageIO` definition."""

374

375 from OpenImageIO import TypeDesc # noqa: PLC0415

376

377 path = os.path.join(self._temporary_directory, "8-bit.png")

378 RGB = full((1, 1, 3), 255, np.uint8)

379 write_image_OpenImageIO(RGB, path, bit_depth="uint8")

380 image = read_image_OpenImageIO(path, bit_depth="uint8")

381 np.testing.assert_equal(np.squeeze(RGB), image)

382

383 path = os.path.join(self._temporary_directory, "16-bit.png")

384 RGB = full((1, 1, 3), 65535, np.uint16)

385 write_image_OpenImageIO(RGB, path, bit_depth="uint16")

386 image = read_image_OpenImageIO(path, bit_depth="uint16")

387 np.testing.assert_equal(np.squeeze(RGB), image)

388

389 source_path = os.path.join(ROOT_RESOURCES, "Overflowing_Gradient.png")

390 source_image = read_image_OpenImageIO(source_path, bit_depth="uint8")

391 target_path = os.path.join(

392 self._temporary_directory, "Overflowing_Gradient.png"

393 )

394 RGB = np.arange(0, 256, 1, dtype=np.uint8)[None] * 2

395 write_image_OpenImageIO(RGB, target_path, bit_depth="uint8")

396 target_image = read_image_OpenImageIO(source_path, bit_depth="uint8")

397 np.testing.assert_equal(source_image, target_image)

398 np.testing.assert_equal(np.squeeze(RGB), target_image)

399

400 source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")

401 source_image = read_image_OpenImageIO(

402 source_path,

403 additional_data=False,

404 )

405 target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")

406 write_image_OpenImageIO(source_image, target_path)

407 target_image = read_image_OpenImageIO(

408 target_path,

409 additional_data=False,

410 )

411 np.testing.assert_equal(source_image, target_image)

412 assert target_image.shape == (1267, 1274, 3)

413 assert target_image.dtype is np.dtype("float32")

414

415 chromaticities = (

416 0.73470,

417 0.26530,

418 0.00000,

419 1.00000,

420 0.00010,

421 -0.07700,

422 0.32168,

423 0.33767,

424 )

425 write_attributes = [

426 Image_Specification_Attribute("customBooleanFlag", True),

427 Image_Specification_Attribute(

428 "chromaticities", chromaticities, TypeDesc("float[8]")

429 ),

430 Image_Specification_Attribute("compression", "none"),

431 ]

432 write_image_OpenImageIO(target_image, target_path, attributes=write_attributes)

433 target_image, read_attributes = read_image_OpenImageIO(

434 target_path, additional_data=True

435 )

436 for write_attribute in write_attributes:

437 attribute_exists = False

438 for read_attribute in read_attributes:

439 if write_attribute.name == read_attribute.name:

440 attribute_exists = True

441 if isinstance(write_attribute.value, tuple):

442 np.testing.assert_allclose(

443 write_attribute.value,

444 read_attribute.value,

445 atol=TOLERANCE_ABSOLUTE_TESTS,

446 )

447 else:

448 assert write_attribute.value == read_attribute.value

449

450 attest(

451 attribute_exists,

452 f'"{write_attribute.name}" attribute was not found on image!',

453 )

454

455

456 class TestReadImageImageio:

457 """

458 Define :func:`colour.io.image.read_image_Imageio` definition unit tests

459 methods.

460 """

461

462 def test_read_image_Imageio(self) -> None:

463 """Test :func:`colour.io.image.read_image_Imageio` definition."""

464

465 image = read_image_Imageio(os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"))

466 assert image.shape == (1267, 1274, 3)

467 assert image.dtype is np.dtype("float32")

468

469 image = read_image_Imageio(

470 os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),

471 "float16",

472 )

473 assert image.shape == (1267, 1274, 3)

474 assert image.dtype is np.dtype("float16")

475

476 image = read_image_Imageio(os.path.join(ROOT_RESOURCES, "Single_Channel.exr"))

477 assert image.shape == (256, 256)

478

479 image = read_image_Imageio(

480 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"), "uint8"

481 )

482 assert image.shape == (128, 256, 4)

483 assert image.dtype is np.dtype("uint8")

484 assert np.min(image) == 0

485 assert np.max(image) == 255

486

487 image = read_image_Imageio(

488 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"), "uint16"

489 )

490 assert image.shape == (128, 256, 4)

491 assert image.dtype is np.dtype("uint16")

492 assert np.min(image) == 0

493 assert np.max(image) == 65535

494

495 image = read_image_Imageio(

496 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"), "float16"

497 )

498 assert image.dtype is np.dtype("float16")

499 assert np.min(image) == 0.0

500 assert np.max(image) == 1.0

501

502 image = read_image_Imageio(

503 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"), "float32"

504 )

505 assert image.dtype is np.dtype("float32")

506 assert np.min(image) == 0.0

507 assert np.max(image) == 1.0

508

509

510 class TestWriteImageImageio:

511 """

512 Define :func:`colour.io.image.write_image_Imageio` definition unit

513 tests methods.

514 """

515

516 def setup_method(self) -> None:

517 """Initialise the common tests attributes."""

518

519 self._temporary_directory = tempfile.mkdtemp()

520

521 def teardown_method(self) -> None:

522 """After tests actions."""

523

524 shutil.rmtree(self._temporary_directory)

525

526 def test_write_image_Imageio(self) -> None:

527 """Test :func:`colour.io.image.write_image_Imageio` definition."""

528

529 source_path = os.path.join(ROOT_RESOURCES, "Overflowing_Gradient.png")

530 source_image = read_image_Imageio(source_path, bit_depth="uint8")

531 target_path = os.path.join(

532 self._temporary_directory, "Overflowing_Gradient.png"

533 )

534 RGB = np.arange(0, 256, 1, dtype=np.uint8)[None] * 2

535 write_image_Imageio(RGB, target_path, bit_depth="uint8")

536 target_image = read_image_Imageio(target_path, bit_depth="uint8")

537 np.testing.assert_equal(np.squeeze(RGB), target_image)

538 np.testing.assert_equal(source_image, target_image)

539

540 @pytest.mark.skipif(

541 platform.system() == "Linux",

542 reason="EXR tests are breaking on Linux",

543 )

544 def test_write_image_Imageio_exr(self) -> None:

545 """

546 Test :func:`colour.io.image.write_image_Imageio` definition with EXR

547 files.

548 """

549

550 source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")

551 source_image = read_image_Imageio(source_path)

552 target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")

553 write_image_Imageio(source_image, target_path)

554 target_image = read_image_Imageio(target_path)

555 np.testing.assert_allclose(

556 source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS

557 )

558 assert target_image.shape == (1267, 1274, 3)

559 assert target_image.dtype is np.dtype("float32")

560

561 target_path = os.path.join(self._temporary_directory, "Full_White.exr")

562 target_image = full((32, 16, 3), 1e6, dtype=np.float16)

563 write_image_Imageio(target_image, target_path)

564 target_image = read_image_Imageio(target_path)

565 assert np.max(target_image) == np.inf

566

567 target_image = full((32, 16, 3), 1e6)

568 write_image_Imageio(target_image, target_path)

569 target_image = read_image_Imageio(target_path)

570 assert np.max(target_image) == 1e6

571

572

573 class TestReadImage:

574 """

575 Define :func:`colour.io.image.read_image` definition unit tests

576 methods.

577 """

578

579 def test_read_image(self) -> None:

580 """Test :func:`colour.io.image.read_image` definition."""

581

582 image = read_image(os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"))

583 assert image.shape == (1267, 1274, 3)

584 assert image.dtype is np.dtype("float32")

585

586 image = read_image(os.path.join(ROOT_RESOURCES, "Single_Channel.exr"))

587 assert image.shape == (256, 256)

588

589

590@pytest.mark.skipif(

591 platform.system() == "Windows",

592 reason="OpenImageIO crashes on Windows due to thread-safety issues",

593)

594 class TestWriteImage:

595 """Define :func:`colour.io.image.write_image` definition unit tests methods."""

596

597 def setup_method(self) -> None:

598 """Initialise the common tests attributes."""

599

600 self._temporary_directory = tempfile.mkdtemp()

601

602 def teardown_method(self) -> None:

603 """After tests actions."""

604

605 shutil.rmtree(self._temporary_directory)

606

607 def test_write_image(self) -> None:

608 """Test :func:`colour.io.image.write_image` definition."""

609

610 source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")

611 source_image = read_image(source_path)

612 target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")

613 write_image(source_image, target_path)

614 target_image = read_image(target_path)

615 np.testing.assert_allclose(

616 source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS

617 )

618 assert target_image.shape == (1267, 1274, 3)

619 assert target_image.dtype is np.dtype("float32")

620

621

622 class TestAs3ChannelsImage:

623 """

624 Define :func:`colour.io.image.as_3_channels_image` definition unit tests

625 methods.

626 """

627

628 def test_as_3_channels_image(self) -> None:

629 """Test :func:`colour.io.image.as_3_channels_image` definition."""

630

631 a = 0.18

632 b = np.array([[[0.18, 0.18, 0.18]]])

633 np.testing.assert_equal(as_3_channels_image(a), b)

634 a = np.array([0.18])

635 np.testing.assert_equal(as_3_channels_image(a), b)

636 a = np.array([0.18, 0.18, 0.18])

637 np.testing.assert_equal(as_3_channels_image(a), b)

638 a = np.array([[0.18, 0.18, 0.18]])

639 np.testing.assert_equal(as_3_channels_image(a), b)

640 a = np.array([[[0.18, 0.18, 0.18]]])

641 np.testing.assert_equal(as_3_channels_image(a), b)

642 a = np.array([[[[0.18, 0.18, 0.18]]]])

643 np.testing.assert_equal(as_3_channels_image(a), b)

644 a = np.array([[0.18, 0.18, 0.18], [0.20, 0.20, 0.20]])

645 result = as_3_channels_image(a)

646 assert result.shape == (1, 2, 3)

647

648 def test_raise_exception_as_3_channels_image(self) -> None:

649 """

650 Test :func:`colour.io.image.as_3_channels_image` definition raised

651 exception.

652 """

653

654 pytest.raises(

655 ValueError,

656 as_3_channels_image,

657 [

658 [

659 [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]],

660 [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]],

661 ],

662 [

663 [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]],

664 [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]],

665 ],

666 ],

667 )

668

669 pytest.raises(

670 ValueError,

671 as_3_channels_image,

672 [0.18, 0.18, 0.18, 0.18],

673 )