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 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=True,
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 isinstance(ds, h5py.Dataset) and len(ds.shape) >= 1 and ds.dtype.kind in ("b", "i", "u", "f", "c"):
110
- keys.append({"key": k, "shape": list(ds.shape), "dtype": str(ds.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 isinstance(f[k], h5py.Dataset)}
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", # S7: explicit backend — enables permessage-deflate compression
2780
- ws_per_message_deflate=True, # S7: negotiate deflate with browser (transparent)
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: load the first array so the session is
4709
- # created, then store keys so the viewer can show a picker.
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
- _load_key = _array_keys[0]["key"] if _array_keys else None
4727
- data, spatial_meta = load_data_with_meta(filepath, key=_load_key)
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
- if args.dims:
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 = _parse_hex_color(colors_raw[i]) if i < len(colors_raw) else None
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 = _parse_hex_color(colors_raw[i]) if i < len(colors_raw) else None
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