414 lines
14 KiB
Python
414 lines
14 KiB
Python
"""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)
|