arrayview 0.48.3__tar.gz → 0.48.4__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.48.3 → arrayview-0.48.4}/PKG-INFO +1 -1
- {arrayview-0.48.3 → arrayview-0.48.4}/pyproject.toml +1 -1
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_diff.py +60 -44
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_render.py +49 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_rendering.py +48 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_websocket.py +48 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_session.py +3 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_viewer.html +300 -80
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_backend_shared.py +23 -1
- {arrayview-0.48.3 → arrayview-0.48.4}/.gitignore +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/LICENSE +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/README.md +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/__init__.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/__main__.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_app.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_bootstrap_app.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_config.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_diagnostics.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_extra_ports.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_icon.png +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_io.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_platform.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_loading.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_server.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_shell.html +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_source_safety.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_torch.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_tutorial.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_vscode_extension.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/_watch.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/src/arrayview/todo.md +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/capture_v_animation.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/conftest.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_1d_arrays.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_api.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_browser.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_cli.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_config.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_dicom.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_drop_import.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_instance_registry.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_io.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_large_arrays.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_nifti_series.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_source_safety.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_torch.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_tutorial.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/test_vsix_bundle.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/ui_audit.py +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.48.3 → arrayview-0.48.4}/tests/visual_smoke.py +0 -0
|
@@ -85,6 +85,43 @@ def _resize_normalized_like(source: np.ndarray, target: np.ndarray) -> np.ndarra
|
|
|
85
85
|
return np.array(image, dtype=np.float32) / 255.0
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def _render_values(session, dim_x, dim_y, idx_tuple, complex_mode, log_scale):
|
|
89
|
+
"""Extract a slice in the array's own units (complex/log applied, no rescale)."""
|
|
90
|
+
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
91
|
+
data = apply_complex_mode(raw, complex_mode)
|
|
92
|
+
if log_scale:
|
|
93
|
+
data = np.log1p(np.abs(data))
|
|
94
|
+
return np.asarray(data, dtype=np.float32)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _render_values_mosaic(session, dim_x, dim_y, dim_z, idx_tuple, complex_mode, log_scale):
|
|
98
|
+
"""Mosaic of every dim_z slice in the array's own units, plus separator mask."""
|
|
99
|
+
idx_list = list(idx_tuple)
|
|
100
|
+
frames = [
|
|
101
|
+
_render_values(
|
|
102
|
+
session,
|
|
103
|
+
dim_x,
|
|
104
|
+
dim_y,
|
|
105
|
+
[i if j == dim_z else idx_list[j] for j in range(len(session.shape))],
|
|
106
|
+
complex_mode,
|
|
107
|
+
log_scale,
|
|
108
|
+
)
|
|
109
|
+
for i in range(session.shape[dim_z])
|
|
110
|
+
]
|
|
111
|
+
all_data = np.stack(frames)
|
|
112
|
+
grid, _, _ = _build_mosaic_grid(all_data, len(frames))
|
|
113
|
+
return grid.astype(np.float32), np.isnan(grid)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _resize_values_like(source: np.ndarray, target: np.ndarray) -> np.ndarray:
|
|
117
|
+
"""Resize float source data to target shape without quantising it."""
|
|
118
|
+
if source.shape == target.shape:
|
|
119
|
+
return source
|
|
120
|
+
image = _pil_image().fromarray(np.nan_to_num(source).astype(np.float32), mode="F")
|
|
121
|
+
image = image.resize((target.shape[1], target.shape[0]), _pil_image().BILINEAR)
|
|
122
|
+
return np.array(image, dtype=np.float32)
|
|
123
|
+
|
|
124
|
+
|
|
88
125
|
def _compute_diff(
|
|
89
126
|
session_a,
|
|
90
127
|
session_b,
|
|
@@ -114,65 +151,41 @@ def _compute_diff(
|
|
|
114
151
|
idx_b = _parse_indices(indices_b)[:ndim_b] if indices_b is not None else idx_tuple[:ndim_b]
|
|
115
152
|
nan_mask = None
|
|
116
153
|
|
|
154
|
+
# True A−B in the arrays' own units. Each side used to be rescaled to its
|
|
155
|
+
# own per-slice display range first, which cancelled real differences
|
|
156
|
+
# (B = 0.9·A showed as zero) and made the map flicker while scrolling.
|
|
117
157
|
if dim_z >= 0:
|
|
118
|
-
a, nan_mask_a =
|
|
119
|
-
session_a,
|
|
120
|
-
dim_x,
|
|
121
|
-
dim_y,
|
|
122
|
-
dim_z,
|
|
123
|
-
idx_a,
|
|
124
|
-
dr,
|
|
125
|
-
complex_mode,
|
|
126
|
-
log_scale,
|
|
158
|
+
a, nan_mask_a = _render_values_mosaic(
|
|
159
|
+
session_a, dim_x, dim_y, dim_z, idx_a, complex_mode, log_scale
|
|
127
160
|
)
|
|
128
|
-
b, nan_mask_b =
|
|
129
|
-
session_b,
|
|
130
|
-
dim_x,
|
|
131
|
-
dim_y,
|
|
132
|
-
dim_z,
|
|
133
|
-
idx_b,
|
|
134
|
-
dr,
|
|
135
|
-
complex_mode,
|
|
136
|
-
log_scale,
|
|
161
|
+
b, nan_mask_b = _render_values_mosaic(
|
|
162
|
+
session_b, dim_x, dim_y, dim_z, idx_b, complex_mode, log_scale
|
|
137
163
|
)
|
|
138
|
-
|
|
164
|
+
b = _resize_values_like(b, a)
|
|
165
|
+
if nan_mask_b.shape == nan_mask_a.shape:
|
|
166
|
+
nan_mask = nan_mask_a | nan_mask_b
|
|
167
|
+
else:
|
|
168
|
+
nan_mask = nan_mask_a
|
|
139
169
|
else:
|
|
140
|
-
a =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
dim_y,
|
|
144
|
-
idx_a,
|
|
145
|
-
dr,
|
|
146
|
-
complex_mode,
|
|
147
|
-
log_scale,
|
|
148
|
-
)
|
|
149
|
-
b = _render_normalized(
|
|
150
|
-
session_b,
|
|
151
|
-
dim_x,
|
|
152
|
-
dim_y,
|
|
153
|
-
idx_b,
|
|
154
|
-
dr,
|
|
155
|
-
complex_mode,
|
|
156
|
-
log_scale,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
b = _resize_normalized_like(b, a)
|
|
170
|
+
a = _render_values(session_a, dim_x, dim_y, idx_a, complex_mode, log_scale)
|
|
171
|
+
b = _render_values(session_b, dim_x, dim_y, idx_b, complex_mode, log_scale)
|
|
172
|
+
b = _resize_values_like(b, a)
|
|
160
173
|
if diff_mode == 1:
|
|
161
174
|
raw = a - b
|
|
162
175
|
lo = float(np.nanpercentile(raw, 1))
|
|
163
176
|
hi = float(np.nanpercentile(raw, 99))
|
|
164
177
|
vmax = max(abs(lo), abs(hi)) or 1.0
|
|
165
178
|
vmin = -vmax
|
|
166
|
-
colormap = "
|
|
179
|
+
colormap = "icefire"
|
|
167
180
|
elif diff_mode == 2:
|
|
168
181
|
raw = np.abs(a - b)
|
|
169
|
-
vmax = float(
|
|
182
|
+
vmax = float(np.nanmax(raw)) or 1.0
|
|
170
183
|
vmin = 0.0
|
|
171
184
|
colormap = "afmhot"
|
|
172
185
|
else:
|
|
173
186
|
raw = np.abs(a - b) / np.maximum(np.abs(a), 1e-6)
|
|
174
187
|
raw = np.clip(raw, 0.0, 2.0).astype(np.float32)
|
|
175
|
-
vmax = float(
|
|
188
|
+
vmax = float(np.nanmax(raw)) or 1.0
|
|
176
189
|
vmin = 0.0
|
|
177
190
|
colormap = "afmhot"
|
|
178
191
|
|
|
@@ -188,7 +201,7 @@ def _render_diff_rgba(
|
|
|
188
201
|
) -> np.ndarray:
|
|
189
202
|
"""Map raw diff data to RGBA."""
|
|
190
203
|
if vmax > vmin:
|
|
191
|
-
normalized = np.clip((raw - vmin) / (vmax - vmin), 0, 1)
|
|
204
|
+
normalized = np.clip(np.nan_to_num((raw - vmin) / (vmax - vmin)), 0, 1)
|
|
192
205
|
else:
|
|
193
206
|
normalized = np.zeros_like(raw)
|
|
194
207
|
_ensure_lut(colormap)
|
|
@@ -201,9 +214,12 @@ def _render_diff_rgba(
|
|
|
201
214
|
|
|
202
215
|
def _diff_histogram(raw: np.ndarray, bins: int) -> dict[str, object]:
|
|
203
216
|
"""Return histogram payload for raw diff data."""
|
|
217
|
+
raw = raw[np.isfinite(raw)]
|
|
218
|
+
if raw.size == 0:
|
|
219
|
+
raw = np.zeros(1, dtype=np.float32)
|
|
204
220
|
vmin = float(raw.min())
|
|
205
221
|
vmax = float(raw.max())
|
|
206
|
-
counts, edges = np.histogram(raw
|
|
222
|
+
counts, edges = np.histogram(raw, bins=bins)
|
|
207
223
|
return {
|
|
208
224
|
"counts": counts.tolist(),
|
|
209
225
|
"edges": edges.tolist(),
|
|
@@ -34,6 +34,43 @@ _mpl_colormaps = None
|
|
|
34
34
|
_luts_lock = threading.Lock()
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
_ICEFIRE_STOPS = [
|
|
38
|
+
(0.7394, 0.9044, 0.8576),
|
|
39
|
+
(0.6645, 0.8497, 0.8387),
|
|
40
|
+
(0.5749, 0.7908, 0.8202),
|
|
41
|
+
(0.4818, 0.734, 0.8084),
|
|
42
|
+
(0.3863, 0.6778, 0.8043),
|
|
43
|
+
(0.3052, 0.6184, 0.8053),
|
|
44
|
+
(0.2418, 0.5555, 0.8106),
|
|
45
|
+
(0.2141, 0.4873, 0.8157),
|
|
46
|
+
(0.2487, 0.4134, 0.7887),
|
|
47
|
+
(0.2831, 0.3444, 0.715),
|
|
48
|
+
(0.2846, 0.2908, 0.6028),
|
|
49
|
+
(0.2577, 0.2497, 0.4821),
|
|
50
|
+
(0.2205, 0.2121, 0.3737),
|
|
51
|
+
(0.182, 0.1769, 0.2809),
|
|
52
|
+
(0.1492, 0.1465, 0.2053),
|
|
53
|
+
(0.1276, 0.1254, 0.1501),
|
|
54
|
+
(0.1213, 0.1183, 0.1188),
|
|
55
|
+
(0.1538, 0.1167, 0.1227),
|
|
56
|
+
(0.2063, 0.1255, 0.1416),
|
|
57
|
+
(0.2734, 0.1407, 0.1703),
|
|
58
|
+
(0.3514, 0.157, 0.2039),
|
|
59
|
+
(0.4375, 0.1703, 0.2357),
|
|
60
|
+
(0.5298, 0.1784, 0.2601),
|
|
61
|
+
(0.6251, 0.1832, 0.2686),
|
|
62
|
+
(0.7138, 0.2028, 0.2533),
|
|
63
|
+
(0.7898, 0.2469, 0.2268),
|
|
64
|
+
(0.85, 0.3142, 0.2058),
|
|
65
|
+
(0.8971, 0.3932, 0.2012),
|
|
66
|
+
(0.9279, 0.482, 0.2415),
|
|
67
|
+
(0.9479, 0.5722, 0.3318),
|
|
68
|
+
(0.9659, 0.659, 0.4438),
|
|
69
|
+
(0.9829, 0.7447, 0.5597),
|
|
70
|
+
(0.9992, 0.831, 0.6764),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
37
74
|
def _init_luts():
|
|
38
75
|
"""Build colourmap LUTs on first use (defers matplotlib + qmricolors import)."""
|
|
39
76
|
global _mpl_colormaps
|
|
@@ -46,6 +83,12 @@ def _init_luts():
|
|
|
46
83
|
from matplotlib import colormaps
|
|
47
84
|
from matplotlib.colors import LinearSegmentedColormap
|
|
48
85
|
|
|
86
|
+
if "icefire" not in colormaps:
|
|
87
|
+
# seaborn's icefire (blue → near-black → red), sampled at 33 stops so
|
|
88
|
+
# we don't depend on seaborn. Default for the A−B compare center.
|
|
89
|
+
colormaps.register(
|
|
90
|
+
LinearSegmentedColormap.from_list("icefire", _ICEFIRE_STOPS)
|
|
91
|
+
)
|
|
49
92
|
_mpl_colormaps = colormaps
|
|
50
93
|
for name in COLORMAPS:
|
|
51
94
|
lut = np.concatenate(
|
|
@@ -210,6 +253,12 @@ def _read_slice(session, dim_x, dim_y, idx_list, progress):
|
|
|
210
253
|
if progress is None:
|
|
211
254
|
progress = getattr(session, 'progress_sink', None)
|
|
212
255
|
data = session.data
|
|
256
|
+
if not getattr(session, "_ram_upgrade_started", False):
|
|
257
|
+
# Not every loading route asks for this (compare arrays, for one), and
|
|
258
|
+
# without it a scattered plane is re-read from the file on every step.
|
|
259
|
+
from ._session import upgrade_memmap_in_background
|
|
260
|
+
|
|
261
|
+
upgrade_memmap_in_background(session)
|
|
213
262
|
if isinstance(data, np.memmap) and data.filename:
|
|
214
263
|
# A slice scattered one element per page reads far faster as many
|
|
215
264
|
# parallel small reads than by faulting through the map (local disks).
|
|
@@ -441,6 +441,54 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
441
441
|
return Response(status_code=422)
|
|
442
442
|
return _diff_histogram(raw, bins)
|
|
443
443
|
|
|
444
|
+
@app.get("/diff-values/{sid_a}/{sid_b}")
|
|
445
|
+
def get_diff_values(
|
|
446
|
+
sid_a: str,
|
|
447
|
+
sid_b: str,
|
|
448
|
+
dim_x: int,
|
|
449
|
+
dim_y: int,
|
|
450
|
+
indices: str,
|
|
451
|
+
indices_a: str | None = None,
|
|
452
|
+
indices_b: str | None = None,
|
|
453
|
+
dim_z: int = -1,
|
|
454
|
+
dr: int = 1,
|
|
455
|
+
complex_mode: int = 0,
|
|
456
|
+
log_scale: bool = False,
|
|
457
|
+
diff_mode: int = 1,
|
|
458
|
+
):
|
|
459
|
+
"""Raw float32 diff values, so hovering a histogram bin can highlight pixels."""
|
|
460
|
+
session_a = SESSIONS.get(sid_a)
|
|
461
|
+
session_b = SESSIONS.get(sid_b)
|
|
462
|
+
if not session_a or not session_b:
|
|
463
|
+
return Response(status_code=404)
|
|
464
|
+
try:
|
|
465
|
+
raw, _, _, _, _ = _compute_diff(
|
|
466
|
+
session_a,
|
|
467
|
+
session_b,
|
|
468
|
+
dim_x,
|
|
469
|
+
dim_y,
|
|
470
|
+
indices,
|
|
471
|
+
dim_z,
|
|
472
|
+
dr,
|
|
473
|
+
complex_mode,
|
|
474
|
+
log_scale,
|
|
475
|
+
diff_mode,
|
|
476
|
+
indices_a=indices_a,
|
|
477
|
+
indices_b=indices_b,
|
|
478
|
+
)
|
|
479
|
+
except Exception:
|
|
480
|
+
return Response(status_code=422)
|
|
481
|
+
raw = np.ascontiguousarray(raw, dtype=np.float32)
|
|
482
|
+
return Response(
|
|
483
|
+
content=raw.tobytes(),
|
|
484
|
+
media_type="application/octet-stream",
|
|
485
|
+
headers={
|
|
486
|
+
"X-ArrayView-Width": str(raw.shape[1]),
|
|
487
|
+
"X-ArrayView-Height": str(raw.shape[0]),
|
|
488
|
+
"Cache-Control": "no-cache",
|
|
489
|
+
},
|
|
490
|
+
)
|
|
491
|
+
|
|
444
492
|
@app.get("/oblique/{sid}")
|
|
445
493
|
def get_oblique(
|
|
446
494
|
sid: str,
|
|
@@ -50,6 +50,47 @@ _RECOVERABLE_DISCONNECT_GRACE_SECONDS = float(
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
# Leading word of a compressed frame: b"AVZ1" read as a little-endian uint32.
|
|
54
|
+
# A raw frame starts with its sequence number, which never gets this large.
|
|
55
|
+
_FRAME_Z_MAGIC = b"AVZ1"
|
|
56
|
+
# Below this a frame crosses any link quickly enough as it is.
|
|
57
|
+
_FRAME_Z_MIN_BYTES = 32 * 1024
|
|
58
|
+
_frame_z_remote = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _frame_compression_wanted() -> bool:
|
|
62
|
+
"""True when frames should be deflated before they are sent.
|
|
63
|
+
|
|
64
|
+
Over a VS Code tunnel or Remote-SSH forward every frame crosses the network
|
|
65
|
+
as raw RGBA: three compared 224x224 slices are 600 KB per scroll step, and
|
|
66
|
+
that transfer, not rendering, is the lag. Locally the bytes are free, so
|
|
67
|
+
frames stay raw. ``ARRAYVIEW_FRAME_COMPRESS=1``/``0`` forces either way.
|
|
68
|
+
"""
|
|
69
|
+
global _frame_z_remote
|
|
70
|
+
forced = os.environ.get("ARRAYVIEW_FRAME_COMPRESS")
|
|
71
|
+
if forced in ("0", "1"):
|
|
72
|
+
return forced == "1"
|
|
73
|
+
if _frame_z_remote is None:
|
|
74
|
+
try:
|
|
75
|
+
from arrayview._platform import _is_vscode_remote
|
|
76
|
+
|
|
77
|
+
_frame_z_remote = bool(_is_vscode_remote())
|
|
78
|
+
except Exception:
|
|
79
|
+
_frame_z_remote = False
|
|
80
|
+
return _frame_z_remote
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _pack_frame(payload: bytes) -> bytes:
|
|
84
|
+
"""Deflate *payload* behind the magic word, or return it unchanged when
|
|
85
|
+
that does not save at least a third (noise-like images barely shrink)."""
|
|
86
|
+
import zlib
|
|
87
|
+
|
|
88
|
+
packed = zlib.compress(payload, 1)
|
|
89
|
+
if len(packed) * 3 > len(payload) * 2:
|
|
90
|
+
return payload
|
|
91
|
+
return _FRAME_Z_MAGIC + packed
|
|
92
|
+
|
|
93
|
+
|
|
53
94
|
def _advance_viewer_epoch(sid: str) -> int:
|
|
54
95
|
epoch = _session_mod.VIEWER_CONNECTION_EPOCHS.get(sid, 0) + 1
|
|
55
96
|
_session_mod.VIEWER_CONNECTION_EPOCHS[sid] = epoch
|
|
@@ -368,6 +409,10 @@ def register_websocket_routes(app) -> None:
|
|
|
368
409
|
native_request_id = ws.query_params.get("native_request_id")
|
|
369
410
|
launch_request_id = ws.query_params.get("launch_request_id")
|
|
370
411
|
launch_token = ws.query_params.get("launch_token")
|
|
412
|
+
# Only viewers that can inflate frames ask for them packed.
|
|
413
|
+
compress_frames = (
|
|
414
|
+
ws.query_params.get("frame_z") == "1" and _frame_compression_wanted()
|
|
415
|
+
)
|
|
371
416
|
# Answer the handshake before the array is ready. Holding it closed
|
|
372
417
|
# until the load finished made the viewer decide the backend was dead:
|
|
373
418
|
# it gives up after three attempts and shows "could not connect", which
|
|
@@ -752,6 +797,9 @@ def register_websocket_routes(app) -> None:
|
|
|
752
797
|
).tobytes()
|
|
753
798
|
vf_scale = np.array([vf_result["scale"]], dtype=np.float32).tobytes()
|
|
754
799
|
payload += vf_hdr + vf_scale + arrows.tobytes()
|
|
800
|
+
if compress_frames and len(payload) >= _FRAME_Z_MIN_BYTES:
|
|
801
|
+
raw_payload = payload
|
|
802
|
+
payload = await _render(loop, lambda: _pack_frame(raw_payload))
|
|
755
803
|
await ws.send_bytes(payload)
|
|
756
804
|
if perf:
|
|
757
805
|
total_ms = (time.perf_counter() - total_t0) * 1000.0
|
|
@@ -870,6 +870,9 @@ def upgrade_memmap_in_background(session) -> bool:
|
|
|
870
870
|
identical, so a render already holding the lazy array stays correct and the
|
|
871
871
|
slice caches stay valid.
|
|
872
872
|
"""
|
|
873
|
+
if getattr(session, "_ram_upgrade_started", False):
|
|
874
|
+
return False
|
|
875
|
+
session._ram_upgrade_started = True
|
|
873
876
|
data = getattr(session, "data", None)
|
|
874
877
|
filepath = getattr(session, "filepath", None)
|
|
875
878
|
if data is None or not filepath:
|