arrayview 0.48.1__py3-none-any.whl → 0.48.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arrayview/_diff.py +2 -1
- arrayview/_render.py +129 -35
- arrayview/_routes_rendering.py +22 -20
- arrayview/_routes_websocket.py +38 -11
- arrayview/_session.py +172 -26
- arrayview/_synthetic_mri.py +2 -2
- {arrayview-0.48.1.dist-info → arrayview-0.48.2.dist-info}/METADATA +1 -1
- {arrayview-0.48.1.dist-info → arrayview-0.48.2.dist-info}/RECORD +11 -11
- {arrayview-0.48.1.dist-info → arrayview-0.48.2.dist-info}/WHEEL +0 -0
- {arrayview-0.48.1.dist-info → arrayview-0.48.2.dist-info}/entry_points.txt +0 -0
- {arrayview-0.48.1.dist-info → arrayview-0.48.2.dist-info}/licenses/LICENSE +0 -0
arrayview/_diff.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
7
|
from arrayview._render import (
|
|
8
|
+
lut_rgba,
|
|
8
9
|
LUTS,
|
|
9
10
|
_build_mosaic_grid,
|
|
10
11
|
_compute_vmin_vmax,
|
|
@@ -192,7 +193,7 @@ def _render_diff_rgba(
|
|
|
192
193
|
normalized = np.zeros_like(raw)
|
|
193
194
|
_ensure_lut(colormap)
|
|
194
195
|
lut = LUTS.get(colormap, LUTS["gray"])
|
|
195
|
-
rgba = lut
|
|
196
|
+
rgba = lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
196
197
|
if nan_mask is not None and nan_mask.shape == rgba.shape[:2]:
|
|
197
198
|
rgba[nan_mask] = [22, 22, 22, 255]
|
|
198
199
|
return rgba
|
arrayview/_render.py
CHANGED
|
@@ -190,11 +190,13 @@ def extract_slice(session, dim_x, dim_y, idx_list, progress=None):
|
|
|
190
190
|
# slicer below, so the extracted data doesn't depend on them.
|
|
191
191
|
key_idx = tuple(None if i in (dim_x, dim_y) else idx_list[i] for i in range(len(idx_list)))
|
|
192
192
|
key = (dim_x, dim_y, key_idx)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
return _cache_once(
|
|
194
|
+
session, session._raw_lock, "raw_cache", "_raw_bytes", "RAW_CACHE_BYTES",
|
|
195
|
+
key, lambda: _read_slice(session, dim_x, dim_y, idx_list, progress),
|
|
196
|
+
)
|
|
197
|
+
|
|
197
198
|
|
|
199
|
+
def _read_slice(session, dim_x, dim_y, idx_list, progress):
|
|
198
200
|
slicer = [
|
|
199
201
|
slice(None) if i in (dim_x, dim_y) else idx_list[i]
|
|
200
202
|
for i in range(len(session.shape))
|
|
@@ -244,12 +246,6 @@ def extract_slice(session, dim_x, dim_y, idx_list, progress=None):
|
|
|
244
246
|
# buffer as too small. Present the single axis as one row so that
|
|
245
247
|
# invariant holds everywhere instead of at each call site.
|
|
246
248
|
result = result.reshape(1, -1)
|
|
247
|
-
|
|
248
|
-
with session._raw_lock:
|
|
249
|
-
if key not in session.raw_cache: # double-check: another worker may have populated it
|
|
250
|
-
session.raw_cache[key] = result
|
|
251
|
-
session._raw_bytes += result.nbytes
|
|
252
|
-
_evict_lru(session, "_raw_bytes", "raw_cache", "RAW_CACHE_BYTES")
|
|
253
249
|
return result
|
|
254
250
|
|
|
255
251
|
|
|
@@ -274,11 +270,13 @@ def extract_projection(session, dim_x, dim_y, idx_list, proj_dim, proj_mode):
|
|
|
274
270
|
for i in range(len(idx_list))
|
|
275
271
|
)
|
|
276
272
|
key = ("proj", dim_x, dim_y, key_idx, proj_dim, proj_mode)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
273
|
+
return _cache_once(
|
|
274
|
+
session, session._raw_lock, "raw_cache", "_raw_bytes", "RAW_CACHE_BYTES",
|
|
275
|
+
key, lambda: _compute_projection(session, dim_x, dim_y, idx_list, proj_dim, proj_mode),
|
|
276
|
+
)
|
|
281
277
|
|
|
278
|
+
|
|
279
|
+
def _compute_projection(session, dim_x, dim_y, idx_list, proj_dim, proj_mode):
|
|
282
280
|
# Build slicer: display dims + projection dim get slice(None), rest use idx
|
|
283
281
|
slicer = []
|
|
284
282
|
for i in range(len(session.shape)):
|
|
@@ -317,11 +315,6 @@ def extract_projection(session, dim_x, dim_y, idx_list, proj_dim, proj_mode):
|
|
|
317
315
|
result = np.nan_to_num(result).astype(np.complex64)
|
|
318
316
|
else:
|
|
319
317
|
result = np.nan_to_num(result).astype(np.float32)
|
|
320
|
-
with session._raw_lock:
|
|
321
|
-
if key not in session.raw_cache:
|
|
322
|
-
session.raw_cache[key] = result
|
|
323
|
-
session._raw_bytes += result.nbytes
|
|
324
|
-
_evict_lru(session, "_raw_bytes", "raw_cache", "RAW_CACHE_BYTES")
|
|
325
318
|
return result
|
|
326
319
|
|
|
327
320
|
|
|
@@ -358,6 +351,20 @@ def _prepare_display(
|
|
|
358
351
|
return data, vmin, vmax
|
|
359
352
|
|
|
360
353
|
|
|
354
|
+
def lut_rgba(lut: np.ndarray, indices: np.ndarray) -> np.ndarray:
|
|
355
|
+
"""Return ``lut[indices]`` as (..., 4) uint8, gathering whole pixels.
|
|
356
|
+
|
|
357
|
+
Indexing the (256, 4) table copies four bytes one channel at a time; the
|
|
358
|
+
same table viewed as 256 uint32 gathers each pixel in one step, ~4x
|
|
359
|
+
faster on a large mosaic and byte-for-byte identical. The gather keeps
|
|
360
|
+
the index array's memory order, so a transposed slice is made C-order
|
|
361
|
+
first or the uint8 view cannot be taken.
|
|
362
|
+
"""
|
|
363
|
+
table = np.ascontiguousarray(lut, dtype=np.uint8).view(np.uint32).reshape(-1)
|
|
364
|
+
indices = np.ascontiguousarray(indices)
|
|
365
|
+
return table[indices].view(np.uint8).reshape(*indices.shape, 4)
|
|
366
|
+
|
|
367
|
+
|
|
361
368
|
def _ensure_lut(name: str) -> bool:
|
|
362
369
|
"""Ensure name is in LUTS. Returns True if valid."""
|
|
363
370
|
_init_luts()
|
|
@@ -404,7 +411,7 @@ def apply_colormap_rgba(
|
|
|
404
411
|
normalized = np.zeros_like(data)
|
|
405
412
|
_ensure_lut(colormap)
|
|
406
413
|
lut = LUTS.get(colormap, LUTS["gray"])
|
|
407
|
-
rgba = lut
|
|
414
|
+
rgba = lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
408
415
|
alpha_on = getattr(session, "alpha_level", 0) > 0
|
|
409
416
|
if alpha_on:
|
|
410
417
|
if vmax > vmin:
|
|
@@ -835,15 +842,102 @@ def render_mosaic(
|
|
|
835
842
|
vmin_override=None,
|
|
836
843
|
vmax_override=None,
|
|
837
844
|
progress=None,
|
|
845
|
+
fit=None,
|
|
838
846
|
):
|
|
847
|
+
"""Render the mosaic grid of every slice along *dim_z*.
|
|
848
|
+
|
|
849
|
+
``fit=(w, h)`` returns the grid already shrunk to the viewer's canvas
|
|
850
|
+
(as the live socket would before sending) and caches it that way, so a
|
|
851
|
+
grid warmed ahead of time is ready to send: the shrink of a 240-slice
|
|
852
|
+
grid costs as much as rendering it.
|
|
853
|
+
"""
|
|
839
854
|
idx_norm = list(idx_tuple)
|
|
840
855
|
idx_norm[dim_z] = 0
|
|
841
856
|
key = (dim_x, dim_y, dim_z, tuple(idx_norm), colormap, dr, complex_mode, log_scale, mosaic_cols, vmin_override, vmax_override)
|
|
842
|
-
with session._mosaic_lock:
|
|
843
|
-
if key in session.mosaic_cache:
|
|
844
|
-
session.mosaic_cache.move_to_end(key)
|
|
845
|
-
return session.mosaic_cache[key]
|
|
846
857
|
|
|
858
|
+
def build_full():
|
|
859
|
+
return _build_mosaic_rgba(
|
|
860
|
+
session, dim_x, dim_y, dim_z, idx_tuple, colormap, dr, complex_mode,
|
|
861
|
+
log_scale, mosaic_cols, vmin_override, vmax_override, progress,
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
full = lambda: _mosaic_once(session, key, build_full)
|
|
865
|
+
if not fit:
|
|
866
|
+
return full()
|
|
867
|
+
fit_w, fit_h = int(fit[0]), int(fit[1])
|
|
868
|
+
return _mosaic_once(session, key + (("fit", fit_w, fit_h),), lambda: fit_rgba(full(), fit_w, fit_h))
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _mosaic_once(session, key, build):
|
|
872
|
+
return _cache_once(
|
|
873
|
+
session, session._mosaic_lock, "mosaic_cache", "_mosaic_bytes",
|
|
874
|
+
"MOSAIC_CACHE_BYTES", key, build,
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _cache_once(session, lock, cache_attr, bytes_attr, budget_attr, key, build):
|
|
879
|
+
"""Return session.<cache_attr>[key], building it once under concurrent asks.
|
|
880
|
+
|
|
881
|
+
Lookahead and the request it anticipated, a hedged duplicate request, or
|
|
882
|
+
several panes can ask for the same entry at the same moment; without this
|
|
883
|
+
each rebuilt (re-read from disk) what another was half-way through,
|
|
884
|
+
doubling the wait it was meant to hide. Later askers wait for the builder
|
|
885
|
+
and take its result; if the builder fails, the next one builds it itself.
|
|
886
|
+
"""
|
|
887
|
+
cache = getattr(session, cache_attr)
|
|
888
|
+
while True:
|
|
889
|
+
with lock:
|
|
890
|
+
if key in cache:
|
|
891
|
+
cache.move_to_end(key)
|
|
892
|
+
return cache[key]
|
|
893
|
+
inflight_attr = f"_{cache_attr}_inflight"
|
|
894
|
+
inflight = getattr(session, inflight_attr, None)
|
|
895
|
+
if inflight is None:
|
|
896
|
+
inflight = {}
|
|
897
|
+
setattr(session, inflight_attr, inflight)
|
|
898
|
+
event = inflight.get(key)
|
|
899
|
+
owner = event is None
|
|
900
|
+
if owner:
|
|
901
|
+
event = inflight[key] = threading.Event()
|
|
902
|
+
if not owner:
|
|
903
|
+
event.wait()
|
|
904
|
+
if hasattr(event, "value"):
|
|
905
|
+
return event.value # even if too big to have stayed cached
|
|
906
|
+
continue # builder failed: build it here
|
|
907
|
+
try:
|
|
908
|
+
value = build()
|
|
909
|
+
event.value = value
|
|
910
|
+
with lock:
|
|
911
|
+
if key not in cache:
|
|
912
|
+
cache[key] = value
|
|
913
|
+
setattr(session, bytes_attr, getattr(session, bytes_attr) + value.nbytes)
|
|
914
|
+
_evict_lru(session, bytes_attr, cache_attr, budget_attr)
|
|
915
|
+
return value
|
|
916
|
+
finally:
|
|
917
|
+
with lock:
|
|
918
|
+
inflight.pop(key, None)
|
|
919
|
+
event.set()
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def fit_rgba(rgba, canvas_w, canvas_h):
|
|
923
|
+
"""Shrink an RGBA frame to fit the viewer's canvas; unchanged if it fits."""
|
|
924
|
+
h, w = rgba.shape[:2]
|
|
925
|
+
if not (canvas_w and canvas_h and (w > canvas_w or h > canvas_h)):
|
|
926
|
+
return rgba
|
|
927
|
+
from ._imaging import ensure_image
|
|
928
|
+
|
|
929
|
+
image_cls = ensure_image()
|
|
930
|
+
pil = image_cls.fromarray(
|
|
931
|
+
rgba.astype(np.uint8) if rgba.dtype != np.uint8 else rgba, mode="RGBA"
|
|
932
|
+
)
|
|
933
|
+
pil.thumbnail((canvas_w, canvas_h), image_cls.LANCZOS)
|
|
934
|
+
return np.array(pil)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _build_mosaic_rgba(
|
|
938
|
+
session, dim_x, dim_y, dim_z, idx_tuple, colormap, dr, complex_mode,
|
|
939
|
+
log_scale, mosaic_cols, vmin_override, vmax_override, progress,
|
|
940
|
+
):
|
|
847
941
|
n = session.shape[dim_z]
|
|
848
942
|
# Reported per slice rather than estimated: each slice faults in roughly the
|
|
849
943
|
# same number of pages, so slice i of n is an honest fraction of the wait.
|
|
@@ -873,30 +967,30 @@ def render_mosaic(
|
|
|
873
967
|
elif complex_mode == 1 and np.iscomplexobj(session.data):
|
|
874
968
|
vmin, vmax = -float(np.pi), float(np.pi)
|
|
875
969
|
else:
|
|
876
|
-
|
|
877
|
-
|
|
970
|
+
# One call partitions once for both ends; two calls sorted the whole
|
|
971
|
+
# stack twice (~0.14 s of a 240-slice mosaic step).
|
|
972
|
+
vmin, vmax = (float(v) for v in np.percentile(all_data, [1, 99]))
|
|
878
973
|
|
|
879
974
|
if mosaic_cols is not None:
|
|
880
975
|
grid, rows, cols = _build_mosaic_grid(all_data, n, cols=mosaic_cols)
|
|
881
976
|
else:
|
|
882
977
|
grid, rows, cols = _build_mosaic_grid(all_data, n)
|
|
883
978
|
|
|
979
|
+
# Map to LUT indices in place: a mosaic is tens of millions of pixels, and
|
|
980
|
+
# each full-size temporary of the where/clip/divide chain cost ~50 ms.
|
|
884
981
|
nan_mask = np.isnan(grid)
|
|
885
|
-
filled = np.where(nan_mask, vmin, grid)
|
|
886
982
|
if vmax > vmin:
|
|
887
|
-
|
|
983
|
+
grid -= vmin
|
|
984
|
+
grid *= 255.0 / (vmax - vmin)
|
|
985
|
+
np.clip(grid, 0, 255, out=grid)
|
|
986
|
+
grid[nan_mask] = 0
|
|
888
987
|
else:
|
|
889
|
-
|
|
988
|
+
grid[:] = 0
|
|
890
989
|
|
|
891
990
|
_ensure_lut(colormap)
|
|
892
991
|
lut = LUTS.get(colormap, LUTS["gray"])
|
|
893
|
-
rgba = lut
|
|
992
|
+
rgba = lut_rgba(lut, grid.astype(np.uint8))
|
|
894
993
|
rgba[nan_mask] = [22, 22, 22, 255]
|
|
895
|
-
with session._mosaic_lock:
|
|
896
|
-
if key not in session.mosaic_cache:
|
|
897
|
-
session.mosaic_cache[key] = rgba
|
|
898
|
-
session._mosaic_bytes += rgba.nbytes
|
|
899
|
-
_evict_lru(session, "_mosaic_bytes", "mosaic_cache", "MOSAIC_CACHE_BYTES")
|
|
900
994
|
return rgba
|
|
901
995
|
|
|
902
996
|
|
arrayview/_routes_rendering.py
CHANGED
|
@@ -8,6 +8,7 @@ from fastapi.responses import JSONResponse
|
|
|
8
8
|
from arrayview._diff import _compute_diff, _diff_histogram, _render_diff_rgba
|
|
9
9
|
from arrayview._overlays import _composite_mosaic_overlays, _composite_overlays
|
|
10
10
|
from arrayview._render import (
|
|
11
|
+
lut_rgba,
|
|
11
12
|
LUTS,
|
|
12
13
|
_build_mosaic_grid,
|
|
13
14
|
_compute_vmin_vmax,
|
|
@@ -23,7 +24,12 @@ from arrayview._render import (
|
|
|
23
24
|
render_rgb_rgba,
|
|
24
25
|
render_rgba,
|
|
25
26
|
)
|
|
26
|
-
from arrayview._session import
|
|
27
|
+
from arrayview._session import (
|
|
28
|
+
HEAVY_OP_LIMIT_BYTES,
|
|
29
|
+
SESSIONS,
|
|
30
|
+
_schedule_prefetch,
|
|
31
|
+
_schedule_projection_prefetch,
|
|
32
|
+
)
|
|
27
33
|
from arrayview._synthetic_mri import (
|
|
28
34
|
_qmri_adjust_vmin_vmax,
|
|
29
35
|
qmri_display_slice,
|
|
@@ -65,7 +71,7 @@ def render_rgba_from_data(data, colormap, vmin, vmax):
|
|
|
65
71
|
normalized = np.zeros_like(data)
|
|
66
72
|
_ensure_lut(colormap)
|
|
67
73
|
lut = LUTS.get(colormap, LUTS["gray"])
|
|
68
|
-
return lut
|
|
74
|
+
return lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
69
75
|
|
|
70
76
|
|
|
71
77
|
def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
@@ -276,21 +282,17 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
276
282
|
mosaic_cols,
|
|
277
283
|
)
|
|
278
284
|
render_ms = (time.perf_counter() - render_t0) * 1000.0
|
|
279
|
-
# This endpoint is what compare
|
|
280
|
-
# WS path has its own prefetch call).
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if delta:
|
|
291
|
-
direction = 1 if delta > 0 else -1
|
|
292
|
-
session._http_prefetch_prev_idx = idx_tuple
|
|
293
|
-
_schedule_prefetch(session, dim_x, dim_y, list(idx_tuple), slice_dim, direction)
|
|
285
|
+
# This endpoint is what compare, qMRI and multiview panes poll per
|
|
286
|
+
# scroll step (the live WS path has its own prefetch call). No
|
|
287
|
+
# per-request direction field here (stateless HTTP), so the prefetch
|
|
288
|
+
# lane infers it from the position it last saw.
|
|
289
|
+
if projection_mode > 0 and projection_dim >= 0 and not synthetic_mri and session.rgb_axis is None:
|
|
290
|
+
_schedule_projection_prefetch(
|
|
291
|
+
session, dim_x, dim_y, list(idx_tuple), slice_dim,
|
|
292
|
+
projection_dim, projection_mode,
|
|
293
|
+
)
|
|
294
|
+
elif slice_dim >= 0 and dim_z < 0:
|
|
295
|
+
_schedule_prefetch(session, dim_x, dim_y, list(idx_tuple), slice_dim)
|
|
294
296
|
encode_t0 = time.perf_counter()
|
|
295
297
|
img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
|
|
296
298
|
buf = io.BytesIO()
|
|
@@ -504,7 +506,7 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
504
506
|
normalized = np.clip((sampled - vmin) / (vmax - vmin), 0, 1)
|
|
505
507
|
else:
|
|
506
508
|
normalized = np.zeros_like(sampled)
|
|
507
|
-
rgba = lut
|
|
509
|
+
rgba = lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
508
510
|
|
|
509
511
|
img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
|
|
510
512
|
if draft and (grid_w != size_w or grid_h != size_h):
|
|
@@ -578,7 +580,7 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
578
580
|
|
|
579
581
|
_init_luts()
|
|
580
582
|
lut = LUTS.get(colormap if colormap in LUTS else "gray", LUTS["gray"])
|
|
581
|
-
rgba = lut
|
|
583
|
+
rgba = lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
582
584
|
rgba[nan_mask] = [22, 22, 22, 255]
|
|
583
585
|
img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
|
|
584
586
|
buf = io.BytesIO()
|
|
@@ -639,7 +641,7 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
639
641
|
normalized = np.clip((frame - vmin) / (vmax - vmin), 0, 1)
|
|
640
642
|
else:
|
|
641
643
|
normalized = np.zeros_like(frame)
|
|
642
|
-
rgba = lut
|
|
644
|
+
rgba = lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
643
645
|
gif_frames.append(_pil_image().fromarray(rgba[:, :, :3], mode="RGB"))
|
|
644
646
|
|
|
645
647
|
buf = io.BytesIO()
|
arrayview/_routes_websocket.py
CHANGED
|
@@ -11,6 +11,8 @@ from arrayview._analysis import _build_metadata
|
|
|
11
11
|
from arrayview._lifecycle import release_session
|
|
12
12
|
from arrayview._overlays import _composite_mosaic_overlays, _composite_overlays
|
|
13
13
|
from arrayview._render import (
|
|
14
|
+
fit_rgba,
|
|
15
|
+
lut_rgba,
|
|
14
16
|
LUTS,
|
|
15
17
|
_prepare_display,
|
|
16
18
|
_ensure_lut,
|
|
@@ -26,7 +28,9 @@ from arrayview._session import (
|
|
|
26
28
|
SHELL_SOCKETS,
|
|
27
29
|
_render,
|
|
28
30
|
_schedule_overlay_prefetch,
|
|
31
|
+
_schedule_mosaic_prefetch,
|
|
29
32
|
_schedule_prefetch,
|
|
33
|
+
_schedule_projection_prefetch,
|
|
30
34
|
_vprint,
|
|
31
35
|
wait_for_session_ready,
|
|
32
36
|
)
|
|
@@ -246,7 +250,7 @@ def _render_rgba_from_data(data, colormap, vmin, vmax):
|
|
|
246
250
|
normalized = np.zeros_like(data)
|
|
247
251
|
_ensure_lut(colormap)
|
|
248
252
|
lut = LUTS.get(colormap, LUTS["gray"])
|
|
249
|
-
return lut
|
|
253
|
+
return lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
250
254
|
|
|
251
255
|
|
|
252
256
|
def _render_qmri_rgba(
|
|
@@ -287,7 +291,7 @@ def _render_qmri_rgba(
|
|
|
287
291
|
normalized = np.zeros_like(data)
|
|
288
292
|
_ensure_lut(colormap)
|
|
289
293
|
lut = LUTS.get(colormap, LUTS["gray"])
|
|
290
|
-
rgba = lut
|
|
294
|
+
rgba = lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
291
295
|
alpha_on = getattr(session, "alpha_level", 0) > 0
|
|
292
296
|
if alpha_on:
|
|
293
297
|
if vmax > vmin:
|
|
@@ -485,7 +489,6 @@ def register_websocket_routes(app) -> None:
|
|
|
485
489
|
_vmax_ov = msg.get("vmax_override")
|
|
486
490
|
vmin_override = float(_vmin_ov) if _vmin_ov is not None else None
|
|
487
491
|
vmax_override = float(_vmax_ov) if _vmax_ov is not None else None
|
|
488
|
-
direction = int(msg.get("direction", 1))
|
|
489
492
|
slice_dim = int(msg.get("slice_dim", -1))
|
|
490
493
|
canvas_w = int(msg.get("canvas_w", 0))
|
|
491
494
|
canvas_h = int(msg.get("canvas_h", 0))
|
|
@@ -560,6 +563,13 @@ def register_websocket_routes(app) -> None:
|
|
|
560
563
|
)
|
|
561
564
|
h, w = rgba.shape[:2]
|
|
562
565
|
else:
|
|
566
|
+
# Overlays are drawn onto the full-size grid, so only a
|
|
567
|
+
# bare mosaic can come back already fitted to the canvas.
|
|
568
|
+
mosaic_fit = (
|
|
569
|
+
(canvas_w, canvas_h)
|
|
570
|
+
if canvas_w and canvas_h and not msg.get("overlay_sid")
|
|
571
|
+
else None
|
|
572
|
+
)
|
|
563
573
|
rgba = await _render(
|
|
564
574
|
loop,
|
|
565
575
|
lambda: render_mosaic(
|
|
@@ -576,6 +586,7 @@ def register_websocket_routes(app) -> None:
|
|
|
576
586
|
vmin_override=vmin_override,
|
|
577
587
|
vmax_override=vmax_override,
|
|
578
588
|
progress=_render_progress_reporter(loop, ws, label='Building mosaic'),
|
|
589
|
+
fit=mosaic_fit,
|
|
579
590
|
),
|
|
580
591
|
)
|
|
581
592
|
rgba = _composite_mosaic_overlays(
|
|
@@ -718,12 +729,7 @@ def register_websocket_routes(app) -> None:
|
|
|
718
729
|
header = np.array([seq, w, h], dtype=np.uint32).tobytes()
|
|
719
730
|
vminmax = np.array([vmin, vmax], dtype=np.float32).tobytes()
|
|
720
731
|
if canvas_w and canvas_h and (w > canvas_w or h > canvas_h):
|
|
721
|
-
|
|
722
|
-
rgba.astype(np.uint8) if rgba.dtype != np.uint8 else rgba,
|
|
723
|
-
mode="RGBA",
|
|
724
|
-
)
|
|
725
|
-
pil.thumbnail((canvas_w, canvas_h), _pil_image().LANCZOS)
|
|
726
|
-
rgba = np.array(pil)
|
|
732
|
+
rgba = fit_rgba(rgba, canvas_w, canvas_h)
|
|
727
733
|
h, w = rgba.shape[:2]
|
|
728
734
|
header = np.array([seq, w, h], dtype=np.uint32).tobytes()
|
|
729
735
|
payload = header + vminmax + rgba.tobytes()
|
|
@@ -764,9 +770,30 @@ def register_websocket_routes(app) -> None:
|
|
|
764
770
|
}
|
|
765
771
|
)
|
|
766
772
|
|
|
767
|
-
if
|
|
773
|
+
if (
|
|
774
|
+
dim_z < 0 and projection_mode > 0 and projection_dim >= 0
|
|
775
|
+
and not synthetic_mri and session.rgb_axis is None
|
|
776
|
+
):
|
|
777
|
+
_schedule_projection_prefetch(
|
|
778
|
+
session, dim_x, dim_y, list(idx_tuple), slice_dim,
|
|
779
|
+
projection_dim, projection_mode,
|
|
780
|
+
)
|
|
781
|
+
elif slice_dim >= 0 and not (dim_z >= 0):
|
|
782
|
+
# The lane infers direction from where it last was. Panes
|
|
783
|
+
# that hard-code the message's direction (qMRI, compare
|
|
784
|
+
# qMRI) would otherwise always read ahead forward.
|
|
768
785
|
_schedule_prefetch(
|
|
769
|
-
session, dim_x, dim_y, list(idx_tuple), slice_dim
|
|
786
|
+
session, dim_x, dim_y, list(idx_tuple), slice_dim
|
|
787
|
+
)
|
|
788
|
+
elif dim_z >= 0 and not qmri_role and not synthetic_mri and session.rgb_axis is None:
|
|
789
|
+
_schedule_mosaic_prefetch(
|
|
790
|
+
session, dim_x, dim_y, dim_z, list(idx_tuple), slice_dim,
|
|
791
|
+
dict(
|
|
792
|
+
colormap=colormap, dr=dr, complex_mode=complex_mode,
|
|
793
|
+
log_scale=log_scale, mosaic_cols=mosaic_cols,
|
|
794
|
+
vmin_override=vmin_override, vmax_override=vmax_override,
|
|
795
|
+
fit=mosaic_fit,
|
|
796
|
+
),
|
|
770
797
|
)
|
|
771
798
|
stack_axes = tuple(getattr(session.data, "_stack_axes", ()))
|
|
772
799
|
if stack_axes:
|
arrayview/_session.py
CHANGED
|
@@ -294,53 +294,197 @@ def _get_overlay_prefetch_pool():
|
|
|
294
294
|
return _OVERLAY_PREFETCH_POOL
|
|
295
295
|
|
|
296
296
|
|
|
297
|
-
def
|
|
297
|
+
def _prefetch_lane_step(session, lane, current, direction):
|
|
298
|
+
"""Record that *lane* is now at *current*; return (lanes, generation, direction).
|
|
299
|
+
|
|
300
|
+
Panes at the same position in a lane share one generation; moving the
|
|
301
|
+
lane to a new position starts a new one, which stale queued jobs check.
|
|
302
|
+
``direction=None`` infers it from the previous position, keeping the last
|
|
303
|
+
known direction when sibling panes re-request the same position.
|
|
304
|
+
"""
|
|
305
|
+
lanes = getattr(session, "_prefetch_lanes", None)
|
|
306
|
+
if lanes is None:
|
|
307
|
+
lanes = {}
|
|
308
|
+
session._prefetch_lanes = lanes
|
|
309
|
+
state = lanes.get(lane)
|
|
310
|
+
if direction is None:
|
|
311
|
+
if state is None:
|
|
312
|
+
direction = 1
|
|
313
|
+
elif current != state["pos"]:
|
|
314
|
+
direction = 1 if current > state["pos"] else -1
|
|
315
|
+
else:
|
|
316
|
+
direction = state["dir"]
|
|
317
|
+
if state is None or current != state["pos"]:
|
|
318
|
+
now = _time.monotonic()
|
|
319
|
+
state = {
|
|
320
|
+
"gen": (state["gen"] + 1) if state else 1,
|
|
321
|
+
"pos": current,
|
|
322
|
+
"moved_at": now,
|
|
323
|
+
"interval": (now - state["moved_at"]) if state else None,
|
|
324
|
+
"queued": state["queued"] if state else set(),
|
|
325
|
+
}
|
|
326
|
+
lanes[lane] = state
|
|
327
|
+
state["dir"] = direction
|
|
328
|
+
return lanes, state["gen"], direction
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _schedule_prefetch(session, dim_x, dim_y, idx_list, slice_dim, direction=None):
|
|
298
332
|
"""Warm raw_cache for the next PREFETCH_NEIGHBORS slices in *direction*.
|
|
299
333
|
|
|
300
334
|
Runs in a low-priority background thread so it never blocks WS responses.
|
|
301
335
|
Skips prefetch when the estimated byte cost exceeds PREFETCH_BUDGET_BYTES.
|
|
336
|
+
|
|
337
|
+
Lookahead is tracked per lane (display dims + scrolled dim), not per
|
|
338
|
+
session. qMRI maps and multiview planes are several panes of one session;
|
|
339
|
+
a single session-wide generation let each pane's request cancel the
|
|
340
|
+
lookahead its siblings had just queued, so only the last pane was ever
|
|
341
|
+
warmed. Panes at the same position in a lane now share one generation,
|
|
342
|
+
and only moving the lane to a new position supersedes queued work.
|
|
343
|
+
``direction=None`` infers it from the lane's previous position (stateless
|
|
344
|
+
HTTP callers), keeping the last known direction when sibling panes
|
|
345
|
+
re-request the same position.
|
|
302
346
|
"""
|
|
303
347
|
# Import here to avoid circular dependency — extract_slice lives in _render
|
|
304
348
|
from arrayview._render import extract_slice
|
|
305
349
|
|
|
350
|
+
current = idx_list[slice_dim]
|
|
351
|
+
lane = (dim_x, dim_y, slice_dim)
|
|
352
|
+
lanes, _generation, direction = _prefetch_lane_step(session, lane, current, direction)
|
|
353
|
+
state = lanes[lane]
|
|
354
|
+
|
|
306
355
|
n = session.shape[slice_dim]
|
|
307
356
|
# A neighboring collection index can mean decompressing an entire .nii.gz
|
|
308
357
|
# volume, not reading another small slice. Keep that work conservative.
|
|
309
358
|
stack_axes = tuple(getattr(session.data, "_stack_axes", ()))
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
359
|
+
if slice_dim in stack_axes:
|
|
360
|
+
neighbor_count = 1
|
|
361
|
+
elif state.get("interval") is not None and state["interval"] < FAST_SCROLL_INTERVAL_S:
|
|
362
|
+
# A held key outruns a 3-slice window on a slow drive: every step
|
|
363
|
+
# landed on a slice whose read had only just started.
|
|
364
|
+
neighbor_count = FAST_PREFETCH_NEIGHBORS
|
|
365
|
+
else:
|
|
366
|
+
neighbor_count = PREFETCH_NEIGHBORS
|
|
367
|
+
itemsize = 8 if np.iscomplexobj(np.empty(0, getattr(session.data, "dtype", np.float32))) else 4
|
|
368
|
+
slice_bytes = session.shape[dim_y] * session.shape[dim_x] * itemsize
|
|
369
|
+
# Big images look fewer slices ahead rather than none: switching lookahead
|
|
370
|
+
# off left every step of a 2k x 2k stack waiting on its own read. Only a
|
|
371
|
+
# slice that would crowd the raw cache is never warmed.
|
|
372
|
+
neighbor_count = min(neighbor_count, max(1, PREFETCH_BUDGET_BYTES // slice_bytes))
|
|
373
|
+
if slice_bytes > getattr(session, "RAW_CACHE_BYTES", 4 * PREFETCH_BUDGET_BYTES) // 4:
|
|
323
374
|
return
|
|
324
375
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
session._prefetch_generation = generation
|
|
376
|
+
def _key(t):
|
|
377
|
+
idx = list(idx_list)
|
|
378
|
+
idx[slice_dim] = t
|
|
379
|
+
return idx, (dim_x, dim_y, tuple(None if i in (dim_x, dim_y) else idx[i] for i in range(len(idx))))
|
|
330
380
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
381
|
+
# One job per slice so several reads are in flight (a network drive
|
|
382
|
+
# serves parallel readers ~2x faster). A queued job is dropped once the
|
|
383
|
+
# lane has moved past it or turned around; jobs still ahead survive new
|
|
384
|
+
# steps instead of being cancelled and requeued on every key repeat.
|
|
385
|
+
def _warm(t, key, idx):
|
|
386
|
+
try:
|
|
387
|
+
live = lanes[lane]
|
|
388
|
+
if (t - live["pos"]) * live["dir"] <= 0:
|
|
389
|
+
return
|
|
390
|
+
if abs(t - live["pos"]) > FAST_PREFETCH_NEIGHBORS:
|
|
334
391
|
return
|
|
335
|
-
idx = list(idx_list)
|
|
336
|
-
idx[slice_dim] = t
|
|
337
|
-
key_idx = tuple(None if i in (dim_x, dim_y) else idx[i] for i in range(len(idx)))
|
|
338
|
-
key = (dim_x, dim_y, key_idx)
|
|
339
392
|
if key not in session.raw_cache:
|
|
340
393
|
try:
|
|
341
394
|
extract_slice(session, dim_x, dim_y, idx)
|
|
342
395
|
except Exception:
|
|
343
396
|
pass # prefetch errors are silent
|
|
397
|
+
finally:
|
|
398
|
+
state["queued"].discard(key)
|
|
399
|
+
|
|
400
|
+
pool = None
|
|
401
|
+
for step in range(1, neighbor_count + 1):
|
|
402
|
+
t = current + direction * step
|
|
403
|
+
if not 0 <= t < n:
|
|
404
|
+
break
|
|
405
|
+
idx, key = _key(t)
|
|
406
|
+
if key in session.raw_cache or key in state["queued"]:
|
|
407
|
+
continue
|
|
408
|
+
state["queued"].add(key)
|
|
409
|
+
try:
|
|
410
|
+
pool = pool or _get_prefetch_pool()
|
|
411
|
+
pool.submit(_warm, t, key, idx)
|
|
412
|
+
except RuntimeError:
|
|
413
|
+
state["queued"].discard(key)
|
|
414
|
+
return # pool shutting down
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _schedule_mosaic_prefetch(session, dim_x, dim_y, dim_z, idx_list, slice_dim, render_args):
|
|
418
|
+
"""Render the next mosaic along *slice_dim* into mosaic_cache.
|
|
419
|
+
|
|
420
|
+
A mosaic step re-reads and re-colours every slice along *dim_z*, which
|
|
421
|
+
for a large volume is far slower than the hand scrolling it, and the
|
|
422
|
+
slice lookahead does not apply (it warms single slices, not grids).
|
|
423
|
+
One step ahead only: each grid is tens of MB and a CPU-heavy render.
|
|
424
|
+
``render_args`` are render_mosaic's remaining positional/keyword inputs
|
|
425
|
+
(colormap, dr, complex_mode, log_scale, mosaic_cols, vmin/vmax overrides)
|
|
426
|
+
so the warmed grid hits the same cache key as the real request.
|
|
427
|
+
"""
|
|
428
|
+
if slice_dim < 0 or slice_dim == dim_z:
|
|
429
|
+
return
|
|
430
|
+
current = idx_list[slice_dim]
|
|
431
|
+
lane = ("mosaic", dim_x, dim_y, dim_z, slice_dim)
|
|
432
|
+
lanes, generation, direction = _prefetch_lane_step(session, lane, current, None)
|
|
433
|
+
target = current + direction
|
|
434
|
+
if not 0 <= target < session.shape[slice_dim]:
|
|
435
|
+
return
|
|
436
|
+
frames = session.shape[dim_z]
|
|
437
|
+
grid_bytes = frames * session.shape[dim_x] * session.shape[dim_y] * 4
|
|
438
|
+
# Never let the warmed grid push the one on screen out of the cache.
|
|
439
|
+
if 2 * grid_bytes > getattr(session, "MOSAIC_CACHE_BYTES", 0):
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
def _warm():
|
|
443
|
+
if generation != lanes[lane]["gen"]:
|
|
444
|
+
return
|
|
445
|
+
idx = list(idx_list)
|
|
446
|
+
idx[slice_dim] = target
|
|
447
|
+
try:
|
|
448
|
+
from arrayview._render import render_mosaic
|
|
449
|
+
|
|
450
|
+
render_mosaic(session, dim_x, dim_y, dim_z, tuple(idx), **render_args)
|
|
451
|
+
except Exception:
|
|
452
|
+
pass # prefetch errors are silent
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
_get_prefetch_pool().submit(_warm)
|
|
456
|
+
except RuntimeError:
|
|
457
|
+
pass # pool shutting down
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _schedule_projection_prefetch(session, dim_x, dim_y, idx_list, slice_dim, proj_dim, proj_mode):
|
|
461
|
+
"""Compute the next projection along *slice_dim* into raw_cache.
|
|
462
|
+
|
|
463
|
+
A projection collapses a whole sub-volume, so on a lazily read file each
|
|
464
|
+
step waits for a volume read; single-slice lookahead neither predicts it
|
|
465
|
+
nor helps (scrolling the projected dim does not change the picture).
|
|
466
|
+
One step ahead: each costs a full sub-volume read.
|
|
467
|
+
"""
|
|
468
|
+
if slice_dim < 0 or slice_dim in (dim_x, dim_y, proj_dim):
|
|
469
|
+
return
|
|
470
|
+
current = idx_list[slice_dim]
|
|
471
|
+
lane = ("proj", dim_x, dim_y, slice_dim, proj_dim, proj_mode)
|
|
472
|
+
lanes, generation, direction = _prefetch_lane_step(session, lane, current, None)
|
|
473
|
+
target = current + direction
|
|
474
|
+
if not 0 <= target < session.shape[slice_dim]:
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
def _warm():
|
|
478
|
+
if generation != lanes[lane]["gen"]:
|
|
479
|
+
return
|
|
480
|
+
idx = list(idx_list)
|
|
481
|
+
idx[slice_dim] = target
|
|
482
|
+
try:
|
|
483
|
+
from arrayview._render import extract_projection
|
|
484
|
+
|
|
485
|
+
extract_projection(session, dim_x, dim_y, idx, proj_dim, proj_mode)
|
|
486
|
+
except Exception:
|
|
487
|
+
pass # prefetch errors are silent
|
|
344
488
|
|
|
345
489
|
try:
|
|
346
490
|
_get_prefetch_pool().submit(_warm)
|
|
@@ -814,6 +958,8 @@ ZARR_T_DEPTH = 2 # T-frames per chunk for 4D+ arrays
|
|
|
814
958
|
# Neighbor prefetch (Phase 3)
|
|
815
959
|
# ---------------------------------------------------------------------------
|
|
816
960
|
PREFETCH_NEIGHBORS = 3 # slices to prefetch in the scroll direction
|
|
961
|
+
FAST_PREFETCH_NEIGHBORS = 8 # ... while steps come faster than the interval below
|
|
962
|
+
FAST_SCROLL_INTERVAL_S = 0.15
|
|
817
963
|
PREFETCH_BUDGET_BYTES = 16 * 1024 * 1024 # 16 MB max prefetch per request
|
|
818
964
|
|
|
819
965
|
# ---------------------------------------------------------------------------
|
arrayview/_synthetic_mri.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
|
-
from arrayview._render import LUTS, _build_mosaic_grid, _ensure_lut, apply_complex_mode, extract_slice
|
|
7
|
+
from arrayview._render import LUTS, lut_rgba, _build_mosaic_grid, _ensure_lut, apply_complex_mode, extract_slice
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
QMRI_ROLE_ORDER: dict[int, list[str]] = {
|
|
@@ -202,6 +202,6 @@ def render_qmri_mosaic_rgba(
|
|
|
202
202
|
normalized = np.zeros_like(filled)
|
|
203
203
|
_ensure_lut(colormap)
|
|
204
204
|
lut = LUTS.get(colormap, LUTS["gray"])
|
|
205
|
-
rgba = lut
|
|
205
|
+
rgba = lut_rgba(lut, (normalized * 255).astype(np.uint8))
|
|
206
206
|
rgba[nan_mask] = [22, 22, 22, 255]
|
|
207
207
|
return rgba, vmin, vmax
|
|
@@ -7,7 +7,7 @@ arrayview/_bootstrap_app.py,sha256=ljiZ0o5D3nkJqEGcyyVF06dFWxXujBufQjbcn50QzXM,4
|
|
|
7
7
|
arrayview/_config.py,sha256=5ovQTO2I-j3fxCxEKcZKE2pqiahLESGTGS24nQdP61I,13493
|
|
8
8
|
arrayview/_diagnostics.py,sha256=XB0rCoDn3cpfhz4H-ayLktOgCMIoVJ0wM9N0ep_kKkQ,11018
|
|
9
9
|
arrayview/_dicom.py,sha256=tNY353MNWTvcXavHeIkqj3242dbgfH_ctwcx9NDee-E,19607
|
|
10
|
-
arrayview/_diff.py,sha256=
|
|
10
|
+
arrayview/_diff.py,sha256=10Uqvccr7Ocouphl14dMJzLMUHPhsD81LkM1IbsmLqg,5826
|
|
11
11
|
arrayview/_extra_ports.py,sha256=MZcx04NI9Eg4coydb-yPIHp7uJbE6UscyYGnw8fxid0,11125
|
|
12
12
|
arrayview/_icon.png,sha256=a8RTAbvweLhh3hzD5eGg76Peq-KSDcS6YrCphRb_HEs,2748
|
|
13
13
|
arrayview/_imaging.py,sha256=MQX1uy4m1lofOeoLDhCp6cHO5p_V5fXm4qlSu9Ykn-c,534
|
|
@@ -19,7 +19,7 @@ arrayview/_launcher.py,sha256=Hljfeq4AmxVTDiDgYyMtHT5eiatgFdrvcr6bq75onZY,243267
|
|
|
19
19
|
arrayview/_lifecycle.py,sha256=bqG0oKr-e-5S1ssCTlGmF-yBE47ilrpO1S_Owfjs9Z8,5365
|
|
20
20
|
arrayview/_overlays.py,sha256=CksypG9rWfCz_3RRGIeyaKeXse17AFxvkEjmYyXnIIE,5399
|
|
21
21
|
arrayview/_platform.py,sha256=qSWjRcx9rLIdLvKrRArSueFnbyn5GPKfTEMOc5mCIUs,29970
|
|
22
|
-
arrayview/_render.py,sha256=
|
|
22
|
+
arrayview/_render.py,sha256=W2FyrBYNt2dwim_CjYMM5U_Dq_vL5DEU4PL6_ybxIZ8,38564
|
|
23
23
|
arrayview/_routes_analysis.py,sha256=Xz9Ysr04u07pzSRB4Fzmcl6XwVxyaG6xCWxGwYZi_80,33085
|
|
24
24
|
arrayview/_routes_drop.py,sha256=FC4e7STdCo3hYIA0C4Yy9Ka2UKFx7nLDU5dkujmc1Vo,12477
|
|
25
25
|
arrayview/_routes_export.py,sha256=Ha8mMO2E9TlRL5x25-ADV8_8xUiC-jRz9N1oWwgKOsg,3898
|
|
@@ -28,18 +28,18 @@ arrayview/_routes_persistence.py,sha256=T03csHb7ppGT-8yMDf227XkqhnhjgSLYj0RW2wmg
|
|
|
28
28
|
arrayview/_routes_preferences.py,sha256=xoMHbPKliYspgjaclhx2r8HbP9mFo9Td-nTF76eWVMg,4680
|
|
29
29
|
arrayview/_routes_preload.py,sha256=KOsankNguxdIWmFW9iZcY9p2SvUN1mOKY7KkW1qMGcc,1729
|
|
30
30
|
arrayview/_routes_query.py,sha256=HEEmMKuFycUTBNVUSWtUFXCnWB7ZrM3hv9IZWbXz11o,25012
|
|
31
|
-
arrayview/_routes_rendering.py,sha256=
|
|
31
|
+
arrayview/_routes_rendering.py,sha256=HEA0okhZeiOADOEMdntOYe-mPMP2b_hfBBoAgiyQ-M4,22793
|
|
32
32
|
arrayview/_routes_segmentation.py,sha256=x2bBjQffZryeTMmOH38YdBWethw1o3KpA8V0chJcZHI,23599
|
|
33
33
|
arrayview/_routes_state.py,sha256=T_xoYcRlv7L47ydTxxiXb0Fp5M5x9KR2U-wkxaVeuMY,12462
|
|
34
34
|
arrayview/_routes_vectorfield.py,sha256=OWZtAONwRKQHdYnbDAMbOVf0q6pH7kqV8fa1icDhKHM,9734
|
|
35
|
-
arrayview/_routes_websocket.py,sha256=
|
|
35
|
+
arrayview/_routes_websocket.py,sha256=78XmMyuD_-KjG5dHpzbhtN4dOa5m5BeyHfASAFYg85c,43552
|
|
36
36
|
arrayview/_segmentation.py,sha256=aENWHmefrKDScXhcZL1X3KgYXE784dpMkmFFo83qnrw,7377
|
|
37
37
|
arrayview/_server.py,sha256=Ejh-6rWpWo0luhBhtsVq1Z4RFIyOPVv5nzExHtXb7iA,23042
|
|
38
|
-
arrayview/_session.py,sha256=
|
|
38
|
+
arrayview/_session.py,sha256=D4I6FLazLEXbo0IdkyRrzhvMNhvAI6dSyYu5I3tMCaM,42137
|
|
39
39
|
arrayview/_session_spec.py,sha256=mKDfMRsPYQdEmN9hLXmDU_9N7etiDyBXr8mz-fB8QBo,6825
|
|
40
40
|
arrayview/_shell.html,sha256=vNa2Kiwx_eoB5H329jolYafLhwJDCH9DsJafBqvVMWU,13194
|
|
41
41
|
arrayview/_source_safety.py,sha256=cI7JAL7DWYuxuB_LnYExvJZaICX2R2qJEJWD1M0z_gg,27664
|
|
42
|
-
arrayview/_synthetic_mri.py,sha256=
|
|
42
|
+
arrayview/_synthetic_mri.py,sha256=KcSYC26dgjBH25bUoqHT0IgJIzOxgXCSLkWr-ZnFq08,7385
|
|
43
43
|
arrayview/_torch.py,sha256=13sqIMCUu_-b9SQ9KfDCExDz2UMStL87EDVjnbJxAEo,7536
|
|
44
44
|
arrayview/_tutorial.py,sha256=2zZHJGJDa4n30GE1-9ADjKFAifGBH9vvHXP4uDt7BaA,19547
|
|
45
45
|
arrayview/_vectorfield.py,sha256=eDqMCMEGjO8Jdt-6Auh01cz3WD9dbF7xYUWD9xHAf1E,8115
|
|
@@ -52,8 +52,8 @@ arrayview/_watch.py,sha256=PxxS_5hcj_S6xQf9Ep4p1HLRkrI5iDMh1aRa4olV7ew,1873
|
|
|
52
52
|
arrayview/arrayview-opener.vsix,sha256=44uiTM39qj_RXji3pFQaR8TA1jhNB3LYwVU_dGFbP4c,62284
|
|
53
53
|
arrayview/gsap.min.js,sha256=VGihm4idI0E1uqrWezWsiScB9F_jMosn7qs6pPSkeac,72304
|
|
54
54
|
arrayview/todo.md,sha256=KhnRzpn6_jezR0nDDB61-zhox-rbOmEGW-A7Z0_7FnU,622
|
|
55
|
-
arrayview-0.48.
|
|
56
|
-
arrayview-0.48.
|
|
57
|
-
arrayview-0.48.
|
|
58
|
-
arrayview-0.48.
|
|
59
|
-
arrayview-0.48.
|
|
55
|
+
arrayview-0.48.2.dist-info/METADATA,sha256=iF32fWHFv__muQOvQPW_UDhkozFyTaaj4yGhot_ps3U,2288
|
|
56
|
+
arrayview-0.48.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
57
|
+
arrayview-0.48.2.dist-info/entry_points.txt,sha256=-0s08axutXkTbINiKs0Q7iv-33aWuTaamUg2q2-NyQ4,60
|
|
58
|
+
arrayview-0.48.2.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
|
|
59
|
+
arrayview-0.48.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|