arrayview 0.10.1__tar.gz → 0.12.0__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.10.1 → arrayview-0.12.0}/AGENTS.md +9 -3
- arrayview-0.12.0/PKG-INFO +60 -0
- arrayview-0.12.0/README.md +25 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/pyproject.toml +1 -1
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/ARCHITECTURE.md +8 -2
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_config.py +15 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_io.py +72 -1
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_launcher.py +5 -2
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_render.py +16 -4
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_server.py +291 -169
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_session.py +24 -1
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_shell.html +28 -10
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_stdio_server.py +6 -6
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_viewer.html +3382 -2162
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_vscode.py +80 -25
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_browser.py +32 -49
- arrayview-0.12.0/tests/test_command_reachability.py +120 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_interactions.py +34 -27
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_mode_matrix.py +31 -33
- arrayview-0.12.0/tests/test_mode_roundtrip.py +346 -0
- arrayview-0.12.0/tests/test_nifti_meta.py +59 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/visual_smoke.py +1 -1
- {arrayview-0.10.1 → arrayview-0.12.0}/uv.lock +1 -1
- arrayview-0.10.1/PKG-INFO +0 -136
- arrayview-0.10.1/README.md +0 -101
- {arrayview-0.10.1 → arrayview-0.12.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.gitignore +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.python-version +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.tmp-vsix/extension/extension.js +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/.tmp-vsix/extension/package.json +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/LICENSE +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/comparing.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/configuration.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/display.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/index.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/loading.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/logo.png +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/measurement.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/remote.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/docs/viewing.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/matlab/arrayview.m +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/mkdocs.yml +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/scripts/demo.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/scripts/release.sh +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/conftest.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_api.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_cli.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_config.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/test_torch.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/tests/ui_audit.py +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/vscode-extension/LICENSE +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/vscode-extension/extension.js +0 -0
- {arrayview-0.10.1 → arrayview-0.12.0}/vscode-extension/package.json +0 -0
|
@@ -2,6 +2,9 @@ Read `src/arrayview/ARCHITECTURE.md` for codebase orientation.
|
|
|
2
2
|
|
|
3
3
|
# ArrayView
|
|
4
4
|
|
|
5
|
+
I haven't written or read a single line of code in src so when you ask me questions/input,
|
|
6
|
+
keep it simple with some simple examples.
|
|
7
|
+
|
|
5
8
|
## Skills
|
|
6
9
|
|
|
7
10
|
Load the relevant skill before touching the corresponding area.
|
|
@@ -24,6 +27,7 @@ Load the relevant skill before touching the corresponding area.
|
|
|
24
27
|
- For visual/animation features, propose 2-3 options BEFORE implementing
|
|
25
28
|
- UI visibility changes go through reconcilers (`_reconcileUI`/`_reconcileLayout`/`_reconcileCompareState`/`_reconcileCbVisibility`), not inline `style.display` or `classList` toggles in mode functions
|
|
26
29
|
- All colorbar state (animation, window/level, hover, drag) flows through `primaryCb` ColorBar instance — never read/write legacy globals. Multiview colorbars sync via `primaryCb`.
|
|
30
|
+
- Keybinds flow through the command registry (`commands` / `keybinds` in `_viewer.html`), not inline keydown branches. The help overlay auto-generates from command `title` fields — do not hand-edit it.
|
|
27
31
|
|
|
28
32
|
## Execution
|
|
29
33
|
|
|
@@ -32,9 +36,11 @@ Always use **subagent-driven development** for implementation. Commit completed
|
|
|
32
36
|
## Testing
|
|
33
37
|
|
|
34
38
|
```bash
|
|
35
|
-
uv run pytest tests/test_api.py -v
|
|
36
|
-
uv run pytest tests/test_browser.py -v
|
|
37
|
-
uv run
|
|
39
|
+
uv run pytest tests/test_api.py -v # HTTP API
|
|
40
|
+
uv run pytest tests/test_browser.py -v # Playwright
|
|
41
|
+
uv run pytest tests/test_mode_roundtrip.py -v # mode state round-trip
|
|
42
|
+
uv run pytest tests/test_command_reachability.py -v # command when-clause matrix
|
|
43
|
+
uv run python tests/visual_smoke.py # screenshots
|
|
38
44
|
```
|
|
39
45
|
|
|
40
46
|
After any UI change, use `/ui-consistency-audit` to verify across all modes.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arrayview
|
|
3
|
+
Version: 0.12.0
|
|
4
|
+
Summary: Fast multi-dimensional array viewer
|
|
5
|
+
Project-URL: Home, https://github.com/oscarvanderheide/arrayview
|
|
6
|
+
Project-URL: Source, https://github.com/oscarvanderheide/arrayview
|
|
7
|
+
Author-email: Oscar <oscarvanderheide@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: array,mri,npy,viewer,visualization
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: fastapi>=0.129.0
|
|
18
|
+
Requires-Dist: h5py>=3.0
|
|
19
|
+
Requires-Dist: matplotlib>=3.9.0
|
|
20
|
+
Requires-Dist: nibabel>=5.3.3
|
|
21
|
+
Requires-Dist: numpy>=2.4.2
|
|
22
|
+
Requires-Dist: pillow>=12.1.1
|
|
23
|
+
Requires-Dist: pyqt5>=5.15; sys_platform == 'linux'
|
|
24
|
+
Requires-Dist: pyqtwebengine>=5.15; sys_platform == 'linux'
|
|
25
|
+
Requires-Dist: python-multipart>=0.0.22
|
|
26
|
+
Requires-Dist: pywebview>=6.1
|
|
27
|
+
Requires-Dist: qmricolors>=0.1.2
|
|
28
|
+
Requires-Dist: qtpy>=2.0; sys_platform == 'linux'
|
|
29
|
+
Requires-Dist: scipy>=1.10
|
|
30
|
+
Requires-Dist: tifffile>=2023.1.1
|
|
31
|
+
Requires-Dist: uvicorn>=0.41.0
|
|
32
|
+
Requires-Dist: websockets>=14.0
|
|
33
|
+
Requires-Dist: zarr>=2.17
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# <img src="docs/logo.png" height="36"> arrayview
|
|
37
|
+
|
|
38
|
+
This is what looking at arrays should feel like.
|
|
39
|
+
|
|
40
|
+
Load up `.npy`, `.nii`, `.h5`, `.mat` and friends. Works locally, in Jupyter, over SSH and VS Code tunnels. Press `?` once you're in.
|
|
41
|
+
|
|
42
|
+
## CLI
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uvx arrayview your_array.npy
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Python
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv add arrayview
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from arrayview import view
|
|
56
|
+
view(arr)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
[docs →](https://oscarvanderheide.github.io/arrayview/)
|
|
60
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# <img src="docs/logo.png" height="36"> arrayview
|
|
2
|
+
|
|
3
|
+
This is what looking at arrays should feel like.
|
|
4
|
+
|
|
5
|
+
Load up `.npy`, `.nii`, `.h5`, `.mat` and friends. Works locally, in Jupyter, over SSH and VS Code tunnels. Press `?` once you're in.
|
|
6
|
+
|
|
7
|
+
## CLI
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uvx arrayview your_array.npy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Python
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add arrayview
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from arrayview import view
|
|
21
|
+
view(arr)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
[docs →](https://oscarvanderheide.github.io/arrayview/)
|
|
25
|
+
|
|
@@ -52,7 +52,7 @@ Detection logic lives in `_platform.py`. Display opening logic lives in `_launch
|
|
|
52
52
|
| `_stdio_server.py` | 791 | Stdio transport for VS Code direct webview — JSON stdin, binary stdout |
|
|
53
53
|
| `_torch.py` | 217 | PyTorch integration: `view_batch()`, `TrainingMonitor` (lazy torch import) |
|
|
54
54
|
| `_vscode.py` | 1014 | VS Code extension install/management, signal-file IPC, shared-memory IPC, browser opening |
|
|
55
|
-
| `_viewer.html` |
|
|
55
|
+
| `_viewer.html` | 15600 | **The entire frontend** — CSS + JS in a single file, all viewing modes |
|
|
56
56
|
| `_shell.html` | 174 | Tab-bar shell for native pywebview — wraps viewer iframes, manages multi-tab sessions |
|
|
57
57
|
|
|
58
58
|
## Frontend (_viewer.html)
|
|
@@ -89,7 +89,7 @@ The frontend is a single self-contained HTML file (~15k lines). No build step, n
|
|
|
89
89
|
| Rendering Pipeline | `updateView()`, play/animate, screenshot capture |
|
|
90
90
|
| ROI and Selection Modes | Rectangle/ellipse ROI drawing, statistics computation |
|
|
91
91
|
| nnInteractive Segmentation | Click-to-segment UI, mask overlay, undo stack |
|
|
92
|
-
| Keyboard Shortcuts |
|
|
92
|
+
| Keyboard Shortcuts | Command registry (`commands` / `keybinds` / `makeContext` / `evalWhen` / `dispatchCommand`) + `/`-triggered command palette. Keydown handler is a thin dispatcher prefix |
|
|
93
93
|
| Mode Transitions | Compare/multiview/qMRI enter/exit, crosshair animation |
|
|
94
94
|
| Scroll, Zoom, and Pan | Mouse wheel slice scroll, pinch zoom, scroll-to-zoom |
|
|
95
95
|
| Immersive Mode, Cross-Fade, and Visual Effects | Zen mode, fullscreen (K), animated transitions |
|
|
@@ -136,6 +136,9 @@ Pill-shaped badges below the canvas showing active visualization transforms. **C
|
|
|
136
136
|
### Dynamic Islands
|
|
137
137
|
Floating UI panels that appear/disappear based on context: ROI statistics, segmentation controls, colorbar hover, dimension sliders. Must be tested across all viewing modes (normal, immersive, multiview, compare).
|
|
138
138
|
|
|
139
|
+
### Command Registry
|
|
140
|
+
All keybinds flow through a VS Code-style command registry in `_viewer.html`. Three tables: `commands` (id → `{title, when, run}`), `keybinds` (key+modifiers → command id), and `makeContext(state)` (mode/state flag bag). `dispatchCommand(e)` is wired as a prefix to the keydown handler; on a match it evaluates `when` against the context and runs the command, otherwise falls through. The help overlay is auto-generated from command `title` fields — never hand-edit it. A `/`-triggered command palette fuzzy-searches all commands. Cross-mode enablement is guarded by `tests/test_command_reachability.py`.
|
|
141
|
+
|
|
139
142
|
### Reconcilers
|
|
140
143
|
Functions in the "UI Validation and Reconciliation" section (~line 13666) that enforce consistent UI state. When mode changes happen, reconcilers update visibility of containers, colorbars, dynamic islands, and compare sub-mode UI. There are four:
|
|
141
144
|
1. **Unified UI reconciler** — master state enforcer
|
|
@@ -156,6 +159,9 @@ A dedicated daemon thread (`_session.py`) runs all CPU-heavy rendering off the a
|
|
|
156
159
|
### Dynamic Islands
|
|
157
160
|
Must verify island positioning and visibility across normal, immersive, multiview, and compare modes. Islands use absolute/fixed positioning that breaks if parent containers change.
|
|
158
161
|
|
|
162
|
+
### Keybind Changes
|
|
163
|
+
Keybinds live in the `commands` + `keybinds` tables, not in the keydown handler. Adding or changing a keybind means editing those tables and (if needed) extending `makeContext` / `evalWhen`. The help overlay regenerates itself from command `title` fields — do not edit overlay HTML directly.
|
|
164
|
+
|
|
159
165
|
### Layout Debugging
|
|
160
166
|
When debugging layout issues: identify the root cause (which scale function, which reconciler, which CSS rule) before applying fixes. Symptoms in one mode often originate from shared code affecting all modes.
|
|
161
167
|
|
|
@@ -41,6 +41,21 @@ def get_viewer_colormaps() -> list[str] | None:
|
|
|
41
41
|
return None
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
_VALID_THEMES = {"dark", "light", "solarized", "nord"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_viewer_theme() -> str | None:
|
|
48
|
+
"""Return user-configured default theme name, or None if not configured."""
|
|
49
|
+
cfg = load_config()
|
|
50
|
+
viewer_cfg = cfg.get("viewer", {})
|
|
51
|
+
if not isinstance(viewer_cfg, dict):
|
|
52
|
+
return None
|
|
53
|
+
theme = viewer_cfg.get("theme")
|
|
54
|
+
if isinstance(theme, str) and theme.strip().lower() in _VALID_THEMES:
|
|
55
|
+
return theme.strip().lower()
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
44
59
|
def get_nninteractive_url() -> str | None:
|
|
45
60
|
"""Return configured nnInteractive server URL, or None.
|
|
46
61
|
|
|
@@ -124,6 +124,71 @@ def _select_npz_array(npz, filepath):
|
|
|
124
124
|
print(f" Please enter a number between 1 and {len(entries)}.")
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def _load_nifti_with_meta(filepath):
|
|
128
|
+
"""Load a NIfTI file, canonical-reorient, return (array, meta).
|
|
129
|
+
|
|
130
|
+
meta is a dict with keys:
|
|
131
|
+
affine : original 4x4 affine (RAS+ mm)
|
|
132
|
+
affine_canonical : 4x4 affine after as_closest_canonical
|
|
133
|
+
voxel_sizes : tuple (sx, sy, sz) in mm, post-reorient
|
|
134
|
+
axis_labels : tuple of 3 strs from {"R","L","A","P","S","I"}
|
|
135
|
+
— positive direction of each canonical axis
|
|
136
|
+
is_oblique : bool — True if rotation part has off-diagonal magnitude > 1e-3
|
|
137
|
+
after normalizing voxel sizes
|
|
138
|
+
"""
|
|
139
|
+
nib = _nib()
|
|
140
|
+
img = nib.load(filepath)
|
|
141
|
+
original_affine = np.asarray(img.affine, dtype=np.float64)
|
|
142
|
+
canon = nib.as_closest_canonical(img)
|
|
143
|
+
affine_canonical = np.asarray(canon.affine, dtype=np.float64)
|
|
144
|
+
|
|
145
|
+
# NOTE: reorient requires materializing axis permutes/flips. .nii.gz is
|
|
146
|
+
# already eager (gzip not seekable), so this is free; .nii loses mmap as a
|
|
147
|
+
# necessary cost to apply the reorient.
|
|
148
|
+
arr = np.asarray(canon.dataobj)
|
|
149
|
+
|
|
150
|
+
rot = affine_canonical[:3, :3]
|
|
151
|
+
voxel_sizes = tuple(float(np.linalg.norm(rot[:, i])) for i in range(3))
|
|
152
|
+
|
|
153
|
+
# Direction of each canonical axis (sign of diagonal after normalizing)
|
|
154
|
+
norm_rot = np.zeros((3, 3))
|
|
155
|
+
for i in range(3):
|
|
156
|
+
if voxel_sizes[i] > 0:
|
|
157
|
+
norm_rot[:, i] = rot[:, i] / voxel_sizes[i]
|
|
158
|
+
pos_labels = ("R", "A", "S")
|
|
159
|
+
neg_labels = ("L", "P", "I")
|
|
160
|
+
axis_labels = tuple(
|
|
161
|
+
pos_labels[i] if norm_rot[i, i] >= 0 else neg_labels[i] for i in range(3)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Oblique = off-diagonal of normalized rotation has |val| > tol
|
|
165
|
+
off_diag_max = 0.0
|
|
166
|
+
for i in range(3):
|
|
167
|
+
for j in range(3):
|
|
168
|
+
if i != j:
|
|
169
|
+
off_diag_max = max(off_diag_max, abs(norm_rot[i, j]))
|
|
170
|
+
is_oblique = bool(off_diag_max > 1e-3)
|
|
171
|
+
|
|
172
|
+
meta = {
|
|
173
|
+
"affine": original_affine,
|
|
174
|
+
"affine_canonical": affine_canonical,
|
|
175
|
+
"voxel_sizes": voxel_sizes,
|
|
176
|
+
"axis_labels": axis_labels,
|
|
177
|
+
"is_oblique": is_oblique,
|
|
178
|
+
}
|
|
179
|
+
return arr, meta
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def load_data_with_meta(filepath):
|
|
183
|
+
"""Like load_data but also returns spatial metadata for NIfTI files.
|
|
184
|
+
|
|
185
|
+
Returns (array, meta_or_None). meta is None for non-NIfTI formats.
|
|
186
|
+
"""
|
|
187
|
+
if filepath.endswith(".nii") or filepath.endswith(".nii.gz"):
|
|
188
|
+
return _load_nifti_with_meta(filepath)
|
|
189
|
+
return load_data(filepath), None
|
|
190
|
+
|
|
191
|
+
|
|
127
192
|
def load_data(filepath):
|
|
128
193
|
if filepath.endswith(".npy"):
|
|
129
194
|
return np.load(filepath, mmap_mode="r")
|
|
@@ -133,7 +198,13 @@ def load_data(filepath):
|
|
|
133
198
|
if len(keys) == 1:
|
|
134
199
|
return npz[keys[0]]
|
|
135
200
|
return _select_npz_array(npz, filepath)
|
|
136
|
-
elif filepath.endswith(".nii
|
|
201
|
+
elif filepath.endswith(".nii.gz"):
|
|
202
|
+
# Gzip streams aren't seekable, so nibabel's lazy dataobj decompresses
|
|
203
|
+
# large portions of the file on every arbitrary slice access. Materialize
|
|
204
|
+
# the volume up front so subsequent slicing is cheap.
|
|
205
|
+
return np.asarray(_nib().load(filepath).dataobj)
|
|
206
|
+
elif filepath.endswith(".nii"):
|
|
207
|
+
# Uncompressed NIfTI is memory-mapped via dataobj — slicing is cheap.
|
|
137
208
|
return _nib().load(filepath).dataobj
|
|
138
209
|
elif filepath.endswith(".zarr") or filepath.endswith(".zarr.zip"):
|
|
139
210
|
import zarr
|
|
@@ -1654,10 +1654,10 @@ def _serve_daemon(
|
|
|
1654
1654
|
).start()
|
|
1655
1655
|
|
|
1656
1656
|
def _load():
|
|
1657
|
-
from arrayview._io import load_data
|
|
1657
|
+
from arrayview._io import load_data, load_data_with_meta
|
|
1658
1658
|
|
|
1659
1659
|
try:
|
|
1660
|
-
data =
|
|
1660
|
+
data, spatial_meta = load_data_with_meta(filepath)
|
|
1661
1661
|
if cleanup:
|
|
1662
1662
|
try:
|
|
1663
1663
|
os.unlink(filepath)
|
|
@@ -1667,6 +1667,9 @@ def _serve_daemon(
|
|
|
1667
1667
|
data, filepath=None if cleanup else filepath, name=name
|
|
1668
1668
|
)
|
|
1669
1669
|
session.sid = sid
|
|
1670
|
+
session.spatial_meta = spatial_meta
|
|
1671
|
+
if spatial_meta is not None:
|
|
1672
|
+
session.original_volume = data
|
|
1670
1673
|
if rgb:
|
|
1671
1674
|
from arrayview._render import _setup_rgb
|
|
1672
1675
|
|
|
@@ -111,7 +111,10 @@ def _compute_vmin_vmax(session, data, dr=0, complex_mode=0):
|
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
def extract_slice(session, dim_x, dim_y, idx_list):
|
|
114
|
-
|
|
114
|
+
# Mask out indices along the displayed dims — they're slice(None) in the
|
|
115
|
+
# slicer below, so the extracted data doesn't depend on them.
|
|
116
|
+
key_idx = tuple(None if i in (dim_x, dim_y) else idx_list[i] for i in range(len(idx_list)))
|
|
117
|
+
key = (dim_x, dim_y, key_idx)
|
|
115
118
|
if key in session.raw_cache:
|
|
116
119
|
session.raw_cache.move_to_end(key)
|
|
117
120
|
return session.raw_cache[key]
|
|
@@ -154,7 +157,12 @@ def extract_projection(session, dim_x, dim_y, idx_list, proj_dim, proj_mode):
|
|
|
154
157
|
|
|
155
158
|
proj_mode: 1=max, 2=min, 3=mean, 4=std, 5=sos (sum of squares)
|
|
156
159
|
"""
|
|
157
|
-
|
|
160
|
+
# Mask out indices along the displayed/projected dims — they're slice(None).
|
|
161
|
+
key_idx = tuple(
|
|
162
|
+
None if i in (dim_x, dim_y, proj_dim) else idx_list[i]
|
|
163
|
+
for i in range(len(idx_list))
|
|
164
|
+
)
|
|
165
|
+
key = ("proj", dim_x, dim_y, key_idx, proj_dim, proj_mode)
|
|
158
166
|
if key in session.raw_cache:
|
|
159
167
|
session.raw_cache.move_to_end(key)
|
|
160
168
|
return session.raw_cache[key]
|
|
@@ -659,10 +667,12 @@ def render_mosaic(
|
|
|
659
667
|
complex_mode=0,
|
|
660
668
|
log_scale=False,
|
|
661
669
|
mosaic_cols=None,
|
|
670
|
+
vmin_override=None,
|
|
671
|
+
vmax_override=None,
|
|
662
672
|
):
|
|
663
673
|
idx_norm = list(idx_tuple)
|
|
664
674
|
idx_norm[dim_z] = 0
|
|
665
|
-
key = (dim_x, dim_y, dim_z, tuple(idx_norm), colormap, dr, complex_mode, log_scale, mosaic_cols)
|
|
675
|
+
key = (dim_x, dim_y, dim_z, tuple(idx_norm), colormap, dr, complex_mode, log_scale, mosaic_cols, vmin_override, vmax_override)
|
|
666
676
|
if key in session.mosaic_cache:
|
|
667
677
|
session.mosaic_cache.move_to_end(key)
|
|
668
678
|
return session.mosaic_cache[key]
|
|
@@ -682,7 +692,9 @@ def render_mosaic(
|
|
|
682
692
|
frames = [np.log1p(np.abs(f)).astype(np.float32) for f in frames]
|
|
683
693
|
all_data = np.stack(frames)
|
|
684
694
|
|
|
685
|
-
if
|
|
695
|
+
if vmin_override is not None and vmax_override is not None:
|
|
696
|
+
vmin, vmax = float(vmin_override), float(vmax_override)
|
|
697
|
+
elif complex_mode == 1 and np.iscomplexobj(session.data):
|
|
686
698
|
vmin, vmax = -float(np.pi), float(np.pi)
|
|
687
699
|
else:
|
|
688
700
|
vmin = float(np.percentile(all_data, 1))
|