micro-reader 0.0.1__tar.gz → 0.0.2__tar.gz
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-0.0.1 → micro_reader-0.0.2}/PKG-INFO +18 -2
- {micro_reader-0.0.1 → micro_reader-0.0.2}/README.md +17 -1
- {micro_reader-0.0.1 → micro_reader-0.0.2}/pyproject.toml +1 -1
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/__init__.py +2 -0
- micro_reader-0.0.2/src/micro_reader/_io.py +159 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/czi.py +14 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/mrc.py +32 -1
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/nd2.py +5 -1
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/tiff.py +20 -0
- micro_reader-0.0.1/src/micro_reader/_io.py +0 -83
- {micro_reader-0.0.1 → micro_reader-0.0.2}/.gitignore +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/LICENSE +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_axes.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_base.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_codecs.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_errors.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_index.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_jpeg_restarts.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_memory.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_metadata.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_pool.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_registry.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_stream.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/__init__.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_czi_metadata.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_lif_metadata.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_nd2_legacy.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_nd2_metadata.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_nd2_structure.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_tiff_metadata.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/ims.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/lif.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/oir.py +0 -0
- {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/raster.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: micro-reader
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Low-level, memory-bounded pixel reading for microscopy formats: read exactly the rectangle you ask for.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bugraoezdemir/micro-reader
|
|
6
6
|
Project-URL: Source, https://github.com/bugraoezdemir/micro-reader
|
|
@@ -677,7 +677,23 @@ You can also set it through the environment:
|
|
|
677
677
|
export MICRO_READER_MEMORY_LIMIT=8GB
|
|
678
678
|
```
|
|
679
679
|
|
|
680
|
-
|
|
680
|
+
The limit covers the blocks being decoded at once. Decoded blocks are also kept for reuse,
|
|
681
|
+
so a block shared by neighbouring reads is not decoded twice. All open files share one
|
|
682
|
+
cache limit, 256 MB by default:
|
|
683
|
+
|
|
684
|
+
```python
|
|
685
|
+
micro_reader.set_cache_limit("512MB")
|
|
686
|
+
micro_reader.cache_limit()
|
|
687
|
+
micro_reader.cached_bytes() # what the caches hold now
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
or `export MICRO_READER_CACHE_LIMIT=512MB`. The process then uses at most the memory
|
|
691
|
+
limit plus the cache limit for decoding, however many files are open. One exception: a
|
|
692
|
+
single picture (PNG, JPEG, ... up to 256 MB) is kept decoded by its own open file.
|
|
693
|
+
|
|
694
|
+
For uncompressed data, only the requested rows are read. When you ask for part of
|
|
695
|
+
each row, the rest of the row is read along with it if that is faster than reading the
|
|
696
|
+
rows one by one.
|
|
681
697
|
|
|
682
698
|
Compressed data is stored in blocks such as TIFF strips or tiles, CZI subblocks, and ND2 frames. Small blocks are decoded as a whole. Blocks larger than 16 MB are read in parts where the codec allows it.
|
|
683
699
|
|
|
@@ -625,7 +625,23 @@ You can also set it through the environment:
|
|
|
625
625
|
export MICRO_READER_MEMORY_LIMIT=8GB
|
|
626
626
|
```
|
|
627
627
|
|
|
628
|
-
|
|
628
|
+
The limit covers the blocks being decoded at once. Decoded blocks are also kept for reuse,
|
|
629
|
+
so a block shared by neighbouring reads is not decoded twice. All open files share one
|
|
630
|
+
cache limit, 256 MB by default:
|
|
631
|
+
|
|
632
|
+
```python
|
|
633
|
+
micro_reader.set_cache_limit("512MB")
|
|
634
|
+
micro_reader.cache_limit()
|
|
635
|
+
micro_reader.cached_bytes() # what the caches hold now
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
or `export MICRO_READER_CACHE_LIMIT=512MB`. The process then uses at most the memory
|
|
639
|
+
limit plus the cache limit for decoding, however many files are open. One exception: a
|
|
640
|
+
single picture (PNG, JPEG, ... up to 256 MB) is kept decoded by its own open file.
|
|
641
|
+
|
|
642
|
+
For uncompressed data, only the requested rows are read. When you ask for part of
|
|
643
|
+
each row, the rest of the row is read along with it if that is faster than reading the
|
|
644
|
+
rows one by one.
|
|
629
645
|
|
|
630
646
|
Compressed data is stored in blocks such as TIFF strips or tiles, CZI subblocks, and ND2 frames. Small blocks are decoded as a whole. Blocks larger than 16 MB are read in parts where the codec allows it.
|
|
631
647
|
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "micro-reader"
|
|
9
|
-
version = "0.0.
|
|
9
|
+
version = "0.0.2"
|
|
10
10
|
description = "Low-level, memory-bounded pixel reading for microscopy formats: read exactly the rectangle you ask for."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.11"
|
|
@@ -8,6 +8,7 @@ from ._base import INDEX_NAMES, SAMPLED_INDICES, File, Image, ReadStats
|
|
|
8
8
|
from ._metadata import Channel, Metadata, Quantity
|
|
9
9
|
from ._errors import CorruptFileError, MicroReaderError, UnsupportedFormatError
|
|
10
10
|
from ._memory import MemoryLimitError, memory_limit, set_memory_limit
|
|
11
|
+
from ._io import cache_limit, cached_bytes, set_cache_limit
|
|
11
12
|
from ._pool import decode_threads, set_decode_threads
|
|
12
13
|
from ._registry import BACKENDS, open # noqa: A004 - the public entry point
|
|
13
14
|
from .backends.czi import DamagedFileWarning
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
"open", "File", "Image", "INDEX_NAMES", "SAMPLED_INDICES", "ReadStats", "BACKENDS",
|
|
19
20
|
"Metadata", "Channel", "Quantity",
|
|
20
21
|
"decode_threads", "set_decode_threads", "memory_limit", "set_memory_limit",
|
|
22
|
+
"cache_limit", "set_cache_limit", "cached_bytes",
|
|
21
23
|
"MicroReaderError", "UnsupportedFormatError", "CorruptFileError", "MemoryLimitError", "DamagedFileWarning",
|
|
22
24
|
"__version__",
|
|
23
25
|
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Pieces every backend that reads segments on the decode pool needs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import builtins
|
|
5
|
+
import threading
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ._errors import CorruptFileError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ThreadHandles:
|
|
14
|
+
"""One read-only file handle per thread for one file (seek + read on a
|
|
15
|
+
shared handle is not thread-safe), kept open until :meth:`close`."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, path: str):
|
|
18
|
+
self.path = path
|
|
19
|
+
self._local = threading.local()
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
self._all: list = []
|
|
22
|
+
|
|
23
|
+
def get(self):
|
|
24
|
+
fh = getattr(self._local, "fh", None)
|
|
25
|
+
if fh is None:
|
|
26
|
+
fh = self._local.fh = builtins.open(self.path, "rb", buffering=0)
|
|
27
|
+
with self._lock:
|
|
28
|
+
self._all.append(fh)
|
|
29
|
+
return fh
|
|
30
|
+
|
|
31
|
+
def read(self, offset: int, size: int) -> bytes:
|
|
32
|
+
fh = self.get()
|
|
33
|
+
fh.seek(offset)
|
|
34
|
+
data = fh.read(size)
|
|
35
|
+
if len(data) != size:
|
|
36
|
+
raise CorruptFileError(f"{self.path!r}: data at byte {offset} ends early "
|
|
37
|
+
"(truncated file?)")
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
def close(self) -> None:
|
|
41
|
+
with self._lock:
|
|
42
|
+
handles, self._all = self._all, []
|
|
43
|
+
for fh in handles:
|
|
44
|
+
fh.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#: what all decoded-block caches of a process may hold together, by default
|
|
48
|
+
DEFAULT_CACHE_LIMIT = 256 << 20
|
|
49
|
+
|
|
50
|
+
# One least-recently-used order over every cache of the process, so the number of
|
|
51
|
+
# open files does not multiply the memory the caches hold: (cache id, key) ->
|
|
52
|
+
# (cache, nbytes). Reentrant: a cache that is garbage-collected releases its bytes
|
|
53
|
+
# from __del__, which may run while this thread holds the lock.
|
|
54
|
+
_shared_lock = threading.RLock()
|
|
55
|
+
_shared: OrderedDict = OrderedDict()
|
|
56
|
+
_shared_bytes = 0
|
|
57
|
+
_cache_limit = None
|
|
58
|
+
_ids = iter(range(1 << 62))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cache_limit() -> int:
|
|
62
|
+
"""What all decoded-block caches of this process may hold together, in bytes."""
|
|
63
|
+
global _cache_limit
|
|
64
|
+
if _cache_limit is None:
|
|
65
|
+
import os
|
|
66
|
+
from ._memory import parse_size
|
|
67
|
+
env = os.environ.get("MICRO_READER_CACHE_LIMIT")
|
|
68
|
+
_cache_limit = parse_size(env) if env else DEFAULT_CACHE_LIMIT
|
|
69
|
+
return _cache_limit
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def set_cache_limit(limit) -> None:
|
|
73
|
+
"""Set what all decoded-block caches of this process may hold together (bytes or
|
|
74
|
+
e.g. "512MB"; 0 keeps nothing); None restores the default (the
|
|
75
|
+
``MICRO_READER_CACHE_LIMIT`` environment variable, else 256 MB). Least recently
|
|
76
|
+
used blocks are dropped at once if the caches hold more."""
|
|
77
|
+
global _cache_limit
|
|
78
|
+
from ._memory import parse_size
|
|
79
|
+
with _shared_lock:
|
|
80
|
+
_cache_limit = None if limit is None else max(0, parse_size(limit))
|
|
81
|
+
_evict_shared(cache_limit())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cached_bytes() -> int:
|
|
85
|
+
"""What the decoded-block caches of this process hold now, in bytes."""
|
|
86
|
+
with _shared_lock:
|
|
87
|
+
return _shared_bytes
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _evict_shared(limit: int) -> None:
|
|
91
|
+
global _shared_bytes
|
|
92
|
+
while _shared_bytes > limit and _shared:
|
|
93
|
+
(_, key), (cache, nbytes) = _shared.popitem(last=False)
|
|
94
|
+
cache._drop(key)
|
|
95
|
+
_shared_bytes -= nbytes
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DecodeCache:
|
|
99
|
+
"""Decoded segments (tiles, strips, chunks), least recently used out.
|
|
100
|
+
|
|
101
|
+
Keeps a segment shared by neighbouring reads from being decoded twice.
|
|
102
|
+
Segments above a quarter of ``max_bytes`` are not kept (a whole plane from
|
|
103
|
+
a single-strip file would evict everything else). ``max_bytes`` caps this
|
|
104
|
+
cache (one file or scene); every cache of the process together stays within
|
|
105
|
+
:func:`cache_limit`, so many open files do not hold many times the memory."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, max_bytes: int):
|
|
108
|
+
self.max_bytes = max_bytes
|
|
109
|
+
self._items: OrderedDict = OrderedDict()
|
|
110
|
+
self._bytes = 0
|
|
111
|
+
self._id = next(_ids)
|
|
112
|
+
|
|
113
|
+
def get(self, key):
|
|
114
|
+
with _shared_lock:
|
|
115
|
+
seg = self._items.get(key)
|
|
116
|
+
if seg is not None:
|
|
117
|
+
self._items.move_to_end(key)
|
|
118
|
+
_shared.move_to_end((self._id, key))
|
|
119
|
+
return seg
|
|
120
|
+
|
|
121
|
+
def put(self, key, seg: np.ndarray) -> None:
|
|
122
|
+
global _shared_bytes
|
|
123
|
+
if seg.nbytes > self.max_bytes // 4 or seg.nbytes > cache_limit():
|
|
124
|
+
return
|
|
125
|
+
seg.setflags(write=False)
|
|
126
|
+
with _shared_lock:
|
|
127
|
+
if key in self._items:
|
|
128
|
+
return
|
|
129
|
+
self._items[key] = seg
|
|
130
|
+
self._bytes += seg.nbytes
|
|
131
|
+
_shared[(self._id, key)] = (self, seg.nbytes)
|
|
132
|
+
_shared_bytes += seg.nbytes
|
|
133
|
+
while self._bytes > self.max_bytes:
|
|
134
|
+
old_key = next(iter(self._items))
|
|
135
|
+
_shared.pop((self._id, old_key))
|
|
136
|
+
_shared_bytes -= self._items[old_key].nbytes
|
|
137
|
+
self._drop(old_key)
|
|
138
|
+
_evict_shared(cache_limit())
|
|
139
|
+
|
|
140
|
+
def _drop(self, key) -> None:
|
|
141
|
+
"""Forget *key* (the shared order is updated by the caller)."""
|
|
142
|
+
seg = self._items.pop(key, None)
|
|
143
|
+
if seg is not None:
|
|
144
|
+
self._bytes -= seg.nbytes
|
|
145
|
+
|
|
146
|
+
def clear(self) -> None:
|
|
147
|
+
global _shared_bytes
|
|
148
|
+
with _shared_lock:
|
|
149
|
+
for key, seg in self._items.items():
|
|
150
|
+
if _shared.pop((self._id, key), None) is not None:
|
|
151
|
+
_shared_bytes -= seg.nbytes
|
|
152
|
+
self._items.clear()
|
|
153
|
+
self._bytes = 0
|
|
154
|
+
|
|
155
|
+
def __del__(self):
|
|
156
|
+
try:
|
|
157
|
+
self.clear()
|
|
158
|
+
except Exception: # noqa: BLE001 - interpreter exit
|
|
159
|
+
pass
|
|
@@ -548,6 +548,20 @@ class _CziImage(Image):
|
|
|
548
548
|
fh.seek(base + (y0 - sy0) * s.width * item)
|
|
549
549
|
if fh.readinto(memoryview(buf).cast("B")) != buf.nbytes:
|
|
550
550
|
raise CorruptFileError(f"{self._czi.path!r}: subblock data ends early")
|
|
551
|
+
elif y1 - y0 > 1 and (s.width - width) * item <= SEPARATE_READ_BYTES:
|
|
552
|
+
# a few skipped bytes per row cost less than one more read per row: the span
|
|
553
|
+
# from the first row's columns to the last row's, in one read
|
|
554
|
+
rows, row = y1 - y0, s.width * self._samples
|
|
555
|
+
span = (rows - 1) * row + width * self._samples
|
|
556
|
+
flat = np.empty(span, self._file_dtype)
|
|
557
|
+
fh.seek(base + ((y0 - sy0) * s.width + (x0 - sx0)) * item)
|
|
558
|
+
if fh.readinto(memoryview(flat).cast("B")) != flat.nbytes:
|
|
559
|
+
raise CorruptFileError(f"{self._czi.path!r}: subblock data ends early")
|
|
560
|
+
buf = np.lib.stride_tricks.as_strided(
|
|
561
|
+
flat, shape=(rows, width * self._samples),
|
|
562
|
+
strides=(row * flat.itemsize, flat.itemsize))
|
|
563
|
+
self._place(buf, target, rows, width)
|
|
564
|
+
return span
|
|
551
565
|
else:
|
|
552
566
|
buf = np.empty((y1 - y0, width * self._samples), self._file_dtype)
|
|
553
567
|
for i in range(y1 - y0):
|
|
@@ -31,6 +31,7 @@ from .._metadata import Channel, Metadata, quantity
|
|
|
31
31
|
NAME = "mrc"
|
|
32
32
|
HEADER_BYTES = 1024
|
|
33
33
|
CHUNK_BYTES = 4 << 20 # full-width rows are read this many bytes at a time
|
|
34
|
+
SEPARATE_READ_BYTES = 32 << 10 # bytes worth skipping rather than issuing one more read
|
|
34
35
|
EXTENSIONS = {".mrc", ".mrcs", ".map", ".st", ".ali", ".rec"}
|
|
35
36
|
|
|
36
37
|
#: MRC2014 mode -> numpy dtype. Mode 0 is *signed* int8 in MRC2014; older
|
|
@@ -93,9 +94,13 @@ class _MrcImage(Image):
|
|
|
93
94
|
# bottom of the image. Rows are returned top-down, as Bio-Formats
|
|
94
95
|
# (and so eubi-bridge, Fiji, OME-Zarr viewers) presents them: display
|
|
95
96
|
# row y is file row ny-1-y.
|
|
97
|
+
read = out.size
|
|
96
98
|
with builtins.open(self._path, "rb", buffering=0) as fh:
|
|
97
99
|
if xs.stop - xs.start == nx:
|
|
98
100
|
self._read_full_rows(fh, zs, ys, out)
|
|
101
|
+
elif (ys.stop - ys.start > 1
|
|
102
|
+
and (nx - (xs.stop - xs.start)) * item <= SEPARATE_READ_BYTES):
|
|
103
|
+
read = self._read_row_spans(fh, zs, ys, xs, out)
|
|
99
104
|
else:
|
|
100
105
|
length = out.shape[2] * item
|
|
101
106
|
for i, z in enumerate(range(zs.start, zs.stop)):
|
|
@@ -107,7 +112,33 @@ class _MrcImage(Image):
|
|
|
107
112
|
f"{self._path!r}: short read at section {z}")
|
|
108
113
|
if not self._file_dtype.isnative:
|
|
109
114
|
out = out.byteswap(inplace=True).view(self.dtype)
|
|
110
|
-
return out,
|
|
115
|
+
return out, read
|
|
116
|
+
|
|
117
|
+
def _read_row_spans(self, fh, zs, ys, xs, out) -> int:
|
|
118
|
+
"""Part of each row, when the columns skipped per row are few: they cost less
|
|
119
|
+
than one more read per row (as in LIF). Each chunk of rows is one read, from the
|
|
120
|
+
first row's columns to the last row's, reversed into place (bottom-up rows).
|
|
121
|
+
Returns the values read."""
|
|
122
|
+
nz, ny, nx = self.shape
|
|
123
|
+
item = self._file_dtype.itemsize
|
|
124
|
+
width = xs.stop - xs.start
|
|
125
|
+
per_chunk = max(1, CHUNK_BYTES // (nx * item))
|
|
126
|
+
flat = np.empty(min(per_chunk, ys.stop - ys.start) * nx, dtype=self._file_dtype)
|
|
127
|
+
first, stop = ny - ys.stop, ny - ys.start # file rows, bottom-up
|
|
128
|
+
read = 0
|
|
129
|
+
for i, z in enumerate(range(zs.start, zs.stop)):
|
|
130
|
+
for f in range(first, stop, per_chunk):
|
|
131
|
+
n = min(per_chunk, stop - f)
|
|
132
|
+
span = (n - 1) * nx + width
|
|
133
|
+
fh.seek(self._offset + ((z * ny + f) * nx + xs.start) * item)
|
|
134
|
+
if fh.readinto(memoryview(flat[:span]).cast("B")) != span * item:
|
|
135
|
+
raise CorruptFileError(f"{self._path!r}: short read at section {z}")
|
|
136
|
+
rows = np.lib.stride_tricks.as_strided(flat, shape=(n, width),
|
|
137
|
+
strides=(nx * item, item))
|
|
138
|
+
top = ny - f - n - ys.start # display row of file row f+n-1
|
|
139
|
+
out[i, top:top + n] = rows[::-1]
|
|
140
|
+
read += span
|
|
141
|
+
return read
|
|
111
142
|
|
|
112
143
|
def _read_full_rows(self, fh, zs, ys, out):
|
|
113
144
|
"""Full-width rows of a section are contiguous in the file: read them
|
|
@@ -36,6 +36,7 @@ from . import _nd2_structure as structure
|
|
|
36
36
|
NAME = "nd2"
|
|
37
37
|
DECODE_CACHE_BYTES = 64 << 20
|
|
38
38
|
RAW_TASK_BYTES = 8 << 20 # uncompressed frames are read in row pieces of this size
|
|
39
|
+
SEPARATE_READ_BYTES = 32 << 10 # bytes worth skipping rather than issuing one more read
|
|
39
40
|
STREAM_PIECE_BYTES = 8 << 20 # rows of a streamed "lossless" frame decoded at a time
|
|
40
41
|
TIMESTAMP_BYTES = 8 # each frame chunk starts with a float64 timestamp
|
|
41
42
|
|
|
@@ -168,7 +169,10 @@ class _Nd2Image(Image):
|
|
|
168
169
|
def _raw_tasks(self, start, dst, cs, ys, xs, ss):
|
|
169
170
|
f, a = self._f, self._f.attrs
|
|
170
171
|
per_task = max(1, RAW_TASK_BYTES // a.width_bytes)
|
|
171
|
-
|
|
172
|
+
# whole rows when the columns skipped per row are few: fewer bytes than another read
|
|
173
|
+
# call costs (as in LIF), or at most half the row; else only the needed bytes per row
|
|
174
|
+
skipped = a.width_bytes - (xs.stop - xs.start) * f.pixel_bytes
|
|
175
|
+
whole_rows = (xs.stop - xs.start) * 2 >= a.width or skipped <= SEPARATE_READ_BYTES
|
|
172
176
|
for y0 in range(ys.start, ys.stop, per_task):
|
|
173
177
|
y1 = min(ys.stop, y0 + per_task)
|
|
174
178
|
part = dst[:, y0 - ys.start:y1 - ys.start]
|
|
@@ -54,6 +54,7 @@ NAME = "tiff"
|
|
|
54
54
|
MAGIC = (b"II*\x00", b"MM\x00*", b"II+\x00", b"MM\x00+") # classic and BigTIFF
|
|
55
55
|
CHUNK_BYTES = 4 << 20 # buffered raw rows are read this many bytes at a time
|
|
56
56
|
RAW_TASK_BYTES = 8 << 20 # uncompressed strip runs are split into pool tasks of this size
|
|
57
|
+
SEPARATE_READ_BYTES = 32 << 10 # bytes worth skipping rather than issuing one more read
|
|
57
58
|
DECODE_CACHE_BYTES = 64 << 20 # decoded segments kept per scene
|
|
58
59
|
STREAM_PIECE_BYTES = 8 << 20 # rows of a streamed segment decoded at a time (reserved)
|
|
59
60
|
#: image codecs that decode a rectangle alone (``_area_into``)
|
|
@@ -467,6 +468,25 @@ class _TiffImage(Image):
|
|
|
467
468
|
fh.seek(offset + rows[0] * row_len * item)
|
|
468
469
|
_readinto(fh, dst, self._path)
|
|
469
470
|
return dst.size
|
|
471
|
+
width = cols[1] - cols[0]
|
|
472
|
+
if (cols != (0, stride) and rows[1] - rows[0] > 1
|
|
473
|
+
and (stride - width) * nc * item <= SEPARATE_READ_BYTES):
|
|
474
|
+
# a few skipped bytes per row cost less than one more read per row (as in
|
|
475
|
+
# LIF): read the span from the first row's columns to the last row's, in
|
|
476
|
+
# chunks, and keep the columns asked for
|
|
477
|
+
per_chunk = max(1, CHUNK_BYTES // (row_len * item))
|
|
478
|
+
flat = np.empty(min(per_chunk, rows[1] - rows[0]) * row_len, self._file_dtype)
|
|
479
|
+
read = 0
|
|
480
|
+
for a in range(rows[0], rows[1], per_chunk):
|
|
481
|
+
n = min(per_chunk, rows[1] - a)
|
|
482
|
+
span = (n - 1) * row_len + width * nc
|
|
483
|
+
fh.seek(offset + (a * stride + cols[0]) * nc * item)
|
|
484
|
+
_readinto(fh, flat[:span], self._path)
|
|
485
|
+
rows_view = np.lib.stride_tricks.as_strided(
|
|
486
|
+
flat, shape=(n, width, nc), strides=(row_len * item, nc * item, item))
|
|
487
|
+
dst[a - rows[0]:a - rows[0] + n] = rows_view[..., cs]
|
|
488
|
+
read += span
|
|
489
|
+
return read
|
|
470
490
|
if cols != (0, stride) and direct and dst[0].flags.c_contiguous:
|
|
471
491
|
width = cols[1] - cols[0]
|
|
472
492
|
for r in range(rows[0], rows[1]):
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""Pieces every backend that reads segments on the decode pool needs."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import builtins
|
|
5
|
-
import threading
|
|
6
|
-
from collections import OrderedDict
|
|
7
|
-
|
|
8
|
-
import numpy as np
|
|
9
|
-
|
|
10
|
-
from ._errors import CorruptFileError
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ThreadHandles:
|
|
14
|
-
"""One read-only file handle per thread for one file (seek + read on a
|
|
15
|
-
shared handle is not thread-safe), kept open until :meth:`close`."""
|
|
16
|
-
|
|
17
|
-
def __init__(self, path: str):
|
|
18
|
-
self.path = path
|
|
19
|
-
self._local = threading.local()
|
|
20
|
-
self._lock = threading.Lock()
|
|
21
|
-
self._all: list = []
|
|
22
|
-
|
|
23
|
-
def get(self):
|
|
24
|
-
fh = getattr(self._local, "fh", None)
|
|
25
|
-
if fh is None:
|
|
26
|
-
fh = self._local.fh = builtins.open(self.path, "rb", buffering=0)
|
|
27
|
-
with self._lock:
|
|
28
|
-
self._all.append(fh)
|
|
29
|
-
return fh
|
|
30
|
-
|
|
31
|
-
def read(self, offset: int, size: int) -> bytes:
|
|
32
|
-
fh = self.get()
|
|
33
|
-
fh.seek(offset)
|
|
34
|
-
data = fh.read(size)
|
|
35
|
-
if len(data) != size:
|
|
36
|
-
raise CorruptFileError(f"{self.path!r}: data at byte {offset} ends early "
|
|
37
|
-
"(truncated file?)")
|
|
38
|
-
return data
|
|
39
|
-
|
|
40
|
-
def close(self) -> None:
|
|
41
|
-
with self._lock:
|
|
42
|
-
handles, self._all = self._all, []
|
|
43
|
-
for fh in handles:
|
|
44
|
-
fh.close()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class DecodeCache:
|
|
48
|
-
"""Decoded segments (tiles, strips, chunks), least recently used out.
|
|
49
|
-
|
|
50
|
-
Keeps a segment shared by neighbouring reads from being decoded twice.
|
|
51
|
-
Segments above a quarter of the budget are not kept (a whole plane from
|
|
52
|
-
a single-strip file would evict everything else)."""
|
|
53
|
-
|
|
54
|
-
def __init__(self, max_bytes: int):
|
|
55
|
-
self.max_bytes = max_bytes
|
|
56
|
-
self._items: OrderedDict = OrderedDict()
|
|
57
|
-
self._bytes = 0
|
|
58
|
-
self._lock = threading.Lock()
|
|
59
|
-
|
|
60
|
-
def get(self, key):
|
|
61
|
-
with self._lock:
|
|
62
|
-
seg = self._items.get(key)
|
|
63
|
-
if seg is not None:
|
|
64
|
-
self._items.move_to_end(key)
|
|
65
|
-
return seg
|
|
66
|
-
|
|
67
|
-
def put(self, key, seg: np.ndarray) -> None:
|
|
68
|
-
if seg.nbytes > self.max_bytes // 4:
|
|
69
|
-
return
|
|
70
|
-
seg.setflags(write=False)
|
|
71
|
-
with self._lock:
|
|
72
|
-
if key in self._items:
|
|
73
|
-
return
|
|
74
|
-
self._items[key] = seg
|
|
75
|
-
self._bytes += seg.nbytes
|
|
76
|
-
while self._bytes > self.max_bytes:
|
|
77
|
-
_, old = self._items.popitem(last=False)
|
|
78
|
-
self._bytes -= old.nbytes
|
|
79
|
-
|
|
80
|
-
def clear(self) -> None:
|
|
81
|
-
with self._lock:
|
|
82
|
-
self._items.clear()
|
|
83
|
-
self._bytes = 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|