arrayview 0.47.3__tar.gz → 0.47.5__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.
- {arrayview-0.47.3 → arrayview-0.47.5}/PKG-INFO +1 -1
- {arrayview-0.47.3 → arrayview-0.47.5}/pyproject.toml +1 -1
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_io.py +418 -48
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_launcher.py +13 -11
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_render.py +11 -1
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_loading.py +12 -11
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_query.py +1 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_session.py +10 -7
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_source_safety.py +2 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_viewer.html +83 -13
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_vscode_extension.py +1 -1
- arrayview-0.47.5/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_api.py +26 -2
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_cli.py +28 -0
- arrayview-0.47.5/tests/test_io.py +264 -0
- arrayview-0.47.3/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.47.3/tests/test_io.py +0 -53
- {arrayview-0.47.3 → arrayview-0.47.5}/.gitignore +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/LICENSE +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/README.md +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/__init__.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/__main__.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_app.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_bootstrap_app.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_config.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_diagnostics.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_diff.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_extra_ports.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_icon.png +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_platform.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_server.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_shell.html +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_torch.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_tutorial.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/_watch.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/src/arrayview/todo.md +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/capture_v_animation.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/conftest.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_1d_arrays.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_backend_shared.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_browser.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_config.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_dicom.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_drop_import.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_instance_registry.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_large_arrays.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_nifti_series.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_source_safety.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_torch.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_tutorial.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/test_vsix_bundle.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/ui_audit.py +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.47.3 → arrayview-0.47.5}/tests/visual_smoke.py +0 -0
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import fnmatch
|
|
6
|
+
import functools
|
|
6
7
|
import gzip
|
|
7
8
|
import glob
|
|
8
9
|
import os
|
|
9
10
|
import re
|
|
11
|
+
import sys
|
|
10
12
|
import threading
|
|
13
|
+
import zlib
|
|
11
14
|
from concurrent.futures import ThreadPoolExecutor
|
|
12
15
|
from collections import OrderedDict
|
|
13
16
|
|
|
@@ -114,6 +117,23 @@ def list_npz_keys(filepath):
|
|
|
114
117
|
return keys
|
|
115
118
|
|
|
116
119
|
|
|
120
|
+
# MATLAB class -> dtype scipy.io.loadmat returns for it (complex is not
|
|
121
|
+
# visible in the header; the picker only shows this when several arrays exist).
|
|
122
|
+
_MAT_CLASS_DTYPES = {
|
|
123
|
+
"double": "float64",
|
|
124
|
+
"single": "float32",
|
|
125
|
+
"int8": "int8",
|
|
126
|
+
"uint8": "uint8",
|
|
127
|
+
"int16": "int16",
|
|
128
|
+
"uint16": "uint16",
|
|
129
|
+
"int32": "int32",
|
|
130
|
+
"uint32": "uint32",
|
|
131
|
+
"int64": "int64",
|
|
132
|
+
"uint64": "uint64",
|
|
133
|
+
"logical": "uint8",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
117
137
|
def list_mat_keys(filepath):
|
|
118
138
|
"""Return [{key, shape, dtype}] for each array in a .mat file.
|
|
119
139
|
|
|
@@ -123,6 +143,17 @@ def list_mat_keys(filepath):
|
|
|
123
143
|
try:
|
|
124
144
|
import scipy.io
|
|
125
145
|
|
|
146
|
+
# The headers alone settle the common single-array file, whose array
|
|
147
|
+
# is loaded right after this anyway; reading it here too doubled the
|
|
148
|
+
# wait on network shares.
|
|
149
|
+
numeric = [
|
|
150
|
+
(name, shape, cls)
|
|
151
|
+
for name, shape, cls in scipy.io.whosmat(filepath)
|
|
152
|
+
if cls in _MAT_CLASS_DTYPES
|
|
153
|
+
]
|
|
154
|
+
if len(numeric) == 1:
|
|
155
|
+
name, shape, cls = numeric[0]
|
|
156
|
+
return [{"key": name, "shape": list(shape), "dtype": _MAT_CLASS_DTYPES[cls]}]
|
|
126
157
|
mat = scipy.io.loadmat(filepath)
|
|
127
158
|
keys = []
|
|
128
159
|
for k, v in mat.items():
|
|
@@ -435,12 +466,100 @@ def _load_nifti_with_meta(filepath):
|
|
|
435
466
|
random-access, but it is no longer silently reordered.
|
|
436
467
|
"""
|
|
437
468
|
img, meta = _nifti_header_with_meta(filepath, native=True)
|
|
438
|
-
|
|
439
|
-
img.dataobj
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
469
|
+
if filepath.endswith(".nii"):
|
|
470
|
+
return img.dataobj, meta
|
|
471
|
+
source = img
|
|
472
|
+
if _nifti_voxel_bytes(img) <= npy_eager_limit():
|
|
473
|
+
raw = read_gzip_file_parallel(filepath)
|
|
474
|
+
if raw is not None:
|
|
475
|
+
try:
|
|
476
|
+
source = type(img).from_bytes(raw)
|
|
477
|
+
except Exception:
|
|
478
|
+
source = img
|
|
479
|
+
return _nifti_display_array(source.dataobj), meta
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _nifti_voxel_bytes(img) -> int:
|
|
483
|
+
try:
|
|
484
|
+
return int(np.prod(img.shape)) * np.dtype(img.get_data_dtype()).itemsize
|
|
485
|
+
except Exception:
|
|
486
|
+
return 1 << 62
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
_GZIP_READ_CHUNK_BYTES = 8 << 20
|
|
490
|
+
_GZIP_READ_THREADS = 8
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def read_gzip_file_parallel(filepath):
|
|
494
|
+
"""Decompress a whole .gz file, reading it with several reads in flight.
|
|
495
|
+
|
|
496
|
+
A gzip stream can only be decoded front to back, so the decode is one
|
|
497
|
+
thread; but the bytes can arrive out of order. Reading 8 MB chunks in
|
|
498
|
+
parallel and decoding each as soon as its turn comes overlaps the two, so
|
|
499
|
+
on an SMB share the wait is close to the parallel read alone (~1.3 s for a
|
|
500
|
+
110 MB file) instead of a single slow reader followed by the decode.
|
|
501
|
+
Handles multi-member files. Returns None on any read or decode error so
|
|
502
|
+
the caller can fall back to its ordinary reader.
|
|
503
|
+
"""
|
|
504
|
+
try:
|
|
505
|
+
size = os.path.getsize(filepath)
|
|
506
|
+
fd = os.open(filepath, os.O_RDONLY)
|
|
507
|
+
except OSError:
|
|
508
|
+
return None
|
|
509
|
+
buf = bytearray(size)
|
|
510
|
+
view = memoryview(buf)
|
|
511
|
+
starts = list(range(0, size, _GZIP_READ_CHUNK_BYTES))
|
|
512
|
+
ready = [threading.Event() for _ in starts]
|
|
513
|
+
failed = []
|
|
514
|
+
|
|
515
|
+
def _read(i):
|
|
516
|
+
start = starts[i]
|
|
517
|
+
stop = min(start + _GZIP_READ_CHUNK_BYTES, size)
|
|
518
|
+
pos = start
|
|
519
|
+
try:
|
|
520
|
+
while pos < stop:
|
|
521
|
+
n = os.preadv(fd, [view[pos:stop]], pos)
|
|
522
|
+
if n <= 0:
|
|
523
|
+
raise OSError("unexpected end of file")
|
|
524
|
+
pos += n
|
|
525
|
+
except Exception as exc: # noqa: BLE001 - reported to the decoder
|
|
526
|
+
failed.append(exc)
|
|
527
|
+
finally:
|
|
528
|
+
ready[i].set()
|
|
529
|
+
|
|
530
|
+
out = []
|
|
531
|
+
try:
|
|
532
|
+
with ThreadPoolExecutor(
|
|
533
|
+
max_workers=max(1, min(_GZIP_READ_THREADS, len(starts))),
|
|
534
|
+
thread_name_prefix="arrayview-gzip-read",
|
|
535
|
+
) as pool:
|
|
536
|
+
for i in range(len(starts)):
|
|
537
|
+
pool.submit(_read, i)
|
|
538
|
+
decoder = zlib.decompressobj(wbits=31)
|
|
539
|
+
fed = False
|
|
540
|
+
for i, start in enumerate(starts):
|
|
541
|
+
ready[i].wait()
|
|
542
|
+
if failed:
|
|
543
|
+
return None
|
|
544
|
+
chunk = view[start:min(start + _GZIP_READ_CHUNK_BYTES, size)]
|
|
545
|
+
while chunk:
|
|
546
|
+
fed = True
|
|
547
|
+
out.append(decoder.decompress(chunk))
|
|
548
|
+
if not decoder.eof:
|
|
549
|
+
break
|
|
550
|
+
# End of one gzip member: any rest starts the next one.
|
|
551
|
+
chunk = decoder.unused_data
|
|
552
|
+
decoder = zlib.decompressobj(wbits=31)
|
|
553
|
+
fed = False
|
|
554
|
+
if fed and not decoder.eof:
|
|
555
|
+
return None # truncated: let the ordinary reader report it
|
|
556
|
+
if not out:
|
|
557
|
+
return None
|
|
558
|
+
except (zlib.error, OSError):
|
|
559
|
+
return None
|
|
560
|
+
finally:
|
|
561
|
+
os.close(fd)
|
|
562
|
+
return b"".join(out)
|
|
444
563
|
|
|
445
564
|
|
|
446
565
|
# ---------------------------------------------------------------------------
|
|
@@ -667,7 +786,7 @@ class _FileSeries:
|
|
|
667
786
|
"""Lazy 4D/5D view over a directory of same-shape array files.
|
|
668
787
|
|
|
669
788
|
Like ``_NiftiSeries`` but works with any supported file format
|
|
670
|
-
(.npy, .npz, .zarr, .pt/.pth, .h5/.hdf5, .tif/.tiff, .mat).
|
|
789
|
+
(.npy, .npz, .zarr, .pt/.pth, .h5/.hdf5, .tif/.tiff, .mat, .gipl).
|
|
671
790
|
Uses ``load_data`` for each volume — no nibabel reorientation.
|
|
672
791
|
"""
|
|
673
792
|
|
|
@@ -940,6 +1059,8 @@ def _get_ext(filepath):
|
|
|
940
1059
|
return ".nii.gz"
|
|
941
1060
|
if lower.endswith(".zarr.zip"):
|
|
942
1061
|
return ".zarr.zip"
|
|
1062
|
+
if lower.endswith(".gipl.gz"):
|
|
1063
|
+
return ".gipl.gz"
|
|
943
1064
|
return os.path.splitext(lower)[1]
|
|
944
1065
|
|
|
945
1066
|
|
|
@@ -950,7 +1071,7 @@ def _is_supported_collection_path(filepath):
|
|
|
950
1071
|
def _strip_array_ext(filepath):
|
|
951
1072
|
base = os.path.basename(filepath)
|
|
952
1073
|
lower = base.lower()
|
|
953
|
-
for suffix in (".nii.gz", ".zarr.zip"):
|
|
1074
|
+
for suffix in (".nii.gz", ".zarr.zip", ".gipl.gz"):
|
|
954
1075
|
if lower.endswith(suffix):
|
|
955
1076
|
return base[: -len(suffix)]
|
|
956
1077
|
return os.path.splitext(base)[0]
|
|
@@ -1599,41 +1720,220 @@ def mmap_first_frame_is_cheap(arr, total_bytes) -> bool:
|
|
|
1599
1720
|
return runs <= max_runs and faulted <= fraction * total_bytes
|
|
1600
1721
|
|
|
1601
1722
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1723
|
+
# Fetching the first frame's pages one by one beats reading the whole file
|
|
1724
|
+
# only on a local disk: on an SMB share the same 65k small reads of a 1.6 GB
|
|
1725
|
+
# file took 11 s against 0.7 s locally.
|
|
1726
|
+
_FIRST_FRAME_PREFETCH_MAX_PAGES = 1 << 17
|
|
1727
|
+
_FIRST_FRAME_PREFETCH_THREADS = 16
|
|
1604
1728
|
|
|
1605
|
-
For a large array this read is the whole wait between a click and the
|
|
1606
|
-
first frame — 9 s for a 1.6 GB file on a cold cache — and ``np.load``
|
|
1607
|
-
reports nothing while it runs. Copying in pieces costs the same reads
|
|
1608
|
-
and makes the wait explainable.
|
|
1609
1729
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1730
|
+
def _on_known_local_disk(filepath) -> bool:
|
|
1731
|
+
if not sys.platform.startswith("linux"):
|
|
1732
|
+
return False
|
|
1733
|
+
try:
|
|
1734
|
+
from ._source_safety import _KNOWN_LOCAL_STAGE_FILESYSTEMS, mount_for_path
|
|
1735
|
+
|
|
1736
|
+
mount = mount_for_path(os.path.realpath(filepath))
|
|
1737
|
+
except Exception:
|
|
1738
|
+
return False
|
|
1739
|
+
return mount is not None and mount.filesystem in _KNOWN_LOCAL_STAGE_FILESYSTEMS
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
def _viewer_start_index(shape, dims):
|
|
1743
|
+
"""The slice the viewer opens on; mirrors its ``_initialIndices``."""
|
|
1744
|
+
ref = max(shape[dims[0]], shape[dims[1]])
|
|
1745
|
+
return tuple(
|
|
1746
|
+
size // 2 if (i in dims or size >= 0.25 * ref) else 0
|
|
1747
|
+
for i, size in enumerate(shape)
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
def prefetch_first_frame_pages(mapped, filepath) -> bool:
|
|
1752
|
+
"""Pull just the pages of the viewer's first frame into memory, in parallel.
|
|
1753
|
+
|
|
1754
|
+
When the first frame is one element per page across the whole file (a
|
|
1755
|
+
C-order array viewed along its leading axes), reading the file first made
|
|
1756
|
+
a 1.6 GB array wait 3-4 s cold for its picture. Fetching only the pages the
|
|
1757
|
+
frame touches, many at a time, takes ~0.7 s; the memmap then renders the
|
|
1758
|
+
frame from memory while the full read continues in the background.
|
|
1759
|
+
|
|
1760
|
+
Returns False, having read nothing, when this would not pay off: not a
|
|
1761
|
+
known local disk, the viewer would start elsewhere, or too many pages.
|
|
1613
1762
|
"""
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1763
|
+
from ._session import _default_start_dims_for_data, _viewer_start_dims_for_shape
|
|
1764
|
+
|
|
1765
|
+
shape = tuple(int(s) for s in mapped.shape)
|
|
1766
|
+
if len(shape) < 3 or _default_start_dims_for_data(mapped) is not None:
|
|
1767
|
+
return False
|
|
1768
|
+
dims = _viewer_start_dims_for_shape(shape)
|
|
1769
|
+
if dims is None:
|
|
1770
|
+
return False
|
|
1771
|
+
return prefetch_slice_pages(mapped, filepath, dims, _viewer_start_index(shape, dims))
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
@functools.lru_cache(maxsize=64)
|
|
1775
|
+
def _on_known_local_disk_cached(filepath) -> bool:
|
|
1776
|
+
return _on_known_local_disk(filepath)
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
def prefetch_slice_pages(mapped, filepath, dims, index) -> bool:
|
|
1780
|
+
"""Fetch the pages of one 2-D slice of a memmap with parallel reads.
|
|
1781
|
+
|
|
1782
|
+
Faulting such a slice in through the map is the slow way: readahead drags
|
|
1783
|
+
in the pages between the wanted elements (6 s for one slice of a 1.6 GB
|
|
1784
|
+
C-order file), and without readahead each page is its own wait (7.7 s).
|
|
1785
|
+
Many small reads in flight take ~0.7 s on a local disk. Only worth it when
|
|
1786
|
+
the slice is scattered one element per page; returns False otherwise, or
|
|
1787
|
+
off a known local disk, having read nothing.
|
|
1788
|
+
"""
|
|
1789
|
+
shape = tuple(int(s) for s in mapped.shape)
|
|
1790
|
+
strides = tuple(int(s) for s in mapped.strides)
|
|
1791
|
+
runs, _faulted = first_frame_fault_cost(
|
|
1792
|
+
shape, mapped.dtype.itemsize, strides, dims, mapped.nbytes
|
|
1793
|
+
)
|
|
1794
|
+
if runs <= 1024 or not _on_known_local_disk_cached(str(filepath)):
|
|
1795
|
+
return False
|
|
1796
|
+
base = int(mapped.offset) + sum(
|
|
1797
|
+
int(index[i]) * strides[i] for i in range(len(shape)) if i not in dims
|
|
1798
|
+
)
|
|
1799
|
+
rows = np.arange(shape[dims[0]], dtype=np.int64) * strides[dims[0]]
|
|
1800
|
+
cols = np.arange(shape[dims[1]], dtype=np.int64) * strides[dims[1]]
|
|
1801
|
+
starts = (base + rows[:, None] + cols[None, :]).ravel()
|
|
1802
|
+
ends = starts + (mapped.dtype.itemsize - 1)
|
|
1803
|
+
pages = np.unique(np.concatenate((starts, ends)) // _MMAP_PAGE_BYTES)
|
|
1804
|
+
if pages.size > _FIRST_FRAME_PREFETCH_MAX_PAGES:
|
|
1805
|
+
return False
|
|
1806
|
+
|
|
1807
|
+
try:
|
|
1808
|
+
fd = os.open(filepath, os.O_RDONLY)
|
|
1809
|
+
except OSError:
|
|
1810
|
+
return False
|
|
1811
|
+
try:
|
|
1812
|
+
# Only the pages asked for: readahead around each would multiply the I/O.
|
|
1628
1813
|
try:
|
|
1629
|
-
|
|
1630
|
-
except
|
|
1814
|
+
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_RANDOM)
|
|
1815
|
+
except (AttributeError, OSError):
|
|
1631
1816
|
pass
|
|
1817
|
+
offsets = (pages * _MMAP_PAGE_BYTES).tolist()
|
|
1818
|
+
n = _FIRST_FRAME_PREFETCH_THREADS
|
|
1819
|
+
|
|
1820
|
+
def _warm(part):
|
|
1821
|
+
for offset in part:
|
|
1822
|
+
os.pread(fd, _MMAP_PAGE_BYTES, offset)
|
|
1823
|
+
|
|
1824
|
+
with ThreadPoolExecutor(max_workers=n, thread_name_prefix="arrayview-slice-prefetch") as pool:
|
|
1825
|
+
for _ in pool.map(_warm, [offsets[k::n] for k in range(n)]):
|
|
1826
|
+
pass
|
|
1827
|
+
except OSError:
|
|
1828
|
+
return False
|
|
1829
|
+
finally:
|
|
1830
|
+
os.close(fd)
|
|
1831
|
+
return True
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
def _h5_contiguous_memmap(filepath, ds):
|
|
1835
|
+
"""Map an HDF5 dataset straight from the file, or None if it can't be.
|
|
1836
|
+
|
|
1837
|
+
MATLAB v7.3 writes large arrays as one uncompressed contiguous block, so
|
|
1838
|
+
the same first-frame shortcut as .npy applies: the viewer can draw a slice
|
|
1839
|
+
without reading the whole file first. Chunked, compressed or filtered
|
|
1840
|
+
datasets are not one block and return None.
|
|
1841
|
+
"""
|
|
1842
|
+
try:
|
|
1843
|
+
if ds.chunks is not None or ds.compression is not None:
|
|
1844
|
+
return None
|
|
1845
|
+
offset = ds.id.get_offset()
|
|
1846
|
+
if offset is None:
|
|
1847
|
+
return None
|
|
1848
|
+
dtype = ds.dtype
|
|
1849
|
+
if dtype.names:
|
|
1850
|
+
# MATLAB complex: {real, imag} of one float type, packed, which is
|
|
1851
|
+
# byte-for-byte numpy's complex layout.
|
|
1852
|
+
if set(dtype.names) != {"real", "imag"}:
|
|
1853
|
+
return None
|
|
1854
|
+
real_t, real_off = dtype.fields["real"][:2]
|
|
1855
|
+
imag_t, imag_off = dtype.fields["imag"][:2]
|
|
1856
|
+
# Only single precision: the eager path downcasts complex to
|
|
1857
|
+
# complex64, and the mapped array must come out the same.
|
|
1858
|
+
if (
|
|
1859
|
+
real_t != imag_t
|
|
1860
|
+
or real_t.kind != "f"
|
|
1861
|
+
or real_t.itemsize != 4
|
|
1862
|
+
or real_off != 0
|
|
1863
|
+
or imag_off != real_t.itemsize
|
|
1864
|
+
or dtype.itemsize != 2 * real_t.itemsize
|
|
1865
|
+
):
|
|
1866
|
+
return None
|
|
1867
|
+
dtype = np.dtype(f"{real_t.byteorder}c{2 * real_t.itemsize}")
|
|
1868
|
+
elif dtype.kind not in "biuf":
|
|
1869
|
+
return None
|
|
1870
|
+
if offset + int(np.prod(ds.shape)) * dtype.itemsize > os.path.getsize(filepath):
|
|
1871
|
+
return None
|
|
1872
|
+
return np.memmap(filepath, dtype=dtype, mode="r", offset=offset, shape=ds.shape)
|
|
1873
|
+
except Exception:
|
|
1874
|
+
return None
|
|
1875
|
+
|
|
1876
|
+
|
|
1877
|
+
_NPY_READ_CHUNK_BYTES = 8 << 20
|
|
1878
|
+
_NPY_READ_THREADS = 8
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def read_npy_into_ram(mapped, progress=None):
|
|
1882
|
+
"""Read a memmapped .npy into RAM with several reads in flight.
|
|
1883
|
+
|
|
1884
|
+
For a large array this read is the whole wait between a click and the
|
|
1885
|
+
first frame. One sequential reader leaves a network share mostly idle:
|
|
1886
|
+
on SMB a 220 MB file took 5.5 s that way and ~3 s with eight readers.
|
|
1887
|
+
Local disks are not slowed by it.
|
|
1888
|
+
|
|
1889
|
+
The reads land directly in the result's memory, in the file's own byte
|
|
1890
|
+
order: a .npy body is contiguous in either C or Fortran order, so the
|
|
1891
|
+
result is allocated in that order and filled byte for byte. *progress*,
|
|
1892
|
+
if given, is called with ``(done_bytes, total_bytes)`` as chunks land.
|
|
1893
|
+
"""
|
|
1894
|
+
order = "F" if mapped.flags.f_contiguous and not mapped.flags.c_contiguous else "C"
|
|
1895
|
+
out = np.empty(mapped.shape, dtype=mapped.dtype, order=order)
|
|
1896
|
+
total = int(out.nbytes)
|
|
1897
|
+
if total == 0:
|
|
1898
|
+
return out
|
|
1899
|
+
dst = memoryview(out.reshape(-1, order="A").view(np.uint8))
|
|
1900
|
+
offset = int(mapped.offset)
|
|
1901
|
+
lock = threading.Lock()
|
|
1902
|
+
done = [0]
|
|
1903
|
+
|
|
1904
|
+
fd = os.open(mapped.filename, os.O_RDONLY)
|
|
1905
|
+
try:
|
|
1906
|
+
def _read(start):
|
|
1907
|
+
stop = min(start + _NPY_READ_CHUNK_BYTES, total)
|
|
1908
|
+
pos = start
|
|
1909
|
+
while pos < stop:
|
|
1910
|
+
n = os.preadv(fd, [dst[pos:stop]], offset + pos)
|
|
1911
|
+
if n <= 0:
|
|
1912
|
+
raise OSError(f"unexpected end of file reading {mapped.filename!r}")
|
|
1913
|
+
pos += n
|
|
1914
|
+
if progress is not None:
|
|
1915
|
+
with lock:
|
|
1916
|
+
done[0] += stop - start
|
|
1917
|
+
finished = done[0]
|
|
1918
|
+
try:
|
|
1919
|
+
progress(finished, total)
|
|
1920
|
+
except Exception:
|
|
1921
|
+
pass
|
|
1922
|
+
|
|
1923
|
+
starts = range(0, total, _NPY_READ_CHUNK_BYTES)
|
|
1924
|
+
with ThreadPoolExecutor(
|
|
1925
|
+
max_workers=min(_NPY_READ_THREADS, len(starts)),
|
|
1926
|
+
thread_name_prefix="arrayview-npy-read",
|
|
1927
|
+
) as pool:
|
|
1928
|
+
for _ in pool.map(_read, starts):
|
|
1929
|
+
pass
|
|
1930
|
+
finally:
|
|
1931
|
+
os.close(fd)
|
|
1632
1932
|
return out
|
|
1633
1933
|
|
|
1634
1934
|
|
|
1635
1935
|
def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select=None,
|
|
1636
|
-
progress=None):
|
|
1936
|
+
progress=None, fast_first_frame=False):
|
|
1637
1937
|
"""Like load_data but also returns spatial metadata for NIfTI files.
|
|
1638
1938
|
|
|
1639
1939
|
Returns (array, meta_or_None). meta is None for non-NIfTI formats.
|
|
@@ -1659,10 +1959,12 @@ def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select
|
|
|
1659
1959
|
return _load_file_series(filepath, load=load, stack=stack)
|
|
1660
1960
|
if filepath.endswith(".nii") or filepath.endswith(".nii.gz"):
|
|
1661
1961
|
return _load_nifti_with_meta(filepath)
|
|
1662
|
-
return load_data(
|
|
1962
|
+
return load_data(
|
|
1963
|
+
filepath, key=key, progress=progress, fast_first_frame=fast_first_frame
|
|
1964
|
+
), None
|
|
1663
1965
|
|
|
1664
1966
|
|
|
1665
|
-
def load_data(filepath, key=None, *, progress=None):
|
|
1967
|
+
def load_data(filepath, key=None, *, progress=None, fast_first_frame=False):
|
|
1666
1968
|
if filepath.lower().endswith(".dcm"):
|
|
1667
1969
|
from ._dicom import load_dicom_series
|
|
1668
1970
|
|
|
@@ -1683,16 +1985,19 @@ def load_data(filepath, key=None, *, progress=None):
|
|
|
1683
1985
|
# read runs behind it (see mmap_first_frame_is_cheap) or the viewer shows
|
|
1684
1986
|
# a spinner until the read completes.
|
|
1685
1987
|
size = os.path.getsize(filepath)
|
|
1686
|
-
if size >= npy_eager_limit():
|
|
1687
|
-
return np.load(filepath, mmap_mode="r")
|
|
1688
1988
|
mapped = np.load(filepath, mmap_mode="r")
|
|
1989
|
+
if size >= npy_eager_limit():
|
|
1990
|
+
if fast_first_frame:
|
|
1991
|
+
prefetch_first_frame_pages(mapped, filepath)
|
|
1992
|
+
return mapped
|
|
1689
1993
|
if mmap_first_frame_is_cheap(mapped, size):
|
|
1690
1994
|
# Session creation upgrades this to an in-RAM array in the background.
|
|
1691
1995
|
return mapped
|
|
1692
|
-
if
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1996
|
+
if fast_first_frame and prefetch_first_frame_pages(mapped, filepath):
|
|
1997
|
+
# Same as above: the first frame's pages are already in memory, and
|
|
1998
|
+
# the full read runs behind the viewer.
|
|
1999
|
+
return mapped
|
|
2000
|
+
return read_npy_into_ram(mapped, progress)
|
|
1696
2001
|
elif filepath.endswith(".npz"):
|
|
1697
2002
|
npz = np.load(filepath)
|
|
1698
2003
|
try:
|
|
@@ -1738,6 +2043,8 @@ def load_data(filepath, key=None, *, progress=None):
|
|
|
1738
2043
|
import tifffile
|
|
1739
2044
|
|
|
1740
2045
|
return tifffile.imread(filepath)
|
|
2046
|
+
elif filepath.lower().endswith((".gipl", ".gipl.gz")):
|
|
2047
|
+
return _load_gipl(filepath)
|
|
1741
2048
|
elif filepath.endswith(".mat"):
|
|
1742
2049
|
try:
|
|
1743
2050
|
import scipy.io
|
|
@@ -1762,10 +2069,13 @@ def load_data(filepath, key=None, *, progress=None):
|
|
|
1762
2069
|
|
|
1763
2070
|
f = h5py.File(filepath, "r")
|
|
1764
2071
|
arrays = {k: f[k] for k in f.keys() if _is_viewable_h5_dataset(f[k])}
|
|
1765
|
-
if key is not None:
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2072
|
+
if key is not None or len(arrays) == 1:
|
|
2073
|
+
ds = arrays[key] if key is not None else next(iter(arrays.values()))
|
|
2074
|
+
mapped = _h5_contiguous_memmap(filepath, ds)
|
|
2075
|
+
if mapped is not None and mmap_first_frame_is_cheap(mapped, mapped.nbytes):
|
|
2076
|
+
# Session creation upgrades this to an in-RAM array in the background.
|
|
2077
|
+
return mapped
|
|
2078
|
+
return _fix_mat_complex(np.array(ds))
|
|
1769
2079
|
raise ValueError(
|
|
1770
2080
|
f".mat (v7.3) file contains multiple datasets: {list(arrays.keys())}. "
|
|
1771
2081
|
"Select one in the viewer or pass a key."
|
|
@@ -1783,10 +2093,68 @@ def load_data(filepath, key=None, *, progress=None):
|
|
|
1783
2093
|
return data
|
|
1784
2094
|
raise ValueError(
|
|
1785
2095
|
"Unsupported format. Supported: .npy, .npz, .nii/.nii.gz, .zarr, "
|
|
1786
|
-
".pt/.pth, .h5/.hdf5, .tif/.tiff, .mat"
|
|
2096
|
+
".pt/.pth, .h5/.hdf5, .tif/.tiff, .mat, .gipl/.gipl.gz"
|
|
1787
2097
|
)
|
|
1788
2098
|
|
|
1789
2099
|
|
|
2100
|
+
# GIPL (Guys Image Processing Lab) image types -> numpy dtypes. Data is
|
|
2101
|
+
# big-endian; complex types store interleaved real/imaginary components.
|
|
2102
|
+
_GIPL_DTYPES = {
|
|
2103
|
+
1: ">u1", # binary, one value per byte
|
|
2104
|
+
7: ">i1",
|
|
2105
|
+
8: ">u1",
|
|
2106
|
+
15: ">i2",
|
|
2107
|
+
16: ">u2",
|
|
2108
|
+
31: ">u4",
|
|
2109
|
+
32: ">i4",
|
|
2110
|
+
64: ">f4",
|
|
2111
|
+
65: ">f8",
|
|
2112
|
+
144: ">i2", # complex short
|
|
2113
|
+
160: ">i4", # complex int
|
|
2114
|
+
192: ">f4", # complex float
|
|
2115
|
+
193: ">f8", # complex double
|
|
2116
|
+
}
|
|
2117
|
+
_GIPL_COMPLEX = frozenset([144, 160, 192, 193])
|
|
2118
|
+
_GIPL_MAGIC = (4026526128, 719555000)
|
|
2119
|
+
_GIPL_HEADER_SIZE = 256
|
|
2120
|
+
|
|
2121
|
+
|
|
2122
|
+
def _load_gipl(filepath):
|
|
2123
|
+
"""Read a .gipl/.gipl.gz file into an array indexed (x, y, z, t).
|
|
2124
|
+
|
|
2125
|
+
Trailing singleton dimensions are dropped, matching how a NIfTI volume
|
|
2126
|
+
of the same data is shown.
|
|
2127
|
+
"""
|
|
2128
|
+
import gzip
|
|
2129
|
+
|
|
2130
|
+
opener = gzip.open if filepath.lower().endswith(".gz") else open
|
|
2131
|
+
with opener(filepath, "rb") as f:
|
|
2132
|
+
raw = f.read()
|
|
2133
|
+
if len(raw) < _GIPL_HEADER_SIZE:
|
|
2134
|
+
raise ValueError(f"{filepath!r} is too short to be a GIPL file.")
|
|
2135
|
+
header = raw[:_GIPL_HEADER_SIZE]
|
|
2136
|
+
magic = int(np.frombuffer(header, ">u4", count=1, offset=252)[0])
|
|
2137
|
+
if magic not in _GIPL_MAGIC:
|
|
2138
|
+
raise ValueError(f"{filepath!r} is not a GIPL file (bad magic number).")
|
|
2139
|
+
dims = [int(d) for d in np.frombuffer(header, ">u2", count=4, offset=0)]
|
|
2140
|
+
image_type = int(np.frombuffer(header, ">u2", count=1, offset=8)[0])
|
|
2141
|
+
if image_type not in _GIPL_DTYPES:
|
|
2142
|
+
raise ValueError(f"{filepath!r} has unsupported GIPL image type {image_type}.")
|
|
2143
|
+
dims = [max(d, 1) for d in dims]
|
|
2144
|
+
is_complex = image_type in _GIPL_COMPLEX
|
|
2145
|
+
count = int(np.prod(dims)) * (2 if is_complex else 1)
|
|
2146
|
+
data = np.frombuffer(raw, _GIPL_DTYPES[image_type], count=count, offset=_GIPL_HEADER_SIZE)
|
|
2147
|
+
data = data.astype(data.dtype.newbyteorder("="))
|
|
2148
|
+
if is_complex:
|
|
2149
|
+
data = data.astype(np.float64 if data.itemsize == 8 else np.float32)
|
|
2150
|
+
data = data[0::2] + 1j * data[1::2]
|
|
2151
|
+
# x varies fastest on disk.
|
|
2152
|
+
arr = data.reshape(dims[::-1]).transpose(3, 2, 1, 0)
|
|
2153
|
+
while arr.ndim > 2 and arr.shape[-1] == 1:
|
|
2154
|
+
arr = arr[..., 0]
|
|
2155
|
+
return np.ascontiguousarray(arr)
|
|
2156
|
+
|
|
2157
|
+
|
|
1790
2158
|
def _tensor_to_numpy(obj, source="object"):
|
|
1791
2159
|
"""Convert a tensor-like object (PyTorch, etc.) to a numpy array."""
|
|
1792
2160
|
if isinstance(obj, np.ndarray):
|
|
@@ -1823,13 +2191,15 @@ _SUPPORTED_EXTS = frozenset(
|
|
|
1823
2191
|
".tif",
|
|
1824
2192
|
".tiff",
|
|
1825
2193
|
".mat",
|
|
2194
|
+
".gipl",
|
|
2195
|
+
".gipl.gz",
|
|
1826
2196
|
".dcm",
|
|
1827
2197
|
]
|
|
1828
2198
|
)
|
|
1829
2199
|
|
|
1830
2200
|
# Formats that load the entire array into RAM (no mmap/lazy access).
|
|
1831
2201
|
# RAM guard only applies to these.
|
|
1832
|
-
FULL_LOAD_EXTS = frozenset([".pt", ".pth", ".tif", ".tiff", ".mat"])
|
|
2202
|
+
FULL_LOAD_EXTS = frozenset([".pt", ".pth", ".tif", ".tiff", ".mat", ".gipl", ".gipl.gz"])
|
|
1833
2203
|
|
|
1834
2204
|
|
|
1835
2205
|
def _peek_file_shape(fpath: str, ext: str):
|
|
@@ -4853,6 +4853,7 @@ def _serve_daemon(
|
|
|
4853
4853
|
key=_load_key,
|
|
4854
4854
|
load=collection_load,
|
|
4855
4855
|
stack=collection_stack,
|
|
4856
|
+
fast_first_frame=True,
|
|
4856
4857
|
)
|
|
4857
4858
|
dir_overlay_items = None
|
|
4858
4859
|
collection_spatial_ndim = None
|
|
@@ -6211,17 +6212,18 @@ def arrayview():
|
|
|
6211
6212
|
pattern for _name, pattern in _normalize_dir_overlay_specs(args.overlay or [])
|
|
6212
6213
|
)
|
|
6213
6214
|
collection_inputs.extend(args.overlay_dir or [])
|
|
6214
|
-
|
|
6215
|
-
pattern
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6215
|
+
if os.environ.get("ARRAYVIEW_SKIP_SOURCE_STAGING", "1") == "0":
|
|
6216
|
+
for pattern in collection_inputs:
|
|
6217
|
+
pattern = lexical_abspath(pattern)
|
|
6218
|
+
probe_prefix = scan_root_before_magic(pattern)
|
|
6219
|
+
mount = direct_network_mount(probe_prefix)
|
|
6220
|
+
nested_mount = network_mount_below(probe_prefix)
|
|
6221
|
+
if mount is not None or nested_mount is not None:
|
|
6222
|
+
_print_failure(
|
|
6223
|
+
"network directory collections are not accessed directly. "
|
|
6224
|
+
"Copy the collection to local storage before using --match."
|
|
6225
|
+
)
|
|
6226
|
+
sys.exit(1)
|
|
6225
6227
|
if len(dir_patterns) == 1 and os.path.isdir(dir_patterns[0]):
|
|
6226
6228
|
dir_patterns = [os.path.join(dir_patterns[0], "**", "*")]
|
|
6227
6229
|
dir_overlay_specs = [
|
|
@@ -207,7 +207,17 @@ def extract_slice(session, dim_x, dim_y, idx_list, progress=None):
|
|
|
207
207
|
# already served from cache.
|
|
208
208
|
if progress is None:
|
|
209
209
|
progress = getattr(session, 'progress_sink', None)
|
|
210
|
-
|
|
210
|
+
data = session.data
|
|
211
|
+
if isinstance(data, np.memmap) and data.filename:
|
|
212
|
+
# A slice scattered one element per page reads far faster as many
|
|
213
|
+
# parallel small reads than by faulting through the map (local disks).
|
|
214
|
+
from ._io import prefetch_slice_pages
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
prefetch_slice_pages(data, data.filename, (dim_x, dim_y), idx_list)
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
extracted = _extract_with_progress(data, slicer, progress)
|
|
211
221
|
if dim_x < dim_y:
|
|
212
222
|
extracted = extracted.T
|
|
213
223
|
# scipy.io.loadmat returns complex arrays as structured dtypes; convert here.
|