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