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.
- micro_reader/__init__.py +23 -0
- micro_reader/_axes.py +196 -0
- micro_reader/_base.py +268 -0
- micro_reader/_codecs.py +439 -0
- micro_reader/_errors.py +14 -0
- micro_reader/_index.py +53 -0
- micro_reader/_io.py +83 -0
- micro_reader/_jpeg_restarts.py +174 -0
- micro_reader/_memory.py +161 -0
- micro_reader/_metadata.py +163 -0
- micro_reader/_pool.py +85 -0
- micro_reader/_registry.py +104 -0
- micro_reader/_stream.py +317 -0
- micro_reader/backends/__init__.py +1 -0
- micro_reader/backends/_czi_metadata.py +188 -0
- micro_reader/backends/_lif_metadata.py +125 -0
- micro_reader/backends/_nd2_legacy.py +370 -0
- micro_reader/backends/_nd2_metadata.py +224 -0
- micro_reader/backends/_nd2_structure.py +280 -0
- micro_reader/backends/_tiff_metadata.py +287 -0
- micro_reader/backends/czi.py +948 -0
- micro_reader/backends/ims.py +409 -0
- micro_reader/backends/lif.py +713 -0
- micro_reader/backends/mrc.py +184 -0
- micro_reader/backends/nd2.py +264 -0
- micro_reader/backends/oir.py +439 -0
- micro_reader/backends/raster.py +568 -0
- micro_reader/backends/tiff.py +858 -0
- micro_reader-0.0.1.dist-info/METADATA +741 -0
- micro_reader-0.0.1.dist-info/RECORD +32 -0
- micro_reader-0.0.1.dist-info/WHEEL +4 -0
- micro_reader-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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)
|