Coverage for io/image.py: 72%

1"""

2 Image Input / Output Utilities

3==============================

4

5 Define image-related input/output utility objects for colour science

6 applications.

7"""

8

9 from __future__ import annotations

10

11 import typing

12 from dataclasses import dataclass, field

13

14 import numpy as np

15

16 if typing.TYPE_CHECKING:

17 from colour.hints import (

18 Any,

19 ArrayLike,

20 DTypeReal,

21 Literal,

22 NDArrayFloat,

23 PathLike,

24 Sequence,

25 Tuple,

26 Type,

27 )

28

29 from colour.hints import NDArrayReal, cast

30 from colour.utilities import (

31 CanonicalMapping,

32 as_float_array,

33 as_int_array,

34 attest,

35 filter_kwargs,

36 is_imageio_installed,

37 is_openimageio_installed,

38 optional,

39 required,

40 tstack,

41 usage_warning,

42 validate_method,

43)

44 from colour.utilities.deprecation import handle_arguments_deprecation

45

46 __author__ = "Colour Developers"

47 __copyright__ = "Copyright 2013 Colour Developers"

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

49 __maintainer__ = "Colour Developers"

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

51 __status__ = "Production"

52

53 __all__ = [

54 "Image_Specification_BitDepth",

55 "Image_Specification_Attribute",

56 "MAPPING_BIT_DEPTH",

57 "add_attributes_to_image_specification_OpenImageIO",

58 "image_specification_OpenImageIO",

59 "convert_bit_depth",

60 "read_image_OpenImageIO",

61 "read_image_Imageio",

62 "READ_IMAGE_METHODS",

63 "read_image",

64 "write_image_OpenImageIO",

65 "write_image_Imageio",

66 "WRITE_IMAGE_METHODS",

67 "write_image",

68 "as_3_channels_image",

69]

70

71

72@dataclass(frozen=True)

73 class Image_Specification_BitDepth:

74 """

75 Define a bit-depth specification for image processing operations.

76

77 Parameters

78 ----------

79 name

80 Attribute name identifying the bit-depth specification.

81 numpy

82 Object representing the *NumPy* bit-depth data type.

83 openimageio

84 Object representing the *OpenImageIO* bit-depth specification.

85 """

86

87 name: str

88 numpy: Type[DTypeReal]

89 openimageio: Any

90

91

92@dataclass

93 class Image_Specification_Attribute:

94 """

95 Define an image specification attribute for OpenImageIO operations.

96

97 Parameters

98 ----------

99 name

100 Attribute name identifying the metadata field.

101 value

102 Attribute value containing the metadata content.

103 type_

104 Attribute type as an *OpenImageIO* :class:`TypeDesc` class instance

105 specifying the data type of the value.

106 """

107

108 name: str

109 value: Any

110 type_: OpenImageIO.TypeDesc | None = field( # noqa: F821, RUF100 # pyright: ignore # noqa: F821

111 default_factory=lambda: None

112 )

113

114

115 if is_openimageio_installed(): # pragma: no cover

116 from OpenImageIO import ImageSpec # pyright: ignore

117 from OpenImageIO import DOUBLE, FLOAT, HALF, UINT8, UINT16

118

119 MAPPING_BIT_DEPTH: CanonicalMapping = CanonicalMapping(

120 {

121 "uint8": Image_Specification_BitDepth("uint8", np.uint8, UINT8),

122 "uint16": Image_Specification_BitDepth("uint16", np.uint16, UINT16),

123 "float16": Image_Specification_BitDepth("float16", np.float16, HALF),

124 "float32": Image_Specification_BitDepth("float32", np.float32, FLOAT),

125 "float64": Image_Specification_BitDepth("float64", np.float64, DOUBLE),

126 }

127 )

128 if not typing.TYPE_CHECKING and hasattr(np, "float128"): # pragma: no cover

129 MAPPING_BIT_DEPTH["float128"] = Image_Specification_BitDepth(

130 "float128", np.float128, DOUBLE

131 )

132 else: # pragma: no cover

133

134 class ImageSpec:

135 attribute: Any

136

137 MAPPING_BIT_DEPTH: CanonicalMapping = CanonicalMapping(

138 {

139 "uint8": Image_Specification_BitDepth("uint8", np.uint8, None),

140 "uint16": Image_Specification_BitDepth("uint16", np.uint16, None),

141 "float16": Image_Specification_BitDepth("float16", np.float16, None),

142 "float32": Image_Specification_BitDepth("float32", np.float32, None),

143 "float64": Image_Specification_BitDepth("float64", np.float64, None),

144 }

145 )

146 if not typing.TYPE_CHECKING and hasattr(np, "float128"): # pragma: no cover

147 MAPPING_BIT_DEPTH["float128"] = Image_Specification_BitDepth(

148 "float128", np.float128, None

149 )

150

151

152 def add_attributes_to_image_specification_OpenImageIO(

153 image_specification: ImageSpec, attributes: Sequence

154) -> ImageSpec:

155 """

156 Add the specified attributes to the specified *OpenImageIO* image

157 specification.

158

159 Apply metadata attributes to an existing image specification object,

160 enabling customization of image properties such as compression,

161 colour space information, or other format-specific metadata.

162

163 Parameters

164 ----------

165 image_specification

166 *OpenImageIO* image specification to modify.

167 attributes

168 Sequence of :class:`colour.io.Image_Specification_Attribute`

169 instances containing metadata to apply to the image

170 specification.

171

172 Returns

173 -------

174 :class:`ImageSpec`

175 Modified *OpenImageIO* image specification with applied

176 attributes.

177

178 Examples

179 --------

180 >>> image_specification = image_specification_OpenImageIO(

181 ... 1920, 1080, 3, "float16"

182 ... ) # doctest: +SKIP

183 >>> compression = Image_Specification_Attribute("Compression", "none")

184 >>> image_specification = add_attributes_to_image_specification_OpenImageIO(

185 ... image_specification, [compression]

186 ... ) # doctest: +SKIP

187 >>> image_specification.extra_attribs[0].value # doctest: +SKIP

188 'none'

189 """

190

191 for attribute in attributes:

192 name = str(attribute.name)

193 value = (

194 str(attribute.value)

195 if isinstance(attribute.value, str)

196 else attribute.value

197 )

198 type_ = attribute.type_

199 if attribute.type_ is None:

200 image_specification.attribute(name, value)

201 else:

202 image_specification.attribute(name, type_, value)

203

204 return image_specification

205

206

207 def image_specification_OpenImageIO(

208 width: int,

209 height: int,

210 channels: int,

211 bit_depth: Literal[

212 "uint8", "uint16", "float16", "float32", "float64", "float128"

213 ] = "float32",

214 attributes: Sequence | None = None,

215) -> ImageSpec:

216 """

217 Create an *OpenImageIO* image specification.

218

219 Parameters

220 ----------

221 width

222 Image width.

223 height

224 Image height.

225 channels

226 Image channel count.

227 bit_depth

228 Bit-depth to create the image with. The bit-depth conversion

229 behaviour is ruled directly by *OpenImageIO*.

230 attributes

231 An array of :class:`colour.io.Image_Specification_Attribute`

232 class instances used to set attributes of the image.

233

234 Returns

235 -------

236 :class:`ImageSpec`

237 *OpenImageIO* image specification.

238

239 Examples

240 --------

241 >>> compression = Image_Specification_Attribute("Compression", "none")

242 >>> image_specification_OpenImageIO(

243 ... 1920, 1080, 3, "float16", [compression]

244 ... ) # doctest: +SKIP

245 <OpenImageIO.ImageSpec object at 0x...>

246 """

247

248 from OpenImageIO import ImageSpec # noqa: PLC0415

249

250 attributes = cast("list", optional(attributes, []))

251

252 bit_depth_specification = MAPPING_BIT_DEPTH[bit_depth]

253

254 image_specification = ImageSpec(

255 width, height, channels, bit_depth_specification.openimageio

256 )

257

258 add_attributes_to_image_specification_OpenImageIO(

259 image_specification, # pyright: ignore

260 attributes or [],

261 )

262

263 return image_specification # pyright: ignore

264

265

266 def convert_bit_depth(

267 a: ArrayLike,

268 bit_depth: Literal[

269 "uint8", "uint16", "float16", "float32", "float64", "float128"

270 ] = "float32",

271) -> NDArrayReal:

272 """

273 Convert the specified array to the specified bit-depth.

274

275 The conversion path is determined by the current bit-depth of the input

276 array and the target bit-depth. Supports conversions between unsigned

277 integers, floating-point types, and mixed type conversions with

278 appropriate scaling.

279

280 Parameters

281 ----------

282 a

283 Array to convert to the specified bit-depth.

284 bit_depth

285 Target bit-depth. Supported types include unsigned integers

286 ("uint8", "uint16") and floating-point ("float16", "float32",

287 "float64", "float128").

288

289 Returns

290 -------

291 :class:`numpy.ndarray`

292 Array converted to the specified bit-depth.

293

294 Raises

295 ------

296 AssertionError

297 If the source or target bit-depth is not supported.

298

299 Examples

300 --------

301 >>> a = np.array([0.0, 0.5, 1.0])

302 >>> convert_bit_depth(a, "uint8")

303 array([ 0, 128, 255], dtype=uint8)

304 >>> convert_bit_depth(a, "uint16")

305 array([ 0, 32768, 65535], dtype=uint16)

306 >>> convert_bit_depth(a, "float16")

307 array([ 0. , 0.5, 1. ], dtype=float16)

308 >>> a = np.array([0, 128, 255], dtype=np.uint8)

309 >>> convert_bit_depth(a, "uint16")

310 array([ 0, 32896, 65535], dtype=uint16)

311 >>> convert_bit_depth(a, "float32") # doctest: +ELLIPSIS

312 array([ 0. , 0.501960..., 1. ], dtype=float32)

313 """

314

315 a = np.asarray(a)

316

317 bit_depths = ", ".join(sorted(MAPPING_BIT_DEPTH.keys()))

318

319 attest(

320 bit_depth in bit_depths,

321 f'Incorrect bit-depth was specified, it must be one of: "{bit_depths}"!',

322 )

323

324 attest(

325 str(a.dtype) in bit_depths,

326 f'Image bit-depth must be one of: "{bit_depths}"!',

327 )

328

329 source_dtype = str(a.dtype)

330 target_dtype = MAPPING_BIT_DEPTH[bit_depth].numpy

331

332 if source_dtype == "uint8":

333 if bit_depth == "uint16":

334 a = a.astype(target_dtype) * 257

335 elif bit_depth in ("float16", "float32", "float64", "float128"):

336 a = (a / 255).astype(target_dtype)

337 elif source_dtype == "uint16":

338 if bit_depth == "uint8":

339 a = (a / 257).astype(target_dtype)

340 elif bit_depth in ("float16", "float32", "float64", "float128"):

341 a = (a / 65535).astype(target_dtype)

342 elif source_dtype in ("float16", "float32", "float64", "float128"):

343 if bit_depth == "uint8":

344 a = np.around(a * 255).astype(target_dtype)

345 elif bit_depth == "uint16":

346 a = np.around(a * 65535).astype(target_dtype)

347 elif bit_depth in ("float16", "float32", "float64", "float128"):

348 a = a.astype(target_dtype)

349

350 return a

351

352

353@typing.overload

354@required("OpenImageIO")

355 def read_image_OpenImageIO(

356 path: str | PathLike,

357 bit_depth: Literal[

358 "uint8", "uint16", "float16", "float32", "float64", "float128"

359 ] = ...,

360 additional_data: Literal[True] = True,

361 **kwargs: Any,

362) -> Tuple[NDArrayReal, Tuple[Image_Specification_Attribute, ...]]: ...

363

364

365@typing.overload

366@required("OpenImageIO")

367 def read_image_OpenImageIO(

368 path: str | PathLike,

369 bit_depth: Literal[

370 "uint8", "uint16", "float16", "float32", "float64", "float128"

371 ] = ...,

372 *,

373 additional_data: Literal[False],

374 **kwargs: Any,

375) -> NDArrayReal: ...

376

377

378@typing.overload

379@required("OpenImageIO")

380 def read_image_OpenImageIO(

381 path: str | PathLike,

382 bit_depth: Literal["uint8", "uint16", "float16", "float32", "float64", "float128"],

383 additional_data: Literal[False],

384 **kwargs: Any,

385) -> NDArrayReal: ...

386

387

388@required("OpenImageIO")

389 def read_image_OpenImageIO(

390 path: str | PathLike,

391 bit_depth: Literal[

392 "uint8", "uint16", "float16", "float32", "float64", "float128"

393 ] = "float32",

394 additional_data: bool = False,

395 **kwargs: Any,

396) -> NDArrayReal | Tuple[NDArrayReal, Tuple[Image_Specification_Attribute, ...]]:

397 """

398 Read image data from the specified path using *OpenImageIO*.

399

400 Load image data from the file system with support for various bit-depth

401 formats. The bit-depth conversion behaviour is controlled by

402 *OpenImageIO*, with this function performing only the final type

403 conversion after reading.

404

405 Parameters

406 ----------

407 path

408 Path to the image file.

409 bit_depth

410 Target bit-depth for the returned image data. The bit-depth

411 conversion is handled by *OpenImageIO* during the read operation,

412 with this function converting to the appropriate *NumPy* data type

413 afterwards.

414 additional_data

415 Whether to return additional metadata from the image file.

416

417 Returns

418 -------

419 :class:`numpy.ndarray` or :class:`tuple`

420 Image data as an array when ``additional_data`` is ``False``, or a

421 tuple containing the image data and a tuple of

422 :class:`colour.io.Image_Specification_Attribute` instances when

423 ``additional_data`` is ``True``.

424

425 Notes

426 -----

427 - For convenience, single channel images are squeezed to 2D arrays.

428

429 Examples

430 --------

431 >>> import os

432 >>> import colour

433 >>> path = os.path.join(

434 ... colour.__path__[0],

435 ... "io",

436 ... "tests",

437 ... "resources",

438 ... "CMS_Test_Pattern.exr",

439 ... )

440 >>> image = read_image_OpenImageIO(path) # doctest: +SKIP

441 """

442

443 from OpenImageIO import ImageInput # noqa: PLC0415

444

445 path = str(path)

446

447 kwargs = handle_arguments_deprecation(

448 {

449 "ArgumentRenamed": [["attributes", "additional_data"]],

450 },

451 **kwargs,

452 )

453

454 additional_data = kwargs.get("additional_data", additional_data)

455

456 bit_depth_specification = MAPPING_BIT_DEPTH[bit_depth]

457

458 image_input = ImageInput.open(path)

459 image_specification = image_input.spec()

460

461 shape = (

462 image_specification.height,

463 image_specification.width,

464 image_specification.nchannels,

465 )

466

467 image = image_input.read_image(bit_depth_specification.openimageio)

468 image_input.close()

469

470 image = np.reshape(np.array(image, dtype=bit_depth_specification.numpy), shape)

471 image = cast("NDArrayReal", np.squeeze(image))

472

473 if additional_data:

474 extra_attributes = [

475 Image_Specification_Attribute(

476 attribute.name, attribute.value, attribute.type

477 )

478 for attribute in image_specification.extra_attribs

479 ]

480

481 return image, tuple(extra_attributes)

482

483 return image

484

485

486@required("Imageio")

487 def read_image_Imageio(

488 path: str | PathLike,

489 bit_depth: Literal[

490 "uint8", "uint16", "float16", "float32", "float64", "float128"

491 ] = "float32",

492 **kwargs: Any,

493) -> NDArrayReal:

494 """

495 Read image data from the specified path using *Imageio*.

496

497 Parameters

498 ----------

499 path

500 Path to the image file.

501 bit_depth

502 Target bit-depth for the returned image data. The image data is

503 converted with :func:`colour.io.convert_bit_depth` definition after

504 reading the image.

505

506 Other Parameters

507 ----------------

508 kwargs

509 Keywords arguments.

510

511 Returns

512 -------

513 :class:`numpy.ndarray`

514 Image data.

515

516 Notes

517 -----

518 - For convenience, single channel images are squeezed to 2D arrays.

519

520 Examples

521 --------

522 >>> import os

523 >>> import colour

524 >>> path = os.path.join(

525 ... colour.__path__[0],

526 ... "io",

527 ... "tests",

528 ... "resources",

529 ... "CMS_Test_Pattern.exr",

530 ... )

531 >>> image = read_image_Imageio(path)

532 >>> image.shape # doctest: +SKIP

533 (1267, 1274, 3)

534 >>> image.dtype

535 dtype('float32')

536 """

537

538 from imageio.v2 import imread # noqa: PLC0415

539

540 path = str(path)

541

542 image = np.squeeze(imread(path, **kwargs))

543

544 return convert_bit_depth(image, bit_depth)

545

546

547 READ_IMAGE_METHODS: CanonicalMapping = CanonicalMapping(

548 {

549 "Imageio": read_image_Imageio,

550 "OpenImageIO": read_image_OpenImageIO,

551 }

552)

553 READ_IMAGE_METHODS.__doc__ = """

554 Supported image reading methods.

555"""

556

557

558 def read_image(

559 path: str | PathLike,

560 bit_depth: Literal[

561 "uint8", "uint16", "float16", "float32", "float64", "float128"

562 ] = "float32",

563 method: Literal["Imageio", "OpenImageIO"] | str = "OpenImageIO",

564 **kwargs: Any,

565) -> NDArrayReal:

566 """

567 Read image data from the specified path.

568

569 Load and optionally convert image data from various formats,

570 supporting multiple bit-depth conversions and backend libraries for

571 flexible image I/O operations in colour science workflows.

572

573 Parameters

574 ----------

575 path

576 Path to the image file.

577 bit_depth

578 Target bit-depth for the returned image data. For the *Imageio*

579 method, image data is converted using

580 :func:`colour.io.convert_bit_depth` after reading. For the

581 *OpenImageIO* method, bit-depth conversion is handled by the

582 library with this parameter controlling only the final data type.

583 method

584 Image reading backend library. Defaults to *OpenImageIO* with

585 automatic fallback to *Imageio* if unavailable.

586

587 Other Parameters

588 ----------------

589 additional_data

590 {:func:`colour.io.read_image_OpenImageIO`},

591 Whether to return additional metadata with the image data.

592

593 Returns

594 -------

595 :class:`numpy.ndarray`

596 Image data as a NumPy array with the specified bit-depth.

597

598 Notes

599 -----

600 - If the specified method is *OpenImageIO* but the library is not

601 available, reading will be performed by *Imageio*.

602 - If the specified method is *Imageio*, ``kwargs`` is passed

603 directly to the wrapped definition.

604 - For convenience, single channel images are squeezed to 2D arrays.

605

606 Examples

607 --------

608 >>> import os

609 >>> import colour

610 >>> path = os.path.join(

611 ... colour.__path__[0],

612 ... "io",

613 ... "tests",

614 ... "resources",

615 ... "CMS_Test_Pattern.exr",

616 ... )

617 >>> image = read_image(path)

618 >>> image.shape # doctest: +SKIP

619 (1267, 1274, 3)

620 >>> image.dtype

621 dtype('float32')

622 """

623

624 if method.lower() == "imageio" and not is_imageio_installed(): # pragma: no cover

625 usage_warning(

626 '"Imageio" related API features are not available, '

627 'switching to "OpenImageIO"!'

628 )

629 method = "openimageio"

630

631 method = validate_method(method, tuple(READ_IMAGE_METHODS))

632

633 function = READ_IMAGE_METHODS[method]

634

635 if method == "openimageio": # pragma: no cover

636 kwargs = filter_kwargs(function, **kwargs)

637

638 return function(path, bit_depth, **kwargs)

639

640

641@required("OpenImageIO")

642 def write_image_OpenImageIO(

643 image: ArrayLike,

644 path: str | PathLike,

645 bit_depth: Literal[

646 "uint8", "uint16", "float16", "float32", "float64", "float128"

647 ] = "float32",

648 attributes: Sequence | None = None,

649) -> bool:

650 """

651 Write image data to the specified path using *OpenImageIO*.

652

653 Parameters

654 ----------

655 image

656 Image data to write.

657 path

658 Path to the image file.

659 bit_depth

660 Bit-depth to write the image at. The bit-depth conversion behaviour

661 is ruled directly by *OpenImageIO*.

662 attributes

663 An array of :class:`colour.io.Image_Specification_Attribute` class

664 instances used to set attributes of the image.

665

666 Returns

667 -------

668 :class:`bool`

669 Definition success.

670

671 Examples

672 --------

673 Basic image writing:

674

675 >>> import os

676 >>> import colour

677 >>> path = os.path.join(

678 ... colour.__path__[0],

679 ... "io",

680 ... "tests",

681 ... "resources",

682 ... "CMS_Test_Pattern.exr",

683 ... )

684 >>> image = read_image(path) # doctest: +SKIP

685 >>> path = os.path.join(

686 ... colour.__path__[0],

687 ... "io",

688 ... "tests",

689 ... "resources",

690 ... "CMSTestPattern.tif",

691 ... )

692 >>> write_image_OpenImageIO(image, path) # doctest: +SKIP

693 True

694

695 Advanced image writing while setting attributes:

696

697 >>> compression = Image_Specification_Attribute("Compression", "none")

698 >>> write_image_OpenImageIO(image, path, "uint8", [compression])

699 ... # doctest: +SKIP

700 True

701

702 Writing an "ACES" compliant "EXR" file:

703

704 >>> from OpenImageIO import TypeDesc

705 >>> chromaticities = (

706 ... 0.7347,

707 ... 0.2653,

708 ... 0.0,

709 ... 1.0,

710 ... 0.0001,

711 ... -0.077,

712 ... 0.32168,

713 ... 0.33767,

714 ... )

715 >>> attributes = [

716 ... Image_Specification_Attribute("openexr:ACESContainerPolicy", "relaxed"),

717 ... Image_Specification_Attribute(

718 ... "chromaticities", chromaticities, TypeDesc("float[8]")

719 ... ),

720 ... Image_Specification_Attribute("compression", "none"),

721 ... ]

722 >>> write_image_OpenImageIO(image, path, attributes=attributes) # doctest: +SKIP

723 True

724

725 Notes

726 -----

727 - When using ``openexr:ACESContainerPolicy`` with ``relaxed`` mode,

728 *OpenImageIO* automatically sets the ``colorInteropId`` attribute to

729 ``lin_ap0_scene`` for ACES-compliant files.

730 - The ``acesImageContainerFlag`` attribute should not be set manually

731 in *OpenImageIO* 3.1.7.0+, as it triggers strict ACES validation.

732 Use ``openexr:ACESContainerPolicy`` instead.

733 """

734

735 from OpenImageIO import ImageOutput # noqa: PLC0415

736

737 image = as_float_array(image)

738 path = str(path)

739

740 attributes = cast("list", optional(attributes, []))

741

742 bit_depth_specification = MAPPING_BIT_DEPTH[bit_depth]

743

744 if bit_depth_specification.numpy in [np.uint8, np.uint16]:

745 minimum, maximum = (

746 np.iinfo(bit_depth_specification.numpy).min,

747 np.iinfo(bit_depth_specification.numpy).max,

748 )

749 image = np.clip(image * maximum, minimum, maximum)

750

751 image = as_int_array(image, bit_depth_specification.numpy)

752

753 image = image.astype(bit_depth_specification.numpy)

754

755 if image.ndim == 2:

756 height, width = image.shape

757 channels = 1

758 else:

759 height, width, channels = image.shape

760

761 image_specification = image_specification_OpenImageIO(

762 width, height, channels, bit_depth, attributes

763 )

764

765 image_output = ImageOutput.create(path)

766

767 image_output.open(path, image_specification) # pyright: ignore

768 success = image_output.write_image(image)

769

770 image_output.close()

771

772 return success

773

774

775@required("Imageio")

776 def write_image_Imageio(

777 image: ArrayLike,

778 path: str | PathLike,

779 bit_depth: Literal[

780 "uint8", "uint16", "float16", "float32", "float64", "float128"

781 ] = "float32",

782 **kwargs: Any,

783) -> bytes | None:

784 """

785 Write image data to the specified path using *Imageio*.

786

787 Parameters

788 ----------

789 image

790 Image data to write.

791 path

792 Path to the image file.

793 bit_depth

794 Bit-depth to write the image at. The image data is converted with

795 :func:`colour.io.convert_bit_depth` definition prior to writing.

796

797 Other Parameters

798 ----------------

799 kwargs

800 Keywords arguments passed to the underlying *Imageio* ``imwrite``

801 function.

802

803 Returns

804 -------

805 :class:`bytes` or :py:data:`None`

806 Image data written as bytes if successful, :py:data:`None`

807 otherwise.

808

809 Notes

810 -----

811 - Control how images are saved by the *Freeimage* backend using the

812 ``flags`` keyword argument with desired values. See the *Load /

813 Save flag constants* section in

814 https://sourceforge.net/p/freeimage/svn/HEAD/tree/FreeImage/trunk/Source/FreeImage.h

815

816 Examples

817 --------

818 >>> import os

819 >>> import colour

820 >>> path = os.path.join(

821 ... colour.__path__[0],

822 ... "io",

823 ... "tests",

824 ... "resources",

825 ... "CMS_Test_Pattern.exr",

826 ... )

827 >>> image = read_image(path) # doctest: +SKIP

828 >>> path = os.path.join(

829 ... colour.__path__[0],

830 ... "io",

831 ... "tests",

832 ... "resources",

833 ... "CMSTestPattern.tif",

834 ... )

835 >>> write_image_Imageio(image, path) # doctest: +SKIP

836 True

837 """

838

839 from imageio.v2 import imwrite # noqa: PLC0415

840

841 path = str(path)

842

843 if all(

844 [

845 path.lower().endswith(".exr"),

846 bit_depth in ("float32", "float64", "float128"),

847 ]

848 ):

849 # Ensures that "OpenEXR" images are saved as "Float32" according to the

850 # image bit-depth.

851 kwargs["flags"] = 0x0001

852

853 image = convert_bit_depth(image, bit_depth)

854

855 return imwrite(path, image, **kwargs)

856

857

858 WRITE_IMAGE_METHODS: CanonicalMapping = CanonicalMapping(

859 {

860 "Imageio": write_image_Imageio,

861 "OpenImageIO": write_image_OpenImageIO,

862 }

863)

864 WRITE_IMAGE_METHODS.__doc__ = """

865 Supported image writing methods.

866"""

867

868

869 def write_image(

870 image: ArrayLike,

871 path: str | PathLike,

872 bit_depth: Literal[

873 "uint8", "uint16", "float16", "float32", "float64", "float128"

874 ] = "float32",

875 method: Literal["Imageio", "OpenImageIO"] | str = "OpenImageIO",

876 **kwargs: Any,

877) -> bool:

878 """

879 Write image data to the specified path.

880

881 Parameters

882 ----------

883 image

884 Image data to write.

885 path

886 Path to the image file.

887 bit_depth

888 Bit-depth to write the image at. For the *Imageio* method, the

889 image data is converted with :func:`colour.io.convert_bit_depth`

890 definition prior to writing the image.

891 method

892 Image writing backend library.

893

894 Other Parameters

895 ----------------

896 attributes

897 {:func:`colour.io.write_image_OpenImageIO`},

898 An array of :class:`colour.io.Image_Specification_Attribute` class

899 instances used to set attributes of the image.

900

901 Returns

902 -------

903 :class:`bool`

904 Definition success.

905

906 Notes

907 -----

908 - If the specified method is *OpenImageIO* but the library is not

909 available writing will be performed by *Imageio*.

910 - If the specified method is *Imageio*, ``kwargs`` is passed directly

911 to the wrapped definition.

912 - It is possible to control how the images are saved by the

913 *Freeimage* backend by using the ``flags`` keyword argument and

914 passing a desired value. See the *Load / Save flag constants*

915 section in

916 https://sourceforge.net/p/freeimage/svn/HEAD/tree/FreeImage/trunk/Source/FreeImage.h

917

918 Examples

919 --------

920 Basic image writing:

921

922 >>> import os

923 >>> import colour

924 >>> path = os.path.join(

925 ... colour.__path__[0],

926 ... "io",

927 ... "tests",

928 ... "resources",

929 ... "CMS_Test_Pattern.exr",

930 ... )

931 >>> image = read_image(path) # doctest: +SKIP

932 >>> path = os.path.join(

933 ... colour.__path__[0],

934 ... "io",

935 ... "tests",

936 ... "resources",

937 ... "CMSTestPattern.tif",

938 ... )

939 >>> write_image(image, path) # doctest: +SKIP

940 True

941

942 Advanced image writing while setting attributes using *OpenImageIO*:

943

944 >>> compression = Image_Specification_Attribute("Compression", "none")

945 >>> write_image(image, path, bit_depth="uint8", attributes=[compression])

946 ... # doctest: +SKIP

947 True

948 """

949

950 if method.lower() == "imageio" and not is_imageio_installed(): # pragma: no cover

951 usage_warning(

952 '"Imageio" related API features are not available, '

953 'switching to "OpenImageIO"!'

954 )

955 method = "openimageio"

956

957 method = validate_method(method, tuple(WRITE_IMAGE_METHODS))

958

959 function = WRITE_IMAGE_METHODS[method]

960

961 if method == "openimageio": # pragma: no cover

962 kwargs = filter_kwargs(function, **kwargs)

963

964 return function(image, path, bit_depth, **kwargs)

965

966

967 def as_3_channels_image(a: ArrayLike) -> NDArrayFloat:

968 """

969 Convert the specified array :math:`a` to a 3-channel image-like

970 representation.

971

972 Parameters

973 ----------

974 a

975 Array :math:`a` to convert to a 3-channel image-like representation.

976

977 Returns

978 -------

979 :class:`numpy.ndarray`

980 3-channel image-like representation of array :math:`a`.

981

982 Raises

983 ------

984 ValueError

985 If the array has more than 3 dimensions or more than 1 or 3 channels.

986

987 Examples

988 --------

989 >>> as_3_channels_image(0.18)

990 array([[[ 0.18, 0.18, 0.18]]])

991 >>> as_3_channels_image([0.18])

992 array([[[ 0.18, 0.18, 0.18]]])

993 >>> as_3_channels_image([0.18, 0.18, 0.18])

994 array([[[ 0.18, 0.18, 0.18]]])

995 >>> as_3_channels_image([[0.18, 0.18, 0.18]])

996 array([[[ 0.18, 0.18, 0.18]]])

997 >>> as_3_channels_image([[[0.18, 0.18, 0.18]]])

998 array([[[ 0.18, 0.18, 0.18]]])

999 >>> as_3_channels_image([[[[0.18, 0.18, 0.18]]]])

1000 array([[[ 0.18, 0.18, 0.18]]])

1001 """

1002

1003 a = np.squeeze(as_float_array(a))

1004

1005 if len(a.shape) > 3:

1006 error = (

1007 "Array has more than 3-dimensions and cannot be converted to a "

1008 "3-channels image-like representation!"

1009 )

1010

1011 raise ValueError(error)

1012

1013 if len(a.shape) > 0 and a.shape[-1] not in (1, 3):

1014 error = (

1015 "Array has more than 1 or 3 channels and cannot be converted to a "

1016 "3-channels image-like representation!"

1017 )

1018

1019 raise ValueError(error)

1020

1021 if len(a.shape) == 0 or a.shape[-1] == 1:

1022 a = tstack([a, a, a])

1023

1024 if len(a.shape) == 1:

1025 a = a[None, None, ...]

1026 elif len(a.shape) == 2:

1027 a = a[None, ...]

1028

1029 return a