| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- """Read/Write TIFF files using tifffile.
- .. note::
- To use this plugin you need to have `tifffile
- <https://github.com/cgohlke/tifffile>`_ installed::
- pip install tifffile
- This plugin wraps tifffile, a powerful library to manipulate TIFF files. It
- superseeds our previous tifffile plugin and aims to expose all the features of
- tifffile.
- The plugin treats individual TIFF series as ndimages. A series is a sequence of
- TIFF pages that, when combined describe a meaningful unit, e.g., a volumetric
- image (where each slice is stored on an individual page) or a multi-color
- staining picture (where each stain is stored on an individual page). Different
- TIFF flavors/variants use series in different ways and, as such, the resulting
- reading behavior may vary depending on the program used while creating a
- particular TIFF file.
- Methods
- -------
- .. note::
- Check the respective function for a list of supported kwargs and detailed
- documentation.
- .. autosummary::
- :toctree:
- TifffilePlugin.read
- TifffilePlugin.iter
- TifffilePlugin.write
- TifffilePlugin.properties
- TifffilePlugin.metadata
- Additional methods available inside the :func:`imopen <imageio.v3.imopen>`
- context:
- .. autosummary::
- :toctree:
- TifffilePlugin.iter_pages
- """
- from io import BytesIO
- from typing import Any, Dict, Optional, cast
- import warnings
- import numpy as np
- import tifffile
- from ..core.request import URI_BYTES, InitializationError, Request
- from ..core.v3_plugin_api import ImageProperties, PluginV3
- from ..typing import ArrayLike
- def _get_resolution(page: tifffile.TiffPage) -> Dict[str, Any]:
- metadata = {}
- try:
- metadata["resolution_unit"] = page.tags[296].value.value
- except KeyError:
- # tag 296 missing
- return metadata
- try:
- resolution_x = page.tags[282].value
- resolution_y = page.tags[283].value
- metadata["resolution"] = (
- resolution_x[0] / resolution_x[1],
- resolution_y[0] / resolution_y[1],
- )
- except KeyError:
- # tag 282 or 283 missing
- pass
- except ZeroDivisionError:
- warnings.warn(
- "Ignoring resolution metadata because at least one direction has a 0 "
- "denominator.",
- RuntimeWarning,
- )
- return metadata
- class TifffilePlugin(PluginV3):
- """Support for tifffile as backend.
- Parameters
- ----------
- request : iio.Request
- A request object that represents the users intent. It provides a
- standard interface for a plugin to access the various ImageResources.
- Check the docs for details.
- kwargs : Any
- Additional kwargs are forwarded to tifffile's constructor, i.e.
- to ``TiffFile`` for reading or ``TiffWriter`` for writing.
- """
- def __init__(self, request: Request, **kwargs) -> None:
- super().__init__(request)
- self._fh = None
- if request.mode.io_mode == "r":
- try:
- self._fh = tifffile.TiffFile(request.get_file(), **kwargs)
- except tifffile.tifffile.TiffFileError:
- raise InitializationError("Tifffile can not read this file.")
- else:
- self._fh = tifffile.TiffWriter(request.get_file(), **kwargs)
- # ---------------------
- # Standard V3 Interface
- # ---------------------
- def read(self, *, index: int = None, page: int = None, **kwargs) -> np.ndarray:
- """Read a ndimage or page.
- The ndimage returned depends on the value of both ``index`` and
- ``page``. ``index`` selects the series to read and ``page`` allows
- selecting a single page from the selected series. If ``index=None``,
- ``page`` is understood as a flat index, i.e., the selection ignores
- individual series inside the file. If both ``index`` and ``page`` are
- ``None``, then all the series are read and returned as a batch.
- Parameters
- ----------
- index : int
- If ``int``, select the ndimage (series) located at that index inside
- the file and return ``page`` from it. If ``None`` and ``page`` is
- ``int`` read the page located at that (flat) index inside the file.
- If ``None`` and ``page=None``, read all ndimages from the file and
- return them as a batch.
- page : int
- If ``None`` return the full selected ndimage. If ``int``, read the
- page at the selected index and return it.
- kwargs : Any
- Additional kwargs are forwarded to TiffFile's ``as_array`` method.
- Returns
- -------
- ndarray : np.ndarray
- The decoded ndimage or page.
- """
- if "key" not in kwargs:
- kwargs["key"] = page
- elif page is not None:
- raise ValueError("Can't use `page` and `key` at the same time.")
- # set plugin default for ``index``
- if index is not None and "series" in kwargs:
- raise ValueError("Can't use `series` and `index` at the same time.")
- elif "series" in kwargs:
- index = kwargs.pop("series")
- elif index is not None:
- pass
- else:
- index = 0
- if index is Ellipsis and page is None:
- # read all series in the file and return them as a batch
- ndimage = np.stack([x for x in self.iter(**kwargs)])
- else:
- index = None if index is Ellipsis else index
- ndimage = self._fh.asarray(series=index, **kwargs)
- return ndimage
- def iter(self, **kwargs) -> np.ndarray:
- """Yield ndimages from the TIFF.
- Parameters
- ----------
- kwargs : Any
- Additional kwargs are forwarded to the TiffPageSeries' ``as_array``
- method.
- Yields
- ------
- ndimage : np.ndarray
- A decoded ndimage.
- """
- for sequence in self._fh.series:
- yield sequence.asarray(**kwargs)
- def write(
- self, ndimage: ArrayLike, *, is_batch: bool = False, **kwargs
- ) -> Optional[bytes]:
- """Save a ndimage as TIFF.
- Parameters
- ----------
- ndimage : ArrayLike
- The ndimage to encode and write to the ImageResource.
- is_batch : bool
- If True, the first dimension of the given ndimage is treated as a
- batch dimension and each element will create a new series.
- kwargs : Any
- Additional kwargs are forwarded to TiffWriter's ``write`` method.
- Returns
- -------
- encoded_image : bytes
- If the ImageResource is ``"<bytes>"``, return the encoded bytes.
- Otherwise write returns None.
- Notes
- -----
- Incremental writing is supported. Subsequent calls to ``write`` will
- create new series unless ``contiguous=True`` is used, in which case the
- call to write will append to the current series.
- """
- if not is_batch:
- ndimage = np.asarray(ndimage)[None, :]
- for image in ndimage:
- self._fh.write(image, **kwargs)
- if self._request._uri_type == URI_BYTES:
- self._fh.close()
- file = cast(BytesIO, self._request.get_file())
- return file.getvalue()
- def metadata(
- self, *, index: int = Ellipsis, page: int = None, exclude_applied: bool = True
- ) -> Dict[str, Any]:
- """Format-Specific TIFF metadata.
- The metadata returned depends on the value of both ``index`` and
- ``page``. ``index`` selects a series and ``page`` allows selecting a
- single page from the selected series. If ``index=Ellipsis``, ``page`` is
- understood as a flat index, i.e., the selection ignores individual
- series inside the file. If ``index=Ellipsis`` and ``page=None`` then
- global (file-level) metadata is returned.
- Parameters
- ----------
- index : int
- Select the series of which to extract metadata from. If Ellipsis, treat
- page as a flat index into the file's pages.
- page : int
- If not None, select the page of which to extract metadata from. If
- None, read series-level metadata or, if ``index=...`` global,
- file-level metadata.
- exclude_applied : bool
- For API compatibility. Currently ignored.
- Returns
- -------
- metadata : dict
- A dictionary with information regarding the tiff flavor (file-level)
- or tiff tags (page-level).
- """
- if index is not Ellipsis and page is not None:
- target = self._fh.series[index].pages[page]
- elif index is not Ellipsis and page is None:
- # This is based on my understanding that series-level metadata is
- # stored in the first TIFF page.
- target = self._fh.series[index].pages[0]
- elif index is Ellipsis and page is not None:
- target = self._fh.pages[page]
- else:
- target = None
- metadata = {}
- if target is None:
- # return file-level metadata
- metadata["byteorder"] = self._fh.byteorder
- for flag in tifffile.TIFF.FILE_FLAGS:
- flag_value = getattr(self._fh, "is_" + flag)
- metadata["is_" + flag] = flag_value
- if flag_value and hasattr(self._fh, flag + "_metadata"):
- flavor_metadata = getattr(self._fh, flag + "_metadata")
- if isinstance(flavor_metadata, tuple):
- metadata.update(flavor_metadata[0])
- else:
- metadata.update(flavor_metadata)
- else:
- # tifffile may return a TiffFrame instead of a page
- target = target.keyframe
- metadata.update({tag.name: tag.value for tag in target.tags})
- metadata.update(
- {
- "planar_configuration": target.planarconfig,
- "compression": target.compression,
- "predictor": target.predictor,
- "orientation": None, # TODO
- "description1": target.description1,
- "description": target.description,
- "software": target.software,
- **_get_resolution(target),
- "datetime": target.datetime,
- }
- )
- return metadata
- def properties(self, *, index: int = None, page: int = None) -> ImageProperties:
- """Standardized metadata.
- The properties returned depend on the value of both ``index`` and
- ``page``. ``index`` selects a series and ``page`` allows selecting a
- single page from the selected series. If ``index=Ellipsis``, ``page`` is
- understood as a flat index, i.e., the selection ignores individual
- series inside the file. If ``index=Ellipsis`` and ``page=None`` then
- global (file-level) properties are returned. If ``index=Ellipsis``
- and ``page=...``, file-level properties for the flattened index are
- returned.
- Parameters
- ----------
- index : int
- If ``int``, select the ndimage (series) located at that index inside
- the file. If ``Ellipsis`` and ``page`` is ``int`` extract the
- properties of the page located at that (flat) index inside the file.
- If ``Ellipsis`` and ``page=None``, return the properties for the
- batch of all ndimages in the file.
- page : int
- If ``None`` return the properties of the full ndimage. If ``...``
- return the properties of the flattened index. If ``int``,
- return the properties of the page at the selected index only.
- Returns
- -------
- image_properties : ImageProperties
- The standardized metadata (properties) of the selected ndimage or series.
- """
- index = index or 0
- page_idx = 0 if page in (None, Ellipsis) else page
- if index is Ellipsis:
- target_page = self._fh.pages[page_idx]
- else:
- target_page = self._fh.series[index].pages[page_idx]
- if index is Ellipsis and page is None:
- n_series = len(self._fh.series)
- props = ImageProperties(
- shape=(n_series, *target_page.shape),
- dtype=target_page.dtype,
- n_images=n_series,
- is_batch=True,
- spacing=_get_resolution(target_page).get("resolution"),
- )
- elif index is Ellipsis and page is Ellipsis:
- n_pages = len(self._fh.pages)
- props = ImageProperties(
- shape=(n_pages, *target_page.shape),
- dtype=target_page.dtype,
- n_images=n_pages,
- is_batch=True,
- spacing=_get_resolution(target_page).get("resolution"),
- )
- else:
- props = ImageProperties(
- shape=target_page.shape,
- dtype=target_page.dtype,
- is_batch=False,
- spacing=_get_resolution(target_page).get("resolution"),
- )
- return props
- def close(self) -> None:
- if self._fh is not None:
- self._fh.close()
- super().close()
- # ------------------------------
- # Add-on Interface inside imopen
- # ------------------------------
- def iter_pages(self, index=..., **kwargs):
- """Yield pages from a TIFF file.
- This generator walks over the flat index of the pages inside an
- ImageResource and yields them in order.
- Parameters
- ----------
- index : int
- The index of the series to yield pages from. If Ellipsis, walk over
- the file's flat index (and ignore individual series).
- kwargs : Any
- Additional kwargs are passed to TiffPage's ``as_array`` method.
- Yields
- ------
- page : np.ndarray
- A page stored inside the TIFF file.
- """
- if index is Ellipsis:
- pages = self._fh.pages
- else:
- pages = self._fh.series[index]
- for page in pages:
- yield page.asarray(**kwargs)
|