arrayview 0.27.2__py3-none-any.whl → 0.28.0__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/ARCHITECTURE.md CHANGED
@@ -6,14 +6,12 @@
6
6
  CLI / Python API
7
7
  ├─ view() / _launcher.py → FastAPI server (_server.py) [network mode]
8
8
  │ /ws/{sid} WebSocket, /load register arrays, /seg/* segmentation
9
- └─ Stdio server (_stdio_server.py) [direct webview mode]
10
- stdin/stdout JSON+binary, no network — VS Code extension spawns subprocess
11
9
 
12
- Server (either mode)
10
+ Server
13
11
  ├─ _session.py Session objects, caches, render thread
14
12
  ├─ _render.py Slice extraction → RGBA → PNG pipeline
15
13
  ├─ _analysis.py / _diff.py / _overlays.py / _vectorfield.py
16
- │ Shared backend helpers used by FastAPI and stdio
14
+ │ Shared backend helpers used by routes and WebSocket handlers
17
15
  └─ _io.py File loading (numpy, nifti, zarr, DICOM, …)
18
16
 
19
17
  Frontend (_viewer.html — single self-contained HTML file)
@@ -29,7 +27,7 @@ Frontend (_viewer.html — single self-contained HTML file)
29
27
  |--------------------|------------------------------------|-------------|
30
28
  | Jupyter | Inline iframe | network |
31
29
  | VS Code local | Webview panel (network) | network |
32
- | VS Code tunnel | Direct webview (stdio) | stdio |
30
+ | VS Code tunnel | Webview panel (forwarded WebSocket)| network |
33
31
  | Julia | System browser | network |
34
32
  | CLI / Python script | Native pywebview | network |
35
33
  | SSH terminal | Prints URL — user forwards port with `ssh -L` | network |
@@ -54,10 +52,9 @@ Detection logic lives in `_platform.py`. Display opening logic lives in `_launch
54
52
  | `_segmentation.py` | 227 | nnInteractive segmentation client (pure HTTP, no nnInteractive dependency) |
55
53
  | `_server.py` | 3704 | FastAPI app, all REST + WebSocket routes, HTML template serving |
56
54
  | `_session.py` | 344 | `Session` class, global state (sockets, loops), render thread, prefetch, cache budgets, constants |
57
- | `_stdio_server.py` | 767 | Stdio transport for VS Code direct webview — JSON stdin, binary stdout |
58
55
  | `_torch.py` | 217 | PyTorch integration: `view_batch()`, `TrainingMonitor` (lazy torch import) |
59
56
  | `_vectorfield.py` | 231 | Shared vector field layout validation and arrow sampling |
60
- | `_vscode.py` | 1014 | VS Code extension install/management, signal-file IPC, shared-memory IPC, browser opening |
57
+ | `_vscode.py` | 1014 | VS Code extension install/management, signal-file IPC, tunnel URL opening |
61
58
  | `_viewer.html` | 24103 | **The entire frontend** — CSS + JS in a single file, all viewing modes |
62
59
  | `_shell.html` | 174 | Tab-bar shell for native pywebview — wraps viewer iframes, manages multi-tab sessions |
63
60
 
@@ -80,7 +77,7 @@ The frontend is a single self-contained HTML file (~24k lines). No build step, n
80
77
  **JavaScript (lines ~2439–24103)**
81
78
  | Section | What it covers |
82
79
  |---------|----------------|
83
- | Constants and Transport Setup | WS URL construction, stdio/postMessage transport abstraction |
80
+ | Constants and Transport Setup | HTTP/WebSocket URL construction |
84
81
  | Viewer State Variables | All mutable state: current slice indices, zoom, mode flags |
85
82
  | Mode Registry | Mode name → enter/exit function mapping |
86
83
  | PanManager | Canvas panning state machine (normal + compare modes) |
@@ -218,7 +215,7 @@ Slice data arrives as raw binary (RGBA bytes). The header format and byte offset
218
215
  └─ Open display # _launcher.py → _vscode.py / pywebview / browser
219
216
 
220
217
  2. Browser loads _viewer.html # _server.py serves from package resources
221
- ├─ Establish WebSocket /ws/{sid} # or stdio transport for VS Code direct
218
+ ├─ Establish WebSocket /ws/{sid}
222
219
  └─ Fetch /meta/{sid} # Session metadata: shape, dtype, colormaps
223
220
 
224
221
  3. User scrolls / interacts
@@ -233,6 +230,3 @@ Slice data arrives as raw binary (RGBA bytes). The header format and byte offset
233
230
  ├─ drawImage() to canvas
234
231
  └─ Update info bar, colorbar, eggs
235
232
  ```
236
-
237
- ### Stdio Transport Variant (VS Code Direct Webview)
238
- Same pipeline, but `_stdio_server.py` replaces the FastAPI+WebSocket layer. Messages are JSON on stdin, binary responses are length-prefixed on stdout. The VS Code extension bridges between the webview's `postMessage` and the subprocess stdio.
arrayview/__init__.py CHANGED
@@ -7,12 +7,13 @@ __all__ = [
7
7
  "arrayview",
8
8
  "view",
9
9
  "view_batch",
10
+ "view_dir",
10
11
  "zarr_chunk_preset",
11
12
  ]
12
13
 
13
14
 
14
15
  def __getattr__(name: str):
15
- if name in {"arrayview", "view", "ViewHandle"}:
16
+ if name in {"arrayview", "view", "view_dir", "ViewHandle"}:
16
17
  from arrayview import _launcher
17
18
 
18
19
  return getattr(_launcher, name)
arrayview/_analysis.py CHANGED
@@ -19,7 +19,7 @@ def _visible_shape(session) -> list[int]:
19
19
 
20
20
 
21
21
  def _build_metadata(session) -> dict:
22
- """Build metadata for HTTP and stdio transports."""
22
+ """Build metadata shared by HTTP routes and WebSocket startup."""
23
23
  target_shape = session.spatial_shape if session.rgb_axis is not None else session.shape
24
24
  meta = {
25
25
  "shape": [int(s) for s in target_shape],
arrayview/_io.py CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import fnmatch
5
6
  import os
7
+ import threading
8
+ from collections import OrderedDict
6
9
 
7
10
  import numpy as np
8
11
 
@@ -174,17 +177,438 @@ def _load_nifti_with_meta(filepath):
174
177
  return arr, meta
175
178
 
176
179
 
177
- def load_data_with_meta(filepath, key=None):
180
+ # ---------------------------------------------------------------------------
181
+ # File series — lazy 4D/5D view over a directory of same-shape volumes
182
+ # ---------------------------------------------------------------------------
183
+
184
+
185
+ class _NiftiSeries:
186
+ """Lazy 4D/5D view over a directory of same-shape NIfTI files.
187
+
188
+ ``file_matrix`` is a P x M list-of-lists of filepaths (P patients,
189
+ M modalities). With M == 1 the shape is ``(*vol_shape, P)``; with
190
+ M > 1 it is ``(*vol_shape, P, M)``. ``__getitem__`` maps the trailing
191
+ stack axis/axes to the source file, opens it (canonical reorient,
192
+ LRU-cached), and forwards the spatial key so only the requested slice
193
+ is materialised.
194
+ """
195
+
196
+ _av_lazy = True
197
+
198
+ def __init__(self, file_matrix, vol_shape, dtype, spatial_meta=None):
199
+ self._file_matrix = file_matrix
200
+ self._vol_shape = tuple(vol_shape)
201
+ self._dtype = np.dtype(dtype)
202
+ self._spatial_meta = spatial_meta
203
+ n_patients = len(file_matrix)
204
+ n_modalities = len(file_matrix[0]) if file_matrix else 0
205
+ if n_modalities <= 1:
206
+ self.shape = (*self._vol_shape, n_patients)
207
+ self._stack_axes = (len(self.shape) - 1,)
208
+ else:
209
+ self.shape = (*self._vol_shape, n_patients, n_modalities)
210
+ self._stack_axes = (len(self.shape) - 2, len(self.shape) - 1)
211
+ self.ndim = len(self.shape)
212
+ self._vol_cache: OrderedDict = OrderedDict()
213
+ self._vol_cache_cap = int(
214
+ os.environ.get("ARRAYVIEW_NIFTI_SERIES_VOL_CACHE", 3)
215
+ )
216
+ self._cache_lock = threading.Lock()
217
+
218
+ @property
219
+ def dtype(self):
220
+ return self._dtype
221
+
222
+ def _get_volume(self, p_idx, m_idx):
223
+ cache_key = (p_idx, m_idx)
224
+ with self._cache_lock:
225
+ if cache_key in self._vol_cache:
226
+ self._vol_cache.move_to_end(cache_key)
227
+ return self._vol_cache[cache_key]
228
+ filepath = self._file_matrix[p_idx][m_idx]
229
+ nib = _nib()
230
+ img = nib.load(filepath)
231
+ canon = nib.as_closest_canonical(img)
232
+ vol = np.asarray(canon.dataobj)
233
+ with self._cache_lock:
234
+ self._vol_cache[cache_key] = vol
235
+ self._vol_cache.move_to_end(cache_key)
236
+ while len(self._vol_cache) > self._vol_cache_cap:
237
+ self._vol_cache.popitem(last=False)
238
+ return vol
239
+
240
+ def __getitem__(self, key):
241
+ if not isinstance(key, tuple):
242
+ key = (key,)
243
+ has_special = any(k is None or k is Ellipsis for k in key)
244
+ if not has_special and len(key) <= self.ndim:
245
+ padded = list(key) + [slice(None)] * (self.ndim - len(key))
246
+ stack_vals = [padded[i] for i in self._stack_axes]
247
+ if all(isinstance(s, (int, np.integer)) for s in stack_vals):
248
+ spatial_positions = [
249
+ i for i in range(self.ndim) if i not in self._stack_axes
250
+ ]
251
+ spatial_key = tuple(padded[i] for i in spatial_positions)
252
+ n_p = len(self._file_matrix)
253
+ n_m = len(self._file_matrix[0]) if self._file_matrix else 0
254
+ p_idx = int(stack_vals[0])
255
+ if p_idx < 0:
256
+ p_idx += n_p
257
+ if len(self._stack_axes) == 1:
258
+ m_idx = 0
259
+ else:
260
+ m_idx = int(stack_vals[1])
261
+ if m_idx < 0:
262
+ m_idx += n_m
263
+ vol = self._get_volume(p_idx, m_idx)
264
+ return vol[spatial_key]
265
+ return np.asarray(self)[key]
266
+
267
+ def __array__(self, dtype=None):
268
+ result = np.empty(self.shape, dtype=self._dtype)
269
+ for p, row in enumerate(self._file_matrix):
270
+ for m, fpath in enumerate(row):
271
+ vol = self._get_volume(p, m)
272
+ if len(self._stack_axes) == 1:
273
+ result[..., p] = vol
274
+ else:
275
+ result[..., p, m] = vol
276
+ if dtype is not None:
277
+ result = result.astype(dtype)
278
+ return result
279
+
280
+
281
+ class _FileSeries:
282
+ """Lazy 4D/5D view over a directory of same-shape array files.
283
+
284
+ Like ``_NiftiSeries`` but works with any supported file format
285
+ (.npy, .npz, .zarr, .pt/.pth, .h5/.hdf5, .tif/.tiff, .mat).
286
+ Uses ``load_data`` for each volume — no nibabel reorientation.
287
+ """
288
+
289
+ _av_lazy = True
290
+
291
+ def __init__(self, file_matrix, vol_shape, dtype, spatial_meta=None):
292
+ self._file_matrix = file_matrix
293
+ self._vol_shape = tuple(vol_shape)
294
+ self._dtype = np.dtype(dtype)
295
+ self._spatial_meta = spatial_meta
296
+ n_patients = len(file_matrix)
297
+ n_modalities = len(file_matrix[0]) if file_matrix else 0
298
+ if n_modalities <= 1:
299
+ self.shape = (*self._vol_shape, n_patients)
300
+ self._stack_axes = (len(self.shape) - 1,)
301
+ else:
302
+ self.shape = (*self._vol_shape, n_patients, n_modalities)
303
+ self._stack_axes = (len(self.shape) - 2, len(self.shape) - 1)
304
+ self.ndim = len(self.shape)
305
+ self._vol_cache: OrderedDict = OrderedDict()
306
+ self._vol_cache_cap = int(
307
+ os.environ.get("ARRAYVIEW_FILE_SERIES_VOL_CACHE", 3)
308
+ )
309
+ self._cache_lock = threading.Lock()
310
+
311
+ @property
312
+ def dtype(self):
313
+ return self._dtype
314
+
315
+ def _get_volume(self, p_idx, m_idx):
316
+ cache_key = (p_idx, m_idx)
317
+ with self._cache_lock:
318
+ if cache_key in self._vol_cache:
319
+ self._vol_cache.move_to_end(cache_key)
320
+ return self._vol_cache[cache_key]
321
+ filepath = self._file_matrix[p_idx][m_idx]
322
+ vol = load_data(filepath)
323
+ with self._cache_lock:
324
+ self._vol_cache[cache_key] = vol
325
+ while len(self._vol_cache) > self._vol_cache_cap:
326
+ self._vol_cache.popitem(last=False)
327
+ return vol
328
+
329
+ def __getitem__(self, key):
330
+ if not isinstance(key, tuple):
331
+ key = (key,)
332
+ has_special = any(k is None or k is Ellipsis for k in key)
333
+ if not has_special and len(key) <= self.ndim:
334
+ padded = list(key) + [slice(None)] * (self.ndim - len(key))
335
+ stack_vals = [padded[i] for i in self._stack_axes]
336
+ if all(isinstance(s, (int, np.integer)) for s in stack_vals):
337
+ spatial_positions = [
338
+ i for i in range(self.ndim) if i not in self._stack_axes
339
+ ]
340
+ spatial_key = tuple(padded[i] for i in spatial_positions)
341
+ n_p = len(self._file_matrix)
342
+ n_m = len(self._file_matrix[0]) if self._file_matrix else 0
343
+ p_idx = int(stack_vals[0])
344
+ if p_idx < 0:
345
+ p_idx += n_p
346
+ if len(self._stack_axes) == 1:
347
+ m_idx = 0
348
+ else:
349
+ m_idx = int(stack_vals[1])
350
+ if m_idx < 0:
351
+ m_idx += n_m
352
+ vol = self._get_volume(p_idx, m_idx)
353
+ return vol[spatial_key]
354
+ return np.asarray(self)[key]
355
+
356
+ def __array__(self, dtype=None):
357
+ result = np.empty(self.shape, dtype=self._dtype)
358
+ for p, row in enumerate(self._file_matrix):
359
+ for m, fpath in enumerate(row):
360
+ vol = self._get_volume(p, m)
361
+ if len(self._stack_axes) == 1:
362
+ result[..., p] = vol
363
+ else:
364
+ result[..., p, m] = vol
365
+ if dtype is not None:
366
+ result = result.astype(dtype)
367
+ return result
368
+
369
+
370
+ def _is_nifti_path(filepath):
371
+ """True if *filepath* ends with .nii or .nii.gz."""
372
+ return filepath.endswith(".nii") or filepath.endswith(".nii.gz")
373
+
374
+
375
+ def _get_ext(filepath):
376
+ """Return the extension of *filepath*, normalising .nii.gz and .zarr.zip."""
377
+ lower = filepath.lower()
378
+ if lower.endswith(".nii.gz"):
379
+ return ".nii.gz"
380
+ if lower.endswith(".zarr.zip"):
381
+ return ".zarr.zip"
382
+ return os.path.splitext(lower)[1]
383
+
384
+
385
+ def _load_file_series(path, select=None):
386
+ """Build a lazy series from a directory of supported array files.
387
+
388
+ Walks *path* recursively, groups files of any supported format by
389
+ immediate parent folder (= patient). With *select* (a list of fnmatch
390
+ patterns), picks one file per pattern per patient → 5D ``(*vol, P, M)``.
391
+ Without *select*, requires exactly one file per patient → 4D ``(*vol, P)``.
392
+
393
+ If every file is NIfTI (.nii/.nii.gz), delegates to
394
+ ``_load_nifti_series`` for canonical reorientation. Otherwise builds
395
+ a ``_FileSeries`` using ``load_data`` per volume.
396
+
397
+ Returns ``(series, spatial_meta)``.
398
+ """
399
+ # ── collect files by parent directory ──────────────────────────
400
+ patients: dict[str, list[str]] = {}
401
+ all_nifti = True
402
+
403
+ for root, dirs, files in os.walk(path):
404
+ supported = sorted(
405
+ os.path.join(root, f)
406
+ for f in files
407
+ if _get_ext(f) in _SUPPORTED_EXTS
408
+ )
409
+ if supported:
410
+ patients[root] = supported
411
+ if all_nifti:
412
+ all_nifti = all(_is_nifti_path(p) for p in supported)
413
+ # A directory with supported files is the series unit. Do not
414
+ # treat nested sub-folders as additional patients.
415
+ dirs[:] = []
416
+
417
+ if not patients:
418
+ raise ValueError(
419
+ f"No supported array files found under {path!r}. "
420
+ f"Supported: {', '.join(sorted(_SUPPORTED_EXTS))}"
421
+ )
422
+
423
+ # NIfTI-only → preserve canonical reorientation + lazy dataobj slicing
424
+ if all_nifti:
425
+ return _load_nifti_series(path, select=select)
426
+
427
+ # ── build file matrix ──────────────────────────────────────────
428
+ select_patterns = select or []
429
+ patient_dirs = sorted(patients.keys())
430
+ file_matrix: list[list[str]] = []
431
+
432
+ if select_patterns:
433
+ for pdir in patient_dirs:
434
+ selected: list[str] = []
435
+ for pattern in select_patterns:
436
+ matches = [
437
+ f
438
+ for f in patients[pdir]
439
+ if fnmatch.fnmatch(os.path.basename(f), pattern)
440
+ ]
441
+ if not matches:
442
+ raise ValueError(
443
+ f"Folder {pdir!r}: no file matches --select "
444
+ f"pattern {pattern!r}. Available: "
445
+ f"{[os.path.basename(f) for f in patients[pdir]]}"
446
+ )
447
+ if len(matches) > 1:
448
+ raise ValueError(
449
+ f"Folder {pdir!r}: multiple files match "
450
+ f"--select pattern {pattern!r}: "
451
+ f"{[os.path.basename(f) for f in matches]}. "
452
+ "Make patterns more specific."
453
+ )
454
+ selected.append(matches[0])
455
+ file_matrix.append(selected)
456
+ else:
457
+ for pdir in patient_dirs:
458
+ files = patients[pdir]
459
+ if len(files) == 1:
460
+ file_matrix.append([files[0]])
461
+ else:
462
+ raise ValueError(
463
+ f"Folder {pdir!r} contains {len(files)} files: "
464
+ f"{[os.path.basename(f) for f in files]}. "
465
+ "Use --select PATTERN to pick one (or more) per folder. "
466
+ "Example: --select '*t1*' --select '*t2*' --select '*flair*'"
467
+ )
468
+
469
+ # ── validate shape / dtype uniformity ──────────────────────────
470
+ ref_shape = None
471
+ ref_dtype = None
472
+ for row in file_matrix:
473
+ for fpath in row:
474
+ arr = load_data(fpath)
475
+ shape = arr.shape
476
+ dtype = arr.dtype
477
+ if ref_shape is None:
478
+ ref_shape = shape
479
+ ref_dtype = dtype
480
+ elif shape != ref_shape:
481
+ raise ValueError(
482
+ f"Shape mismatch: {os.path.basename(fpath)!r} has shape "
483
+ f"{shape}, expected {ref_shape}."
484
+ )
485
+ elif dtype != ref_dtype:
486
+ raise ValueError(
487
+ f"Dtype mismatch: {os.path.basename(fpath)!r} has dtype "
488
+ f"{dtype}, expected {ref_dtype}."
489
+ )
490
+
491
+ series = _FileSeries(file_matrix, ref_shape, ref_dtype, spatial_meta=None)
492
+ return series, None
493
+
494
+
495
+ def _load_nifti_series(path, select=None):
496
+ """Build a lazy ``_NiftiSeries`` from a directory of NIfTI files.
497
+
498
+ Walks *path* recursively, groups ``.nii``/``.nii.gz`` by immediate parent
499
+ folder (= patient). With *select* (a list of fnmatch patterns), picks one
500
+ file per pattern per patient → 5D ``(*vol, P, M)``. Without *select*,
501
+ requires exactly one NIfTI per patient → 4D ``(*vol, P)``.
502
+
503
+ Returns ``(series, spatial_meta)``.
504
+ """
505
+ nib = _nib()
506
+
507
+ patients: dict[str, list[str]] = {}
508
+ for root, dirs, files in os.walk(path):
509
+ nii_files = sorted(
510
+ os.path.join(root, f)
511
+ for f in files
512
+ if f.endswith(".nii") or f.endswith(".nii.gz")
513
+ )
514
+ if nii_files:
515
+ patients[root] = nii_files
516
+ # A directory with NIfTI files is the series unit. Do not treat
517
+ # nested mask/derived-output folders as additional patients.
518
+ dirs[:] = []
519
+
520
+ if not patients:
521
+ raise ValueError(
522
+ f"No NIfTI files (.nii/.nii.gz) found under {path!r}. "
523
+ "If the folder contains DICOM (.dcm) files, convert them to NIfTI "
524
+ "first (e.g. `dcm2niix -o <out> <dicom_dir>`)."
525
+ )
526
+
527
+ select_patterns = select or []
528
+ patient_dirs = sorted(patients.keys())
529
+ file_matrix: list[list[str]] = []
530
+
531
+ if select_patterns:
532
+ for pdir in patient_dirs:
533
+ selected: list[str] = []
534
+ for pattern in select_patterns:
535
+ matches = [
536
+ f
537
+ for f in patients[pdir]
538
+ if fnmatch.fnmatch(os.path.basename(f), pattern)
539
+ ]
540
+ if not matches:
541
+ raise ValueError(
542
+ f"Patient folder {pdir!r}: no file matches --select "
543
+ f"pattern {pattern!r}. Available: "
544
+ f"{[os.path.basename(f) for f in patients[pdir]]}"
545
+ )
546
+ if len(matches) > 1:
547
+ raise ValueError(
548
+ f"Patient folder {pdir!r}: multiple files match "
549
+ f"--select pattern {pattern!r}: "
550
+ f"{[os.path.basename(f) for f in matches]}. "
551
+ "Make patterns more specific."
552
+ )
553
+ selected.append(matches[0])
554
+ file_matrix.append(selected)
555
+ else:
556
+ for pdir in patient_dirs:
557
+ nii_files = patients[pdir]
558
+ if len(nii_files) == 1:
559
+ file_matrix.append([nii_files[0]])
560
+ else:
561
+ raise ValueError(
562
+ f"Patient folder {pdir!r} contains {len(nii_files)} NIfTI "
563
+ f"files: {[os.path.basename(f) for f in nii_files]}. "
564
+ "Use --select PATTERN to pick one (or more) per patient. "
565
+ "Example: --select '*t1*' --select '*t2*' --select '*flair*'"
566
+ )
567
+
568
+ ref_shape = None
569
+ ref_dtype = None
570
+ for row in file_matrix:
571
+ for fpath in row:
572
+ img = nib.load(fpath)
573
+ shape = tuple(int(s) for s in img.shape)
574
+ dtype = img.get_data_dtype()
575
+ if ref_shape is None:
576
+ ref_shape = shape
577
+ ref_dtype = dtype
578
+ elif shape != ref_shape:
579
+ raise ValueError(
580
+ f"Shape mismatch: {os.path.basename(fpath)!r} has shape "
581
+ f"{shape}, expected {ref_shape}."
582
+ )
583
+ elif dtype != ref_dtype:
584
+ raise ValueError(
585
+ f"Dtype mismatch: {os.path.basename(fpath)!r} has dtype "
586
+ f"{dtype}, expected {ref_dtype}."
587
+ )
588
+
589
+ _, spatial_meta = _load_nifti_with_meta(file_matrix[0][0])
590
+ series = _NiftiSeries(file_matrix, ref_shape, ref_dtype, spatial_meta=spatial_meta)
591
+ return series, spatial_meta
592
+
593
+
594
+ def load_data_with_meta(filepath, key=None, select=None):
178
595
  """Like load_data but also returns spatial metadata for NIfTI files.
179
596
 
180
597
  Returns (array, meta_or_None). meta is None for non-NIfTI formats.
598
+ When *filepath* is a directory, loads it as a file series (see
599
+ ``_load_file_series``).
181
600
  """
601
+ if os.path.isdir(filepath):
602
+ return _load_file_series(filepath, select=select)
182
603
  if filepath.endswith(".nii") or filepath.endswith(".nii.gz"):
183
604
  return _load_nifti_with_meta(filepath)
184
605
  return load_data(filepath, key=key), None
185
606
 
186
607
 
187
608
  def load_data(filepath, key=None):
609
+ if os.path.isdir(filepath):
610
+ series, _meta = _load_file_series(filepath)
611
+ return series
188
612
  if filepath.endswith(".npy"):
189
613
  # Eager-load small-to-medium files into RAM. mmap_mode="r" on a C-order
190
614
  # 4D+ array forces scattered page faults for every orthogonal slice
@@ -326,6 +750,9 @@ FULL_LOAD_EXTS = frozenset([".pt", ".pth", ".tif", ".tiff", ".mat"])
326
750
  def _peek_file_shape(fpath: str, ext: str):
327
751
  """Try to return shape quickly without loading the full array. Returns None on failure."""
328
752
  try:
753
+ if os.path.isdir(fpath):
754
+ series, _ = _load_file_series(fpath)
755
+ return list(series.shape)
329
756
  if ext == ".npy":
330
757
  arr = np.load(fpath, mmap_mode="r", allow_pickle=False)
331
758
  return list(arr.shape)
arrayview/_launch_plan.py CHANGED
@@ -37,7 +37,6 @@ class Invocation(_StrEnum):
37
37
  PYTHON = "python"
38
38
  JUPYTER = "jupyter"
39
39
  JULIA = "julia"
40
- STDIO = "stdio"
41
40
  CODEX = "codex"
42
41
 
43
42
 
@@ -52,8 +51,6 @@ class Environment(_StrEnum):
52
51
 
53
52
  class Transport(_StrEnum):
54
53
  HTTP = "http"
55
- STDIO_FILE = "stdio_file"
56
- STDIO_SHM = "stdio_shm"
57
54
  NONE = "none"
58
55
 
59
56
 
@@ -77,7 +74,6 @@ class Registration(_StrEnum):
77
74
  HTTP_LOAD = "http_load"
78
75
  DAEMON_STARTUP = "daemon_startup"
79
76
  IN_PROCESS_SESSION = "in_process_session"
80
- STDIO_REGISTER = "stdio_register"
81
77
  RELAY = "relay"
82
78
 
83
79