micro-reader 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,858 @@
1
+ """TIFF family: TIFF, BigTIFF, OME-TIFF, ImageJ hyperstacks (tifffile, a dependency).
2
+
3
+ tifffile (Gohlke, BSD-3) parses the structure -- pages, series, axes -- and
4
+ decodes compressed segments. Pixels are read here rather than through
5
+ tifffile's ``aszarr``: zarr reads whole chunks, and a TIFF chunk is a stored
6
+ strip or tile, which for the common one-strip-per-plane files is the whole
7
+ plane (PLAN.md, "M1 design").
8
+
9
+ - Uncompressed pages: positioned reads of exactly the requested rows and
10
+ columns, whatever the layout; file-adjacent strips are read together.
11
+ - Compressed pages: only the strips / tiles touching the rectangle are read
12
+ and decoded, in parallel on the shared decode pool (``_pool``), and a
13
+ bounded cache keeps a segment shared by neighbouring reads from being
14
+ decoded twice.
15
+ - Decoding: micro-reader's own codecs (``_codecs``: micro-reader-codecs,
16
+ else a reference decoder) for the compressions microscopy TIFFs use --
17
+ LZW, Deflate, PackBits, LZMA, zstd, JPEG, JPEG 2000, WebP, JPEG XL, PNG,
18
+ EER (Falcon cryo-EM cameras) -- with the horizontal and floating-point
19
+ predictors, packed unsigned depths and FillOrder 2. Anything else
20
+ (CCITT, JPEG XR, LERC, float24, ...) goes through tifffile's decoder, which
21
+ needs imagecodecs for most of them.
22
+
23
+ Images and axes (docs/AXES.md): the array axes ``T C Z Y X S`` keep the
24
+ series' own order (lower case; EER frames ``I`` are ``t``). Every other
25
+ tifffile axis makes separate images, indexed by: ``A`` (angle) view, ``R`` /
26
+ ``M`` / ``J K`` tile, LSM positions ``P`` scene, ``P`` (phase) phase, ``H``
27
+ lifetime, ``E`` (lambda) emission, and ``I`` / ``Q`` / ``L`` / ``V`` (sequence,
28
+ other, exposure, event) sequence; several letters of one index are counted
29
+ as one (raveled). ``open(path, rename={"sequence": "z"})`` reads such a
30
+ dimension as an axis instead (plain multi-page TIFFs). Each series is a
31
+ scene otherwise. Images of one series share its file handles and
32
+ decoded-segment cache.
33
+
34
+ Resolution levels (OME-TIFF SubIFDs, SVS / NDPI-style pyramids: tifffile's
35
+ ``series.levels``) are each image's ``resolutions``; with
36
+ ``resolutions="all"`` they are images of their own, indexed by ``resolution``.
37
+
38
+ Not yet: volumetric tiles (ImageDepth), OME series spread over several files.
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import itertools
43
+ import os
44
+ import threading
45
+
46
+ import numpy as np
47
+
48
+ from .. import _codecs, _jpeg_restarts, _memory, _pool, _stream
49
+ from .._base import INDEX_NAMES, File, Image
50
+ from .._errors import CorruptFileError, MicroReaderError, UnsupportedFormatError
51
+ from .._io import DecodeCache, ThreadHandles
52
+
53
+ NAME = "tiff"
54
+ MAGIC = (b"II*\x00", b"MM\x00*", b"II+\x00", b"MM\x00+") # classic and BigTIFF
55
+ CHUNK_BYTES = 4 << 20 # buffered raw rows are read this many bytes at a time
56
+ RAW_TASK_BYTES = 8 << 20 # uncompressed strip runs are split into pool tasks of this size
57
+ DECODE_CACHE_BYTES = 64 << 20 # decoded segments kept per scene
58
+ STREAM_PIECE_BYTES = 8 << 20 # rows of a streamed segment decoded at a time (reserved)
59
+ #: image codecs that decode a rectangle alone (``_area_into``)
60
+ AREA_DECODERS = {"jpeg2k": _codecs.jpeg2k_decode_area_into, "jxl": _codecs.jxl_decode_area_into}
61
+ JPEG_COMPRESSIONS = {6, 7, 33007, 34892}
62
+
63
+ #: TIFF Compression -> micro-reader's own decoder. Byte codecs give the
64
+ #: stored bytes (then the predictor applies); image codecs give pixels.
65
+ BYTE_CODECS = {1: "none", 5: "lzw", 8: "zlib", 32946: "zlib", 32773: "packbits",
66
+ 34925: "lzma", 34926: "zstd", 50000: "zstd"}
67
+ IMAGE_CODECS = {7: "jpeg", 33007: "jpeg", 34892: "jpeg",
68
+ 33003: "jpeg2k", 33004: "jpeg2k", 33005: "jpeg2k", 34712: "jpeg2k",
69
+ 34927: "webp", 50001: "webp", 50002: "jxl", 52546: "jxl", 34933: "png"}
70
+ #: EER (Falcon cryo-EM cameras): compression -> run-length bits; 65002
71
+ #: ("EER v2") takes them and the sub-pixel bits from tags 65007-65009
72
+ EER_CODES = {65000: 8, 65001: 7, 65002: None}
73
+
74
+
75
+ def eer_parameters(key) -> tuple:
76
+ """(skip, horizontal, vertical) bits of *key*'s EER stream (as tifffile
77
+ reads them; micro-reader decodes at the camera's pixel size, tifffile's
78
+ default, superres 0)."""
79
+ if key.compression == 65002:
80
+ return (int(key.tags.valueof(65007, 7)), int(key.tags.valueof(65008, 2)),
81
+ int(key.tags.valueof(65009, 2)))
82
+ return EER_CODES[key.compression], 2, 2
83
+
84
+
85
+ _BYTE_DECODERS = {"lzw": _codecs.lzw_decode_into, "zlib": _codecs.zlib_decompress_into,
86
+ "packbits": _codecs.packbits_decode_into,
87
+ "lzma": _codecs.lzma_decompress_into}
88
+ _IMAGE_DECODERS = {"jpeg2k": _codecs.jpeg2k_decode_into, "webp": _codecs.webp_decode_into,
89
+ "jxl": _codecs.jxl_decode_into, "png": _codecs.png_decode_into}
90
+
91
+
92
+ def own_codec(key):
93
+ """micro-reader's decoder for tifffile keyframe *key*'s segments, or
94
+ None when tifffile's decoder must do it."""
95
+ dtype = np.dtype(key.dtype)
96
+ if dtype.kind == "c" or not isinstance(key.bitspersample, int):
97
+ return None # complex; RGB565-style depths
98
+ c, predictor = key.compression, key.predictor
99
+ if c in BYTE_CODECS:
100
+ if key.is_subsampled or predictor not in (1, 2, 3):
101
+ return None
102
+ if key.bitspersample != 8 * dtype.itemsize:
103
+ # packed unsigned depths (1-bit masks, 2 / 4 / 10 / 12 / 14 / 24 bit):
104
+ # unpacked here; signed and float24 stay tifffile's, and TIFF
105
+ # predictors are only defined for whole-byte samples
106
+ if dtype.kind not in "ub" or predictor != 1 or key.bitspersample > 32:
107
+ return None
108
+ if (predictor == 2 and dtype.kind not in "iu") or (predictor == 3 and dtype.kind != "f"):
109
+ return None
110
+ return BYTE_CODECS[c]
111
+ if key.fillorder != 1:
112
+ return None # bit order of image codestreams
113
+ if c in EER_CODES:
114
+ # only files that are EER (BigTIFF with the acquisition metadata tag,
115
+ # as tifffile checks): codes from 65000 are a private range
116
+ if getattr(key, "is_eer", False) and key.planarconfig == 1 \
117
+ and key.samplesperpixel == 1 and dtype.kind in "bu" and dtype.itemsize in (1, 2, 4):
118
+ return "eer"
119
+ return None
120
+ if c in IMAGE_CODECS and predictor == 1 and key.planarconfig == 1:
121
+ # the codestream carries its own precision (e.g. 12-bit JPEG in uint16);
122
+ # the decoders give 8- or 16-bit samples
123
+ if dtype.kind != "u" or dtype.itemsize > 2:
124
+ return None
125
+ codec = IMAGE_CODECS[c]
126
+ if codec == "jpeg" and getattr(key, "jpegheader", None):
127
+ return None # NDPI: header outside the segments
128
+ if codec != "jpeg" and key.is_subsampled:
129
+ return None
130
+ return codec
131
+ return None
132
+
133
+
134
+ def jpeg_colour(key) -> str:
135
+ """How the JPEG segments of *key* store colour (as tifffile decides):
136
+ RGB without JFIF marker is stored untransformed (e.g. Aperio SVS)."""
137
+ if key.extrasamples:
138
+ return "auto"
139
+ if key.photometric == 6:
140
+ return "ycbcr"
141
+ if key.photometric == 2 and not key.is_jfif:
142
+ return "rgb"
143
+ return "auto"
144
+
145
+
146
+ def unpack(stored: np.ndarray, shape: tuple, bits: int, dtype: np.dtype,
147
+ byteorder: str) -> np.ndarray:
148
+ """Packed samples of one segment -> (rows, cols, samples) of *dtype*.
149
+ Below a byte, and at 10 / 12 / 14 bits, TIFF packs samples as one bit
150
+ stream, most significant bit first, each row starting on a byte
151
+ boundary. Whole-byte depths that are not a numpy size (24-bit) are
152
+ stored in the file's byte order (as libtiff does; tifffile returns
153
+ little-endian 24-bit samples shifted left by 8 bits)."""
154
+ rows, cols, samples = shape
155
+ runlen = cols * samples
156
+ out = np.empty(shape, dtype.newbyteorder("=") if dtype.kind != "b" else np.uint8)
157
+ if bits % 8 == 0 and byteorder == "<":
158
+ nb = bits // 8
159
+ b = stored.reshape(rows, -1)[:, :runlen * nb].reshape(rows, runlen, nb).astype(np.uint32)
160
+ value = sum(b[..., k] << np.uint32(8 * k) for k in range(nb))
161
+ out[...] = value.reshape(shape)
162
+ else:
163
+ _codecs.unpack_bits_into(stored, out, bits, runlen)
164
+ return out.view(np.bool_) if dtype.kind == "b" else out
165
+
166
+
167
+ def unpredict(seg: np.ndarray, predictor: int) -> np.ndarray:
168
+ """Undo TIFF's horizontal (2) or floating-point (3) predictor on a
169
+ segment (rows, cols, samples) in the file's dtype; returns it in native
170
+ byte order. Floating point (libtiff ``fpAcc``): per row, bytes summed
171
+ with a stride of one pixel, then byte planes, most significant first."""
172
+ rows, cols, nc = seg.shape
173
+ if predictor == 2:
174
+ out = seg.astype(seg.dtype.newbyteorder("="), copy=False)
175
+ np.cumsum(out, axis=1, dtype=out.dtype, out=out)
176
+ return out
177
+ item = seg.dtype.itemsize
178
+ b = np.ascontiguousarray(seg).view(np.uint8).reshape(rows, cols * item, nc)
179
+ np.cumsum(b, axis=1, dtype=np.uint8, out=b)
180
+ planes = b.reshape(rows, item, cols * nc).transpose(0, 2, 1)
181
+ values = np.ascontiguousarray(planes).view(f">{seg.dtype.kind}{item}")
182
+ return values.reshape(rows, cols, nc).astype(seg.dtype.newbyteorder("="))
183
+
184
+ #: tifffile axis letter -> array axis (docs/AXES.md)
185
+ AXIS_LETTERS = {"X": "x", "Y": "y", "Z": "z", "T": "t", "C": "c", "S": "s"}
186
+ #: tifffile axis letter -> image index (LSM ``P`` is a position: "scene");
187
+ #: letters tifffile does not name are "sequence" too
188
+ INDEX_LETTERS = {"A": "view", "R": "tile", "M": "tile", "J": "tile", "K": "tile",
189
+ "P": "phase", "H": "lifetime", "E": "emission",
190
+ "I": "sequence", "Q": "sequence", "L": "sequence", "V": "sequence"}
191
+ #: indices besides "scene", in index order
192
+ OTHER_INDICES = tuple(n for n in INDEX_NAMES if n not in ("scene", "resolution"))
193
+ #: this backend can make an index an axis (open(..., rename=...))
194
+ RENAMES = True
195
+
196
+
197
+ def translate_axes(axes: str, *, lsm: bool = False, eer: bool = False, where: str = "",
198
+ rename: dict = None):
199
+ """tifffile series axes -> (array axis letter or None per axis, image
200
+ index or None per axis); *rename* makes an index an axis instead."""
201
+ rename = rename or {}
202
+ letters, levels = [], []
203
+ for a in axes.upper():
204
+ if a in AXIS_LETTERS:
205
+ level, letter = None, AXIS_LETTERS[a]
206
+ elif a == "I" and eer:
207
+ level, letter = None, "t"
208
+ else:
209
+ level = "scene" if (a == "P" and lsm) else INDEX_LETTERS.get(a, "sequence")
210
+ letter = None
211
+ if level in rename:
212
+ level, letter = None, rename[level]
213
+ levels.append(level)
214
+ letters.append(letter)
215
+ used = [x for x in letters if x]
216
+ doubled = sorted({x for x in used if used.count(x) > 1})
217
+ if doubled:
218
+ raise UnsupportedFormatError(
219
+ f"{where}: series axes {axes!r} give axis {doubled[0]!r} twice"
220
+ + (f" (with rename={rename})" if rename else ""))
221
+ return letters, levels
222
+
223
+
224
+ #: tifffile page axes -> their positions in the normalised page shape
225
+ #: (separate sample, depth, length, width, contig sample)
226
+ _PAGE_AXES = {"YX": (2, 3), "YXS": (2, 3, 4), "SYX": (0, 2, 3)}
227
+ #: view of one page's part of the result as (separate sample, y, x, contig sample)
228
+ _AS_NORMALISED = {"YX": lambda a: a[None, :, :, None],
229
+ "YXS": lambda a: a[None],
230
+ "SYX": lambda a: a[..., None]}
231
+
232
+
233
+ def sniff(head: bytes, path: str) -> bool:
234
+ return head[:4] in MAGIC
235
+
236
+
237
+ def _readinto(fh, buf: np.ndarray, path: str) -> None:
238
+ if fh.readinto(memoryview(buf).cast("B")) != buf.nbytes:
239
+ raise CorruptFileError(f"{path!r}: pixel data ends early (truncated file?)")
240
+
241
+
242
+ class _TiffImage(Image):
243
+ """One image of a tifffile series (base level): the whole series, or one
244
+ view / tile / LSM position of it (*fixed*: series axis -> index)."""
245
+
246
+ def __init__(self, tif, index: int, series, letters: list, *, fixed: dict = None,
247
+ key: dict = None, share: "_TiffImage" = None):
248
+ image_key = key
249
+ key = series.keyframe
250
+ where = f"{os.path.basename(tif.filehandle.path)!r} series {index}"
251
+ if series.is_multifile:
252
+ raise UnsupportedFormatError(f"{where}: series spread over several files")
253
+ if key.dtype is None:
254
+ raise UnsupportedFormatError(
255
+ f"{where}: data type not supported (SampleFormat {key.sampleformat}, "
256
+ f"{key.bitspersample}-bit)")
257
+ if key.shaped[1] != 1:
258
+ raise UnsupportedFormatError(f"{where}: volumetric pages (ImageDepth)")
259
+ if key.axes not in _PAGE_AXES:
260
+ raise UnsupportedFormatError(f"{where}: page axes {key.axes!r}")
261
+ # tifffile stacks the pages and reshapes them to the series shape, so
262
+ # the pages must be its trailing dimensions. Only the shapes must
263
+ # match: series metadata may name the page axes differently (e.g. an
264
+ # RGB-planar page 'SYX' labelled 'ZYX').
265
+ shape, axes, k = tuple(series.shape), series.axes, len(key.shape)
266
+ if shape[len(shape) - k:] != tuple(key.shape):
267
+ raise UnsupportedFormatError(
268
+ f"{where}: pages ({key.axes} {key.shape}) do not map onto the "
269
+ f"trailing axes of the series ({axes} {shape})")
270
+
271
+ self._tif = tif
272
+ self._series = series
273
+ self._key = key
274
+ self._path = tif.filehandle.path
275
+ self._lead_shape = shape[:len(shape) - k]
276
+ self._page_pos = _PAGE_AXES[key.axes]
277
+ self._sep, _, self._length, self._width, self._contig = key.shaped
278
+ self._file_dtype = np.dtype(key.dtype).newbyteorder(tif.byteorder)
279
+ self._raw = (key.compression == 1 and key.predictor == 1 and key.fillorder == 1
280
+ and not key.is_subsampled and key.sampleformat != 5
281
+ and key.bitspersample == 8 * self._file_dtype.itemsize)
282
+ self._as_normalised = _AS_NORMALISED[key.axes]
283
+ self._tiled = bool(key.is_tiled)
284
+ if self._tiled:
285
+ self._seg_rows, self._seg_cols = key.tilelength, key.tilewidth
286
+ else:
287
+ self._seg_rows, self._seg_cols = (key.rowsperstrip or self._length), self._width
288
+ self._segs_down = -(-self._length // self._seg_rows)
289
+ self._segs_across = -(-self._width // self._seg_cols)
290
+
291
+ # Series that list only their first IFD although they hold many
292
+ # pages (ImageJ hyperstacks > 4 GB, MetaMorph STK): the pages follow
293
+ # each other as one contiguous block.
294
+ npages = int(np.prod(self._lead_shape, dtype=np.int64))
295
+ self._page_bytes = int(np.prod(key.shaped, dtype=np.int64)) * self._file_dtype.itemsize
296
+ self._contiguous = None
297
+ if npages > len(series.pages):
298
+ if series.dataoffset is None or not self._raw:
299
+ raise UnsupportedFormatError(f"{where}: truncated series that is not contiguous")
300
+ if os.path.getsize(self._path) < series.dataoffset + npages * self._page_bytes:
301
+ raise CorruptFileError(f"{where}: truncated file")
302
+ self._contiguous = series.dataoffset
303
+
304
+ if share is None: # the first image of this series
305
+ self._pages: dict = {} # page -> (offsets, bytecounts, jpegtables) | None
306
+ self._lock = threading.Lock() # tifffile parses IFDs through one file handle
307
+ self._handles = ThreadHandles(self._path)
308
+ #: decoded segments kept for reuse (``decode_cache.max_bytes`` to
309
+ #: resize); shared by the images of one series
310
+ self.decode_cache = DecodeCache(DECODE_CACHE_BYTES)
311
+ #: (page, segment) -> RestartIndex | None, of JPEG segments too
312
+ #: large to decode whole (``_jpeg_rows_into``)
313
+ self._restarts: dict = {}
314
+ else:
315
+ self._pages, self._lock = share._pages, share._lock
316
+ self._handles, self.decode_cache = share._handles, share.decode_cache
317
+ self._restarts = share._restarts
318
+ self._closed = False
319
+ #: micro-reader's own decoder for the segments; None: tifffile's
320
+ self._codec = None if self._raw else own_codec(key)
321
+ self._predictor = key.predictor
322
+ self._jpeg_colour = jpeg_colour(key)
323
+ if not self._raw and self._codec is None:
324
+ self._decode = key.decode # tifffile builds its decode function once
325
+
326
+ # view / tile / position axes fixed at one index: not axes of the image
327
+ self._fixed = dict(fixed or {})
328
+ if any(pos >= len(self._lead_shape) for pos in self._fixed):
329
+ raise UnsupportedFormatError(f"{where}: a view / tile axis inside the pages ({axes})")
330
+ self._full_shape = shape
331
+ keep = [i for i in range(len(shape)) if i not in self._fixed]
332
+ unit = None
333
+ if not self._raw:
334
+ per_pos = {0: 1, 2: self._seg_rows, 3: self._seg_cols, 4: self._contig}
335
+ unit = (1,) * len(self._lead_shape) + tuple(per_pos[p] for p in self._page_pos)
336
+ unit = [unit[i] for i in keep]
337
+ super().__init__(index, "".join(letters[i] for i in keep), [shape[i] for i in keep],
338
+ series.dtype, name=series.name or f"series{index}", read_unit=unit,
339
+ index=image_key)
340
+
341
+ # -- page index ---------------------------------------------------------
342
+
343
+ def _page(self, p: int):
344
+ """(offsets, bytecounts, jpegtables) of page *p*, None if it is missing."""
345
+ try:
346
+ return self._pages[p]
347
+ except KeyError:
348
+ pass
349
+ with self._lock:
350
+ if p not in self._pages:
351
+ key = self._key
352
+ if self._contiguous is not None:
353
+ shift = self._contiguous + p * self._page_bytes - key.dataoffsets[0]
354
+ entry = ([o + shift for o in key.dataoffsets], key.databytecounts, None)
355
+ else:
356
+ page = self._series.pages[p] # parses the IFD on first access
357
+ entry = None if page is None else (
358
+ page.dataoffsets, page.databytecounts,
359
+ getattr(page, "jpegtables", None) or key.jpegtables)
360
+ self._pages[p] = entry
361
+ return self._pages[p]
362
+
363
+ # -- reading --------------------------------------------------------------
364
+
365
+ def _read_block(self, block):
366
+ if self._closed:
367
+ raise ValueError("this TIFF image has been closed: its file was closed "
368
+ "(File.close() or the end of a with block)")
369
+ if not self._fixed:
370
+ return self._read_series(block)
371
+ it = iter(block)
372
+ full = tuple(slice(self._fixed[i], self._fixed[i] + 1) if i in self._fixed else next(it)
373
+ for i in range(len(self._full_shape)))
374
+ out, read = self._read_series(full)
375
+ return out.reshape(tuple(s.stop - s.start for s in block)), read
376
+
377
+ def _read_series(self, block):
378
+ """Read *block* over all axes of the series."""
379
+ nlead = len(self._lead_shape)
380
+ lead, inner = block[:nlead], block[nlead:]
381
+ # the page part of the request as (separate sample, y, x, contig sample)
382
+ norm = {0: slice(0, self._sep), 4: slice(0, self._contig)}
383
+ norm.update(zip(self._page_pos, inner))
384
+ ss, ys, xs, cs = norm[0], norm[2], norm[3], norm[4]
385
+
386
+ out = np.empty(tuple(s.stop - s.start for s in block), dtype=self.dtype)
387
+ raws, decodes = [], []
388
+ for idx in itertools.product(*(range(s.start, s.stop) for s in lead)):
389
+ p = int(np.ravel_multi_index(idx, self._lead_shape)) if nlead else 0
390
+ dest = self._as_normalised(out[tuple(i - s.start for i, s in zip(idx, lead))])
391
+ self._plan_page(p, ss, ys, xs, cs, dest, raws, decodes)
392
+ # every page's uncompressed pieces and compressed segments, on all cores
393
+ tasks = itertools.chain(
394
+ (lambda job=job: self._read_rect(self._handles.get(), *job) for job in raws),
395
+ self._decode_tasks(decodes))
396
+ return out, _pool.run(tasks)
397
+
398
+ def _plan_page(self, p, ss, ys, xs, cs, dest, raws, decodes) -> None:
399
+ """What page *p* contributes: empty parts are filled here; uncompressed
400
+ pieces go to *raws* and compressed segments to *decodes*, to run
401
+ together on the pool."""
402
+ entry = self._page(p)
403
+ if entry is None:
404
+ dest[...] = self._key.nodata
405
+ return
406
+ offsets, counts, jpegtables = entry
407
+ rows, cols = self._seg_rows, self._seg_cols
408
+ r0, r1 = ys.start // rows, (ys.stop - 1) // rows + 1
409
+ c0, c1 = xs.start // cols, (xs.stop - 1) // cols + 1
410
+ per_sample = self._segs_down * self._segs_across
411
+ for d, s in zip(dest, range(ss.start, ss.stop)):
412
+ if self._raw and not self._tiled:
413
+ self._plan_raw_strips(offsets, counts, s * per_sample, r0, r1, ys, xs, cs, d, raws)
414
+ continue
415
+ for tr in range(r0, r1):
416
+ for tc in range(c0, c1):
417
+ i = s * per_sample + tr * self._segs_across + tc
418
+ sy, sx = tr * rows, tc * cols
419
+ y0, y1 = max(ys.start, sy), min(ys.stop, sy + rows)
420
+ x0, x1 = max(xs.start, sx), min(xs.stop, sx + cols)
421
+ dst = d[y0 - ys.start:y1 - ys.start, x0 - xs.start:x1 - xs.start]
422
+ if not counts[i] or not offsets[i]: # sparse segment
423
+ dst[...] = self._key.nodata
424
+ elif self._raw: # uncompressed tile
425
+ raws.append((offsets[i], cols, (y0 - sy, y1 - sy), (x0 - sx, x1 - sx),
426
+ cs, dst))
427
+ else:
428
+ decodes.append((p, i, offsets[i], counts[i], jpegtables, dst,
429
+ (0, slice(y0 - sy, y1 - sy), slice(x0 - sx, x1 - sx), cs)))
430
+
431
+ def _plan_raw_strips(self, offsets, counts, base, r0, r1, ys, xs, cs, d, raws) -> None:
432
+ """Uncompressed strips: strips that follow each other in the file are
433
+ one run, and runs are cut into pieces of about RAW_TASK_BYTES so a
434
+ single large read still spreads over the pool."""
435
+ rows, width = self._seg_rows, self._width
436
+ row_bytes = width * self._contig * self._file_dtype.itemsize
437
+ strip_bytes = rows * row_bytes
438
+ per_task = max(1, RAW_TASK_BYTES // row_bytes)
439
+ tr = r0
440
+ while tr < r1:
441
+ i = base + tr
442
+ end = tr + 1
443
+ while (end < r1 and offsets[base + end] == offsets[base + end - 1] + strip_bytes
444
+ and counts[base + end - 1] >= strip_bytes):
445
+ end += 1
446
+ sy = tr * rows
447
+ y0, y1 = max(ys.start, sy), min(ys.stop, end * rows)
448
+ if not counts[i] or not offsets[i]:
449
+ d[y0 - ys.start:y1 - ys.start] = self._key.nodata
450
+ else:
451
+ for a in range(y0, y1, per_task):
452
+ b = min(y1, a + per_task)
453
+ raws.append((offsets[i], width, (a - sy, b - sy), (xs.start, xs.stop),
454
+ cs, d[a - ys.start:b - ys.start]))
455
+ tr = end
456
+
457
+ def _read_rect(self, fh, offset, stride, rows, cols, cs, dst) -> int:
458
+ """Rows ``rows`` and columns ``cols`` of an uncompressed segment of
459
+ width *stride* stored at *offset*, into *dst* (rows, cols, samples).
460
+ Straight into *dst* when its memory has the file's layout (native
461
+ byte order, all samples, contiguous rows); else through a buffer."""
462
+ item, nc = self._file_dtype.itemsize, self._contig
463
+ row_len = stride * nc
464
+ direct = ((item == 1 or self._file_dtype.isnative)
465
+ and (cs.start, cs.stop) == (0, nc))
466
+ if cols == (0, stride) and direct and dst.flags.c_contiguous:
467
+ fh.seek(offset + rows[0] * row_len * item)
468
+ _readinto(fh, dst, self._path)
469
+ return dst.size
470
+ if cols != (0, stride) and direct and dst[0].flags.c_contiguous:
471
+ width = cols[1] - cols[0]
472
+ for r in range(rows[0], rows[1]):
473
+ fh.seek(offset + (r * stride + cols[0]) * nc * item)
474
+ _readinto(fh, dst[r - rows[0]], self._path)
475
+ return (rows[1] - rows[0]) * width * nc
476
+ if cols == (0, stride):
477
+ # whole rows are contiguous: read them in chunks
478
+ per_chunk = max(1, CHUNK_BYTES // (row_len * item))
479
+ buf = np.empty((min(per_chunk, rows[1] - rows[0]), stride, nc), self._file_dtype)
480
+ for a in range(rows[0], rows[1], per_chunk):
481
+ n = min(per_chunk, rows[1] - a)
482
+ fh.seek(offset + a * row_len * item)
483
+ _readinto(fh, buf[:n], self._path)
484
+ dst[a - rows[0]:a - rows[0] + n] = buf[:n, :, cs]
485
+ return (rows[1] - rows[0]) * row_len
486
+ width = cols[1] - cols[0]
487
+ buf = np.empty((width, nc), self._file_dtype)
488
+ for r in range(rows[0], rows[1]):
489
+ fh.seek(offset + (r * stride + cols[0]) * nc * item)
490
+ _readinto(fh, buf, self._path)
491
+ dst[r - rows[0]] = buf[:, cs]
492
+ return (rows[1] - rows[0]) * width * nc
493
+
494
+ def _decode_tasks(self, decodes):
495
+ """For each collected segment: copy it from the cache, or yield a
496
+ task that reads, decodes and copies it on a pool thread -- the whole
497
+ segment when it fits the memory budget, else only the rows asked
498
+ for, streamed (``_stream_into``)."""
499
+ for p, i, offset, count, jpegtables, dst, part in decodes:
500
+ seg = self.decode_cache.get((p, i))
501
+ if seg is not None:
502
+ dst[...] = seg[part]
503
+ continue
504
+ rows, cols, samples = self._segment_shape(i)
505
+ nbytes = rows * cols * samples * self._file_dtype.itemsize
506
+ if _memory.should_stream(nbytes) and self._streamable():
507
+ yield lambda job=(i, offset, count, dst, part): self._stream_into(*job)
508
+ continue
509
+ if _memory.should_stream(nbytes) and self._codec in AREA_DECODERS:
510
+ yield lambda job=(offset, count, dst, part): self._area_into(*job)
511
+ continue
512
+ what = (f"{self._path!r}: a {rows} x {cols} TIFF segment compressed with "
513
+ f"{getattr(self._key.compression, 'name', self._key.compression)}")
514
+ if _memory.should_stream(nbytes) and self._codec == "jpeg":
515
+ yield lambda job=(p, i, offset, count, jpegtables, dst, part), n=nbytes, w=what: \
516
+ self._jpeg_rows_into(*job, n, w)
517
+ continue
518
+ if not _memory.fits(nbytes):
519
+ raise _memory.too_large(what, nbytes)
520
+ yield lambda job=(p, i, offset, count, jpegtables, dst, part), n=nbytes, w=what: \
521
+ self._reserved(n, w, self._decode_into, *job)
522
+
523
+ def _area_into(self, offset, count, dst, part) -> int:
524
+ """Rows ``part[1]`` and columns ``part[2]`` of a JPEG 2000 or JPEG XL
525
+ segment too large to cache, decoded alone (OpenJPEG decodes only the
526
+ code-blocks covering them, jxl-oxide the groups), in bands of rows
527
+ within the budget. Runs on a pool thread."""
528
+ decode = AREA_DECODERS[self._codec]
529
+ r0, r1, c0, c1 = part[1].start, part[1].stop, part[2].start, part[2].stop
530
+ data = self._handles.read(offset, count)
531
+ row_bytes = (c1 - c0) * self._contig * self.dtype.itemsize
532
+ per_band = max(1, min(STREAM_PIECE_BYTES, _memory.memory_limit() // 4) // row_bytes)
533
+ for a in range(r0, r1, per_band): # bands within the budget
534
+ b = min(r1, a + per_band)
535
+ area = np.empty((b - a, c1 - c0, self._contig), self.dtype)
536
+ with _memory.reserve(area.nbytes, f"{self._path!r}: part of a segment"):
537
+ got = decode(data, area, c0, a, c1, b)
538
+ if (got[1], got[0], got[2]) != area.shape:
539
+ raise CorruptFileError(
540
+ f"{self._path!r}: {self._codec} area decodes to {got[1]} x {got[0]} x "
541
+ f"{got[2]}, expected {area.shape}")
542
+ dst[a - r0:b - r0] = area[..., part[3]]
543
+ return (r1 - r0) * (c1 - c0) * self._contig
544
+
545
+ def _jpeg_rows_into(self, p, i, offset, count, jpegtables, dst, part, nbytes, what) -> int:
546
+ """Rows ``part[1]`` of a JPEG segment too large to cache: cut out at
547
+ its restart markers and decoded in bands of rows (``_jpeg_restarts``);
548
+ without restart markers, or progressive, decoded whole within the
549
+ memory limit. Runs on a pool thread."""
550
+ index = self._restart_index(p, i, offset, count, jpegtables)
551
+ row_bytes = self._segment_shape(i)[1] * self._contig * self.dtype.itemsize
552
+ if not _jpeg_restarts.usable(index, row_bytes, _memory.fits):
553
+ if not _memory.fits(nbytes):
554
+ raise _memory.too_large(what + " that cannot be cut at restart markers", nbytes)
555
+ return self._reserved(nbytes, what, self._decode_into,
556
+ p, i, offset, count, jpegtables, dst, part)
557
+ shift = len(jpegtables) - 4 if jpegtables else 0 # tables before the segment's header
558
+ r0, r1 = part[1].start, part[1].stop
559
+ piece = min(STREAM_PIECE_BYTES, _memory.memory_limit() // 4)
560
+ for a, b, rows in _jpeg_restarts.read_rows(
561
+ index, lambda s, e: self._handles.read(offset + s - shift, e - s), r0, r1,
562
+ lambda data, out: _codecs.jpeg_decode_into(data, out, colour=self._jpeg_colour),
563
+ (self._contig,), self.dtype, piece,
564
+ lambda n: _memory.reserve(n, f"{self._path!r}: rows of a JPEG segment")):
565
+ dst[a - r0:b - r0] = rows[:, part[2], part[3]]
566
+ return (r1 - r0) * index.width * self._contig
567
+
568
+ def _restart_index(self, p, i, offset, count, jpegtables):
569
+ """The ``RestartIndex`` of JPEG segment *i* of page *p* (None: it
570
+ cannot be cut), built once from the whole compressed segment."""
571
+ key = (p, i)
572
+ if key not in self._restarts:
573
+ with _memory.reserve(count, f"{self._path!r}: a compressed JPEG segment"):
574
+ data = self._handles.read(offset, count)
575
+ if jpegtables: # abbreviated stream: tables from JPEGTables
576
+ data = bytes(jpegtables[:-2]) + bytes(data[2:])
577
+ index = _jpeg_restarts.RestartIndex.build(data)
578
+ with self._lock:
579
+ self._restarts[key] = index
580
+ return self._restarts[key]
581
+
582
+ @staticmethod
583
+ def _reserved(nbytes, what, decode, *job) -> int:
584
+ """*decode(*job)* holding *nbytes* of the memory budget."""
585
+ with _memory.reserve(nbytes, what):
586
+ return decode(*job)
587
+
588
+ def _streamable(self) -> bool:
589
+ """Whether this page's segments can be decoded row by row: a byte
590
+ codec micro-reader streams, no bit reversal (FillOrder 2)."""
591
+ codec = self._codec
592
+ return (codec in _stream.STREAMABLE and _stream.streamable(codec)
593
+ and self._key.fillorder == 1 and codec in BYTE_CODECS.values())
594
+
595
+ def _stream_into(self, i, offset, count, dst, part) -> int:
596
+ """Rows ``part[1]`` of segment *i*, decoded as a stream, into *dst*
597
+ -- in pieces of rows, so a tall region of a wide segment never
598
+ holds more than a piece. Runs on a pool thread."""
599
+ key = self._key
600
+ codec = self._codec
601
+ _, cols, samples = self._segment_shape(i)
602
+ bits = key.bitspersample
603
+ packed = bits != 8 * self._file_dtype.itemsize
604
+ row_bytes = (cols * samples * bits + 7) // 8 if packed else cols * samples * self._file_dtype.itemsize
605
+ r0, r1 = part[1].start, part[1].stop
606
+ per_piece = max(1, min(STREAM_PIECE_BYTES, _memory.memory_limit() // 4) // row_bytes)
607
+ for a in range(r0, r1, per_piece):
608
+ b = min(r1, a + per_piece)
609
+ with _memory.reserve((b - a) * row_bytes, f"{self._path!r}: rows of a segment"):
610
+ buf = np.empty((b - a) * row_bytes, np.uint8)
611
+ n = _stream.read(self._path, offset, count, codec, a * row_bytes, buf,
612
+ size=self._segment_shape(i)[0] * row_bytes)
613
+ if n < buf.nbytes: # short segment: zeros, as decoded whole
614
+ buf[n:] = 0
615
+ if packed:
616
+ rows_ = unpack(buf, (b - a, cols, samples), bits, self._file_dtype,
617
+ key.parent.byteorder)
618
+ else:
619
+ rows_ = buf.view(self._file_dtype).reshape(b - a, cols, samples)
620
+ if self._predictor != 1:
621
+ rows_ = unpredict(rows_, self._predictor)
622
+ dst[a - r0:b - r0] = rows_[:, part[2], part[3]]
623
+ return (r1 - r0) * cols * samples
624
+
625
+ def _decode_into(self, p, i, offset, count, jpegtables, dst, part) -> int:
626
+ """Read and decode segment *i* of page *p*, copy ``seg[part]`` into
627
+ *dst*, keep it in the cache if it is small enough; return the number
628
+ of pixels decoded. Runs on a pool thread."""
629
+ data = self._handles.read(offset, count)
630
+ if self._codec is not None:
631
+ seg = self._decode_segment(data, i, jpegtables)
632
+ else:
633
+ kwargs = {"_fullsize": False}
634
+ if self._key.compression in JPEG_COMPRESSIONS:
635
+ kwargs.update(jpegtables=jpegtables, jpegheader=self._key.jpegheader)
636
+ try:
637
+ seg, _, _ = self._decode(data, i, **kwargs)
638
+ except Exception as exc: # noqa: BLE001 - codec specific
639
+ raise self._decode_error(exc) from exc
640
+ dst[...] = seg[part]
641
+ self.decode_cache.put((p, i), seg)
642
+ return seg.size
643
+
644
+ def _segment_shape(self, i: int) -> tuple:
645
+ """(rows, cols, samples) of segment *i*: tiles are always whole;
646
+ the last strip of a plane holds only the rows that are left."""
647
+ rows = self._seg_rows
648
+ if not self._tiled:
649
+ strip = i % self._segs_down
650
+ rows = min(rows, self._length - strip * rows)
651
+ return rows, self._seg_cols, self._contig
652
+
653
+ def _decode_segment(self, data, i: int, jpegtables) -> np.ndarray:
654
+ """Segment *i* with micro-reader's own codecs -> (1, rows, cols,
655
+ samples), the shape tifffile's decoder gives."""
656
+ shape = self._segment_shape(i)
657
+ codec = self._codec
658
+ if codec in BYTE_CODECS.values():
659
+ if self._key.fillorder == 2: # bits of every stored byte reversed
660
+ data = _codecs.reverse_bits(data)
661
+ bits = self._key.bitspersample
662
+ packed = bits != 8 * self._file_dtype.itemsize
663
+ rows, cols, samples = shape
664
+ run_bytes = (cols * samples * bits + 7) // 8 # packed rows start on a byte
665
+ # the stored bytes: the samples themselves, or packed ones
666
+ stored = np.empty(rows * run_bytes, np.uint8) if packed else \
667
+ np.empty(shape, self._file_dtype)
668
+ if codec == "none":
669
+ n = min(len(data), stored.nbytes)
670
+ memoryview(stored).cast("B")[:n] = memoryview(data)[:n]
671
+ elif codec == "zstd":
672
+ _codecs.zstd_decompress_into(data, stored)
673
+ n = stored.nbytes
674
+ else:
675
+ n = _BYTE_DECODERS[codec](data, stored, partial=True)
676
+ if n < stored.nbytes: # short segment (e.g. LZW without EOI): zeros
677
+ memoryview(stored).cast("B")[n:] = bytes(stored.nbytes - n)
678
+ seg = unpack(stored, shape, bits, self._file_dtype, self._key.parent.byteorder) \
679
+ if packed else stored
680
+ if self._predictor != 1:
681
+ seg = unpredict(seg, self._predictor)
682
+ return seg[None]
683
+ seg = np.empty(shape, self.dtype)
684
+ if codec == "eer":
685
+ _codecs.eer_decode_into(data, seg.reshape(shape[:2]), *eer_parameters(self._key))
686
+ return seg[None]
687
+ if codec == "jpeg":
688
+ if jpegtables: # abbreviated stream: tables from JPEGTables
689
+ data = bytes(jpegtables[:-2]) + bytes(data[2:])
690
+ got = _codecs.jpeg_decode_into(data, seg, colour=self._jpeg_colour)
691
+ else:
692
+ got = _IMAGE_DECODERS[codec](data, seg)
693
+ if (got[1], got[0], got[2]) != shape:
694
+ raise CorruptFileError(
695
+ f"{self._path!r}: segment {i} decodes to {got[1]} x {got[0]} x {got[2]}, "
696
+ f"the TIFF tags say {shape[0]} x {shape[1]} x {shape[2]}")
697
+ return seg[None]
698
+
699
+ def _decode_error(self, exc: Exception) -> Exception:
700
+ """A tifffile decode failure, explained when imagecodecs is missing."""
701
+ try:
702
+ import imagecodecs # noqa: F401
703
+ except ImportError:
704
+ key = self._key
705
+ return UnsupportedFormatError(
706
+ f"{self._path!r}: TIFF compression {key.compression} "
707
+ f"({getattr(key.compression, 'name', '?')}, predictor {key.predictor}, "
708
+ f"{key.bitspersample}-bit, fill order {key.fillorder}) is decoded by "
709
+ "imagecodecs, which is not installed: pip install imagecodecs")
710
+ return CorruptFileError(f"{self._path!r}: {exc}")
711
+
712
+ def _read_metadata(self):
713
+ from ._tiff_metadata import tiff_metadata
714
+ n = self.shape[self.axes.index("c")] if "c" in self.axes else 1
715
+ return tiff_metadata(self, n, self._meta_cache)
716
+
717
+ def close(self) -> None:
718
+ """This image is unusable afterwards; the file handles it shares with
719
+ the other images of its series close with the file."""
720
+ self._closed = True
721
+
722
+ def _release(self) -> None:
723
+ self._handles.close()
724
+ self.decode_cache.clear()
725
+ _stream.forget(self._path)
726
+
727
+
728
+ class _TiffFile(File):
729
+ backend = NAME
730
+
731
+ def __init__(self, path, images, tif):
732
+ self.format = tiff_format(tif)
733
+ super().__init__(path, images)
734
+ self._tif = tif
735
+
736
+ def close(self) -> None:
737
+ super().close()
738
+ for image in self.images:
739
+ getattr(image, "_release", lambda: None)()
740
+ self._tif.close()
741
+
742
+
743
+ #: tifffile's file flags -> File.format, first match wins
744
+ TIFF_FORMATS = (("is_ome", "ome-tiff"), ("is_imagej", "imagej"), ("is_lsm", "lsm"),
745
+ ("is_stk", "stk"), ("is_eer", "eer"), ("is_svs", "svs"), ("is_ndpi", "ndpi"))
746
+
747
+
748
+ def tiff_format(tif) -> str:
749
+ """The TIFF variant: a microscopy flavour tifffile recognises, else
750
+ "bigtiff" or "tiff"."""
751
+ for flag, name in TIFF_FORMATS:
752
+ if getattr(tif, flag, False):
753
+ return name
754
+ return "bigtiff" if tif.is_bigtiff else "tiff"
755
+
756
+
757
+ def _images(tif, series: list, path: str, tiles: str, resolutions: str = "base",
758
+ rename: dict = None) -> list:
759
+ """The images of *series*: one per series and view / tile / position;
760
+ each with its pyramid (tifffile's ``series.levels``) as ``resolutions``,
761
+ listed as images of their own with *resolutions* = "all"."""
762
+ plans, levels = [], set()
763
+ for i, s in enumerate(series):
764
+ where = f"{os.path.basename(path)!r} series {i}"
765
+ letters, frame = translate_axes(s.axes, lsm=bool(tif.is_lsm),
766
+ eer=bool(getattr(s.keyframe, "is_eer", False)),
767
+ where=where, rename=rename)
768
+ # frame axes of more than one index make images; size 1 is just fixed
769
+ by_level: dict = {}
770
+ for pos, level in enumerate(frame):
771
+ if level and s.shape[pos] > 1:
772
+ by_level.setdefault(level, []).append(pos)
773
+ levels.update(level for level in by_level if level != "scene")
774
+ plans.append((s, letters, frame, by_level))
775
+ levels = [level for level in OTHER_INDICES if level in levels]
776
+ if tiles == "stitched" and "tile" in levels:
777
+ raise UnsupportedFormatError(
778
+ f"{path!r}: tiles='stitched': micro-reader does not compose TIFF tiles "
779
+ "(each tile is its own image)")
780
+ listed = resolutions == "all"
781
+ images, scene = [], 0
782
+ for n, (s, letters, frame, by_level) in enumerate(plans):
783
+ positions = [pos for pos, level in enumerate(frame) if level]
784
+ varying = [pos for pos in positions if s.shape[pos] > 1]
785
+ first = None
786
+ # lower resolution levels with the same axes (a pyramid shrinks y / x)
787
+ lower = [lv for lv in list(getattr(s, "levels", None) or [s])[1:]
788
+ if len(lv.shape) == len(s.shape) and lv.axes == s.axes]
789
+ level_first: dict = {}
790
+ scenes_here = int(np.prod([s.shape[p] for p in by_level.get("scene", [])]))
791
+ for values in itertools.product(*(range(s.shape[p]) for p in varying)):
792
+ fixed = {pos: 0 for pos in positions}
793
+ fixed.update(zip(varying, values))
794
+ key = {"scene": scene + _ravel(fixed, by_level.get("scene", []), s.shape)}
795
+ for level in levels:
796
+ key[level] = _ravel(fixed, by_level.get(level, []), s.shape)
797
+ if listed:
798
+ key["resolution"] = 0
799
+ image = _TiffImage(tif, len(images), s, letters, fixed=fixed, key=key, share=first)
800
+ image._series_index = n # position in the listed series
801
+ first = first or image
802
+ images.append(image)
803
+ pyramid = [image]
804
+ for k, level_series in enumerate(lower, start=1):
805
+ level_image = _TiffImage(tif, len(images), level_series, letters, fixed=fixed,
806
+ key={**key, "resolution": k},
807
+ share=level_first.get(k))
808
+ level_image._series_index = n
809
+ level_first.setdefault(k, level_image)
810
+ pyramid.append(level_image)
811
+ if listed:
812
+ images.append(level_image)
813
+ if len(pyramid) > 1:
814
+ for member in pyramid:
815
+ member._resolutions = pyramid
816
+ scene += scenes_here
817
+ images.sort(key=lambda im: tuple(im.index.values()))
818
+ meta_cache: dict = {} # metadata parsed once per file
819
+ for n, image in enumerate(images):
820
+ image._position = n
821
+ for member in image.resolutions:
822
+ member._meta_cache = meta_cache
823
+ return images
824
+
825
+
826
+ def _ravel(fixed: dict, positions: list, shape) -> int:
827
+ """The flat index of *fixed*'s values at *positions* (0 without any)."""
828
+ if not positions:
829
+ return 0
830
+ return int(np.ravel_multi_index([fixed[p] for p in positions],
831
+ [shape[p] for p in positions]))
832
+
833
+
834
+ def open(path: str, tiles: str = "separate", resolutions: str = "base", # noqa: A001 - backend protocol
835
+ rename: dict = None) -> _TiffFile:
836
+ try:
837
+ import tifffile
838
+ except ImportError as exc:
839
+ raise MicroReaderError(
840
+ "reading TIFF needs tifffile, a dependency of micro-reader: pip install tifffile") from exc
841
+ try:
842
+ tif = tifffile.TiffFile(path)
843
+ except tifffile.TiffFileError as exc:
844
+ raise CorruptFileError(f"{path!r}: {exc}") from exc
845
+ try:
846
+ series = list(tif.series)
847
+ if tif.is_lsm:
848
+ # Zeiss LSM stores a small RGB preview of every plane (reduced
849
+ # pages); Bio-Formats does not list them, and neither does
850
+ # micro-reader: they are not data
851
+ series = [s for s in series if not s.keyframe.is_reduced]
852
+ if not series:
853
+ raise CorruptFileError(f"{path!r} contains no image series")
854
+ images = _images(tif, series, path, tiles, resolutions, rename)
855
+ except BaseException:
856
+ tif.close()
857
+ raise
858
+ return _TiffFile(path, images, tif)