tmapslide 0.2.2__tar.gz → 0.2.3__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.
- {tmapslide-0.2.2 → tmapslide-0.2.3}/PKG-INFO +12 -9
- {tmapslide-0.2.2 → tmapslide-0.2.3}/README.md +11 -8
- {tmapslide-0.2.2 → tmapslide-0.2.3}/pyproject.toml +1 -1
- {tmapslide-0.2.2 → tmapslide-0.2.3}/src/tmapslide/__init__.py +1 -1
- {tmapslide-0.2.2 → tmapslide-0.2.3}/src/tmapslide/_slide.py +13 -11
- {tmapslide-0.2.2 → tmapslide-0.2.3}/src/tmapslide/_tmapformat.py +97 -68
- {tmapslide-0.2.2 → tmapslide-0.2.3}/.gitignore +0 -0
- {tmapslide-0.2.2 → tmapslide-0.2.3}/LICENSE +0 -0
- {tmapslide-0.2.2 → tmapslide-0.2.3}/examples/read_region.py +0 -0
- {tmapslide-0.2.2 → tmapslide-0.2.3}/src/tmapslide/_cache.py +0 -0
- {tmapslide-0.2.2 → tmapslide-0.2.3}/src/tmapslide/_exceptions.py +0 -0
- {tmapslide-0.2.2 → tmapslide-0.2.3}/tests/test_basic.py +0 -0
- {tmapslide-0.2.2 → tmapslide-0.2.3}/tests/test_synthetic.py +0 -0
- {tmapslide-0.2.2 → tmapslide-0.2.3}/tests/tmap_fixtures.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tmapslide
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Pure Python UNIC TMAP whole-slide image reader with OpenSlide-compatible API
|
|
5
5
|
Project-URL: Homepage, https://github.com/yifanfeng97/tmapslide
|
|
6
6
|
Project-URL: Documentation, https://github.com/yifanfeng97/tmapslide#readme
|
|
@@ -115,14 +115,17 @@ TMAP backend (median of 5 runs, same files, same machine):
|
|
|
115
115
|
|
|
116
116
|
| Scenario | TMAP07 | TMAP06 |
|
|
117
117
|
|---|---|---|
|
|
118
|
-
| Open slide |
|
|
119
|
-
| Cold 1024² region @L0 | **
|
|
120
|
-
| Random 512² region @L0 | **1.
|
|
121
|
-
| Warm 512² region ×50 | **
|
|
122
|
-
|
|
123
|
-
>
|
|
124
|
-
>
|
|
125
|
-
>
|
|
118
|
+
| Open slide | 68 ms vs 46 ms | **80 ms vs 220 ms (2.8×)** |
|
|
119
|
+
| Cold 1024² region @L0 | **3.6 ms vs 15.7 ms (4.4×)** | **2.8 ms vs 12.3 ms (4.4×)** |
|
|
120
|
+
| Random 512² region @L0 | **1.0 ms vs 5.2 ms (5.0×)** | **1.0 ms vs 5.6 ms (5.6×)** |
|
|
121
|
+
| Warm 512² region ×50 | **45 ms vs 242 ms (5.4×)** | **55 ms vs 373 ms (6.8×)** |
|
|
122
|
+
|
|
123
|
+
> The two libraries pay for indexing in opposite places. tmapslide builds a
|
|
124
|
+
> validated tile index (grid + lazy tile tables) once at open; ASlide defers
|
|
125
|
+
> that work into every read, re-decoding tiles per call. That is why TMAP07
|
|
126
|
+
> open runs ~20 ms slower while every read is 4–7× faster — for any workload
|
|
127
|
+
> that reads more than a handful of regions, tmapslide comes out far ahead.
|
|
128
|
+
> Slide open is a once-per-process cost.
|
|
126
129
|
|
|
127
130
|
## 📖 API
|
|
128
131
|
|
|
@@ -84,14 +84,17 @@ TMAP backend (median of 5 runs, same files, same machine):
|
|
|
84
84
|
|
|
85
85
|
| Scenario | TMAP07 | TMAP06 |
|
|
86
86
|
|---|---|---|
|
|
87
|
-
| Open slide |
|
|
88
|
-
| Cold 1024² region @L0 | **
|
|
89
|
-
| Random 512² region @L0 | **1.
|
|
90
|
-
| Warm 512² region ×50 | **
|
|
91
|
-
|
|
92
|
-
>
|
|
93
|
-
>
|
|
94
|
-
>
|
|
87
|
+
| Open slide | 68 ms vs 46 ms | **80 ms vs 220 ms (2.8×)** |
|
|
88
|
+
| Cold 1024² region @L0 | **3.6 ms vs 15.7 ms (4.4×)** | **2.8 ms vs 12.3 ms (4.4×)** |
|
|
89
|
+
| Random 512² region @L0 | **1.0 ms vs 5.2 ms (5.0×)** | **1.0 ms vs 5.6 ms (5.6×)** |
|
|
90
|
+
| Warm 512² region ×50 | **45 ms vs 242 ms (5.4×)** | **55 ms vs 373 ms (6.8×)** |
|
|
91
|
+
|
|
92
|
+
> The two libraries pay for indexing in opposite places. tmapslide builds a
|
|
93
|
+
> validated tile index (grid + lazy tile tables) once at open; ASlide defers
|
|
94
|
+
> that work into every read, re-decoding tiles per call. That is why TMAP07
|
|
95
|
+
> open runs ~20 ms slower while every read is 4–7× faster — for any workload
|
|
96
|
+
> that reads more than a handful of regions, tmapslide comes out far ahead.
|
|
97
|
+
> Slide open is a once-per-process cost.
|
|
95
98
|
|
|
96
99
|
## 📖 API
|
|
97
100
|
|
|
@@ -114,7 +114,6 @@ class OpenSlide:
|
|
|
114
114
|
"_info",
|
|
115
115
|
"_properties",
|
|
116
116
|
"_associated_images",
|
|
117
|
-
"_tile_lookup",
|
|
118
117
|
"_background_rgba",
|
|
119
118
|
"_io_lock",
|
|
120
119
|
"_data_paths",
|
|
@@ -138,12 +137,6 @@ class OpenSlide:
|
|
|
138
137
|
except Exception as e:
|
|
139
138
|
raise OpenSlideUnsupportedFormatError(f"Cannot parse TMAP file: {e}")
|
|
140
139
|
|
|
141
|
-
# (level, x, y) -> tile index, for grid lookup in read_region.
|
|
142
|
-
self._tile_lookup: Dict[Tuple[int, int, int], int] = {}
|
|
143
|
-
for level in self._info.levels:
|
|
144
|
-
for idx, t in enumerate(level.tiles):
|
|
145
|
-
self._tile_lookup[(level.index, t.x, t.y)] = idx
|
|
146
|
-
|
|
147
140
|
try:
|
|
148
141
|
self._file_handle = open(filename, "rb")
|
|
149
142
|
except Exception as e:
|
|
@@ -416,13 +409,22 @@ class OpenSlide:
|
|
|
416
409
|
|
|
417
410
|
self._ensure_open_handle()
|
|
418
411
|
|
|
412
|
+
grid = lvl.grid
|
|
413
|
+
gcols = lvl.grid_cols
|
|
414
|
+
glen = len(grid) if grid is not None else 0
|
|
415
|
+
|
|
419
416
|
for ty in range(ty_start, ty_end):
|
|
417
|
+
tile_y = ty * th
|
|
420
418
|
for tx in range(tx_start, tx_end):
|
|
421
|
-
|
|
422
|
-
tile_y = ty * th
|
|
423
|
-
idx = self._tile_lookup.get((level, tile_x, tile_y))
|
|
424
|
-
if idx is None:
|
|
419
|
+
if grid is None or tx < 0:
|
|
425
420
|
continue
|
|
421
|
+
cell = ty * gcols + tx
|
|
422
|
+
if cell >= glen:
|
|
423
|
+
continue
|
|
424
|
+
idx = grid[cell]
|
|
425
|
+
if idx < 0:
|
|
426
|
+
continue
|
|
427
|
+
tile_x = tx * tw
|
|
426
428
|
tile = self._read_decoded_tile(lvl, idx)
|
|
427
429
|
crop_x0 = max(0, x0 - tile_x)
|
|
428
430
|
crop_y0 = max(0, y0 - tile_y)
|
|
@@ -41,6 +41,7 @@ TMAP06 layout
|
|
|
41
41
|
import io
|
|
42
42
|
import os
|
|
43
43
|
import struct
|
|
44
|
+
from collections.abc import Sequence
|
|
44
45
|
from dataclasses import dataclass, field
|
|
45
46
|
from typing import Dict, List, Optional, Tuple
|
|
46
47
|
|
|
@@ -60,12 +61,13 @@ REC07_SIZE = 40
|
|
|
60
61
|
REC07_HEADER_SKIP = 24
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
@dataclass
|
|
64
|
+
@dataclass(slots=True)
|
|
64
65
|
class TmapTile:
|
|
65
66
|
"""One pyramid tile pointing at a JPEG blob in a data file.
|
|
66
67
|
|
|
67
68
|
``file_id`` selects the file: 0 is the main TMAP, 1+ are .DT sidecar
|
|
68
|
-
files (TMAP06 multi-file slides).
|
|
69
|
+
files (TMAP06 multi-file slides). Slotted for fast bulk construction:
|
|
70
|
+
TMAP07 slides materialize 100k+ tiles at open time.
|
|
69
71
|
"""
|
|
70
72
|
|
|
71
73
|
level: int
|
|
@@ -78,6 +80,46 @@ class TmapTile:
|
|
|
78
80
|
file_id: int = 0
|
|
79
81
|
|
|
80
82
|
|
|
83
|
+
class _TileList(Sequence):
|
|
84
|
+
"""Lazily materializes TmapTile objects from a level's raw record table.
|
|
85
|
+
|
|
86
|
+
A TMAP07 level-0 table holds 100k+ records; constructing every tile
|
|
87
|
+
up front dominated slide-open time. The record table is kept as raw
|
|
88
|
+
bytes and a tile is unpacked on first access. Public behavior is
|
|
89
|
+
identical to a list of TmapTile.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
__slots__ = ("_level", "_raw", "_rec_idx", "_tw", "_th")
|
|
93
|
+
|
|
94
|
+
def __init__(self, level: int, raw: bytes, rec_idx: List[int], tw: int, th: int):
|
|
95
|
+
self._level = level
|
|
96
|
+
self._raw = raw
|
|
97
|
+
self._rec_idx = rec_idx
|
|
98
|
+
self._tw = tw
|
|
99
|
+
self._th = th
|
|
100
|
+
|
|
101
|
+
def __len__(self) -> int:
|
|
102
|
+
return len(self._rec_idx)
|
|
103
|
+
|
|
104
|
+
def __getitem__(self, i):
|
|
105
|
+
if isinstance(i, slice):
|
|
106
|
+
return [self[j] for j in range(*i.indices(len(self)))]
|
|
107
|
+
base = self._rec_idx[i] * TMAP07_REC_SIZE
|
|
108
|
+
_layer, _focus, x, y, _w, _h, off, size = _REC07.unpack_from(self._raw, base)
|
|
109
|
+
return TmapTile(self._level, off, size, x, y, self._tw, self._th)
|
|
110
|
+
|
|
111
|
+
def __iter__(self):
|
|
112
|
+
unpack = _REC07.unpack_from
|
|
113
|
+
raw = self._raw
|
|
114
|
+
level, tw, th = self._level, self._tw, self._th
|
|
115
|
+
for i in self._rec_idx:
|
|
116
|
+
_layer, _focus, x, y, _w, _h, off, size = unpack(raw, i * TMAP07_REC_SIZE)
|
|
117
|
+
yield TmapTile(level, off, size, x, y, tw, th)
|
|
118
|
+
|
|
119
|
+
def __repr__(self) -> str:
|
|
120
|
+
return f"_TileList(len={len(self._rec_idx)})"
|
|
121
|
+
|
|
122
|
+
|
|
81
123
|
@dataclass
|
|
82
124
|
class TmapLevel:
|
|
83
125
|
"""A pyramid level."""
|
|
@@ -88,7 +130,12 @@ class TmapLevel:
|
|
|
88
130
|
height: int
|
|
89
131
|
tile_width: int
|
|
90
132
|
tile_height: int
|
|
91
|
-
tiles:
|
|
133
|
+
tiles: Sequence[TmapTile] = field(default_factory=list)
|
|
134
|
+
# TMAP07: map from grid cell (row * grid_cols + col) to the index of
|
|
135
|
+
# that tile in `tiles`, or -1 for blank cells. Replaces a hash lookup
|
|
136
|
+
# at read time; None for TMAP06 levels (block-based geometry).
|
|
137
|
+
grid: Optional[List[int]] = None
|
|
138
|
+
grid_cols: int = 0
|
|
92
139
|
|
|
93
140
|
|
|
94
141
|
@dataclass
|
|
@@ -189,19 +236,6 @@ _ASSOC07_NAMES = {
|
|
|
189
236
|
}
|
|
190
237
|
|
|
191
238
|
|
|
192
|
-
def _find_tile_data_start07(f, file_size: int) -> int:
|
|
193
|
-
"""First JPEG SOI at/after the record region -> start of tile data."""
|
|
194
|
-
pos = 0
|
|
195
|
-
step = 1024 * 1024
|
|
196
|
-
while pos < file_size:
|
|
197
|
-
chunk = _read_at(f, pos, min(step, file_size - pos))
|
|
198
|
-
p = chunk.find(b"\xff\xd8\xff")
|
|
199
|
-
if p != -1:
|
|
200
|
-
return pos + p
|
|
201
|
-
pos += len(chunk)
|
|
202
|
-
raise ValueError("TMAP07: no tile data found")
|
|
203
|
-
|
|
204
|
-
|
|
205
239
|
def _parse_assoc07(f, file_size: int, tile_data_end: int) -> List[TmapAssocImage]:
|
|
206
240
|
"""Collect TMAP07 associated images.
|
|
207
241
|
|
|
@@ -284,32 +318,7 @@ def _parse_layer_table07(f, base: int = TMAP07_LAYERINFO_BASE) -> List[dict]:
|
|
|
284
318
|
return entries
|
|
285
319
|
|
|
286
320
|
|
|
287
|
-
|
|
288
|
-
"""Read one level's tile table.
|
|
289
|
-
|
|
290
|
-
Record (40 B), indexed by (row * tile_col + col):
|
|
291
|
-
layer_no u32, focus_no u32, x u32, y u32, w u32, h u32,
|
|
292
|
-
data_offset u64, data_length u32, pad u32
|
|
293
|
-
Blank cells store offset == 0.
|
|
294
|
-
"""
|
|
295
|
-
cols, rows = entry["tile_col"], entry["tile_row"]
|
|
296
|
-
total = cols * rows
|
|
297
|
-
raw = _read_at(f, entry["index_offset"], total * TMAP07_REC_SIZE + 64)
|
|
298
|
-
recs: List[Optional[tuple]] = []
|
|
299
|
-
for i in range(total):
|
|
300
|
-
base = i * TMAP07_REC_SIZE
|
|
301
|
-
if base + TMAP07_REC_SIZE > len(raw):
|
|
302
|
-
break
|
|
303
|
-
layer_no, focus_no, x, y, w, h, off, size = struct.unpack_from(
|
|
304
|
-
"<IIIIIIQI", raw, base
|
|
305
|
-
)
|
|
306
|
-
if off == 0 and size == 0:
|
|
307
|
-
recs.append(None)
|
|
308
|
-
continue
|
|
309
|
-
if size == 0 or off + size > file_size:
|
|
310
|
-
break
|
|
311
|
-
recs.append((off, size, layer_no, focus_no, x, y, w, h))
|
|
312
|
-
return recs
|
|
321
|
+
_REC07 = struct.Struct("<IIIIIIQI") # layer_no, focus_no, x, y, w, h, off u64, size
|
|
313
322
|
|
|
314
323
|
|
|
315
324
|
def _parse_tmap07(path: str) -> TmapFileInfo:
|
|
@@ -327,8 +336,6 @@ def _parse_tmap07(path: str) -> TmapFileInfo:
|
|
|
327
336
|
declared_tiles = struct.unpack_from("<I", head, 0x1C)[0]
|
|
328
337
|
magic = head[:6].decode("ascii", errors="replace")
|
|
329
338
|
|
|
330
|
-
data_start = _find_tile_data_start07(f, file_size)
|
|
331
|
-
|
|
332
339
|
# TMAP07 LayerInfo table at 560.
|
|
333
340
|
entries = _parse_layer_table07(f)
|
|
334
341
|
if not entries:
|
|
@@ -336,32 +343,55 @@ def _parse_tmap07(path: str) -> TmapFileInfo:
|
|
|
336
343
|
|
|
337
344
|
levels: List[TmapLevel] = []
|
|
338
345
|
prop_layer_info = []
|
|
346
|
+
tile_data_end = 0
|
|
339
347
|
for order, e in enumerate(entries):
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
348
|
+
# Single pass over the record table: validate records, fix the
|
|
349
|
+
# level tile size from the first stored record, build the
|
|
350
|
+
# grid-position index, and keep the table as raw bytes — tile
|
|
351
|
+
# objects are materialized lazily by _TileList (100k+ tiles on
|
|
352
|
+
# level 0 would otherwise dominate slide-open time).
|
|
353
|
+
cols = e["tile_col"]
|
|
354
|
+
total = cols * e["tile_row"]
|
|
355
|
+
raw = _read_at(f, e["index_offset"], total * TMAP07_REC_SIZE + 64)
|
|
356
|
+
n = min(total, len(raw) // TMAP07_REC_SIZE)
|
|
357
|
+
unpack = _REC07.unpack_from
|
|
358
|
+
grid = [-1] * total
|
|
359
|
+
rec_idx: List[int] = []
|
|
360
|
+
ri_append = rec_idx.append
|
|
361
|
+
tw = th = None
|
|
362
|
+
level_end = 0
|
|
363
|
+
pos = 0
|
|
364
|
+
for i in range(n):
|
|
365
|
+
_layer, _focus, x, y, w, h, off, size = unpack(raw, i * TMAP07_REC_SIZE)
|
|
366
|
+
if off == 0 and size == 0:
|
|
367
|
+
continue
|
|
368
|
+
if size == 0 or off + size > file_size:
|
|
369
|
+
break
|
|
370
|
+
if tw is None:
|
|
371
|
+
tw = w if 16 <= w <= 4096 else 256
|
|
372
|
+
th = h if 16 <= h <= 4096 else 256
|
|
373
|
+
grid[i] = pos
|
|
374
|
+
pos += 1
|
|
375
|
+
ri_append(i)
|
|
376
|
+
if off + size > level_end:
|
|
377
|
+
level_end = off + size
|
|
378
|
+
if not rec_idx:
|
|
343
379
|
continue
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
x, y = r[4], r[5]
|
|
358
|
-
lvl.tiles.append(
|
|
359
|
-
TmapTile(
|
|
360
|
-
level=order, offset=off, size=size, x=x, y=y,
|
|
361
|
-
width=tw, height=th,
|
|
362
|
-
)
|
|
380
|
+
if level_end > tile_data_end:
|
|
381
|
+
tile_data_end = level_end
|
|
382
|
+
levels.append(
|
|
383
|
+
TmapLevel(
|
|
384
|
+
index=order,
|
|
385
|
+
downsample=float(2**order),
|
|
386
|
+
width=e["width"],
|
|
387
|
+
height=e["height"],
|
|
388
|
+
tile_width=tw,
|
|
389
|
+
tile_height=th,
|
|
390
|
+
tiles=_TileList(order, raw, rec_idx, tw, th),
|
|
391
|
+
grid=grid,
|
|
392
|
+
grid_cols=cols,
|
|
363
393
|
)
|
|
364
|
-
|
|
394
|
+
)
|
|
365
395
|
prop_layer_info.append(
|
|
366
396
|
f"{e['width']}x{e['height']}:{e['tile_col']}x{e['tile_row']}"
|
|
367
397
|
)
|
|
@@ -369,7 +399,6 @@ def _parse_tmap07(path: str) -> TmapFileInfo:
|
|
|
369
399
|
if not levels:
|
|
370
400
|
raise ValueError("TMAP07: no tile records parsed")
|
|
371
401
|
|
|
372
|
-
tile_data_end = max(t.offset + t.size for l in levels for t in l.tiles)
|
|
373
402
|
assoc = _parse_assoc07(f, file_size, tile_data_end)
|
|
374
403
|
|
|
375
404
|
return TmapFileInfo(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|