arrayview 0.46.1__tar.gz → 0.46.2__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.46.1 → arrayview-0.46.2}/PKG-INFO +1 -1
- {arrayview-0.46.1 → arrayview-0.46.2}/pyproject.toml +1 -1
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_analysis.py +31 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_extra_ports.py +1 -1
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_io.py +3 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_launcher.py +156 -4
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_overlays.py +28 -2
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_render.py +31 -17
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_analysis.py +21 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_loading.py +3 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_query.py +74 -1
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_session.py +79 -0
- arrayview-0.46.2/src/arrayview/_tutorial.py +525 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_viewer.html +1918 -1380
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_vscode_extension.py +1 -1
- arrayview-0.46.2/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.46.2/src/arrayview/todo.md +12 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_api.py +1 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_browser.py +252 -44
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_roi_hud.py +26 -9
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_tutorial.py +1 -1
- arrayview-0.46.2/tests/test_tutorial_browser.py +481 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/visual_smoke.py +17 -11
- arrayview-0.46.1/src/arrayview/_tutorial.py +0 -194
- arrayview-0.46.1/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.46.1/src/arrayview/todo.md +0 -12
- arrayview-0.46.1/tests/test_tutorial_browser.py +0 -402
- {arrayview-0.46.1 → arrayview-0.46.2}/.gitignore +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/LICENSE +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/README.md +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/__init__.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/__main__.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_app.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_bootstrap_app.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_config.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_diagnostics.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_diff.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_icon.png +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_platform.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_server.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_shell.html +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_source_safety.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_torch.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/_watch.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/capture_v_animation.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/conftest.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_1d_arrays.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_backend_shared.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_bootstrap_app.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_cli.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_config.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_dicom.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_drop_import.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_instance_registry.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_invocation_contract_matrix.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_io.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_large_arrays.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_launch_plan.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_launch_trace.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_lifecycle_contract.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_loading_server.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_nifti_series.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_render_failure_reporting.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_render_progress.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_session_spec.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_source_safety.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_torch.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_tunnel_warm_path.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_value_formatting.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/test_window_level_preview.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/ui_audit.py +0 -0
- {arrayview-0.46.1 → arrayview-0.46.2}/tests/v_anim_frames/.gitkeep +0 -0
|
@@ -19,6 +19,29 @@ def _visible_shape(session) -> list[int]:
|
|
|
19
19
|
return [int(s) for s in shape]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _code_source(session) -> dict | None:
|
|
23
|
+
"""Say how a copied line of code should name this array.
|
|
24
|
+
|
|
25
|
+
Arrays passed from Python are named by the caller's variable. Arrays
|
|
26
|
+
opened from a file are reopened with the command line and that file.
|
|
27
|
+
"""
|
|
28
|
+
code_source = getattr(session, "code_source", None)
|
|
29
|
+
if code_source:
|
|
30
|
+
return code_source
|
|
31
|
+
filepath = getattr(session, "filepath", None)
|
|
32
|
+
if not filepath:
|
|
33
|
+
return None
|
|
34
|
+
import shlex
|
|
35
|
+
|
|
36
|
+
path = os.path.abspath(filepath)
|
|
37
|
+
home = os.path.expanduser("~")
|
|
38
|
+
if path.startswith(home + os.sep) and shlex.quote(path) == path:
|
|
39
|
+
path = "~" + path[len(home):]
|
|
40
|
+
else:
|
|
41
|
+
path = shlex.quote(path)
|
|
42
|
+
return {"kind": "file", "path": path}
|
|
43
|
+
|
|
44
|
+
|
|
22
45
|
def _build_metadata(session) -> dict:
|
|
23
46
|
"""Build metadata shared by HTTP routes and WebSocket startup."""
|
|
24
47
|
target_shape = session.spatial_shape if session.rgb_axis is not None else session.shape
|
|
@@ -35,6 +58,11 @@ def _build_metadata(session) -> dict:
|
|
|
35
58
|
"is_rgb": session.rgb_axis is not None,
|
|
36
59
|
"has_source_file": bool(getattr(session, "filepath", None)),
|
|
37
60
|
}
|
|
61
|
+
if getattr(session, "initial_view", None):
|
|
62
|
+
meta["initial_view"] = session.initial_view
|
|
63
|
+
code_source = _code_source(session)
|
|
64
|
+
if code_source is not None:
|
|
65
|
+
meta["code_source"] = code_source
|
|
38
66
|
collection_spatial_ndim = getattr(session, "collection_spatial_ndim", None)
|
|
39
67
|
default_shape = target_shape
|
|
40
68
|
if collection_spatial_ndim is not None:
|
|
@@ -74,6 +102,9 @@ def _build_metadata(session) -> dict:
|
|
|
74
102
|
[list(shape) for shape in row]
|
|
75
103
|
for row in session.data.ragged_spatial_shapes
|
|
76
104
|
]
|
|
105
|
+
case_names = getattr(session.data, "case_names", None)
|
|
106
|
+
if case_names:
|
|
107
|
+
meta["case_names"] = [str(name) for name in case_names]
|
|
77
108
|
case_voxel_spacings = getattr(session.data, "case_voxel_spacings", None)
|
|
78
109
|
if case_voxel_spacings:
|
|
79
110
|
meta["case_voxel_spacings"] = [
|
|
@@ -91,7 +91,7 @@ def _start_extra_port() -> tuple[int, dict] | None:
|
|
|
91
91
|
timeout_keep_alive=30,
|
|
92
92
|
ws_ping_interval=None,
|
|
93
93
|
ws="websockets",
|
|
94
|
-
ws_per_message_deflate=
|
|
94
|
+
ws_per_message_deflate=False,
|
|
95
95
|
)
|
|
96
96
|
server = uvicorn.Server(config)
|
|
97
97
|
task = asyncio.create_task(server.serve(sockets=[sock]))
|
|
@@ -1281,6 +1281,9 @@ def load_dir_collection(
|
|
|
1281
1281
|
scan_progress=scan_progress,
|
|
1282
1282
|
scan_label="images",
|
|
1283
1283
|
)
|
|
1284
|
+
# Lets the viewer label each case by its id (e.g. the patient folder)
|
|
1285
|
+
# instead of its position in the stack.
|
|
1286
|
+
data.case_names = [str(case) for case in case_ids]
|
|
1284
1287
|
spatial_shape = data._vol_shape
|
|
1285
1288
|
|
|
1286
1289
|
overlay_items = []
|
|
@@ -1312,6 +1312,8 @@ def _load_session_from_filepath(
|
|
|
1312
1312
|
related_sids: list[str] | None = None,
|
|
1313
1313
|
source_staging_dir: str | None = None,
|
|
1314
1314
|
watch: bool = False,
|
|
1315
|
+
initial_view: dict | None = None,
|
|
1316
|
+
code_source: dict | None = None,
|
|
1315
1317
|
) -> dict:
|
|
1316
1318
|
requested_sid = uuid.uuid4().hex
|
|
1317
1319
|
payload = {
|
|
@@ -1329,6 +1331,10 @@ def _load_session_from_filepath(
|
|
|
1329
1331
|
payload["source_staging_dir"] = source_staging_dir
|
|
1330
1332
|
if watch:
|
|
1331
1333
|
payload["watch"] = True
|
|
1334
|
+
if initial_view:
|
|
1335
|
+
payload["initial_view"] = initial_view
|
|
1336
|
+
if code_source:
|
|
1337
|
+
payload["code_source"] = code_source
|
|
1332
1338
|
if rgb:
|
|
1333
1339
|
payload["rgb"] = True
|
|
1334
1340
|
if dir_patterns:
|
|
@@ -1665,6 +1671,7 @@ def _register_cli_session_with_existing_server_impl(
|
|
|
1665
1671
|
native_request_id: str | None = None,
|
|
1666
1672
|
source_staging_dirs: dict[str, str] | None = None,
|
|
1667
1673
|
watch: bool = False,
|
|
1674
|
+
initial_view: dict | None = None,
|
|
1668
1675
|
_loaded_sids: list[str],
|
|
1669
1676
|
) -> dict[str, object]:
|
|
1670
1677
|
overlay_sids_list: list[str] = []
|
|
@@ -1719,6 +1726,7 @@ def _register_cli_session_with_existing_server_impl(
|
|
|
1719
1726
|
related_sids=[*compare_sids, *overlay_sids_list],
|
|
1720
1727
|
source_staging_dir=(source_staging_dirs or {}).get(base_file),
|
|
1721
1728
|
watch=watch,
|
|
1729
|
+
initial_view=initial_view,
|
|
1722
1730
|
)
|
|
1723
1731
|
if "error" in result:
|
|
1724
1732
|
error = str(result["error"])
|
|
@@ -1797,6 +1805,7 @@ def _handle_cli_existing_server(
|
|
|
1797
1805
|
use_native_shell: bool,
|
|
1798
1806
|
dims_override: tuple[int, int] | None,
|
|
1799
1807
|
watch: bool,
|
|
1808
|
+
initial_view: dict | None = None,
|
|
1800
1809
|
window_mode: str | None,
|
|
1801
1810
|
floating: bool,
|
|
1802
1811
|
is_remote: bool = False,
|
|
@@ -1847,6 +1856,7 @@ def _handle_cli_existing_server(
|
|
|
1847
1856
|
native_request_id=native_request_id,
|
|
1848
1857
|
source_staging_dirs=source_staging_dirs,
|
|
1849
1858
|
watch=watch,
|
|
1859
|
+
initial_view=initial_view,
|
|
1850
1860
|
)
|
|
1851
1861
|
# Inside this try on purpose: the generated bundle is removed in the
|
|
1852
1862
|
# finally below, and these sessions read from it.
|
|
@@ -1902,6 +1912,7 @@ def _handle_cli_existing_server(
|
|
|
1902
1912
|
dims_override=dims_override,
|
|
1903
1913
|
use_native_shell=use_native_shell,
|
|
1904
1914
|
watch=watch,
|
|
1915
|
+
initial_view=initial_view,
|
|
1905
1916
|
window_mode=window_mode,
|
|
1906
1917
|
floating=floating,
|
|
1907
1918
|
is_remote=is_remote,
|
|
@@ -2036,6 +2047,7 @@ def _handle_cli_spawned_daemon(
|
|
|
2036
2047
|
dims_override: tuple[int, int] | None,
|
|
2037
2048
|
use_native_shell: bool,
|
|
2038
2049
|
watch: bool,
|
|
2050
|
+
initial_view: dict | None = None,
|
|
2039
2051
|
window_mode: str | None,
|
|
2040
2052
|
floating: bool,
|
|
2041
2053
|
is_remote: bool,
|
|
@@ -2098,6 +2110,7 @@ def _handle_cli_spawned_daemon(
|
|
|
2098
2110
|
use_native_shell=use_native_shell,
|
|
2099
2111
|
dims_override=dims_override,
|
|
2100
2112
|
watch=watch,
|
|
2113
|
+
initial_view=initial_view,
|
|
2101
2114
|
window_mode=window_mode,
|
|
2102
2115
|
floating=floating,
|
|
2103
2116
|
is_remote=is_remote,
|
|
@@ -2170,6 +2183,7 @@ def _handle_cli_spawned_daemon(
|
|
|
2170
2183
|
f" cleanup_dir={repr(cleanup_dir)},"
|
|
2171
2184
|
f" source_staging_dirs={repr(source_staging_dirs or {})},"
|
|
2172
2185
|
f" watch={watch},"
|
|
2186
|
+
f" initial_view={repr(initial_view)},"
|
|
2173
2187
|
f")"
|
|
2174
2188
|
)
|
|
2175
2189
|
|
|
@@ -2779,8 +2793,10 @@ async def _serve_background(
|
|
|
2779
2793
|
log_level="error",
|
|
2780
2794
|
timeout_keep_alive=30,
|
|
2781
2795
|
ws_ping_interval=None,
|
|
2782
|
-
ws="websockets",
|
|
2783
|
-
|
|
2796
|
+
ws="websockets",
|
|
2797
|
+
# Deflate costs more than rendering a large slice and stalls playback.
|
|
2798
|
+
# A 256×256 frame measured 18 fps with it and 54 fps without it.
|
|
2799
|
+
ws_per_message_deflate=False,
|
|
2784
2800
|
)
|
|
2785
2801
|
server = _uvicorn().Server(config)
|
|
2786
2802
|
if stop_when_closed:
|
|
@@ -3476,6 +3492,30 @@ class ViewHandle(str):
|
|
|
3476
3492
|
return False
|
|
3477
3493
|
|
|
3478
3494
|
|
|
3495
|
+
def _caller_variable_name(obj) -> str | None:
|
|
3496
|
+
"""Name of the caller's variable that holds ``obj``, if there is one.
|
|
3497
|
+
|
|
3498
|
+
Used only to write a copyable ``view(<name>, ...)`` line, so a miss is
|
|
3499
|
+
harmless: expressions such as ``view(a[0])`` simply have no name.
|
|
3500
|
+
"""
|
|
3501
|
+
import inspect
|
|
3502
|
+
|
|
3503
|
+
frame = inspect.currentframe()
|
|
3504
|
+
try:
|
|
3505
|
+
caller = frame.f_back.f_back if frame and frame.f_back else None
|
|
3506
|
+
if caller is None:
|
|
3507
|
+
return None
|
|
3508
|
+
for scope in (caller.f_locals, caller.f_globals):
|
|
3509
|
+
for key, value in list(scope.items()):
|
|
3510
|
+
if value is obj and key.isidentifier() and not key.startswith("_"):
|
|
3511
|
+
return key
|
|
3512
|
+
except Exception:
|
|
3513
|
+
return None
|
|
3514
|
+
finally:
|
|
3515
|
+
del frame
|
|
3516
|
+
return None
|
|
3517
|
+
|
|
3518
|
+
|
|
3479
3519
|
def view(
|
|
3480
3520
|
*arrays,
|
|
3481
3521
|
name=None,
|
|
@@ -3487,6 +3527,14 @@ def view(
|
|
|
3487
3527
|
rgb: bool | list = False,
|
|
3488
3528
|
overlay=None,
|
|
3489
3529
|
floating: bool = False,
|
|
3530
|
+
mode: str | None = None,
|
|
3531
|
+
dims=None,
|
|
3532
|
+
index=None,
|
|
3533
|
+
cmap: str | None = None,
|
|
3534
|
+
vmin: float | None = None,
|
|
3535
|
+
vmax: float | None = None,
|
|
3536
|
+
log: bool = False,
|
|
3537
|
+
complex: str | None = None,
|
|
3490
3538
|
):
|
|
3491
3539
|
"""
|
|
3492
3540
|
Launch the viewer. Does not block the main Python process.
|
|
@@ -3526,6 +3574,16 @@ def view(
|
|
|
3526
3574
|
``overlay`` — a single array or list of arrays to composite as overlays.
|
|
3527
3575
|
Each overlay is assigned an auto-palette color from _OVERLAY_PALETTE.
|
|
3528
3576
|
|
|
3577
|
+
``mode``, ``dims``, ``index``, ``cmap``, ``vmin``/``vmax`` and ``log`` set
|
|
3578
|
+
the view the viewer opens with: the viewing mode (``"ortho"``,
|
|
3579
|
+
``"mosaic"``, ``"qmri"``, ``"qmri-ortho"``, ``"qmri-mosaic"``), the two
|
|
3580
|
+
dimensions shown as x and y (three for ortho: its volume dims; three for
|
|
3581
|
+
the mosaics: x, y and the gridded dim), the position along every
|
|
3582
|
+
dimension, the colormap, the display range and log scale. ``complex``
|
|
3583
|
+
picks what complex data shows: ``"mag"``, ``"phase"``, ``"real"`` or
|
|
3584
|
+
``"imag"`` (``"mag"`` shows the absolute value of real data). Pressing
|
|
3585
|
+
``E`` in the viewer copies the current view as such a call.
|
|
3586
|
+
|
|
3529
3587
|
Returns a ``ViewHandle`` for a single array, or a tuple of ``ViewHandle``
|
|
3530
3588
|
objects for multiple arrays (one per array). In inline/Jupyter mode with
|
|
3531
3589
|
multiple arrays, the IFrame is displayed automatically and a uniform tuple
|
|
@@ -3537,6 +3595,12 @@ def view(
|
|
|
3537
3595
|
import numpy as np
|
|
3538
3596
|
from arrayview._io import _tensor_to_numpy
|
|
3539
3597
|
|
|
3598
|
+
_initial_view = _session_mod.initial_view_spec(
|
|
3599
|
+
mode=mode, dims=dims, index=index, cmap=cmap, vmin=vmin, vmax=vmax, log=log,
|
|
3600
|
+
complex=complex,
|
|
3601
|
+
)
|
|
3602
|
+
_code_name = _caller_variable_name(arrays[0]) if arrays else None
|
|
3603
|
+
|
|
3540
3604
|
height = _normalize_inline_height(height)
|
|
3541
3605
|
_inline_mode_heights = _normalize_inline_mode_heights(mode_heights)
|
|
3542
3606
|
|
|
@@ -3605,6 +3669,13 @@ def view(
|
|
|
3605
3669
|
data = arrays[0]
|
|
3606
3670
|
name = names[0]
|
|
3607
3671
|
rgb_primary = rgbs[0]
|
|
3672
|
+
if _initial_view and "index" in _initial_view:
|
|
3673
|
+
_ndim = len(getattr(data, "shape", ()))
|
|
3674
|
+
if len(_initial_view["index"]) != _ndim:
|
|
3675
|
+
raise ValueError(
|
|
3676
|
+
f"index has {len(_initial_view['index'])} entries but the array "
|
|
3677
|
+
f"has {_ndim} dimensions"
|
|
3678
|
+
)
|
|
3608
3679
|
|
|
3609
3680
|
# --- Normalise string window modes ---
|
|
3610
3681
|
_raw_window = window
|
|
@@ -3634,6 +3705,18 @@ def view(
|
|
|
3634
3705
|
else:
|
|
3635
3706
|
_invocation = Invocation.PYTHON
|
|
3636
3707
|
|
|
3708
|
+
# Julia and MATLAB callers are not Python frames, so only their explicit
|
|
3709
|
+
# name can stand in for the variable.
|
|
3710
|
+
if _code_name is None or _invocation in (Invocation.JULIA, Invocation.MATLAB):
|
|
3711
|
+
_code_name = name if str(name).isidentifier() else "x"
|
|
3712
|
+
_code_source = {
|
|
3713
|
+
"kind": {
|
|
3714
|
+
Invocation.JULIA: "julia",
|
|
3715
|
+
Invocation.MATLAB: "matlab",
|
|
3716
|
+
}.get(_invocation, "python"),
|
|
3717
|
+
"name": _code_name,
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3637
3720
|
if isinstance(_raw_window, str):
|
|
3638
3721
|
_requested_window = _raw_window
|
|
3639
3722
|
elif _raw_window is True:
|
|
@@ -3704,6 +3787,8 @@ def view(
|
|
|
3704
3787
|
mode_heights=_inline_mode_heights,
|
|
3705
3788
|
floating=floating,
|
|
3706
3789
|
launch_context=_launch_context,
|
|
3790
|
+
initial_view=_initial_view,
|
|
3791
|
+
code_source=_code_source,
|
|
3707
3792
|
)
|
|
3708
3793
|
|
|
3709
3794
|
# VS Code tunnel/remote: use the server + WebSocket path. The opener owns
|
|
@@ -3727,6 +3812,7 @@ def view(
|
|
|
3727
3812
|
_tmp_path = _tmp.name
|
|
3728
3813
|
try:
|
|
3729
3814
|
np.save(_tmp_path, _array)
|
|
3815
|
+
_is_primary = not _loaded_sids
|
|
3730
3816
|
_result = _load_session_from_filepath(
|
|
3731
3817
|
port,
|
|
3732
3818
|
_tmp_path,
|
|
@@ -3734,6 +3820,8 @@ def view(
|
|
|
3734
3820
|
rgb=_rgb,
|
|
3735
3821
|
expected_server_id=_expected_server_id,
|
|
3736
3822
|
release_on_disconnect=inline,
|
|
3823
|
+
initial_view=_initial_view if _is_primary else None,
|
|
3824
|
+
code_source=_code_source if _is_primary else None,
|
|
3737
3825
|
)
|
|
3738
3826
|
finally:
|
|
3739
3827
|
try:
|
|
@@ -3830,6 +3918,8 @@ def view(
|
|
|
3830
3918
|
session = _session_mod.Session(data, name=name)
|
|
3831
3919
|
if rgb_primary:
|
|
3832
3920
|
_setup_rgb(session)
|
|
3921
|
+
session.initial_view = _initial_view
|
|
3922
|
+
session.code_source = _code_source
|
|
3833
3923
|
_session_mod.SESSIONS[session.sid] = session
|
|
3834
3924
|
|
|
3835
3925
|
# Register compare sessions (arrays[1:])
|
|
@@ -4260,6 +4350,8 @@ def _view_julia(
|
|
|
4260
4350
|
mode_heights: dict[str, int] | None = None,
|
|
4261
4351
|
floating: bool = False,
|
|
4262
4352
|
launch_context=None,
|
|
4353
|
+
initial_view: dict | None = None,
|
|
4354
|
+
code_source: dict | None = None,
|
|
4263
4355
|
):
|
|
4264
4356
|
"""Julia-specific view() path: run the server in a subprocess so it is
|
|
4265
4357
|
completely independent of Julia's GIL.
|
|
@@ -4281,6 +4373,8 @@ def _view_julia(
|
|
|
4281
4373
|
force_vscode=force_vscode,
|
|
4282
4374
|
floating=floating,
|
|
4283
4375
|
launch_context=launch_context,
|
|
4376
|
+
initial_view=initial_view,
|
|
4377
|
+
code_source=code_source,
|
|
4284
4378
|
)
|
|
4285
4379
|
|
|
4286
4380
|
|
|
@@ -4296,6 +4390,8 @@ def _view_subprocess(
|
|
|
4296
4390
|
force_vscode: bool = False,
|
|
4297
4391
|
floating: bool = False,
|
|
4298
4392
|
launch_context=None,
|
|
4393
|
+
initial_view: dict | None = None,
|
|
4394
|
+
code_source: dict | None = None,
|
|
4299
4395
|
) -> str:
|
|
4300
4396
|
"""Run the viewer in a separate subprocess server.
|
|
4301
4397
|
|
|
@@ -4351,6 +4447,8 @@ def _view_subprocess(
|
|
|
4351
4447
|
expected_server_id=active_server_id,
|
|
4352
4448
|
native_request_id=native_request_id,
|
|
4353
4449
|
release_on_disconnect=True,
|
|
4450
|
+
initial_view=initial_view,
|
|
4451
|
+
code_source=code_source,
|
|
4354
4452
|
)
|
|
4355
4453
|
if "error" in result:
|
|
4356
4454
|
raise RuntimeError(result["error"])
|
|
@@ -4415,7 +4513,9 @@ def _view_subprocess(
|
|
|
4415
4513
|
f"_serve_daemon({repr(tmp_path)}, {port}, {repr(sid)}, "
|
|
4416
4514
|
f"name={repr(name)}, cleanup=True, "
|
|
4417
4515
|
f"persist={persist_daemon}, "
|
|
4418
|
-
f"connect_timeout={repr(daemon_connect_timeout)}, rgb={rgb}
|
|
4516
|
+
f"connect_timeout={repr(daemon_connect_timeout)}, rgb={rgb}, "
|
|
4517
|
+
f"initial_view={repr(initial_view)}, "
|
|
4518
|
+
f"code_source={repr(code_source)})"
|
|
4419
4519
|
)
|
|
4420
4520
|
daemon_proc = subprocess.Popen(
|
|
4421
4521
|
[sys.executable, "-c", script],
|
|
@@ -4550,6 +4650,8 @@ def _serve_empty(port: int) -> None:
|
|
|
4550
4650
|
_server_mod().app,
|
|
4551
4651
|
log_level="error",
|
|
4552
4652
|
timeout_keep_alive=30,
|
|
4653
|
+
ws="websockets",
|
|
4654
|
+
ws_per_message_deflate=False,
|
|
4553
4655
|
)
|
|
4554
4656
|
server = _uvicorn().Server(config)
|
|
4555
4657
|
asyncio.run(server.serve(sockets=socks))
|
|
@@ -4589,6 +4691,8 @@ def _serve_daemon(
|
|
|
4589
4691
|
cleanup_dir: str | None = None,
|
|
4590
4692
|
source_staging_dirs: dict[str, str] | None = None,
|
|
4591
4693
|
watch: bool = False,
|
|
4694
|
+
initial_view: dict | None = None,
|
|
4695
|
+
code_source: dict | None = None,
|
|
4592
4696
|
) -> None:
|
|
4593
4697
|
"""Background server process. Loads data, serves it.
|
|
4594
4698
|
persist=True: never exits (used on remote tunnel so port stays alive).
|
|
@@ -4642,6 +4746,8 @@ def _serve_daemon(
|
|
|
4642
4746
|
bootstrap,
|
|
4643
4747
|
log_level="error",
|
|
4644
4748
|
timeout_keep_alive=30,
|
|
4749
|
+
ws="websockets",
|
|
4750
|
+
ws_per_message_deflate=False,
|
|
4645
4751
|
)
|
|
4646
4752
|
server = _uvicorn().Server(config)
|
|
4647
4753
|
uvicorn_server.append(server)
|
|
@@ -4756,6 +4862,10 @@ def _serve_daemon(
|
|
|
4756
4862
|
if filepath in source_staging_dirs:
|
|
4757
4863
|
session._source_staging_dirs = [source_staging_dirs[filepath]]
|
|
4758
4864
|
session.sid = sid
|
|
4865
|
+
_session_mod.apply_launch_view(
|
|
4866
|
+
session,
|
|
4867
|
+
{"initial_view": initial_view, "code_source": code_source},
|
|
4868
|
+
)
|
|
4759
4869
|
if signature_before_load is not None:
|
|
4760
4870
|
signature_after_load = file_signature(filepath)
|
|
4761
4871
|
if signature_before_load == signature_after_load:
|
|
@@ -5667,6 +5777,28 @@ def arrayview():
|
|
|
5667
5777
|
"Also accepts 0-based integer pair like '2,3'."
|
|
5668
5778
|
),
|
|
5669
5779
|
)
|
|
5780
|
+
parser.add_argument(
|
|
5781
|
+
"--index",
|
|
5782
|
+
metavar="I,J,...",
|
|
5783
|
+
default=None,
|
|
5784
|
+
help="Open at this position: one 0-based index per dimension, e.g. '128,128,40'.",
|
|
5785
|
+
)
|
|
5786
|
+
parser.add_argument(
|
|
5787
|
+
"--mode",
|
|
5788
|
+
default=None,
|
|
5789
|
+
choices=["ortho", "mosaic", "qmri", "qmri-ortho", "qmri-mosaic"],
|
|
5790
|
+
help="Open in this viewing mode. With ortho or a mosaic, --dims takes three dims.",
|
|
5791
|
+
)
|
|
5792
|
+
parser.add_argument("--cmap", default=None, help="Open with this colormap, e.g. 'gray'.")
|
|
5793
|
+
parser.add_argument("--vmin", type=float, default=None, help="Lower end of the display range (with --vmax).")
|
|
5794
|
+
parser.add_argument("--vmax", type=float, default=None, help="Upper end of the display range (with --vmin).")
|
|
5795
|
+
parser.add_argument("--log", action="store_true", help="Open with log scale on.")
|
|
5796
|
+
parser.add_argument(
|
|
5797
|
+
"--complex",
|
|
5798
|
+
default=None,
|
|
5799
|
+
choices=["mag", "phase", "real", "imag"],
|
|
5800
|
+
help="Which part of complex data to show (mag shows |x| of real data).",
|
|
5801
|
+
)
|
|
5670
5802
|
parser.add_argument(
|
|
5671
5803
|
"--name",
|
|
5672
5804
|
default=None,
|
|
@@ -5827,13 +5959,31 @@ def arrayview():
|
|
|
5827
5959
|
return
|
|
5828
5960
|
|
|
5829
5961
|
dims_override: tuple[int, int] | None = None
|
|
5830
|
-
|
|
5962
|
+
view_dims: list[str] | None = None
|
|
5963
|
+
if args.dims and args.mode in ("ortho", "mosaic", "qmri-mosaic"):
|
|
5964
|
+
# Three dims: not an x/y pair, so they only travel with the view.
|
|
5965
|
+
view_dims = args.dims.split(",")
|
|
5966
|
+
elif args.dims:
|
|
5831
5967
|
dims_override = _parse_dims_spec(args.dims)
|
|
5832
5968
|
if dims_override is None:
|
|
5833
5969
|
parser.error(
|
|
5834
5970
|
f"--dims {args.dims!r} is invalid. "
|
|
5835
5971
|
"Use e.g. 'x,y,:,:' or ':,:,x,y' or '0,1'."
|
|
5836
5972
|
)
|
|
5973
|
+
initial_view: dict | None = None
|
|
5974
|
+
try:
|
|
5975
|
+
initial_view = _session_mod.initial_view_spec(
|
|
5976
|
+
mode=args.mode,
|
|
5977
|
+
dims=view_dims or dims_override,
|
|
5978
|
+
index=args.index.split(",") if args.index else None,
|
|
5979
|
+
cmap=args.cmap,
|
|
5980
|
+
vmin=args.vmin,
|
|
5981
|
+
vmax=args.vmax,
|
|
5982
|
+
log=args.log,
|
|
5983
|
+
complex=args.complex,
|
|
5984
|
+
)
|
|
5985
|
+
except ValueError as exc:
|
|
5986
|
+
parser.error(str(exc))
|
|
5837
5987
|
if getattr(args, "stack_flag_removed", False):
|
|
5838
5988
|
parser.error(
|
|
5839
5989
|
"--stack has been renamed to --match, because it never controlled "
|
|
@@ -6458,6 +6608,7 @@ def arrayview():
|
|
|
6458
6608
|
vfield_components_dim=vfield_components_dim,
|
|
6459
6609
|
use_native_shell=use_native_shell,
|
|
6460
6610
|
dims_override=dims_override,
|
|
6611
|
+
initial_view=initial_view,
|
|
6461
6612
|
watch=getattr(args, "watch", False),
|
|
6462
6613
|
window_mode=window_mode,
|
|
6463
6614
|
floating=args.floating,
|
|
@@ -6490,6 +6641,7 @@ def arrayview():
|
|
|
6490
6641
|
overlay_files=[] if args.stack_mode else file_overlay_paths,
|
|
6491
6642
|
overlay_names=[] if args.stack_mode else file_overlay_names,
|
|
6492
6643
|
dims_override=dims_override,
|
|
6644
|
+
initial_view=initial_view,
|
|
6493
6645
|
use_native_shell=use_native_shell,
|
|
6494
6646
|
watch=getattr(args, "watch", False),
|
|
6495
6647
|
window_mode=window_mode,
|
|
@@ -26,6 +26,22 @@ def _parse_hex_color(hex_str: str) -> np.ndarray | None:
|
|
|
26
26
|
return None
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _parse_overlay_color_token(token: str) -> tuple[np.ndarray | None, frozenset[int]]:
|
|
30
|
+
"""Parse one ``overlay_colors`` entry: ``RRGGBB`` or ``RRGGBB~2.5``.
|
|
31
|
+
|
|
32
|
+
The optional ``~`` suffix lists label values of a label-map overlay that
|
|
33
|
+
the viewer has switched off.
|
|
34
|
+
"""
|
|
35
|
+
color_part, _, hidden_part = token.partition("~")
|
|
36
|
+
hidden = set()
|
|
37
|
+
for value in hidden_part.split("."):
|
|
38
|
+
try:
|
|
39
|
+
hidden.add(int(value))
|
|
40
|
+
except ValueError:
|
|
41
|
+
pass
|
|
42
|
+
return _parse_hex_color(color_part), frozenset(hidden)
|
|
43
|
+
|
|
44
|
+
|
|
29
45
|
def _composite_overlays(
|
|
30
46
|
rgba: np.ndarray,
|
|
31
47
|
overlay_sid_str: str | None,
|
|
@@ -50,7 +66,11 @@ def _composite_overlays(
|
|
|
50
66
|
[a.strip() for a in overlay_alphas_str.split(",")] if overlay_alphas_str else []
|
|
51
67
|
)
|
|
52
68
|
for i, sid in enumerate(sids):
|
|
53
|
-
color =
|
|
69
|
+
color, hidden = (
|
|
70
|
+
_parse_overlay_color_token(colors_raw[i])
|
|
71
|
+
if i < len(colors_raw)
|
|
72
|
+
else (None, frozenset())
|
|
73
|
+
)
|
|
54
74
|
alpha = overlay_alpha
|
|
55
75
|
if i < len(alphas_raw):
|
|
56
76
|
try:
|
|
@@ -73,6 +93,7 @@ def _composite_overlays(
|
|
|
73
93
|
is_label=_overlay_is_label_map(sid, ov_raw),
|
|
74
94
|
override_color=color,
|
|
75
95
|
outline_only=overlay_outline,
|
|
96
|
+
hidden_labels=hidden,
|
|
76
97
|
)
|
|
77
98
|
return rgba
|
|
78
99
|
|
|
@@ -114,7 +135,11 @@ def _composite_mosaic_overlays(
|
|
|
114
135
|
n_frames = int(base_shape[dim_z])
|
|
115
136
|
|
|
116
137
|
for i, sid in enumerate(sids):
|
|
117
|
-
color =
|
|
138
|
+
color, hidden = (
|
|
139
|
+
_parse_overlay_color_token(colors_raw[i])
|
|
140
|
+
if i < len(colors_raw)
|
|
141
|
+
else (None, frozenset())
|
|
142
|
+
)
|
|
118
143
|
try:
|
|
119
144
|
alpha = float(alphas_raw[i]) if i < len(alphas_raw) else overlay_alpha
|
|
120
145
|
except ValueError:
|
|
@@ -146,5 +171,6 @@ def _composite_mosaic_overlays(
|
|
|
146
171
|
is_label=_overlay_is_label_map(sid, grid),
|
|
147
172
|
override_color=color,
|
|
148
173
|
outline_only=overlay_outline,
|
|
174
|
+
hidden_labels=hidden,
|
|
149
175
|
)
|
|
150
176
|
return rgba
|
|
@@ -577,6 +577,32 @@ def render_projection_rgba(
|
|
|
577
577
|
# ---------------------------------------------------------------------------
|
|
578
578
|
|
|
579
579
|
|
|
580
|
+
def _overlay_base_axes(
|
|
581
|
+
ov_session, main_shape: tuple[int, ...], dim_x: int, dim_y: int
|
|
582
|
+
) -> tuple[int, ...] | None:
|
|
583
|
+
"""Return the base-array axes an overlay's axes line up with, or None."""
|
|
584
|
+
ov_shape = tuple(int(size) for size in ov_session.shape)
|
|
585
|
+
if len(ov_shape) > len(main_shape):
|
|
586
|
+
return None
|
|
587
|
+
map_key = (main_shape, dim_x, dim_y)
|
|
588
|
+
axis_maps = getattr(ov_session, "_overlay_axis_maps", None)
|
|
589
|
+
if axis_maps is None:
|
|
590
|
+
axis_maps = {}
|
|
591
|
+
ov_session._overlay_axis_maps = axis_maps
|
|
592
|
+
if map_key not in axis_maps:
|
|
593
|
+
axis_maps[map_key] = next(
|
|
594
|
+
(
|
|
595
|
+
base_axes
|
|
596
|
+
for base_axes in itertools.combinations(range(len(main_shape)), len(ov_shape))
|
|
597
|
+
if dim_x in base_axes
|
|
598
|
+
and dim_y in base_axes
|
|
599
|
+
and tuple(main_shape[axis] for axis in base_axes) == ov_shape
|
|
600
|
+
),
|
|
601
|
+
None,
|
|
602
|
+
)
|
|
603
|
+
return axis_maps[map_key]
|
|
604
|
+
|
|
605
|
+
|
|
580
606
|
def _extract_overlay_mask(
|
|
581
607
|
overlay_sid: str | None,
|
|
582
608
|
dim_x: int,
|
|
@@ -604,23 +630,7 @@ def _extract_overlay_mask(
|
|
|
604
630
|
# Match its axes to equal-sized base axes, then index only those axes. This
|
|
605
631
|
# broadcasts the overlay over every omitted axis without allocating a tiled
|
|
606
632
|
# copy (important for masks next to multi-echo or multi-channel data).
|
|
607
|
-
|
|
608
|
-
axis_maps = getattr(ov_session, "_overlay_axis_maps", None)
|
|
609
|
-
if axis_maps is None:
|
|
610
|
-
axis_maps = {}
|
|
611
|
-
ov_session._overlay_axis_maps = axis_maps
|
|
612
|
-
if map_key not in axis_maps:
|
|
613
|
-
axis_maps[map_key] = next(
|
|
614
|
-
(
|
|
615
|
-
base_axes
|
|
616
|
-
for base_axes in itertools.combinations(range(len(main_shape)), len(ov_shape))
|
|
617
|
-
if dim_x in base_axes
|
|
618
|
-
and dim_y in base_axes
|
|
619
|
-
and tuple(main_shape[axis] for axis in base_axes) == ov_shape
|
|
620
|
-
),
|
|
621
|
-
None,
|
|
622
|
-
)
|
|
623
|
-
base_axes = axis_maps[map_key]
|
|
633
|
+
base_axes = _overlay_base_axes(ov_session, main_shape, dim_x, dim_y)
|
|
624
634
|
if base_axes is None:
|
|
625
635
|
return None
|
|
626
636
|
ov_dim_x = base_axes.index(dim_x)
|
|
@@ -663,6 +673,7 @@ def _composite_overlay_mask(
|
|
|
663
673
|
is_label: bool = False,
|
|
664
674
|
override_color: np.ndarray | None = None,
|
|
665
675
|
outline_only: bool = False,
|
|
676
|
+
hidden_labels: frozenset[int] = frozenset(),
|
|
666
677
|
) -> np.ndarray:
|
|
667
678
|
"""Alpha-composite an overlay on top of an RGBA frame.
|
|
668
679
|
|
|
@@ -689,6 +700,8 @@ def _composite_overlay_mask(
|
|
|
689
700
|
# Binary mask with a palette override colour
|
|
690
701
|
use_override = override_color is not None and len(nonzero_labels) == 1
|
|
691
702
|
for lbl in nonzero_labels:
|
|
703
|
+
if int(lbl) in hidden_labels:
|
|
704
|
+
continue
|
|
692
705
|
mask = labels == lbl
|
|
693
706
|
if outline_only:
|
|
694
707
|
mask = _mask_outline(mask)
|
|
@@ -975,6 +988,7 @@ __all__ = [
|
|
|
975
988
|
"_extract_overlay_mask",
|
|
976
989
|
"_composite_overlay_mask",
|
|
977
990
|
"_overlay_is_label_map",
|
|
991
|
+
"_overlay_base_axes",
|
|
978
992
|
"LABEL_COLORS",
|
|
979
993
|
# Mosaic
|
|
980
994
|
"render_mosaic",
|
|
@@ -347,12 +347,14 @@ def register_analysis_routes(app, get_session_or_404) -> None:
|
|
|
347
347
|
results = []
|
|
348
348
|
for roi_idx, roi in enumerate(rois):
|
|
349
349
|
rows = []
|
|
350
|
+
values = []
|
|
350
351
|
for idx in _roi_scope_indices(roi, indices, session.shape, {dim_x, dim_y}):
|
|
351
352
|
raw = extract_slice(session, dim_x, dim_y, idx)
|
|
352
353
|
data = apply_complex_mode(raw, complex_mode)
|
|
353
354
|
mask = _roi_mask_for_shape(roi, data.shape, idx)
|
|
354
355
|
finite = _roi_finite_values(data, mask)
|
|
355
356
|
if finite.size:
|
|
357
|
+
values.append(finite)
|
|
356
358
|
rows.append(
|
|
357
359
|
{
|
|
358
360
|
"indices": list(idx),
|
|
@@ -365,6 +367,8 @@ def register_analysis_routes(app, get_session_or_404) -> None:
|
|
|
365
367
|
}
|
|
366
368
|
)
|
|
367
369
|
combined = _combine_roi_rows(rows)
|
|
370
|
+
if combined is not None:
|
|
371
|
+
combined["hist"] = _roi_value_histogram(values, combined["min"], combined["max"])
|
|
368
372
|
results.append(
|
|
369
373
|
{
|
|
370
374
|
"roi": roi.get("id", roi_idx),
|
|
@@ -898,6 +902,23 @@ def _roi_finite_values(data: np.ndarray, mask: np.ndarray) -> np.ndarray:
|
|
|
898
902
|
return roi[np.isfinite(roi)]
|
|
899
903
|
|
|
900
904
|
|
|
905
|
+
_ROI_HIST_BINS = 24
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _roi_value_histogram(values: list[np.ndarray], lo, hi) -> list[int]:
|
|
909
|
+
"""Pixel counts in equal bins between the ROI's own min and max.
|
|
910
|
+
|
|
911
|
+
The viewer places each ROI's bins on one value axis shared by all ROIs,
|
|
912
|
+
so only the counts travel; the edges follow from min/max.
|
|
913
|
+
"""
|
|
914
|
+
if not values or lo is None or hi is None:
|
|
915
|
+
return []
|
|
916
|
+
counts, _ = np.histogram(
|
|
917
|
+
np.concatenate(values), bins=_ROI_HIST_BINS, range=(float(lo), float(hi) if hi > lo else float(lo) + 1.0)
|
|
918
|
+
)
|
|
919
|
+
return [int(c) for c in counts]
|
|
920
|
+
|
|
921
|
+
|
|
901
922
|
def _combine_roi_rows(rows: list[dict]) -> dict | None:
|
|
902
923
|
if not rows:
|
|
903
924
|
return None
|