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,184 @@
|
|
|
1
|
+
"""MRC (electron / light microscopy and cryo-EM volumes).
|
|
2
|
+
|
|
3
|
+
Written from the public MRC2014 specification (CCP-EM): a 1024-byte header,
|
|
4
|
+
an optional extended header of NSYMBT bytes, then raw pixels with X varying
|
|
5
|
+
fastest, then Y, then Z (sections). Uncompressed, so any rectangle is read
|
|
6
|
+
exactly -- only the bytes it covers, straight into the result. This
|
|
7
|
+
replaces a JVM / Bio-Formats read in eubi-bridge.
|
|
8
|
+
|
|
9
|
+
Supported: modes 0 (int8), 1 (int16), 2 (float32), 4 (complex64),
|
|
10
|
+
6 (uint16), 12 (float16); little or big endian; extended headers; the
|
|
11
|
+
standard axis order (MAPC, MAPR, MAPS) = (1, 2, 3).
|
|
12
|
+
|
|
13
|
+
Orientation: MRC stores rows bottom-up (origin at the lower left, as IMOD
|
|
14
|
+
displays it). Images return them top-down, identical to Bio-Formats, so
|
|
15
|
+
switching eubi-bridge between the two never flips an image. (mrcfile and a
|
|
16
|
+
raw memmap return file order: row 0 there is our last row.)
|
|
17
|
+
Not yet: mode 3 (complex int16), mode 101 (4-bit), permuted axis orders.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import builtins
|
|
22
|
+
import os
|
|
23
|
+
import struct
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from .._base import File, Image
|
|
28
|
+
from .._errors import CorruptFileError, UnsupportedFormatError
|
|
29
|
+
from .._metadata import Channel, Metadata, quantity
|
|
30
|
+
|
|
31
|
+
NAME = "mrc"
|
|
32
|
+
HEADER_BYTES = 1024
|
|
33
|
+
CHUNK_BYTES = 4 << 20 # full-width rows are read this many bytes at a time
|
|
34
|
+
EXTENSIONS = {".mrc", ".mrcs", ".map", ".st", ".ali", ".rec"}
|
|
35
|
+
|
|
36
|
+
#: MRC2014 mode -> numpy dtype. Mode 0 is *signed* int8 in MRC2014; older
|
|
37
|
+
#: writers used it unsigned, which cannot be told apart from the header.
|
|
38
|
+
MODES = {0: "i1", 1: "i2", 2: "f4", 4: "c8", 6: "u2", 12: "f2"}
|
|
39
|
+
UNSUPPORTED_MODES = {3: "complex int16", 101: "4-bit packed"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def sniff(head: bytes, path: str) -> bool:
|
|
43
|
+
"""MRC2000+ files carry 'MAP ' at byte 208; older ones only the extension."""
|
|
44
|
+
if len(head) >= 212 and head[208:212] == b"MAP ":
|
|
45
|
+
return True
|
|
46
|
+
return os.path.splitext(path)[1].lower() in EXTENSIONS
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _byte_order(head: bytes) -> str:
|
|
50
|
+
"""'<' or '>' from the machine stamp (byte 212), else by plausibility."""
|
|
51
|
+
stamp = head[212] if len(head) > 212 else 0
|
|
52
|
+
if stamp in (0x44, 0x41): # 0x44 0x44 (or 0x44 0x41): little endian
|
|
53
|
+
return "<"
|
|
54
|
+
if stamp == 0x11: # 0x11 0x11: big endian
|
|
55
|
+
return ">"
|
|
56
|
+
# No stamp (older writers): the order in which the mode is a known one
|
|
57
|
+
# and the dimensions are positive is the right one.
|
|
58
|
+
for order in ("<", ">"):
|
|
59
|
+
nx, ny, nz, mode = struct.unpack_from(order + "4i", head, 0)
|
|
60
|
+
if mode in MODES or mode in UNSUPPORTED_MODES:
|
|
61
|
+
if nx > 0 and ny > 0 and nz > 0:
|
|
62
|
+
return order
|
|
63
|
+
raise CorruptFileError("cannot determine the byte order of this MRC header")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _MrcImage(Image):
|
|
67
|
+
"""The whole MRC volume as one image, axes 'zyx'."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, path: str, offset: int, shape, file_dtype: np.dtype, sizes=None):
|
|
70
|
+
super().__init__(0, "zyx", shape, file_dtype, name=os.path.basename(path))
|
|
71
|
+
if sizes is not None:
|
|
72
|
+
self._metadata = Metadata(pixel_sizes=sizes, channels=(Channel(),))
|
|
73
|
+
self._path = path
|
|
74
|
+
self._offset = offset
|
|
75
|
+
self._file_dtype = file_dtype
|
|
76
|
+
self._closed = False
|
|
77
|
+
|
|
78
|
+
def _read_block(self, block):
|
|
79
|
+
# Plain reads straight into the result, one per row segment. Not a
|
|
80
|
+
# memory map: mapped pages count as the process's memory (on Windows
|
|
81
|
+
# the whole file, after a full stream), which defeats the point of a
|
|
82
|
+
# memory-bounded reader. Each call opens its own handle, so reads
|
|
83
|
+
# are thread-safe and nothing holds the file between reads.
|
|
84
|
+
if self._closed:
|
|
85
|
+
raise ValueError("this MRC image has been closed: its file was closed "
|
|
86
|
+
"(File.close() or the end of a with block)")
|
|
87
|
+
zs, ys, xs = block
|
|
88
|
+
nz, ny, nx = self.shape
|
|
89
|
+
item = self._file_dtype.itemsize
|
|
90
|
+
out = np.empty((zs.stop - zs.start, ys.stop - ys.start, xs.stop - xs.start),
|
|
91
|
+
dtype=self._file_dtype)
|
|
92
|
+
# MRC's origin is the lower-left corner: the first row stored is the
|
|
93
|
+
# bottom of the image. Rows are returned top-down, as Bio-Formats
|
|
94
|
+
# (and so eubi-bridge, Fiji, OME-Zarr viewers) presents them: display
|
|
95
|
+
# row y is file row ny-1-y.
|
|
96
|
+
with builtins.open(self._path, "rb", buffering=0) as fh:
|
|
97
|
+
if xs.stop - xs.start == nx:
|
|
98
|
+
self._read_full_rows(fh, zs, ys, out)
|
|
99
|
+
else:
|
|
100
|
+
length = out.shape[2] * item
|
|
101
|
+
for i, z in enumerate(range(zs.start, zs.stop)):
|
|
102
|
+
for j, y in enumerate(range(ys.start, ys.stop)):
|
|
103
|
+
row = (z * ny + (ny - 1 - y)) * nx + xs.start
|
|
104
|
+
fh.seek(self._offset + row * item)
|
|
105
|
+
if fh.readinto(memoryview(out[i, j]).cast("B")) != length:
|
|
106
|
+
raise CorruptFileError(
|
|
107
|
+
f"{self._path!r}: short read at section {z}")
|
|
108
|
+
if not self._file_dtype.isnative:
|
|
109
|
+
out = out.byteswap(inplace=True).view(self.dtype)
|
|
110
|
+
return out, out.size
|
|
111
|
+
|
|
112
|
+
def _read_full_rows(self, fh, zs, ys, out):
|
|
113
|
+
"""Full-width rows of a section are contiguous in the file: read them
|
|
114
|
+
in chunks of about CHUNK_BYTES and reverse each chunk into place.
|
|
115
|
+
Still exact; the only extra memory is one chunk."""
|
|
116
|
+
nz, ny, nx = self.shape
|
|
117
|
+
item = self._file_dtype.itemsize
|
|
118
|
+
per_chunk = max(1, CHUNK_BYTES // (nx * item))
|
|
119
|
+
scratch = np.empty((min(per_chunk, ys.stop - ys.start), nx), dtype=self._file_dtype)
|
|
120
|
+
first, stop = ny - ys.stop, ny - ys.start # file rows, bottom-up
|
|
121
|
+
for i, z in enumerate(range(zs.start, zs.stop)):
|
|
122
|
+
for f in range(first, stop, per_chunk):
|
|
123
|
+
n = min(per_chunk, stop - f)
|
|
124
|
+
fh.seek(self._offset + (z * ny + f) * nx * item)
|
|
125
|
+
if fh.readinto(memoryview(scratch[:n]).cast("B")) != n * nx * item:
|
|
126
|
+
raise CorruptFileError(f"{self._path!r}: short read at section {z}")
|
|
127
|
+
top = ny - f - n - ys.start # display row of file row f+n-1
|
|
128
|
+
out[i, top:top + n] = scratch[:n][::-1]
|
|
129
|
+
|
|
130
|
+
def close(self) -> None:
|
|
131
|
+
self._closed = True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class _MrcFile(File):
|
|
135
|
+
backend = NAME
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def open(path: str, tiles: str = "separate", resolutions: str = "base") -> _MrcFile: # noqa: A001 - backend protocol name
|
|
139
|
+
size = os.path.getsize(path)
|
|
140
|
+
if size < HEADER_BYTES:
|
|
141
|
+
raise CorruptFileError(f"{path!r} is smaller than an MRC header")
|
|
142
|
+
with builtins.open(path, "rb") as fh:
|
|
143
|
+
head = fh.read(HEADER_BYTES)
|
|
144
|
+
|
|
145
|
+
order = _byte_order(head)
|
|
146
|
+
nx, ny, nz, mode = struct.unpack_from(order + "4i", head, 0)
|
|
147
|
+
mapc, mapr, maps = struct.unpack_from(order + "3i", head, 64)
|
|
148
|
+
nsymbt, = struct.unpack_from(order + "i", head, 92)
|
|
149
|
+
|
|
150
|
+
if mode in UNSUPPORTED_MODES:
|
|
151
|
+
raise UnsupportedFormatError(
|
|
152
|
+
f"MRC mode {mode} ({UNSUPPORTED_MODES[mode]}) is not supported yet")
|
|
153
|
+
if mode not in MODES:
|
|
154
|
+
raise CorruptFileError(f"unknown MRC mode {mode} in {path!r}")
|
|
155
|
+
if min(nx, ny, nz) <= 0:
|
|
156
|
+
raise CorruptFileError(f"invalid MRC dimensions {(nx, ny, nz)} in {path!r}")
|
|
157
|
+
if (mapc, mapr, maps) not in ((1, 2, 3), (0, 0, 0)):
|
|
158
|
+
raise UnsupportedFormatError(
|
|
159
|
+
f"MRC axis order (MAPC, MAPR, MAPS) = {(mapc, mapr, maps)} is not "
|
|
160
|
+
"supported yet; only the standard (1, 2, 3)")
|
|
161
|
+
if nsymbt < 0:
|
|
162
|
+
raise CorruptFileError(f"negative extended header size {nsymbt} in {path!r}")
|
|
163
|
+
|
|
164
|
+
dtype = np.dtype(MODES[mode]).newbyteorder(order)
|
|
165
|
+
offset = HEADER_BYTES + nsymbt
|
|
166
|
+
needed = offset + nx * ny * nz * dtype.itemsize
|
|
167
|
+
if size < needed:
|
|
168
|
+
raise CorruptFileError(
|
|
169
|
+
f"{path!r} is truncated: the header describes {needed} bytes, "
|
|
170
|
+
f"the file has {size}")
|
|
171
|
+
return _MrcFile(path, [_MrcImage(path, offset, (nz, ny, nx), dtype, _pixel_sizes(head, order))])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _pixel_sizes(head: bytes, order: str) -> dict:
|
|
175
|
+
"""Cell size (CELLA, Å) over the sampling (MX, MY, MZ): Å per pixel,
|
|
176
|
+
as MRC2014 defines it; kept in Å (OME-Zarr's ``angstrom``)."""
|
|
177
|
+
mx, my, mz = struct.unpack_from(order + "3i", head, 28)
|
|
178
|
+
cella = struct.unpack_from(order + "3f", head, 40)
|
|
179
|
+
sizes = {}
|
|
180
|
+
for axis, m, length in zip("xyz", (mx, my, mz), cella):
|
|
181
|
+
q = quantity(length / m, "angstrom") if m > 0 else None
|
|
182
|
+
if q:
|
|
183
|
+
sizes[axis] = q
|
|
184
|
+
return sizes
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Nikon ND2 (version 3 and later; legacy versions 1 / 2 in ``_nd2_legacy``), own reader.
|
|
2
|
+
|
|
3
|
+
A frame is one (time, position, Z) plane of all channels, stored in a chunk
|
|
4
|
+
``ImageDataSeq|<n>!`` after an 8-byte timestamp: ``height`` rows of
|
|
5
|
+
``width_bytes`` bytes (rows may be padded), each row ``width`` pixels of all
|
|
6
|
+
channels and RGB components interleaved. Frame numbers run over the
|
|
7
|
+
experiment loops, outermost first. Structure parsing is ported from nd2
|
|
8
|
+
(Talley Lambert, BSD-3), see ``_nd2_structure``.
|
|
9
|
+
|
|
10
|
+
- Uncompressed frames: only the requested rows are read (all channels
|
|
11
|
+
come along: they are interleaved per pixel).
|
|
12
|
+
- "Lossless" frames are one zlib stream each: decoded whole, cached.
|
|
13
|
+
|
|
14
|
+
Images are the XY positions, indexed by ``scene`` (as bioio-nd2 has them); axes ``tczyx``, plus ``s``
|
|
15
|
+
for RGB components. Legacy JPEG 2000 ND2 (versions 1 / 2): ``_nd2_legacy``.
|
|
16
|
+
Not supported:
|
|
17
|
+
lossy compression, tiled frames, custom loops.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import builtins
|
|
22
|
+
import itertools
|
|
23
|
+
import os
|
|
24
|
+
import threading
|
|
25
|
+
import zlib
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
from .. import _memory, _pool, _stream
|
|
30
|
+
from .._base import File, Image
|
|
31
|
+
from .._errors import CorruptFileError, UnsupportedFormatError
|
|
32
|
+
from .._io import DecodeCache, ThreadHandles
|
|
33
|
+
from . import _nd2_legacy as legacy
|
|
34
|
+
from . import _nd2_structure as structure
|
|
35
|
+
|
|
36
|
+
NAME = "nd2"
|
|
37
|
+
DECODE_CACHE_BYTES = 64 << 20
|
|
38
|
+
RAW_TASK_BYTES = 8 << 20 # uncompressed frames are read in row pieces of this size
|
|
39
|
+
STREAM_PIECE_BYTES = 8 << 20 # rows of a streamed "lossless" frame decoded at a time
|
|
40
|
+
TIMESTAMP_BYTES = 8 # each frame chunk starts with a float64 timestamp
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def sniff(head: bytes, path: str) -> bool:
|
|
44
|
+
if head[:4] == structure.CHUNK_MAGIC.to_bytes(4, "little"):
|
|
45
|
+
return True
|
|
46
|
+
# legacy ND2 is a JPEG 2000 box file: the JP2 signature alone would also
|
|
47
|
+
# claim plain .jp2 images, so the extension decides (the box map at the
|
|
48
|
+
# end is checked on opening)
|
|
49
|
+
return legacy.is_legacy(head) and path.lower().endswith(".nd2")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Nd2Parser:
|
|
53
|
+
"""What the images of one file share: layout, frame offsets, handles."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, path: str):
|
|
56
|
+
self.path = path
|
|
57
|
+
with builtins.open(path, "rb") as fh:
|
|
58
|
+
ver = structure.version(fh.read(structure.START_CHUNK.size))
|
|
59
|
+
if ver < (3, 0):
|
|
60
|
+
raise UnsupportedFormatError(f"{path!r}: legacy ND2 version {ver}")
|
|
61
|
+
cmap = structure.chunkmap(fh)
|
|
62
|
+
|
|
63
|
+
def decoded(name):
|
|
64
|
+
if name not in cmap:
|
|
65
|
+
return {}
|
|
66
|
+
return structure.decode_clx_lite(structure.read_chunk(fh, cmap[name][0]))
|
|
67
|
+
|
|
68
|
+
picture = decoded(b"ImageMetadataSeqLV|0!")
|
|
69
|
+
experiment = decoded(b"ImageMetadataLV!")
|
|
70
|
+
self.attrs = structure.attributes(decoded(b"ImageAttributesLV!"), picture)
|
|
71
|
+
self.loops = structure.experiment(experiment)
|
|
72
|
+
#: raw picture metadata and experiment, for ``_nd2_metadata``
|
|
73
|
+
self.picture = picture.get("SLxPictureMetadata", picture)
|
|
74
|
+
self.experiment = experiment
|
|
75
|
+
if self.attrs.compression == "lossy":
|
|
76
|
+
raise UnsupportedFormatError(f"{path!r}: lossy ND2 compression")
|
|
77
|
+
a = self.attrs
|
|
78
|
+
if a.components % a.channels:
|
|
79
|
+
raise UnsupportedFormatError(f"{path!r}: {a.components} components "
|
|
80
|
+
f"for {a.channels} channels")
|
|
81
|
+
self.samples = a.components // a.channels
|
|
82
|
+
self.dtype = np.dtype(a.dtype)
|
|
83
|
+
self.pixel_bytes = a.components * self.dtype.itemsize
|
|
84
|
+
if a.width_bytes < a.width * self.pixel_bytes:
|
|
85
|
+
raise CorruptFileError(f"{path!r}: rows of {a.width_bytes} bytes cannot hold "
|
|
86
|
+
f"{a.width} pixels of {self.pixel_bytes} bytes")
|
|
87
|
+
kinds = [loop.kind for loop in self.loops]
|
|
88
|
+
if len(set(kinds)) != len(kinds):
|
|
89
|
+
raise UnsupportedFormatError(f"{path!r}: repeated experiment loops {kinds}")
|
|
90
|
+
# frame chunk -> offset of its header; data offsets are read lazily
|
|
91
|
+
self.frames = {int(name[13:-1]): offset for name, (offset, _) in cmap.items()
|
|
92
|
+
if name.startswith(b"ImageDataSeq|")}
|
|
93
|
+
self._data_at: dict = {}
|
|
94
|
+
self._lock = threading.Lock()
|
|
95
|
+
self.handles = ThreadHandles(path)
|
|
96
|
+
|
|
97
|
+
def size(self, kind: str) -> int:
|
|
98
|
+
return next((loop.count for loop in self.loops if loop.kind == kind), 1)
|
|
99
|
+
|
|
100
|
+
def frame_number(self, t: int, p: int, z: int) -> int:
|
|
101
|
+
index = {"T": t, "P": p, "Z": z}
|
|
102
|
+
number = 0
|
|
103
|
+
for loop in self.loops:
|
|
104
|
+
number = number * loop.count + index[loop.kind]
|
|
105
|
+
return number
|
|
106
|
+
|
|
107
|
+
def frame_data(self, number: int):
|
|
108
|
+
"""(offset of the frame's pixels or compressed stream, its length),
|
|
109
|
+
or None for a frame that was never written."""
|
|
110
|
+
where = self._data_at.get(number)
|
|
111
|
+
if where is None:
|
|
112
|
+
header = self.frames.get(number)
|
|
113
|
+
if header is None:
|
|
114
|
+
return None
|
|
115
|
+
fh = self.handles.get()
|
|
116
|
+
start, length = structure.data_offset(fh, header)
|
|
117
|
+
where = self._data_at[number] = (start + TIMESTAMP_BYTES, length - TIMESTAMP_BYTES)
|
|
118
|
+
return where
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _Nd2Image(Image):
|
|
122
|
+
"""One XY position: axes 'tczyx' (+ 's' for RGB components)."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, nd2: Nd2Parser, position: int):
|
|
125
|
+
a = nd2.attrs
|
|
126
|
+
self._f = nd2
|
|
127
|
+
self._p = position
|
|
128
|
+
self._rgb = nd2.samples > 1
|
|
129
|
+
shape = (nd2.size("T"), a.channels, nd2.size("Z"), a.height, a.width)
|
|
130
|
+
axes = "tczyx"
|
|
131
|
+
if self._rgb:
|
|
132
|
+
shape, axes = shape + (nd2.samples,), axes + "s"
|
|
133
|
+
#: decoded "lossless" frames kept for reuse
|
|
134
|
+
self.decode_cache = DecodeCache(DECODE_CACHE_BYTES)
|
|
135
|
+
self._closed = False
|
|
136
|
+
unit = (1, a.channels, 1, a.height, a.width) + ((nd2.samples,) if self._rgb else ()) \
|
|
137
|
+
if a.compression == "lossless" else None
|
|
138
|
+
super().__init__(position, axes, shape, nd2.dtype,
|
|
139
|
+
name=f"{os.path.basename(nd2.path)} position {position}",
|
|
140
|
+
read_unit=unit)
|
|
141
|
+
|
|
142
|
+
def _read_block(self, block):
|
|
143
|
+
if self._closed:
|
|
144
|
+
raise ValueError("this ND2 image has been closed: its file was closed "
|
|
145
|
+
"(File.close() or the end of a with block)")
|
|
146
|
+
ts, cs, zs, ys, xs = block[:5]
|
|
147
|
+
ss = block[5] if self._rgb else slice(0, 1)
|
|
148
|
+
out = np.empty(tuple(s.stop - s.start for s in block), dtype=self.dtype)
|
|
149
|
+
view = out if self._rgb else out[..., None] # always (..., s)
|
|
150
|
+
tasks = []
|
|
151
|
+
for t, z in itertools.product(range(ts.start, ts.stop), range(zs.start, zs.stop)):
|
|
152
|
+
dst = view[t - ts.start, :, z - zs.start] # (c, y, x, s)
|
|
153
|
+
where = self._f.frame_data(self._f.frame_number(t, self._p, z))
|
|
154
|
+
if where is None:
|
|
155
|
+
dst[...] = 0
|
|
156
|
+
elif self._f.attrs.compression == "lossless":
|
|
157
|
+
tasks.append(lambda a=(t, z, where, dst, cs, ys, xs, ss): self._decoded_into(*a))
|
|
158
|
+
else:
|
|
159
|
+
tasks.extend(self._raw_tasks(where[0], dst, cs, ys, xs, ss))
|
|
160
|
+
return out, _pool.run(tasks)
|
|
161
|
+
|
|
162
|
+
def _frame_view(self, buf: np.ndarray, rows: int) -> np.ndarray:
|
|
163
|
+
"""Bytes of *rows* whole rows -> (rows, x, channel, sample)."""
|
|
164
|
+
f, a = self._f, self._f.attrs
|
|
165
|
+
pixels = buf.reshape(rows, a.width_bytes)[:, :a.width * f.pixel_bytes]
|
|
166
|
+
return pixels.view(f.dtype).reshape(rows, a.width, a.channels, f.samples)
|
|
167
|
+
|
|
168
|
+
def _raw_tasks(self, start, dst, cs, ys, xs, ss):
|
|
169
|
+
f, a = self._f, self._f.attrs
|
|
170
|
+
per_task = max(1, RAW_TASK_BYTES // a.width_bytes)
|
|
171
|
+
whole_rows = (xs.stop - xs.start) * 2 >= a.width # else: only the needed bytes per row
|
|
172
|
+
for y0 in range(ys.start, ys.stop, per_task):
|
|
173
|
+
y1 = min(ys.stop, y0 + per_task)
|
|
174
|
+
part = dst[:, y0 - ys.start:y1 - ys.start]
|
|
175
|
+
yield lambda args=(start, y0, y1, part, cs, xs, ss, whole_rows): self._read_rows(*args)
|
|
176
|
+
|
|
177
|
+
def _read_rows(self, start, y0, y1, dst, cs, xs, ss, whole_rows) -> int:
|
|
178
|
+
"""Rows y0:y1 of an uncompressed frame into *dst* (c, y, x, s)."""
|
|
179
|
+
f, a = self._f, self._f.attrs
|
|
180
|
+
fh = f.handles.get()
|
|
181
|
+
n = y1 - y0
|
|
182
|
+
if whole_rows:
|
|
183
|
+
buf = np.empty(n * a.width_bytes, np.uint8)
|
|
184
|
+
fh.seek(start + y0 * a.width_bytes)
|
|
185
|
+
if fh.readinto(memoryview(buf)) != buf.nbytes:
|
|
186
|
+
raise CorruptFileError(f"{f.path!r}: frame data ends early")
|
|
187
|
+
pixels = self._frame_view(buf, n)[:, xs]
|
|
188
|
+
read = n * a.width * a.components
|
|
189
|
+
else:
|
|
190
|
+
width = xs.stop - xs.start
|
|
191
|
+
buf = np.empty((n, width * f.pixel_bytes), np.uint8)
|
|
192
|
+
for i in range(n):
|
|
193
|
+
fh.seek(start + (y0 + i) * a.width_bytes + xs.start * f.pixel_bytes)
|
|
194
|
+
if fh.readinto(memoryview(buf[i])) != buf.shape[1]:
|
|
195
|
+
raise CorruptFileError(f"{f.path!r}: frame data ends early")
|
|
196
|
+
pixels = buf.view(f.dtype).reshape(n, width, a.channels, f.samples)
|
|
197
|
+
read = n * width * a.components
|
|
198
|
+
dst[...] = pixels[:, :, cs, ss].transpose(2, 0, 1, 3)
|
|
199
|
+
return read
|
|
200
|
+
|
|
201
|
+
def _decoded_into(self, t, z, where, dst, cs, ys, xs, ss) -> int:
|
|
202
|
+
frame, read = self.decode_cache.get((t, z)), 0
|
|
203
|
+
a = self._f.attrs
|
|
204
|
+
if frame is None and _memory.should_stream(a.height * a.width_bytes):
|
|
205
|
+
return self._streamed_into(where, dst, cs, ys, xs, ss)
|
|
206
|
+
if frame is None:
|
|
207
|
+
f, a = self._f, self._f.attrs
|
|
208
|
+
with _memory.reserve(a.height * a.width_bytes, f"{f.path!r}: an ND2 frame"):
|
|
209
|
+
data = zlib.decompress(f.handles.read(*where), 15, a.height * a.width_bytes)
|
|
210
|
+
if len(data) < a.height * a.width_bytes:
|
|
211
|
+
raise CorruptFileError(f"{f.path!r}: frame decodes to {len(data)} bytes")
|
|
212
|
+
frame = self._frame_view(np.frombuffer(data, np.uint8)[:a.height * a.width_bytes],
|
|
213
|
+
a.height)
|
|
214
|
+
self.decode_cache.put((t, z), frame)
|
|
215
|
+
read = frame.size
|
|
216
|
+
dst[...] = frame[ys, xs][:, :, cs, ss].transpose(2, 0, 1, 3)
|
|
217
|
+
return read
|
|
218
|
+
|
|
219
|
+
def _read_metadata(self):
|
|
220
|
+
from ._nd2_metadata import nd2_metadata
|
|
221
|
+
return nd2_metadata(self._f.picture, self._f.experiment, self._p,
|
|
222
|
+
dict(zip(self.axes, self.shape)))
|
|
223
|
+
|
|
224
|
+
def _streamed_into(self, where, dst, cs, ys, xs, ss) -> int:
|
|
225
|
+
"""Rows *ys* of a "lossless" frame too large to decode whole: its zlib
|
|
226
|
+
stream decoded as a stream, in pieces of rows (``_stream``)."""
|
|
227
|
+
f, a = self._f, self._f.attrs
|
|
228
|
+
per_piece = max(1, min(STREAM_PIECE_BYTES, _memory.memory_limit() // 4) // a.width_bytes)
|
|
229
|
+
for y0 in range(ys.start, ys.stop, per_piece):
|
|
230
|
+
y1 = min(ys.stop, y0 + per_piece)
|
|
231
|
+
with _memory.reserve((y1 - y0) * a.width_bytes, f"{f.path!r}: rows of a frame"):
|
|
232
|
+
buf = np.empty((y1 - y0) * a.width_bytes, np.uint8)
|
|
233
|
+
n = _stream.read(f.path, where[0], where[1], "zlib", y0 * a.width_bytes, buf,
|
|
234
|
+
size=a.height * a.width_bytes)
|
|
235
|
+
if n < buf.nbytes:
|
|
236
|
+
raise CorruptFileError(f"{f.path!r}: frame ends at {n} of {buf.nbytes} bytes")
|
|
237
|
+
rows = self._frame_view(buf, y1 - y0)
|
|
238
|
+
dst[:, y0 - ys.start:y1 - ys.start] = rows[:, xs][:, :, cs, ss].transpose(2, 0, 1, 3)
|
|
239
|
+
return (ys.stop - ys.start) * a.width * a.components
|
|
240
|
+
|
|
241
|
+
def close(self) -> None:
|
|
242
|
+
self._closed = True
|
|
243
|
+
self.decode_cache.clear()
|
|
244
|
+
_stream.forget(self._f.path)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class _Nd2File(File):
|
|
248
|
+
backend = NAME
|
|
249
|
+
|
|
250
|
+
def __init__(self, path, images, nd2: Nd2Parser):
|
|
251
|
+
super().__init__(path, images)
|
|
252
|
+
self._nd2 = nd2
|
|
253
|
+
|
|
254
|
+
def close(self) -> None:
|
|
255
|
+
super().close()
|
|
256
|
+
self._nd2.handles.close()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def open(path: str, tiles: str = "separate", resolutions: str = "base"): # noqa: A001 - backend protocol name
|
|
260
|
+
with builtins.open(path, "rb") as fh:
|
|
261
|
+
if legacy.is_legacy(fh.read(12)):
|
|
262
|
+
return legacy._LegacyNd2File(path)
|
|
263
|
+
nd2 = Nd2Parser(path)
|
|
264
|
+
return _Nd2File(path, [_Nd2Image(nd2, p) for p in range(nd2.size("P"))], nd2)
|