arrayview 0.12.1__tar.gz → 0.13.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.12.1 → arrayview-0.13.0}/.claude/skills/viewer-ui-checklist/SKILL.md +10 -2
- {arrayview-0.12.1 → arrayview-0.13.0}/AGENTS.md +2 -1
- {arrayview-0.12.1 → arrayview-0.13.0}/PKG-INFO +1 -1
- {arrayview-0.12.1 → arrayview-0.13.0}/pyproject.toml +1 -1
- {arrayview-0.12.1 → arrayview-0.13.0}/scripts/release.sh +1 -1
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_launcher.py +79 -18
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_stdio_server.py +284 -2
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_viewer.html +279 -97
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_vscode.py +131 -10
- arrayview-0.13.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/uv.lock +1 -1
- {arrayview-0.12.1 → arrayview-0.13.0}/vscode-extension/extension.js +179 -39
- arrayview-0.13.0/vscode-extension/package.json +53 -0
- arrayview-0.12.1/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.12.1/vscode-extension/package.json +0 -30
- {arrayview-0.12.1 → arrayview-0.13.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/.gitignore +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/.python-version +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/CONTRIBUTING.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/LICENSE +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/README.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/comparing.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/configuration.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/display.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/index.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/loading.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/logo.png +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/measurement.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/remote.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/docs/viewing.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/matlab/arrayview.m +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/mkdocs.yml +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/scripts/demo.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/conftest.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_api.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_browser.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_cli.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_command_reachability.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_config.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_interactions.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/test_torch.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/ui_audit.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/tests/visual_smoke.py +0 -0
- {arrayview-0.12.1 → arrayview-0.13.0}/vscode-extension/LICENSE +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: viewer-ui-checklist
|
|
3
|
-
description: Use when adding keyboard shortcuts, changing layout, or making any UI change to arrayview. Ensures visual_smoke.py
|
|
3
|
+
description: Use when adding keyboard shortcuts, changing layout, or making any UI change to arrayview. Ensures visual_smoke.py, help overlay, and README stay in sync.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ArrayView UI Checklist
|
|
7
7
|
|
|
8
8
|
## Rule
|
|
9
9
|
|
|
10
|
-
Every UI change to arrayview MUST be reflected in `tests/visual_smoke.py` before the task is complete.
|
|
10
|
+
Every UI change to arrayview MUST be reflected in `tests/visual_smoke.py`, the help overlay, and (when user-facing) `README.md` before the task is complete.
|
|
11
11
|
|
|
12
12
|
## What counts as a UI change
|
|
13
13
|
|
|
@@ -35,6 +35,14 @@ Every UI change to arrayview MUST be reflected in `tests/visual_smoke.py` before
|
|
|
35
35
|
|
|
36
36
|
4. **Open and review** the new screenshots in `tests/smoke_output/`
|
|
37
37
|
|
|
38
|
+
5. **Update `GUIDE_TABS`** in `_viewer.html` if you added, removed, or changed a keybinding
|
|
39
|
+
- Add/update the entry in the appropriate tab and section
|
|
40
|
+
- Include a `hint` for non-obvious shortcuts
|
|
41
|
+
- Use the `docs-style` skill for formatting rules
|
|
42
|
+
|
|
43
|
+
6. **Update `README.md`** if the change is user-facing (new CLI flag, new API, new mode)
|
|
44
|
+
- Use the `docs-style` skill for formatting rules
|
|
45
|
+
|
|
38
46
|
## Red flags — STOP
|
|
39
47
|
|
|
40
48
|
- "The shortcut is too simple to need a smoke test" → ALL shortcuts need entries
|
|
@@ -15,7 +15,8 @@ Load the relevant skill before touching the corresponding area.
|
|
|
15
15
|
| `frontend-designer` | Styling/layout changes to `_viewer.html` |
|
|
16
16
|
| `vscode-simplebrowser` | Extension, signal-file IPC, `_VSCODE_EXT_VERSION` |
|
|
17
17
|
| `invocation-consistency` | Server startup, display-opening, env detection |
|
|
18
|
-
| `
|
|
18
|
+
| `viewer-ui-checklist` | Any UI change (smoke tests, help overlay, README sync) |
|
|
19
|
+
| `docs-style` | README, help overlay, docstrings (formatting rules) |
|
|
19
20
|
|
|
20
21
|
## Non-Negotiables
|
|
21
22
|
|
|
@@ -66,7 +66,7 @@ run() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
# --- Generate release notes ---
|
|
69
|
-
CLAUDE_BIN="${CLAUDE_BIN
|
|
69
|
+
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
|
|
70
70
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "")
|
|
71
71
|
NOTES=""
|
|
72
72
|
|
|
@@ -2102,24 +2102,67 @@ def arrayview():
|
|
|
2102
2102
|
from arrayview._io import load_data
|
|
2103
2103
|
from arrayview._session import SESSIONS, Session
|
|
2104
2104
|
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2105
|
+
# First file is the main array
|
|
2106
|
+
base_path = args.files[0]
|
|
2107
|
+
data = load_data(base_path)
|
|
2108
|
+
session = Session(
|
|
2109
|
+
data=data,
|
|
2110
|
+
filepath=base_path,
|
|
2111
|
+
name=_Path(base_path).name,
|
|
2112
|
+
)
|
|
2113
|
+
if getattr(args, "rgb", False):
|
|
2114
|
+
_setup_rgb(session)
|
|
2115
|
+
|
|
2116
|
+
# Attach vector field if provided
|
|
2117
|
+
if getattr(args, "vectorfield", None):
|
|
2118
|
+
from arrayview._server import _configure_vectorfield
|
|
2119
|
+
|
|
2120
|
+
vf_data = load_data(args.vectorfield)
|
|
2121
|
+
_configure_vectorfield(
|
|
2122
|
+
session, vf_data,
|
|
2123
|
+
getattr(args, "vectorfield_components_dim", None),
|
|
2111
2124
|
)
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2125
|
+
|
|
2126
|
+
SESSIONS[session.sid] = session
|
|
2127
|
+
|
|
2128
|
+
# Load overlay as a separate session
|
|
2129
|
+
overlay_sid = None
|
|
2130
|
+
if getattr(args, "overlay", None):
|
|
2131
|
+
ov_data = load_data(args.overlay)
|
|
2132
|
+
ov_session = Session(
|
|
2133
|
+
data=ov_data,
|
|
2134
|
+
filepath=args.overlay,
|
|
2135
|
+
name="overlay",
|
|
2121
2136
|
)
|
|
2122
|
-
|
|
2137
|
+
SESSIONS[ov_session.sid] = ov_session
|
|
2138
|
+
overlay_sid = ov_session.sid
|
|
2139
|
+
|
|
2140
|
+
# Load compare files (additional positional args + --compare)
|
|
2141
|
+
compare_sids = []
|
|
2142
|
+
compare_paths = list(args.files[1:])
|
|
2143
|
+
if getattr(args, "compare", None):
|
|
2144
|
+
compare_paths.append(args.compare)
|
|
2145
|
+
for cp in compare_paths:
|
|
2146
|
+
cmp_data = load_data(cp)
|
|
2147
|
+
cmp_session = Session(
|
|
2148
|
+
data=cmp_data,
|
|
2149
|
+
filepath=cp,
|
|
2150
|
+
name=_Path(cp).name,
|
|
2151
|
+
)
|
|
2152
|
+
SESSIONS[cmp_session.sid] = cmp_session
|
|
2153
|
+
compare_sids.append(cmp_session.sid)
|
|
2154
|
+
|
|
2155
|
+
info = json.dumps(
|
|
2156
|
+
{
|
|
2157
|
+
"sid": session.sid,
|
|
2158
|
+
"name": session.name,
|
|
2159
|
+
"shape": [int(s) for s in session.shape],
|
|
2160
|
+
"has_vectorfield": session.vfield is not None,
|
|
2161
|
+
"overlay_sid": overlay_sid,
|
|
2162
|
+
"compare_sids": compare_sids or None,
|
|
2163
|
+
}
|
|
2164
|
+
)
|
|
2165
|
+
print(f"SESSION:{info}", file=sys.stderr)
|
|
2123
2166
|
run_stdio_server()
|
|
2124
2167
|
return
|
|
2125
2168
|
|
|
@@ -2662,9 +2705,27 @@ def arrayview():
|
|
|
2662
2705
|
# WebSocket server entirely. The extension spawns a Python subprocess
|
|
2663
2706
|
# with --mode stdio and bridges via postMessage — no ports needed.
|
|
2664
2707
|
is_remote = _is_vscode_remote()
|
|
2665
|
-
if is_remote
|
|
2708
|
+
if is_remote:
|
|
2666
2709
|
_ensure_vscode_extension()
|
|
2667
|
-
|
|
2710
|
+
# Build generic extra CLI args so new flags work without touching the
|
|
2711
|
+
# VS Code extension — they're forwarded verbatim to the subprocess.
|
|
2712
|
+
extra_args: list[str] = []
|
|
2713
|
+
if args.vectorfield:
|
|
2714
|
+
extra_args += ["--vectorfield", os.path.abspath(args.vectorfield)]
|
|
2715
|
+
if vfield_components_dim is not None:
|
|
2716
|
+
extra_args += ["--vectorfield-components-dim", str(vfield_components_dim)]
|
|
2717
|
+
if args.overlay:
|
|
2718
|
+
extra_args += ["--overlay", os.path.abspath(args.overlay)]
|
|
2719
|
+
if args.rgb:
|
|
2720
|
+
extra_args.append("--rgb")
|
|
2721
|
+
# Compare files: pass as additional positional args
|
|
2722
|
+
for cf in compare_files:
|
|
2723
|
+
extra_args.append(cf)
|
|
2724
|
+
_open_direct_via_signal_file(
|
|
2725
|
+
base_file,
|
|
2726
|
+
title=f"ArrayView: {name}",
|
|
2727
|
+
extra_args=extra_args or None,
|
|
2728
|
+
)
|
|
2668
2729
|
return
|
|
2669
2730
|
|
|
2670
2731
|
sid = uuid.uuid4().hex
|
|
@@ -24,7 +24,10 @@ import numpy as np
|
|
|
24
24
|
|
|
25
25
|
from arrayview._io import load_data, load_data_with_meta
|
|
26
26
|
from arrayview._render import (
|
|
27
|
+
LUTS,
|
|
27
28
|
_composite_overlay_mask,
|
|
29
|
+
_compute_vmin_vmax,
|
|
30
|
+
_ensure_lut,
|
|
28
31
|
_extract_overlay_mask,
|
|
29
32
|
_init_luts,
|
|
30
33
|
_overlay_is_label_map,
|
|
@@ -32,6 +35,7 @@ from arrayview._render import (
|
|
|
32
35
|
apply_complex_mode,
|
|
33
36
|
extract_projection,
|
|
34
37
|
extract_slice,
|
|
38
|
+
mosaic_shape,
|
|
35
39
|
render_mosaic,
|
|
36
40
|
render_projection_rgba,
|
|
37
41
|
render_rgb_rgba,
|
|
@@ -485,6 +489,258 @@ def _handle_lebesgue(sid: str, params: dict[str, str]) -> None:
|
|
|
485
489
|
})
|
|
486
490
|
|
|
487
491
|
|
|
492
|
+
def _handle_vectorfield_fetch(sid: str, params: dict) -> None:
|
|
493
|
+
"""Return downsampled vector field arrows for a 2-D view."""
|
|
494
|
+
session = SESSIONS.get(sid)
|
|
495
|
+
if not session or session.vfield is None:
|
|
496
|
+
_write_json({"error": "no vectorfield"})
|
|
497
|
+
return
|
|
498
|
+
dim_x = int(params.get("dim_x", 0))
|
|
499
|
+
dim_y = int(params.get("dim_y", 0))
|
|
500
|
+
indices = params.get("indices", "")
|
|
501
|
+
idx_tuple = tuple(int(x) for x in indices.split(",")) if indices else ()
|
|
502
|
+
t_index = int(params.get("t_index", 0))
|
|
503
|
+
density_offset = int(params.get("density_offset", 0))
|
|
504
|
+
result = _compute_vfield_arrows(session, dim_x, dim_y, idx_tuple, t_index, density_offset)
|
|
505
|
+
if result is None:
|
|
506
|
+
_write_json({"error": "vectorfield computation failed"})
|
|
507
|
+
return
|
|
508
|
+
_write_json({
|
|
509
|
+
"arrows": result["arrows"].tolist(),
|
|
510
|
+
"scale": result["scale"],
|
|
511
|
+
"stride": result["stride"],
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _handle_pixel(sid: str, params: dict) -> None:
|
|
516
|
+
"""Return the raw value at a single pixel."""
|
|
517
|
+
session = SESSIONS.get(sid)
|
|
518
|
+
if not session:
|
|
519
|
+
_write_json({"error": "session not found"})
|
|
520
|
+
return
|
|
521
|
+
if session.rgb_axis is not None:
|
|
522
|
+
_write_json({"value": None})
|
|
523
|
+
return
|
|
524
|
+
dim_x = int(params.get("dim_x", 0))
|
|
525
|
+
dim_y = int(params.get("dim_y", 0))
|
|
526
|
+
indices = params.get("indices", "")
|
|
527
|
+
idx_tuple = tuple(int(x) for x in indices.split(",")) if indices else ()
|
|
528
|
+
px = int(params.get("px", 0))
|
|
529
|
+
py = int(params.get("py", 0))
|
|
530
|
+
complex_mode = int(params.get("complex_mode", 0))
|
|
531
|
+
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
532
|
+
data = apply_complex_mode(raw, complex_mode)
|
|
533
|
+
h, w = data.shape
|
|
534
|
+
if 0 <= py < h and 0 <= px < w:
|
|
535
|
+
v = data[py, px]
|
|
536
|
+
val = None if np.isnan(v) or np.isinf(v) else float(v)
|
|
537
|
+
else:
|
|
538
|
+
val = None
|
|
539
|
+
_write_json({"value": val})
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# ---------------------------------------------------------------------------
|
|
543
|
+
# Diff / Compare helpers
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
def _render_normalized(session, dim_x, dim_y, idx_tuple, dr, complex_mode, log_scale):
|
|
547
|
+
"""Extract a slice and normalize to [0, 1] float32."""
|
|
548
|
+
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
549
|
+
data, vmin, vmax = _prepare_display(session, raw, complex_mode, dr, log_scale)
|
|
550
|
+
if vmax > vmin:
|
|
551
|
+
normalized = np.clip((data - vmin) / (vmax - vmin), 0, 1)
|
|
552
|
+
else:
|
|
553
|
+
normalized = np.zeros_like(data)
|
|
554
|
+
return normalized.astype(np.float32)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _render_normalized_mosaic(session, dim_x, dim_y, dim_z, idx_tuple, dr, complex_mode, log_scale):
|
|
558
|
+
"""Return (float32 normalized mosaic [0,1], nan_mask)."""
|
|
559
|
+
n = session.shape[dim_z]
|
|
560
|
+
idx_list = list(idx_tuple)
|
|
561
|
+
frames_raw = [
|
|
562
|
+
extract_slice(
|
|
563
|
+
session, dim_x, dim_y,
|
|
564
|
+
[i if j == dim_z else idx_list[j] for j in range(len(session.shape))],
|
|
565
|
+
)
|
|
566
|
+
for i in range(n)
|
|
567
|
+
]
|
|
568
|
+
frames = [apply_complex_mode(f, complex_mode) for f in frames_raw]
|
|
569
|
+
if log_scale:
|
|
570
|
+
frames = [np.log1p(np.abs(f)).astype(np.float32) for f in frames]
|
|
571
|
+
all_data = np.stack(frames)
|
|
572
|
+
if log_scale:
|
|
573
|
+
vmin = float(np.percentile(all_data, 1))
|
|
574
|
+
vmax = float(np.percentile(all_data, 99))
|
|
575
|
+
else:
|
|
576
|
+
vmin, vmax = _compute_vmin_vmax(session, all_data, dr, complex_mode)
|
|
577
|
+
rows, cols = mosaic_shape(n)
|
|
578
|
+
H, W = frames[0].shape
|
|
579
|
+
GAP = 2
|
|
580
|
+
total_h = rows * H + (rows - 1) * GAP
|
|
581
|
+
total_w = cols * W + (cols - 1) * GAP
|
|
582
|
+
grid = np.full((total_h, total_w), np.nan, dtype=np.float32)
|
|
583
|
+
for k in range(n):
|
|
584
|
+
r, c = divmod(k, cols)
|
|
585
|
+
r0, c0 = r * (H + GAP), c * (W + GAP)
|
|
586
|
+
grid[r0 : r0 + H, c0 : c0 + W] = all_data[k]
|
|
587
|
+
nan_mask = np.isnan(grid)
|
|
588
|
+
if vmax > vmin:
|
|
589
|
+
normalized = np.clip(np.where(nan_mask, 0.0, (grid - vmin) / (vmax - vmin)), 0, 1)
|
|
590
|
+
else:
|
|
591
|
+
normalized = np.zeros_like(grid)
|
|
592
|
+
return normalized.astype(np.float32), nan_mask
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _compute_diff(session_a, session_b, dim_x, dim_y, indices, dim_z, dr, complex_mode, log_scale, diff_mode):
|
|
596
|
+
"""Shared diff logic for both diff image and diff histogram handlers.
|
|
597
|
+
|
|
598
|
+
Returns (raw_diff, vmin, vmax, colormap, nan_mask_or_None).
|
|
599
|
+
"""
|
|
600
|
+
idx_tuple = tuple(int(x) for x in indices.split(",")) if isinstance(indices, str) else indices
|
|
601
|
+
ndim_a = len(session_a.shape)
|
|
602
|
+
ndim_b = len(session_b.shape)
|
|
603
|
+
idx_a = idx_tuple[:ndim_a]
|
|
604
|
+
idx_b = idx_tuple[:ndim_b]
|
|
605
|
+
nan_mask = None
|
|
606
|
+
|
|
607
|
+
if dim_z >= 0:
|
|
608
|
+
a, nan_mask_a = _render_normalized_mosaic(session_a, dim_x, dim_y, dim_z, idx_a, dr, complex_mode, log_scale)
|
|
609
|
+
b, nan_mask_b = _render_normalized_mosaic(session_b, dim_x, dim_y, dim_z, idx_b, dr, complex_mode, log_scale)
|
|
610
|
+
nan_mask = nan_mask_a | nan_mask_b
|
|
611
|
+
else:
|
|
612
|
+
a = _render_normalized(session_a, dim_x, dim_y, idx_a, dr, complex_mode, log_scale)
|
|
613
|
+
b = _render_normalized(session_b, dim_x, dim_y, idx_b, dr, complex_mode, log_scale)
|
|
614
|
+
|
|
615
|
+
# Resize b to match a if shapes differ
|
|
616
|
+
if a.shape != b.shape:
|
|
617
|
+
b_img = _pil_image().fromarray((b * 255).astype(np.uint8), mode="L")
|
|
618
|
+
b_img = b_img.resize((a.shape[1], a.shape[0]), _pil_image().BILINEAR)
|
|
619
|
+
b = np.array(b_img, dtype=np.float32) / 255.0
|
|
620
|
+
|
|
621
|
+
if diff_mode == 1:
|
|
622
|
+
raw = a - b
|
|
623
|
+
vmin, vmax = -1.0, 1.0
|
|
624
|
+
colormap = "RdBu_r"
|
|
625
|
+
elif diff_mode == 2:
|
|
626
|
+
raw = np.abs(a - b)
|
|
627
|
+
vmax = float(raw.max()) or 1.0
|
|
628
|
+
vmin = 0.0
|
|
629
|
+
colormap = "afmhot"
|
|
630
|
+
else:
|
|
631
|
+
raw = np.abs(a - b) / np.maximum(np.abs(a), 1e-6)
|
|
632
|
+
raw = np.clip(raw, 0.0, 2.0).astype(np.float32)
|
|
633
|
+
vmax = float(raw.max()) or 1.0
|
|
634
|
+
vmin = 0.0
|
|
635
|
+
colormap = "afmhot"
|
|
636
|
+
|
|
637
|
+
return raw, vmin, vmax, colormap, nan_mask
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _handle_diff(sid_a: str, sid_b: str, params: dict) -> None:
|
|
641
|
+
"""Render a diff image and return as base64 JPEG with metadata headers."""
|
|
642
|
+
import base64
|
|
643
|
+
import io
|
|
644
|
+
|
|
645
|
+
session_a = SESSIONS.get(sid_a)
|
|
646
|
+
session_b = SESSIONS.get(sid_b)
|
|
647
|
+
if not session_a or not session_b:
|
|
648
|
+
_write_json({"error": "session not found"})
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
dim_x = int(params.get("dim_x", 0))
|
|
652
|
+
dim_y = int(params.get("dim_y", 0))
|
|
653
|
+
indices = params.get("indices", "")
|
|
654
|
+
dim_z = int(params.get("dim_z", -1))
|
|
655
|
+
dr = int(params.get("dr", 1))
|
|
656
|
+
complex_mode = int(params.get("complex_mode", 0))
|
|
657
|
+
log_scale = params.get("log_scale", "false").lower() == "true"
|
|
658
|
+
diff_mode = int(params.get("diff_mode", 1))
|
|
659
|
+
diff_colormap = params.get("diff_colormap", "")
|
|
660
|
+
_vmin_ov = params.get("vmin_override")
|
|
661
|
+
_vmax_ov = params.get("vmax_override")
|
|
662
|
+
vmin_override = float(_vmin_ov) if _vmin_ov is not None else None
|
|
663
|
+
vmax_override = float(_vmax_ov) if _vmax_ov is not None else None
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
raw, vmin, vmax, colormap, nan_mask = _compute_diff(
|
|
667
|
+
session_a, session_b, dim_x, dim_y, indices,
|
|
668
|
+
dim_z, dr, complex_mode, log_scale, diff_mode,
|
|
669
|
+
)
|
|
670
|
+
except Exception as e:
|
|
671
|
+
_write_json({"error": str(e)})
|
|
672
|
+
return
|
|
673
|
+
|
|
674
|
+
if vmin_override is not None:
|
|
675
|
+
vmin = vmin_override
|
|
676
|
+
if vmax_override is not None:
|
|
677
|
+
vmax = vmax_override
|
|
678
|
+
if diff_colormap and _ensure_lut(diff_colormap):
|
|
679
|
+
colormap = diff_colormap
|
|
680
|
+
|
|
681
|
+
if vmax > vmin:
|
|
682
|
+
normalized = np.clip((raw - vmin) / (vmax - vmin), 0, 1)
|
|
683
|
+
else:
|
|
684
|
+
normalized = np.zeros_like(raw)
|
|
685
|
+
_ensure_lut(colormap)
|
|
686
|
+
lut = LUTS.get(colormap, LUTS["gray"])
|
|
687
|
+
rgba = lut[(normalized * 255).astype(np.uint8)]
|
|
688
|
+
if nan_mask is not None and nan_mask.shape == rgba.shape[:2]:
|
|
689
|
+
rgba[nan_mask] = [22, 22, 22, 255]
|
|
690
|
+
|
|
691
|
+
img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
|
|
692
|
+
buf = io.BytesIO()
|
|
693
|
+
img.save(buf, format="JPEG", quality=90)
|
|
694
|
+
|
|
695
|
+
_write_json({
|
|
696
|
+
"_binary": True,
|
|
697
|
+
"data": base64.b64encode(buf.getvalue()).decode("ascii"),
|
|
698
|
+
"headers": {
|
|
699
|
+
"X-ArrayView-Vmin": str(vmin),
|
|
700
|
+
"X-ArrayView-Vmax": str(vmax),
|
|
701
|
+
"X-ArrayView-Colormap": colormap,
|
|
702
|
+
},
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _handle_diff_histogram(sid_a: str, sid_b: str, params: dict) -> None:
|
|
707
|
+
"""Compute histogram of the diff between two sessions."""
|
|
708
|
+
session_a = SESSIONS.get(sid_a)
|
|
709
|
+
session_b = SESSIONS.get(sid_b)
|
|
710
|
+
if not session_a or not session_b:
|
|
711
|
+
_write_json({"error": "session not found"})
|
|
712
|
+
return
|
|
713
|
+
|
|
714
|
+
dim_x = int(params.get("dim_x", 0))
|
|
715
|
+
dim_y = int(params.get("dim_y", 0))
|
|
716
|
+
indices = params.get("indices", "")
|
|
717
|
+
dim_z = int(params.get("dim_z", -1))
|
|
718
|
+
dr = int(params.get("dr", 1))
|
|
719
|
+
complex_mode = int(params.get("complex_mode", 0))
|
|
720
|
+
log_scale = params.get("log_scale", "false").lower() == "true"
|
|
721
|
+
diff_mode = int(params.get("diff_mode", 1))
|
|
722
|
+
bins = int(params.get("bins", 64))
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
raw, _, _, _, _ = _compute_diff(
|
|
726
|
+
session_a, session_b, dim_x, dim_y, indices,
|
|
727
|
+
dim_z, dr, complex_mode, log_scale, diff_mode,
|
|
728
|
+
)
|
|
729
|
+
except Exception as e:
|
|
730
|
+
_write_json({"error": str(e)})
|
|
731
|
+
return
|
|
732
|
+
|
|
733
|
+
vmin = float(raw.min())
|
|
734
|
+
vmax = float(raw.max())
|
|
735
|
+
counts, edges = np.histogram(raw.ravel(), bins=bins)
|
|
736
|
+
_write_json({
|
|
737
|
+
"counts": counts.tolist(),
|
|
738
|
+
"edges": edges.tolist(),
|
|
739
|
+
"vmin": vmin,
|
|
740
|
+
"vmax": vmax,
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
|
|
488
744
|
def _handle_fetch_proxy(msg: dict) -> None:
|
|
489
745
|
"""Handle proxied fetch requests from the viewer."""
|
|
490
746
|
endpoint = msg.get("endpoint", "")
|
|
@@ -516,6 +772,14 @@ def _handle_fetch_proxy(msg: dict) -> None:
|
|
|
516
772
|
_write_json({"status": "started"})
|
|
517
773
|
elif route == "preload_status" and sid:
|
|
518
774
|
_write_json({"done": 0, "total": 0, "skipped": True})
|
|
775
|
+
elif route == "vectorfield" and sid:
|
|
776
|
+
_handle_vectorfield_fetch(sid, params)
|
|
777
|
+
elif route == "pixel" and sid:
|
|
778
|
+
_handle_pixel(sid, params)
|
|
779
|
+
elif route == "diff" and len(parts) >= 3:
|
|
780
|
+
_handle_diff(parts[1], parts[2], params)
|
|
781
|
+
elif route == "diff-histogram" and len(parts) >= 3:
|
|
782
|
+
_handle_diff_histogram(parts[1], parts[2], params)
|
|
519
783
|
else:
|
|
520
784
|
_write_error(f"unsupported endpoint: {endpoint}")
|
|
521
785
|
|
|
@@ -637,7 +901,7 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
637
901
|
"""Return the rendered viewer HTML with template substitutions."""
|
|
638
902
|
from importlib.resources import files as _pkg_files
|
|
639
903
|
|
|
640
|
-
from arrayview._config import get_viewer_colormaps
|
|
904
|
+
from arrayview._config import get_viewer_colormaps, get_viewer_theme
|
|
641
905
|
from arrayview._render import COLORMAP_GRADIENT_STOPS, COMPLEX_MODES, REAL_MODES
|
|
642
906
|
from arrayview._session import COLORMAPS
|
|
643
907
|
|
|
@@ -648,7 +912,24 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
648
912
|
|
|
649
913
|
sid = msg.get("sid", "")
|
|
650
914
|
|
|
651
|
-
|
|
915
|
+
# Build query string from session info — the extension forwards the full
|
|
916
|
+
# SESSION: payload so new features (overlay_sid, compare, etc.) work
|
|
917
|
+
# without touching extension code.
|
|
918
|
+
qs = f"?sid={sid}&transport=postMessage"
|
|
919
|
+
overlay_sid = msg.get("overlay_sid")
|
|
920
|
+
if overlay_sid:
|
|
921
|
+
qs += f"&overlay_sid={overlay_sid}"
|
|
922
|
+
compare_sids = msg.get("compare_sids")
|
|
923
|
+
if compare_sids:
|
|
924
|
+
sids = compare_sids if isinstance(compare_sids, list) else [s.strip() for s in str(compare_sids).split(",") if s.strip()]
|
|
925
|
+
if sids:
|
|
926
|
+
qs += f"&compare_sid={sids[0]}"
|
|
927
|
+
qs += f"&compare_sids={','.join(sids)}"
|
|
928
|
+
query_val = json.dumps(qs) if sid else "null"
|
|
929
|
+
|
|
930
|
+
_theme_names = ["dark", "light", "solarized", "nord"]
|
|
931
|
+
_cfg_theme = get_viewer_theme()
|
|
932
|
+
_default_theme_idx = _theme_names.index(_cfg_theme) if _cfg_theme in _theme_names else 0
|
|
652
933
|
|
|
653
934
|
html = (
|
|
654
935
|
template.replace("__COLORMAPS__", str(_active_colormaps))
|
|
@@ -656,6 +937,7 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
656
937
|
.replace("__COMPLEX_MODES__", str(COMPLEX_MODES))
|
|
657
938
|
.replace("__REAL_MODES__", str(REAL_MODES))
|
|
658
939
|
.replace("__ARRAYVIEW_QUERY__", query_val)
|
|
940
|
+
.replace("__DEFAULT_THEME_IDX__", str(_default_theme_idx))
|
|
659
941
|
)
|
|
660
942
|
|
|
661
943
|
_write_json({"html": html})
|