arrayview 0.46.0__py3-none-any.whl → 0.46.2__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.
- arrayview/_analysis.py +35 -0
- arrayview/_extra_ports.py +1 -1
- arrayview/_io.py +40 -5
- arrayview/_launcher.py +171 -8
- arrayview/_overlays.py +28 -2
- arrayview/_render.py +31 -17
- arrayview/_routes_analysis.py +21 -0
- arrayview/_routes_loading.py +41 -14
- arrayview/_routes_query.py +74 -1
- arrayview/_session.py +79 -0
- arrayview/_tutorial.py +381 -50
- arrayview/_viewer.html +2468 -1564
- arrayview/_vscode_extension.py +1 -1
- arrayview/arrayview-opener.vsix +0 -0
- arrayview/todo.md +12 -12
- {arrayview-0.46.0.dist-info → arrayview-0.46.2.dist-info}/METADATA +1 -14
- {arrayview-0.46.0.dist-info → arrayview-0.46.2.dist-info}/RECORD +20 -20
- {arrayview-0.46.0.dist-info → arrayview-0.46.2.dist-info}/WHEEL +1 -1
- {arrayview-0.46.0.dist-info → arrayview-0.46.2.dist-info}/entry_points.txt +0 -0
- {arrayview-0.46.0.dist-info → arrayview-0.46.2.dist-info}/licenses/LICENSE +0 -0
arrayview/_analysis.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import itertools
|
|
6
|
+
import os
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
|
|
@@ -18,6 +19,29 @@ def _visible_shape(session) -> list[int]:
|
|
|
18
19
|
return [int(s) for s in shape]
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def _code_source(session) -> dict | None:
|
|
23
|
+
"""Say how a copied line of code should name this array.
|
|
24
|
+
|
|
25
|
+
Arrays passed from Python are named by the caller's variable. Arrays
|
|
26
|
+
opened from a file are reopened with the command line and that file.
|
|
27
|
+
"""
|
|
28
|
+
code_source = getattr(session, "code_source", None)
|
|
29
|
+
if code_source:
|
|
30
|
+
return code_source
|
|
31
|
+
filepath = getattr(session, "filepath", None)
|
|
32
|
+
if not filepath:
|
|
33
|
+
return None
|
|
34
|
+
import shlex
|
|
35
|
+
|
|
36
|
+
path = os.path.abspath(filepath)
|
|
37
|
+
home = os.path.expanduser("~")
|
|
38
|
+
if path.startswith(home + os.sep) and shlex.quote(path) == path:
|
|
39
|
+
path = "~" + path[len(home):]
|
|
40
|
+
else:
|
|
41
|
+
path = shlex.quote(path)
|
|
42
|
+
return {"kind": "file", "path": path}
|
|
43
|
+
|
|
44
|
+
|
|
21
45
|
def _build_metadata(session) -> dict:
|
|
22
46
|
"""Build metadata shared by HTTP routes and WebSocket startup."""
|
|
23
47
|
target_shape = session.spatial_shape if session.rgb_axis is not None else session.shape
|
|
@@ -34,6 +58,11 @@ def _build_metadata(session) -> dict:
|
|
|
34
58
|
"is_rgb": session.rgb_axis is not None,
|
|
35
59
|
"has_source_file": bool(getattr(session, "filepath", None)),
|
|
36
60
|
}
|
|
61
|
+
if getattr(session, "initial_view", None):
|
|
62
|
+
meta["initial_view"] = session.initial_view
|
|
63
|
+
code_source = _code_source(session)
|
|
64
|
+
if code_source is not None:
|
|
65
|
+
meta["code_source"] = code_source
|
|
37
66
|
collection_spatial_ndim = getattr(session, "collection_spatial_ndim", None)
|
|
38
67
|
default_shape = target_shape
|
|
39
68
|
if collection_spatial_ndim is not None:
|
|
@@ -63,6 +92,9 @@ def _build_metadata(session) -> dict:
|
|
|
63
92
|
)
|
|
64
93
|
if getattr(session, "array_keys", None):
|
|
65
94
|
meta["array_keys"] = session.array_keys
|
|
95
|
+
meta["array_key"] = getattr(session, "array_key", None)
|
|
96
|
+
meta["array_prompt"] = bool(getattr(session, "array_prompt", False))
|
|
97
|
+
meta["array_filename"] = os.path.basename(getattr(session, "array_filepath", "") or "")
|
|
66
98
|
if collection_spatial_ndim is not None:
|
|
67
99
|
meta["collection_spatial_ndim"] = int(collection_spatial_ndim)
|
|
68
100
|
if getattr(session.data, "_av_ragged", False):
|
|
@@ -70,6 +102,9 @@ def _build_metadata(session) -> dict:
|
|
|
70
102
|
[list(shape) for shape in row]
|
|
71
103
|
for row in session.data.ragged_spatial_shapes
|
|
72
104
|
]
|
|
105
|
+
case_names = getattr(session.data, "case_names", None)
|
|
106
|
+
if case_names:
|
|
107
|
+
meta["case_names"] = [str(name) for name in case_names]
|
|
73
108
|
case_voxel_spacings = getattr(session.data, "case_voxel_spacings", None)
|
|
74
109
|
if case_voxel_spacings:
|
|
75
110
|
meta["case_voxel_spacings"] = [
|
arrayview/_extra_ports.py
CHANGED
|
@@ -91,7 +91,7 @@ def _start_extra_port() -> tuple[int, dict] | None:
|
|
|
91
91
|
timeout_keep_alive=30,
|
|
92
92
|
ws_ping_interval=None,
|
|
93
93
|
ws="websockets",
|
|
94
|
-
ws_per_message_deflate=
|
|
94
|
+
ws_per_message_deflate=False,
|
|
95
95
|
)
|
|
96
96
|
server = uvicorn.Server(config)
|
|
97
97
|
task = asyncio.create_task(server.serve(sockets=[sock]))
|
arrayview/_io.py
CHANGED
|
@@ -67,6 +67,38 @@ def _is_viewable_mat_array(value):
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def _is_viewable_h5_dataset(ds):
|
|
71
|
+
"""True for HDF5 datasets in a v7.3 .mat file ArrayView can display.
|
|
72
|
+
|
|
73
|
+
MATLAB v7.3 stores complex arrays as a compound dtype with 'real' and
|
|
74
|
+
'imag' fields, which has dtype kind 'V' rather than 'c'.
|
|
75
|
+
"""
|
|
76
|
+
import h5py
|
|
77
|
+
|
|
78
|
+
if not isinstance(ds, h5py.Dataset) or len(ds.shape) < 1:
|
|
79
|
+
return False
|
|
80
|
+
names = ds.dtype.names
|
|
81
|
+
if names:
|
|
82
|
+
return "real" in names and "imag" in names
|
|
83
|
+
return ds.dtype.kind in ("b", "i", "u", "f", "c")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _h5_display_dtype(ds):
|
|
87
|
+
"""Dtype string to show for a v7.3 .mat dataset (compound complex → complex)."""
|
|
88
|
+
if ds.dtype.names:
|
|
89
|
+
return "complex128" if ds.dtype["real"].itemsize == 8 else "complex64"
|
|
90
|
+
return str(ds.dtype)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def array_choice_placeholder():
|
|
94
|
+
"""Stand-in data for a multi-array file whose array is not chosen yet.
|
|
95
|
+
|
|
96
|
+
The session exists so the viewer can open and ask which array to load;
|
|
97
|
+
nothing from the file is read until the user picks one.
|
|
98
|
+
"""
|
|
99
|
+
return np.zeros((2, 2), dtype=np.float32)
|
|
100
|
+
|
|
101
|
+
|
|
70
102
|
def list_npz_keys(filepath):
|
|
71
103
|
"""Return [{key, shape, dtype}] for each ndarray in an .npz file.
|
|
72
104
|
|
|
@@ -106,8 +138,8 @@ def list_mat_keys(filepath):
|
|
|
106
138
|
keys = []
|
|
107
139
|
for k in f.keys():
|
|
108
140
|
ds = f[k]
|
|
109
|
-
if
|
|
110
|
-
keys.append({"key": k, "shape": list(ds.shape), "dtype":
|
|
141
|
+
if _is_viewable_h5_dataset(ds):
|
|
142
|
+
keys.append({"key": k, "shape": list(ds.shape), "dtype": _h5_display_dtype(ds)})
|
|
111
143
|
f.close()
|
|
112
144
|
return keys
|
|
113
145
|
|
|
@@ -1249,6 +1281,9 @@ def load_dir_collection(
|
|
|
1249
1281
|
scan_progress=scan_progress,
|
|
1250
1282
|
scan_label="images",
|
|
1251
1283
|
)
|
|
1284
|
+
# Lets the viewer label each case by its id (e.g. the patient folder)
|
|
1285
|
+
# instead of its position in the stack.
|
|
1286
|
+
data.case_names = [str(case) for case in case_ids]
|
|
1252
1287
|
spatial_shape = data._vol_shape
|
|
1253
1288
|
|
|
1254
1289
|
overlay_items = []
|
|
@@ -1726,11 +1761,11 @@ def load_data(filepath, key=None, *, progress=None):
|
|
|
1726
1761
|
import h5py
|
|
1727
1762
|
|
|
1728
1763
|
f = h5py.File(filepath, "r")
|
|
1729
|
-
arrays = {k: f[k] for k in f.keys() if
|
|
1764
|
+
arrays = {k: f[k] for k in f.keys() if _is_viewable_h5_dataset(f[k])}
|
|
1730
1765
|
if key is not None:
|
|
1731
|
-
return np.array(arrays[key])
|
|
1766
|
+
return _fix_mat_complex(np.array(arrays[key]))
|
|
1732
1767
|
if len(arrays) == 1:
|
|
1733
|
-
return np.array(next(iter(arrays.values())))
|
|
1768
|
+
return _fix_mat_complex(np.array(next(iter(arrays.values()))))
|
|
1734
1769
|
raise ValueError(
|
|
1735
1770
|
f".mat (v7.3) file contains multiple datasets: {list(arrays.keys())}. "
|
|
1736
1771
|
"Select one in the viewer or pass a key."
|
arrayview/_launcher.py
CHANGED
|
@@ -1312,6 +1312,8 @@ def _load_session_from_filepath(
|
|
|
1312
1312
|
related_sids: list[str] | None = None,
|
|
1313
1313
|
source_staging_dir: str | None = None,
|
|
1314
1314
|
watch: bool = False,
|
|
1315
|
+
initial_view: dict | None = None,
|
|
1316
|
+
code_source: dict | None = None,
|
|
1315
1317
|
) -> dict:
|
|
1316
1318
|
requested_sid = uuid.uuid4().hex
|
|
1317
1319
|
payload = {
|
|
@@ -1321,11 +1323,18 @@ def _load_session_from_filepath(
|
|
|
1321
1323
|
"requested_sid": requested_sid,
|
|
1322
1324
|
"release_on_disconnect": release_on_disconnect,
|
|
1323
1325
|
"related_sids": related_sids or [],
|
|
1326
|
+
# Multi-array .npz/.mat: open the first array instead of returning
|
|
1327
|
+
# the key list; the viewer then offers the choice.
|
|
1328
|
+
"array_default": True,
|
|
1324
1329
|
}
|
|
1325
1330
|
if source_staging_dir:
|
|
1326
1331
|
payload["source_staging_dir"] = source_staging_dir
|
|
1327
1332
|
if watch:
|
|
1328
1333
|
payload["watch"] = True
|
|
1334
|
+
if initial_view:
|
|
1335
|
+
payload["initial_view"] = initial_view
|
|
1336
|
+
if code_source:
|
|
1337
|
+
payload["code_source"] = code_source
|
|
1329
1338
|
if rgb:
|
|
1330
1339
|
payload["rgb"] = True
|
|
1331
1340
|
if dir_patterns:
|
|
@@ -1662,6 +1671,7 @@ def _register_cli_session_with_existing_server_impl(
|
|
|
1662
1671
|
native_request_id: str | None = None,
|
|
1663
1672
|
source_staging_dirs: dict[str, str] | None = None,
|
|
1664
1673
|
watch: bool = False,
|
|
1674
|
+
initial_view: dict | None = None,
|
|
1665
1675
|
_loaded_sids: list[str],
|
|
1666
1676
|
) -> dict[str, object]:
|
|
1667
1677
|
overlay_sids_list: list[str] = []
|
|
@@ -1716,6 +1726,7 @@ def _register_cli_session_with_existing_server_impl(
|
|
|
1716
1726
|
related_sids=[*compare_sids, *overlay_sids_list],
|
|
1717
1727
|
source_staging_dir=(source_staging_dirs or {}).get(base_file),
|
|
1718
1728
|
watch=watch,
|
|
1729
|
+
initial_view=initial_view,
|
|
1719
1730
|
)
|
|
1720
1731
|
if "error" in result:
|
|
1721
1732
|
error = str(result["error"])
|
|
@@ -1794,6 +1805,7 @@ def _handle_cli_existing_server(
|
|
|
1794
1805
|
use_native_shell: bool,
|
|
1795
1806
|
dims_override: tuple[int, int] | None,
|
|
1796
1807
|
watch: bool,
|
|
1808
|
+
initial_view: dict | None = None,
|
|
1797
1809
|
window_mode: str | None,
|
|
1798
1810
|
floating: bool,
|
|
1799
1811
|
is_remote: bool = False,
|
|
@@ -1844,6 +1856,7 @@ def _handle_cli_existing_server(
|
|
|
1844
1856
|
native_request_id=native_request_id,
|
|
1845
1857
|
source_staging_dirs=source_staging_dirs,
|
|
1846
1858
|
watch=watch,
|
|
1859
|
+
initial_view=initial_view,
|
|
1847
1860
|
)
|
|
1848
1861
|
# Inside this try on purpose: the generated bundle is removed in the
|
|
1849
1862
|
# finally below, and these sessions read from it.
|
|
@@ -1899,6 +1912,7 @@ def _handle_cli_existing_server(
|
|
|
1899
1912
|
dims_override=dims_override,
|
|
1900
1913
|
use_native_shell=use_native_shell,
|
|
1901
1914
|
watch=watch,
|
|
1915
|
+
initial_view=initial_view,
|
|
1902
1916
|
window_mode=window_mode,
|
|
1903
1917
|
floating=floating,
|
|
1904
1918
|
is_remote=is_remote,
|
|
@@ -2033,6 +2047,7 @@ def _handle_cli_spawned_daemon(
|
|
|
2033
2047
|
dims_override: tuple[int, int] | None,
|
|
2034
2048
|
use_native_shell: bool,
|
|
2035
2049
|
watch: bool,
|
|
2050
|
+
initial_view: dict | None = None,
|
|
2036
2051
|
window_mode: str | None,
|
|
2037
2052
|
floating: bool,
|
|
2038
2053
|
is_remote: bool,
|
|
@@ -2095,6 +2110,7 @@ def _handle_cli_spawned_daemon(
|
|
|
2095
2110
|
use_native_shell=use_native_shell,
|
|
2096
2111
|
dims_override=dims_override,
|
|
2097
2112
|
watch=watch,
|
|
2113
|
+
initial_view=initial_view,
|
|
2098
2114
|
window_mode=window_mode,
|
|
2099
2115
|
floating=floating,
|
|
2100
2116
|
is_remote=is_remote,
|
|
@@ -2167,6 +2183,7 @@ def _handle_cli_spawned_daemon(
|
|
|
2167
2183
|
f" cleanup_dir={repr(cleanup_dir)},"
|
|
2168
2184
|
f" source_staging_dirs={repr(source_staging_dirs or {})},"
|
|
2169
2185
|
f" watch={watch},"
|
|
2186
|
+
f" initial_view={repr(initial_view)},"
|
|
2170
2187
|
f")"
|
|
2171
2188
|
)
|
|
2172
2189
|
|
|
@@ -2776,8 +2793,10 @@ async def _serve_background(
|
|
|
2776
2793
|
log_level="error",
|
|
2777
2794
|
timeout_keep_alive=30,
|
|
2778
2795
|
ws_ping_interval=None,
|
|
2779
|
-
ws="websockets",
|
|
2780
|
-
|
|
2796
|
+
ws="websockets",
|
|
2797
|
+
# Deflate costs more than rendering a large slice and stalls playback.
|
|
2798
|
+
# A 256×256 frame measured 18 fps with it and 54 fps without it.
|
|
2799
|
+
ws_per_message_deflate=False,
|
|
2781
2800
|
)
|
|
2782
2801
|
server = _uvicorn().Server(config)
|
|
2783
2802
|
if stop_when_closed:
|
|
@@ -3473,6 +3492,30 @@ class ViewHandle(str):
|
|
|
3473
3492
|
return False
|
|
3474
3493
|
|
|
3475
3494
|
|
|
3495
|
+
def _caller_variable_name(obj) -> str | None:
|
|
3496
|
+
"""Name of the caller's variable that holds ``obj``, if there is one.
|
|
3497
|
+
|
|
3498
|
+
Used only to write a copyable ``view(<name>, ...)`` line, so a miss is
|
|
3499
|
+
harmless: expressions such as ``view(a[0])`` simply have no name.
|
|
3500
|
+
"""
|
|
3501
|
+
import inspect
|
|
3502
|
+
|
|
3503
|
+
frame = inspect.currentframe()
|
|
3504
|
+
try:
|
|
3505
|
+
caller = frame.f_back.f_back if frame and frame.f_back else None
|
|
3506
|
+
if caller is None:
|
|
3507
|
+
return None
|
|
3508
|
+
for scope in (caller.f_locals, caller.f_globals):
|
|
3509
|
+
for key, value in list(scope.items()):
|
|
3510
|
+
if value is obj and key.isidentifier() and not key.startswith("_"):
|
|
3511
|
+
return key
|
|
3512
|
+
except Exception:
|
|
3513
|
+
return None
|
|
3514
|
+
finally:
|
|
3515
|
+
del frame
|
|
3516
|
+
return None
|
|
3517
|
+
|
|
3518
|
+
|
|
3476
3519
|
def view(
|
|
3477
3520
|
*arrays,
|
|
3478
3521
|
name=None,
|
|
@@ -3484,6 +3527,14 @@ def view(
|
|
|
3484
3527
|
rgb: bool | list = False,
|
|
3485
3528
|
overlay=None,
|
|
3486
3529
|
floating: bool = False,
|
|
3530
|
+
mode: str | None = None,
|
|
3531
|
+
dims=None,
|
|
3532
|
+
index=None,
|
|
3533
|
+
cmap: str | None = None,
|
|
3534
|
+
vmin: float | None = None,
|
|
3535
|
+
vmax: float | None = None,
|
|
3536
|
+
log: bool = False,
|
|
3537
|
+
complex: str | None = None,
|
|
3487
3538
|
):
|
|
3488
3539
|
"""
|
|
3489
3540
|
Launch the viewer. Does not block the main Python process.
|
|
@@ -3523,6 +3574,16 @@ def view(
|
|
|
3523
3574
|
``overlay`` — a single array or list of arrays to composite as overlays.
|
|
3524
3575
|
Each overlay is assigned an auto-palette color from _OVERLAY_PALETTE.
|
|
3525
3576
|
|
|
3577
|
+
``mode``, ``dims``, ``index``, ``cmap``, ``vmin``/``vmax`` and ``log`` set
|
|
3578
|
+
the view the viewer opens with: the viewing mode (``"ortho"``,
|
|
3579
|
+
``"mosaic"``, ``"qmri"``, ``"qmri-ortho"``, ``"qmri-mosaic"``), the two
|
|
3580
|
+
dimensions shown as x and y (three for ortho: its volume dims; three for
|
|
3581
|
+
the mosaics: x, y and the gridded dim), the position along every
|
|
3582
|
+
dimension, the colormap, the display range and log scale. ``complex``
|
|
3583
|
+
picks what complex data shows: ``"mag"``, ``"phase"``, ``"real"`` or
|
|
3584
|
+
``"imag"`` (``"mag"`` shows the absolute value of real data). Pressing
|
|
3585
|
+
``E`` in the viewer copies the current view as such a call.
|
|
3586
|
+
|
|
3526
3587
|
Returns a ``ViewHandle`` for a single array, or a tuple of ``ViewHandle``
|
|
3527
3588
|
objects for multiple arrays (one per array). In inline/Jupyter mode with
|
|
3528
3589
|
multiple arrays, the IFrame is displayed automatically and a uniform tuple
|
|
@@ -3534,6 +3595,12 @@ def view(
|
|
|
3534
3595
|
import numpy as np
|
|
3535
3596
|
from arrayview._io import _tensor_to_numpy
|
|
3536
3597
|
|
|
3598
|
+
_initial_view = _session_mod.initial_view_spec(
|
|
3599
|
+
mode=mode, dims=dims, index=index, cmap=cmap, vmin=vmin, vmax=vmax, log=log,
|
|
3600
|
+
complex=complex,
|
|
3601
|
+
)
|
|
3602
|
+
_code_name = _caller_variable_name(arrays[0]) if arrays else None
|
|
3603
|
+
|
|
3537
3604
|
height = _normalize_inline_height(height)
|
|
3538
3605
|
_inline_mode_heights = _normalize_inline_mode_heights(mode_heights)
|
|
3539
3606
|
|
|
@@ -3602,6 +3669,13 @@ def view(
|
|
|
3602
3669
|
data = arrays[0]
|
|
3603
3670
|
name = names[0]
|
|
3604
3671
|
rgb_primary = rgbs[0]
|
|
3672
|
+
if _initial_view and "index" in _initial_view:
|
|
3673
|
+
_ndim = len(getattr(data, "shape", ()))
|
|
3674
|
+
if len(_initial_view["index"]) != _ndim:
|
|
3675
|
+
raise ValueError(
|
|
3676
|
+
f"index has {len(_initial_view['index'])} entries but the array "
|
|
3677
|
+
f"has {_ndim} dimensions"
|
|
3678
|
+
)
|
|
3605
3679
|
|
|
3606
3680
|
# --- Normalise string window modes ---
|
|
3607
3681
|
_raw_window = window
|
|
@@ -3631,6 +3705,18 @@ def view(
|
|
|
3631
3705
|
else:
|
|
3632
3706
|
_invocation = Invocation.PYTHON
|
|
3633
3707
|
|
|
3708
|
+
# Julia and MATLAB callers are not Python frames, so only their explicit
|
|
3709
|
+
# name can stand in for the variable.
|
|
3710
|
+
if _code_name is None or _invocation in (Invocation.JULIA, Invocation.MATLAB):
|
|
3711
|
+
_code_name = name if str(name).isidentifier() else "x"
|
|
3712
|
+
_code_source = {
|
|
3713
|
+
"kind": {
|
|
3714
|
+
Invocation.JULIA: "julia",
|
|
3715
|
+
Invocation.MATLAB: "matlab",
|
|
3716
|
+
}.get(_invocation, "python"),
|
|
3717
|
+
"name": _code_name,
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3634
3720
|
if isinstance(_raw_window, str):
|
|
3635
3721
|
_requested_window = _raw_window
|
|
3636
3722
|
elif _raw_window is True:
|
|
@@ -3701,6 +3787,8 @@ def view(
|
|
|
3701
3787
|
mode_heights=_inline_mode_heights,
|
|
3702
3788
|
floating=floating,
|
|
3703
3789
|
launch_context=_launch_context,
|
|
3790
|
+
initial_view=_initial_view,
|
|
3791
|
+
code_source=_code_source,
|
|
3704
3792
|
)
|
|
3705
3793
|
|
|
3706
3794
|
# VS Code tunnel/remote: use the server + WebSocket path. The opener owns
|
|
@@ -3724,6 +3812,7 @@ def view(
|
|
|
3724
3812
|
_tmp_path = _tmp.name
|
|
3725
3813
|
try:
|
|
3726
3814
|
np.save(_tmp_path, _array)
|
|
3815
|
+
_is_primary = not _loaded_sids
|
|
3727
3816
|
_result = _load_session_from_filepath(
|
|
3728
3817
|
port,
|
|
3729
3818
|
_tmp_path,
|
|
@@ -3731,6 +3820,8 @@ def view(
|
|
|
3731
3820
|
rgb=_rgb,
|
|
3732
3821
|
expected_server_id=_expected_server_id,
|
|
3733
3822
|
release_on_disconnect=inline,
|
|
3823
|
+
initial_view=_initial_view if _is_primary else None,
|
|
3824
|
+
code_source=_code_source if _is_primary else None,
|
|
3734
3825
|
)
|
|
3735
3826
|
finally:
|
|
3736
3827
|
try:
|
|
@@ -3827,6 +3918,8 @@ def view(
|
|
|
3827
3918
|
session = _session_mod.Session(data, name=name)
|
|
3828
3919
|
if rgb_primary:
|
|
3829
3920
|
_setup_rgb(session)
|
|
3921
|
+
session.initial_view = _initial_view
|
|
3922
|
+
session.code_source = _code_source
|
|
3830
3923
|
_session_mod.SESSIONS[session.sid] = session
|
|
3831
3924
|
|
|
3832
3925
|
# Register compare sessions (arrays[1:])
|
|
@@ -4257,6 +4350,8 @@ def _view_julia(
|
|
|
4257
4350
|
mode_heights: dict[str, int] | None = None,
|
|
4258
4351
|
floating: bool = False,
|
|
4259
4352
|
launch_context=None,
|
|
4353
|
+
initial_view: dict | None = None,
|
|
4354
|
+
code_source: dict | None = None,
|
|
4260
4355
|
):
|
|
4261
4356
|
"""Julia-specific view() path: run the server in a subprocess so it is
|
|
4262
4357
|
completely independent of Julia's GIL.
|
|
@@ -4278,6 +4373,8 @@ def _view_julia(
|
|
|
4278
4373
|
force_vscode=force_vscode,
|
|
4279
4374
|
floating=floating,
|
|
4280
4375
|
launch_context=launch_context,
|
|
4376
|
+
initial_view=initial_view,
|
|
4377
|
+
code_source=code_source,
|
|
4281
4378
|
)
|
|
4282
4379
|
|
|
4283
4380
|
|
|
@@ -4293,6 +4390,8 @@ def _view_subprocess(
|
|
|
4293
4390
|
force_vscode: bool = False,
|
|
4294
4391
|
floating: bool = False,
|
|
4295
4392
|
launch_context=None,
|
|
4393
|
+
initial_view: dict | None = None,
|
|
4394
|
+
code_source: dict | None = None,
|
|
4296
4395
|
) -> str:
|
|
4297
4396
|
"""Run the viewer in a separate subprocess server.
|
|
4298
4397
|
|
|
@@ -4348,6 +4447,8 @@ def _view_subprocess(
|
|
|
4348
4447
|
expected_server_id=active_server_id,
|
|
4349
4448
|
native_request_id=native_request_id,
|
|
4350
4449
|
release_on_disconnect=True,
|
|
4450
|
+
initial_view=initial_view,
|
|
4451
|
+
code_source=code_source,
|
|
4351
4452
|
)
|
|
4352
4453
|
if "error" in result:
|
|
4353
4454
|
raise RuntimeError(result["error"])
|
|
@@ -4412,7 +4513,9 @@ def _view_subprocess(
|
|
|
4412
4513
|
f"_serve_daemon({repr(tmp_path)}, {port}, {repr(sid)}, "
|
|
4413
4514
|
f"name={repr(name)}, cleanup=True, "
|
|
4414
4515
|
f"persist={persist_daemon}, "
|
|
4415
|
-
f"connect_timeout={repr(daemon_connect_timeout)}, rgb={rgb}
|
|
4516
|
+
f"connect_timeout={repr(daemon_connect_timeout)}, rgb={rgb}, "
|
|
4517
|
+
f"initial_view={repr(initial_view)}, "
|
|
4518
|
+
f"code_source={repr(code_source)})"
|
|
4416
4519
|
)
|
|
4417
4520
|
daemon_proc = subprocess.Popen(
|
|
4418
4521
|
[sys.executable, "-c", script],
|
|
@@ -4547,6 +4650,8 @@ def _serve_empty(port: int) -> None:
|
|
|
4547
4650
|
_server_mod().app,
|
|
4548
4651
|
log_level="error",
|
|
4549
4652
|
timeout_keep_alive=30,
|
|
4653
|
+
ws="websockets",
|
|
4654
|
+
ws_per_message_deflate=False,
|
|
4550
4655
|
)
|
|
4551
4656
|
server = _uvicorn().Server(config)
|
|
4552
4657
|
asyncio.run(server.serve(sockets=socks))
|
|
@@ -4586,6 +4691,8 @@ def _serve_daemon(
|
|
|
4586
4691
|
cleanup_dir: str | None = None,
|
|
4587
4692
|
source_staging_dirs: dict[str, str] | None = None,
|
|
4588
4693
|
watch: bool = False,
|
|
4694
|
+
initial_view: dict | None = None,
|
|
4695
|
+
code_source: dict | None = None,
|
|
4589
4696
|
) -> None:
|
|
4590
4697
|
"""Background server process. Loads data, serves it.
|
|
4591
4698
|
persist=True: never exits (used on remote tunnel so port stays alive).
|
|
@@ -4639,6 +4746,8 @@ def _serve_daemon(
|
|
|
4639
4746
|
bootstrap,
|
|
4640
4747
|
log_level="error",
|
|
4641
4748
|
timeout_keep_alive=30,
|
|
4749
|
+
ws="websockets",
|
|
4750
|
+
ws_per_message_deflate=False,
|
|
4642
4751
|
)
|
|
4643
4752
|
server = _uvicorn().Server(config)
|
|
4644
4753
|
uvicorn_server.append(server)
|
|
@@ -4691,6 +4800,7 @@ def _serve_daemon(
|
|
|
4691
4800
|
|
|
4692
4801
|
def _load():
|
|
4693
4802
|
from arrayview._io import (
|
|
4803
|
+
array_choice_placeholder,
|
|
4694
4804
|
load_data,
|
|
4695
4805
|
load_data_with_meta,
|
|
4696
4806
|
load_dir_collection,
|
|
@@ -4705,8 +4815,8 @@ def _serve_daemon(
|
|
|
4705
4815
|
if dir_patterns is None and not cleanup
|
|
4706
4816
|
else None
|
|
4707
4817
|
)
|
|
4708
|
-
# Multi-array .npz/.mat:
|
|
4709
|
-
#
|
|
4818
|
+
# Multi-array .npz/.mat: create the session with a placeholder and
|
|
4819
|
+
# store the keys; the viewer asks which array to load.
|
|
4710
4820
|
_array_keys = None
|
|
4711
4821
|
if dir_patterns is not None:
|
|
4712
4822
|
data, spatial_meta, dir_overlay_items, _summary = load_dir_collection(
|
|
@@ -4723,8 +4833,12 @@ def _serve_daemon(
|
|
|
4723
4833
|
_array_keys = list_array_keys(filepath)
|
|
4724
4834
|
except Exception:
|
|
4725
4835
|
pass
|
|
4726
|
-
|
|
4727
|
-
|
|
4836
|
+
if _array_keys and len(_array_keys) > 1:
|
|
4837
|
+
_load_key = None
|
|
4838
|
+
data, spatial_meta = array_choice_placeholder(), None
|
|
4839
|
+
else:
|
|
4840
|
+
_load_key = _array_keys[0]["key"] if _array_keys else None
|
|
4841
|
+
data, spatial_meta = load_data_with_meta(filepath, key=_load_key)
|
|
4728
4842
|
dir_overlay_items = None
|
|
4729
4843
|
collection_spatial_ndim = None
|
|
4730
4844
|
else:
|
|
@@ -4748,6 +4862,10 @@ def _serve_daemon(
|
|
|
4748
4862
|
if filepath in source_staging_dirs:
|
|
4749
4863
|
session._source_staging_dirs = [source_staging_dirs[filepath]]
|
|
4750
4864
|
session.sid = sid
|
|
4865
|
+
_session_mod.apply_launch_view(
|
|
4866
|
+
session,
|
|
4867
|
+
{"initial_view": initial_view, "code_source": code_source},
|
|
4868
|
+
)
|
|
4751
4869
|
if signature_before_load is not None:
|
|
4752
4870
|
signature_after_load = file_signature(filepath)
|
|
4753
4871
|
if signature_before_load == signature_after_load:
|
|
@@ -4758,6 +4876,9 @@ def _serve_daemon(
|
|
|
4758
4876
|
if _array_keys and len(_array_keys) > 1:
|
|
4759
4877
|
session.array_keys = _array_keys
|
|
4760
4878
|
session.array_filepath = filepath
|
|
4879
|
+
session.array_key = _load_key
|
|
4880
|
+
# Nothing is loaded yet; the viewer asks once which array to show.
|
|
4881
|
+
session.array_prompt = True
|
|
4761
4882
|
if spatial_meta is not None:
|
|
4762
4883
|
session.original_volume = data
|
|
4763
4884
|
if rgb:
|
|
@@ -5656,6 +5777,28 @@ def arrayview():
|
|
|
5656
5777
|
"Also accepts 0-based integer pair like '2,3'."
|
|
5657
5778
|
),
|
|
5658
5779
|
)
|
|
5780
|
+
parser.add_argument(
|
|
5781
|
+
"--index",
|
|
5782
|
+
metavar="I,J,...",
|
|
5783
|
+
default=None,
|
|
5784
|
+
help="Open at this position: one 0-based index per dimension, e.g. '128,128,40'.",
|
|
5785
|
+
)
|
|
5786
|
+
parser.add_argument(
|
|
5787
|
+
"--mode",
|
|
5788
|
+
default=None,
|
|
5789
|
+
choices=["ortho", "mosaic", "qmri", "qmri-ortho", "qmri-mosaic"],
|
|
5790
|
+
help="Open in this viewing mode. With ortho or a mosaic, --dims takes three dims.",
|
|
5791
|
+
)
|
|
5792
|
+
parser.add_argument("--cmap", default=None, help="Open with this colormap, e.g. 'gray'.")
|
|
5793
|
+
parser.add_argument("--vmin", type=float, default=None, help="Lower end of the display range (with --vmax).")
|
|
5794
|
+
parser.add_argument("--vmax", type=float, default=None, help="Upper end of the display range (with --vmin).")
|
|
5795
|
+
parser.add_argument("--log", action="store_true", help="Open with log scale on.")
|
|
5796
|
+
parser.add_argument(
|
|
5797
|
+
"--complex",
|
|
5798
|
+
default=None,
|
|
5799
|
+
choices=["mag", "phase", "real", "imag"],
|
|
5800
|
+
help="Which part of complex data to show (mag shows |x| of real data).",
|
|
5801
|
+
)
|
|
5659
5802
|
parser.add_argument(
|
|
5660
5803
|
"--name",
|
|
5661
5804
|
default=None,
|
|
@@ -5816,13 +5959,31 @@ def arrayview():
|
|
|
5816
5959
|
return
|
|
5817
5960
|
|
|
5818
5961
|
dims_override: tuple[int, int] | None = None
|
|
5819
|
-
|
|
5962
|
+
view_dims: list[str] | None = None
|
|
5963
|
+
if args.dims and args.mode in ("ortho", "mosaic", "qmri-mosaic"):
|
|
5964
|
+
# Three dims: not an x/y pair, so they only travel with the view.
|
|
5965
|
+
view_dims = args.dims.split(",")
|
|
5966
|
+
elif args.dims:
|
|
5820
5967
|
dims_override = _parse_dims_spec(args.dims)
|
|
5821
5968
|
if dims_override is None:
|
|
5822
5969
|
parser.error(
|
|
5823
5970
|
f"--dims {args.dims!r} is invalid. "
|
|
5824
5971
|
"Use e.g. 'x,y,:,:' or ':,:,x,y' or '0,1'."
|
|
5825
5972
|
)
|
|
5973
|
+
initial_view: dict | None = None
|
|
5974
|
+
try:
|
|
5975
|
+
initial_view = _session_mod.initial_view_spec(
|
|
5976
|
+
mode=args.mode,
|
|
5977
|
+
dims=view_dims or dims_override,
|
|
5978
|
+
index=args.index.split(",") if args.index else None,
|
|
5979
|
+
cmap=args.cmap,
|
|
5980
|
+
vmin=args.vmin,
|
|
5981
|
+
vmax=args.vmax,
|
|
5982
|
+
log=args.log,
|
|
5983
|
+
complex=args.complex,
|
|
5984
|
+
)
|
|
5985
|
+
except ValueError as exc:
|
|
5986
|
+
parser.error(str(exc))
|
|
5826
5987
|
if getattr(args, "stack_flag_removed", False):
|
|
5827
5988
|
parser.error(
|
|
5828
5989
|
"--stack has been renamed to --match, because it never controlled "
|
|
@@ -6447,6 +6608,7 @@ def arrayview():
|
|
|
6447
6608
|
vfield_components_dim=vfield_components_dim,
|
|
6448
6609
|
use_native_shell=use_native_shell,
|
|
6449
6610
|
dims_override=dims_override,
|
|
6611
|
+
initial_view=initial_view,
|
|
6450
6612
|
watch=getattr(args, "watch", False),
|
|
6451
6613
|
window_mode=window_mode,
|
|
6452
6614
|
floating=args.floating,
|
|
@@ -6479,6 +6641,7 @@ def arrayview():
|
|
|
6479
6641
|
overlay_files=[] if args.stack_mode else file_overlay_paths,
|
|
6480
6642
|
overlay_names=[] if args.stack_mode else file_overlay_names,
|
|
6481
6643
|
dims_override=dims_override,
|
|
6644
|
+
initial_view=initial_view,
|
|
6482
6645
|
use_native_shell=use_native_shell,
|
|
6483
6646
|
watch=getattr(args, "watch", False),
|
|
6484
6647
|
window_mode=window_mode,
|
arrayview/_overlays.py
CHANGED
|
@@ -26,6 +26,22 @@ def _parse_hex_color(hex_str: str) -> np.ndarray | None:
|
|
|
26
26
|
return None
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _parse_overlay_color_token(token: str) -> tuple[np.ndarray | None, frozenset[int]]:
|
|
30
|
+
"""Parse one ``overlay_colors`` entry: ``RRGGBB`` or ``RRGGBB~2.5``.
|
|
31
|
+
|
|
32
|
+
The optional ``~`` suffix lists label values of a label-map overlay that
|
|
33
|
+
the viewer has switched off.
|
|
34
|
+
"""
|
|
35
|
+
color_part, _, hidden_part = token.partition("~")
|
|
36
|
+
hidden = set()
|
|
37
|
+
for value in hidden_part.split("."):
|
|
38
|
+
try:
|
|
39
|
+
hidden.add(int(value))
|
|
40
|
+
except ValueError:
|
|
41
|
+
pass
|
|
42
|
+
return _parse_hex_color(color_part), frozenset(hidden)
|
|
43
|
+
|
|
44
|
+
|
|
29
45
|
def _composite_overlays(
|
|
30
46
|
rgba: np.ndarray,
|
|
31
47
|
overlay_sid_str: str | None,
|
|
@@ -50,7 +66,11 @@ def _composite_overlays(
|
|
|
50
66
|
[a.strip() for a in overlay_alphas_str.split(",")] if overlay_alphas_str else []
|
|
51
67
|
)
|
|
52
68
|
for i, sid in enumerate(sids):
|
|
53
|
-
color =
|
|
69
|
+
color, hidden = (
|
|
70
|
+
_parse_overlay_color_token(colors_raw[i])
|
|
71
|
+
if i < len(colors_raw)
|
|
72
|
+
else (None, frozenset())
|
|
73
|
+
)
|
|
54
74
|
alpha = overlay_alpha
|
|
55
75
|
if i < len(alphas_raw):
|
|
56
76
|
try:
|
|
@@ -73,6 +93,7 @@ def _composite_overlays(
|
|
|
73
93
|
is_label=_overlay_is_label_map(sid, ov_raw),
|
|
74
94
|
override_color=color,
|
|
75
95
|
outline_only=overlay_outline,
|
|
96
|
+
hidden_labels=hidden,
|
|
76
97
|
)
|
|
77
98
|
return rgba
|
|
78
99
|
|
|
@@ -114,7 +135,11 @@ def _composite_mosaic_overlays(
|
|
|
114
135
|
n_frames = int(base_shape[dim_z])
|
|
115
136
|
|
|
116
137
|
for i, sid in enumerate(sids):
|
|
117
|
-
color =
|
|
138
|
+
color, hidden = (
|
|
139
|
+
_parse_overlay_color_token(colors_raw[i])
|
|
140
|
+
if i < len(colors_raw)
|
|
141
|
+
else (None, frozenset())
|
|
142
|
+
)
|
|
118
143
|
try:
|
|
119
144
|
alpha = float(alphas_raw[i]) if i < len(alphas_raw) else overlay_alpha
|
|
120
145
|
except ValueError:
|
|
@@ -146,5 +171,6 @@ def _composite_mosaic_overlays(
|
|
|
146
171
|
is_label=_overlay_is_label_map(sid, grid),
|
|
147
172
|
override_color=color,
|
|
148
173
|
outline_only=overlay_outline,
|
|
174
|
+
hidden_labels=hidden,
|
|
149
175
|
)
|
|
150
176
|
return rgba
|