tifffile_v3.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. """Read/Write TIFF files using tifffile.
  2. .. note::
  3. To use this plugin you need to have `tifffile
  4. <https://github.com/cgohlke/tifffile>`_ installed::
  5. pip install tifffile
  6. This plugin wraps tifffile, a powerful library to manipulate TIFF files. It
  7. superseeds our previous tifffile plugin and aims to expose all the features of
  8. tifffile.
  9. The plugin treats individual TIFF series as ndimages. A series is a sequence of
  10. TIFF pages that, when combined describe a meaningful unit, e.g., a volumetric
  11. image (where each slice is stored on an individual page) or a multi-color
  12. staining picture (where each stain is stored on an individual page). Different
  13. TIFF flavors/variants use series in different ways and, as such, the resulting
  14. reading behavior may vary depending on the program used while creating a
  15. particular TIFF file.
  16. Methods
  17. -------
  18. .. note::
  19. Check the respective function for a list of supported kwargs and detailed
  20. documentation.
  21. .. autosummary::
  22. :toctree:
  23. TifffilePlugin.read
  24. TifffilePlugin.iter
  25. TifffilePlugin.write
  26. TifffilePlugin.properties
  27. TifffilePlugin.metadata
  28. Additional methods available inside the :func:`imopen <imageio.v3.imopen>`
  29. context:
  30. .. autosummary::
  31. :toctree:
  32. TifffilePlugin.iter_pages
  33. """
  34. from io import BytesIO
  35. from typing import Any, Dict, Optional, cast
  36. import warnings
  37. import numpy as np
  38. import tifffile
  39. from ..core.request import URI_BYTES, InitializationError, Request
  40. from ..core.v3_plugin_api import ImageProperties, PluginV3
  41. from ..typing import ArrayLike
  42. def _get_resolution(page: tifffile.TiffPage) -> Dict[str, Any]:
  43. metadata = {}
  44. try:
  45. metadata["resolution_unit"] = page.tags[296].value.value
  46. except KeyError:
  47. # tag 296 missing
  48. return metadata
  49. try:
  50. resolution_x = page.tags[282].value
  51. resolution_y = page.tags[283].value
  52. metadata["resolution"] = (
  53. resolution_x[0] / resolution_x[1],
  54. resolution_y[0] / resolution_y[1],
  55. )
  56. except KeyError:
  57. # tag 282 or 283 missing
  58. pass
  59. except ZeroDivisionError:
  60. warnings.warn(
  61. "Ignoring resolution metadata because at least one direction has a 0 "
  62. "denominator.",
  63. RuntimeWarning,
  64. )
  65. return metadata
  66. class TifffilePlugin(PluginV3):
  67. """Support for tifffile as backend.
  68. Parameters
  69. ----------
  70. request : iio.Request
  71. A request object that represents the users intent. It provides a
  72. standard interface for a plugin to access the various ImageResources.
  73. Check the docs for details.
  74. kwargs : Any
  75. Additional kwargs are forwarded to tifffile's constructor, i.e.
  76. to ``TiffFile`` for reading or ``TiffWriter`` for writing.
  77. """
  78. def __init__(self, request: Request, **kwargs) -> None:
  79. super().__init__(request)
  80. self._fh = None
  81. if request.mode.io_mode == "r":
  82. try:
  83. self._fh = tifffile.TiffFile(request.get_file(), **kwargs)
  84. except tifffile.tifffile.TiffFileError:
  85. raise InitializationError("Tifffile can not read this file.")
  86. else:
  87. self._fh = tifffile.TiffWriter(request.get_file(), **kwargs)
  88. # ---------------------
  89. # Standard V3 Interface
  90. # ---------------------
  91. def read(self, *, index: int = None, page: int = None, **kwargs) -> np.ndarray:
  92. """Read a ndimage or page.
  93. The ndimage returned depends on the value of both ``index`` and
  94. ``page``. ``index`` selects the series to read and ``page`` allows
  95. selecting a single page from the selected series. If ``index=None``,
  96. ``page`` is understood as a flat index, i.e., the selection ignores
  97. individual series inside the file. If both ``index`` and ``page`` are
  98. ``None``, then all the series are read and returned as a batch.
  99. Parameters
  100. ----------
  101. index : int
  102. If ``int``, select the ndimage (series) located at that index inside
  103. the file and return ``page`` from it. If ``None`` and ``page`` is
  104. ``int`` read the page located at that (flat) index inside the file.
  105. If ``None`` and ``page=None``, read all ndimages from the file and
  106. return them as a batch.
  107. page : int
  108. If ``None`` return the full selected ndimage. If ``int``, read the
  109. page at the selected index and return it.
  110. kwargs : Any
  111. Additional kwargs are forwarded to TiffFile's ``as_array`` method.
  112. Returns
  113. -------
  114. ndarray : np.ndarray
  115. The decoded ndimage or page.
  116. """
  117. if "key" not in kwargs:
  118. kwargs["key"] = page
  119. elif page is not None:
  120. raise ValueError("Can't use `page` and `key` at the same time.")
  121. # set plugin default for ``index``
  122. if index is not None and "series" in kwargs:
  123. raise ValueError("Can't use `series` and `index` at the same time.")
  124. elif "series" in kwargs:
  125. index = kwargs.pop("series")
  126. elif index is not None:
  127. pass
  128. else:
  129. index = 0
  130. if index is Ellipsis and page is None:
  131. # read all series in the file and return them as a batch
  132. ndimage = np.stack([x for x in self.iter(**kwargs)])
  133. else:
  134. index = None if index is Ellipsis else index
  135. ndimage = self._fh.asarray(series=index, **kwargs)
  136. return ndimage
  137. def iter(self, **kwargs) -> np.ndarray:
  138. """Yield ndimages from the TIFF.
  139. Parameters
  140. ----------
  141. kwargs : Any
  142. Additional kwargs are forwarded to the TiffPageSeries' ``as_array``
  143. method.
  144. Yields
  145. ------
  146. ndimage : np.ndarray
  147. A decoded ndimage.
  148. """
  149. for sequence in self._fh.series:
  150. yield sequence.asarray(**kwargs)
  151. def write(
  152. self, ndimage: ArrayLike, *, is_batch: bool = False, **kwargs
  153. ) -> Optional[bytes]:
  154. """Save a ndimage as TIFF.
  155. Parameters
  156. ----------
  157. ndimage : ArrayLike
  158. The ndimage to encode and write to the ImageResource.
  159. is_batch : bool
  160. If True, the first dimension of the given ndimage is treated as a
  161. batch dimension and each element will create a new series.
  162. kwargs : Any
  163. Additional kwargs are forwarded to TiffWriter's ``write`` method.
  164. Returns
  165. -------
  166. encoded_image : bytes
  167. If the ImageResource is ``"<bytes>"``, return the encoded bytes.
  168. Otherwise write returns None.
  169. Notes
  170. -----
  171. Incremental writing is supported. Subsequent calls to ``write`` will
  172. create new series unless ``contiguous=True`` is used, in which case the
  173. call to write will append to the current series.
  174. """
  175. if not is_batch:
  176. ndimage = np.asarray(ndimage)[None, :]
  177. for image in ndimage:
  178. self._fh.write(image, **kwargs)
  179. if self._request._uri_type == URI_BYTES:
  180. self._fh.close()
  181. file = cast(BytesIO, self._request.get_file())
  182. return file.getvalue()
  183. def metadata(
  184. self, *, index: int = Ellipsis, page: int = None, exclude_applied: bool = True
  185. ) -> Dict[str, Any]:
  186. """Format-Specific TIFF metadata.
  187. The metadata returned depends on the value of both ``index`` and
  188. ``page``. ``index`` selects a series and ``page`` allows selecting a
  189. single page from the selected series. If ``index=Ellipsis``, ``page`` is
  190. understood as a flat index, i.e., the selection ignores individual
  191. series inside the file. If ``index=Ellipsis`` and ``page=None`` then
  192. global (file-level) metadata is returned.
  193. Parameters
  194. ----------
  195. index : int
  196. Select the series of which to extract metadata from. If Ellipsis, treat
  197. page as a flat index into the file's pages.
  198. page : int
  199. If not None, select the page of which to extract metadata from. If
  200. None, read series-level metadata or, if ``index=...`` global,
  201. file-level metadata.
  202. exclude_applied : bool
  203. For API compatibility. Currently ignored.
  204. Returns
  205. -------
  206. metadata : dict
  207. A dictionary with information regarding the tiff flavor (file-level)
  208. or tiff tags (page-level).
  209. """
  210. if index is not Ellipsis and page is not None:
  211. target = self._fh.series[index].pages[page]
  212. elif index is not Ellipsis and page is None:
  213. # This is based on my understanding that series-level metadata is
  214. # stored in the first TIFF page.
  215. target = self._fh.series[index].pages[0]
  216. elif index is Ellipsis and page is not None:
  217. target = self._fh.pages[page]
  218. else:
  219. target = None
  220. metadata = {}
  221. if target is None:
  222. # return file-level metadata
  223. metadata["byteorder"] = self._fh.byteorder
  224. for flag in tifffile.TIFF.FILE_FLAGS:
  225. flag_value = getattr(self._fh, "is_" + flag)
  226. metadata["is_" + flag] = flag_value
  227. if flag_value and hasattr(self._fh, flag + "_metadata"):
  228. flavor_metadata = getattr(self._fh, flag + "_metadata")
  229. if isinstance(flavor_metadata, tuple):
  230. metadata.update(flavor_metadata[0])
  231. else:
  232. metadata.update(flavor_metadata)
  233. else:
  234. # tifffile may return a TiffFrame instead of a page
  235. target = target.keyframe
  236. metadata.update({tag.name: tag.value for tag in target.tags})
  237. metadata.update(
  238. {
  239. "planar_configuration": target.planarconfig,
  240. "compression": target.compression,
  241. "predictor": target.predictor,
  242. "orientation": None, # TODO
  243. "description1": target.description1,
  244. "description": target.description,
  245. "software": target.software,
  246. **_get_resolution(target),
  247. "datetime": target.datetime,
  248. }
  249. )
  250. return metadata
  251. def properties(self, *, index: int = None, page: int = None) -> ImageProperties:
  252. """Standardized metadata.
  253. The properties returned depend on the value of both ``index`` and
  254. ``page``. ``index`` selects a series and ``page`` allows selecting a
  255. single page from the selected series. If ``index=Ellipsis``, ``page`` is
  256. understood as a flat index, i.e., the selection ignores individual
  257. series inside the file. If ``index=Ellipsis`` and ``page=None`` then
  258. global (file-level) properties are returned. If ``index=Ellipsis``
  259. and ``page=...``, file-level properties for the flattened index are
  260. returned.
  261. Parameters
  262. ----------
  263. index : int
  264. If ``int``, select the ndimage (series) located at that index inside
  265. the file. If ``Ellipsis`` and ``page`` is ``int`` extract the
  266. properties of the page located at that (flat) index inside the file.
  267. If ``Ellipsis`` and ``page=None``, return the properties for the
  268. batch of all ndimages in the file.
  269. page : int
  270. If ``None`` return the properties of the full ndimage. If ``...``
  271. return the properties of the flattened index. If ``int``,
  272. return the properties of the page at the selected index only.
  273. Returns
  274. -------
  275. image_properties : ImageProperties
  276. The standardized metadata (properties) of the selected ndimage or series.
  277. """
  278. index = index or 0
  279. page_idx = 0 if page in (None, Ellipsis) else page
  280. if index is Ellipsis:
  281. target_page = self._fh.pages[page_idx]
  282. else:
  283. target_page = self._fh.series[index].pages[page_idx]
  284. if index is Ellipsis and page is None:
  285. n_series = len(self._fh.series)
  286. props = ImageProperties(
  287. shape=(n_series, *target_page.shape),
  288. dtype=target_page.dtype,
  289. n_images=n_series,
  290. is_batch=True,
  291. spacing=_get_resolution(target_page).get("resolution"),
  292. )
  293. elif index is Ellipsis and page is Ellipsis:
  294. n_pages = len(self._fh.pages)
  295. props = ImageProperties(
  296. shape=(n_pages, *target_page.shape),
  297. dtype=target_page.dtype,
  298. n_images=n_pages,
  299. is_batch=True,
  300. spacing=_get_resolution(target_page).get("resolution"),
  301. )
  302. else:
  303. props = ImageProperties(
  304. shape=target_page.shape,
  305. dtype=target_page.dtype,
  306. is_batch=False,
  307. spacing=_get_resolution(target_page).get("resolution"),
  308. )
  309. return props
  310. def close(self) -> None:
  311. if self._fh is not None:
  312. self._fh.close()
  313. super().close()
  314. # ------------------------------
  315. # Add-on Interface inside imopen
  316. # ------------------------------
  317. def iter_pages(self, index=..., **kwargs):
  318. """Yield pages from a TIFF file.
  319. This generator walks over the flat index of the pages inside an
  320. ImageResource and yields them in order.
  321. Parameters
  322. ----------
  323. index : int
  324. The index of the series to yield pages from. If Ellipsis, walk over
  325. the file's flat index (and ignore individual series).
  326. kwargs : Any
  327. Additional kwargs are passed to TiffPage's ``as_array`` method.
  328. Yields
  329. ------
  330. page : np.ndarray
  331. A page stored inside the TIFF file.
  332. """
  333. if index is Ellipsis:
  334. pages = self._fh.pages
  335. else:
  336. pages = self._fh.series[index]
  337. for page in pages:
  338. yield page.asarray(**kwargs)