arrayview 0.31.0__tar.gz → 0.31.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.31.0 → arrayview-0.31.2}/PKG-INFO +1 -1
- {arrayview-0.31.0 → arrayview-0.31.2}/pyproject.toml +1 -1
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_analysis.py +9 -2
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_instance_registry.py +6 -0
- arrayview-0.31.2/src/arrayview/_overlays.py +150 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_render.py +37 -19
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_rendering.py +13 -1
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_websocket.py +27 -1
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_session.py +96 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vscode_browser.py +2 -1
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_api.py +84 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_lifecycle_contract.py +6 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_nifti_series.py +111 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_vscode_ack_protocol.py +7 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/uv.lock +1 -1
- {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/test_lifecycle_helpers.js +8 -6
- arrayview-0.31.0/src/arrayview/_overlays.py +0 -74
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/frontend-designer/SKILL.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/SKILL.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/running-code.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/session-management.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/tracing.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/todo-workflow/SKILL.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.github/copilot-instructions.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.github/workflows/docs.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.github/workflows/launch-contracts.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.gitignore +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.ignore +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/AGENTS.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/ROUTER.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/SETUP.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/SYNC.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/architecture.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/conventions.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/decisions.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/frontend.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/lifecycle.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/project-state.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/render-pipeline.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/setup.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/stack.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/INDEX.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/README.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/add-file-format.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/add-server-endpoint.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/animation-verify.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/debug-render.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/debug-vscode-extension-python.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/extend-compare-mode.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/extract-server-route-module.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/frontend-change.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/setup.sh +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.mex/sync.sh +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.opencode/opencode.json +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/theme-dark.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.python-version +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/.vscode/settings.json +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/AGENTS.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/CONTRIBUTING.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/DESIGN.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/LICENSE +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/README.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/comparing.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/configuration.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/display.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/foreign-hosts.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/index.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/launch-routing-refactor.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/lifecycle.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/loading.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/logo.png +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/measurement.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/remote.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/viewing.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/docs/vscode-window-routing-plan.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/experiments/ws_tunnel_diag/ws_tunnel_diag.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/matlab/arrayview.m +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/mkdocs.yml +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/plans/2026-04-14-immersive-animation.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/plans/2026-05-07-unified-colormap-picker.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/plans/egg-placement-mockup.html +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/plans/refactoring_plan.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/plans/roi-analysis-requirements.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/plans/webview/LOG.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/scripts/demo.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/scripts/release.sh +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/__init__.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/__main__.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_app.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_codex_open.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_config.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_diff.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_icon.png +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_io.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_platform.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_loading.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_server.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_shell.html +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_torch.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_viewer.html +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vscode_extension.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/capture_v_animation.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/conftest.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_backend_shared.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_browser.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_cli.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_command_reachability.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_config.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_instance_registry.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_interactions.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_invocation_contract_matrix.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_io.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_large_arrays.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_launch_plan.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_loading_server.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_session_spec.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_torch.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/ui_audit.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/tests/visual_smoke.py +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/AGENTS.md +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/LICENSE +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/extension.js +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/lifecycle_helpers.js +0 -0
- {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/package.json +0 -0
|
@@ -34,7 +34,15 @@ def _build_metadata(session) -> dict:
|
|
|
34
34
|
"is_rgb": session.rgb_axis is not None,
|
|
35
35
|
"has_source_file": bool(getattr(session, "filepath", None)),
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
collection_spatial_ndim = getattr(session, "collection_spatial_ndim", None)
|
|
38
|
+
default_shape = target_shape
|
|
39
|
+
if collection_spatial_ndim is not None:
|
|
40
|
+
spatial_ndim = int(collection_spatial_ndim)
|
|
41
|
+
if 2 <= spatial_ndim <= len(target_shape):
|
|
42
|
+
# Collection axes are appended after the image dimensions. They
|
|
43
|
+
# select a case; they must not influence the startup plane.
|
|
44
|
+
default_shape = target_shape[:spatial_ndim]
|
|
45
|
+
default_dims = _session_mod._startup_dims_for_data(session.data, default_shape)
|
|
38
46
|
if default_dims is not None:
|
|
39
47
|
meta["default_dims"] = [int(default_dims[0]), int(default_dims[1])]
|
|
40
48
|
if getattr(session, "spatial_meta", None) is not None:
|
|
@@ -49,7 +57,6 @@ def _build_metadata(session) -> dict:
|
|
|
49
57
|
)
|
|
50
58
|
if getattr(session, "array_keys", None):
|
|
51
59
|
meta["array_keys"] = session.array_keys
|
|
52
|
-
collection_spatial_ndim = getattr(session, "collection_spatial_ndim", None)
|
|
53
60
|
if collection_spatial_ndim is not None:
|
|
54
61
|
meta["collection_spatial_ndim"] = int(collection_spatial_ndim)
|
|
55
62
|
if getattr(session.data, "_av_ragged", False):
|
|
@@ -115,11 +115,17 @@ def _windows_process_start(pid: int) -> str | None:
|
|
|
115
115
|
handle = kernel32.OpenProcess(0x1000, False, pid)
|
|
116
116
|
if not handle:
|
|
117
117
|
return None
|
|
118
|
+
still_active = 259
|
|
119
|
+
exit_code = ctypes.c_ulong()
|
|
118
120
|
creation = ctypes.c_ulonglong()
|
|
119
121
|
exit_time = ctypes.c_ulonglong()
|
|
120
122
|
kernel = ctypes.c_ulonglong()
|
|
121
123
|
user = ctypes.c_ulonglong()
|
|
122
124
|
try:
|
|
125
|
+
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
|
|
126
|
+
return None
|
|
127
|
+
if exit_code.value != still_active:
|
|
128
|
+
return None
|
|
123
129
|
ok = kernel32.GetProcessTimes(handle, ctypes.byref(creation),
|
|
124
130
|
ctypes.byref(exit_time), ctypes.byref(kernel),
|
|
125
131
|
ctypes.byref(user))
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Shared overlay compositing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from arrayview._render import (
|
|
8
|
+
_build_mosaic_grid,
|
|
9
|
+
_composite_overlay_mask,
|
|
10
|
+
_extract_overlay_mask,
|
|
11
|
+
_overlay_is_label_map,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_hex_color(hex_str: str) -> np.ndarray | None:
|
|
16
|
+
"""Parse a 6-char hex RGB color."""
|
|
17
|
+
h = hex_str.strip().lstrip("#")
|
|
18
|
+
if len(h) != 6:
|
|
19
|
+
return None
|
|
20
|
+
try:
|
|
21
|
+
return np.array(
|
|
22
|
+
[int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)],
|
|
23
|
+
dtype=np.uint8,
|
|
24
|
+
)
|
|
25
|
+
except ValueError:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _composite_overlays(
|
|
30
|
+
rgba: np.ndarray,
|
|
31
|
+
overlay_sid_str: str | None,
|
|
32
|
+
overlay_colors_str: str | None,
|
|
33
|
+
overlay_alpha: float,
|
|
34
|
+
overlay_alphas_str: str | None,
|
|
35
|
+
overlay_outline: bool,
|
|
36
|
+
dim_x: int,
|
|
37
|
+
dim_y: int,
|
|
38
|
+
idx_tuple: tuple[int, ...],
|
|
39
|
+
shape_hw: tuple[int, int],
|
|
40
|
+
base_shape: tuple[int, ...] | None = None,
|
|
41
|
+
) -> np.ndarray:
|
|
42
|
+
"""Composite one or more overlay masks onto RGBA pixels."""
|
|
43
|
+
if not overlay_sid_str:
|
|
44
|
+
return rgba
|
|
45
|
+
sids = [s.strip() for s in overlay_sid_str.split(",") if s.strip()]
|
|
46
|
+
colors_raw = (
|
|
47
|
+
[c.strip() for c in overlay_colors_str.split(",")] if overlay_colors_str else []
|
|
48
|
+
)
|
|
49
|
+
alphas_raw = (
|
|
50
|
+
[a.strip() for a in overlay_alphas_str.split(",")] if overlay_alphas_str else []
|
|
51
|
+
)
|
|
52
|
+
for i, sid in enumerate(sids):
|
|
53
|
+
color = _parse_hex_color(colors_raw[i]) if i < len(colors_raw) else None
|
|
54
|
+
alpha = overlay_alpha
|
|
55
|
+
if i < len(alphas_raw):
|
|
56
|
+
try:
|
|
57
|
+
alpha = float(alphas_raw[i])
|
|
58
|
+
except ValueError:
|
|
59
|
+
alpha = overlay_alpha
|
|
60
|
+
alpha = max(0.0, min(1.0, alpha))
|
|
61
|
+
ov_raw = _extract_overlay_mask(
|
|
62
|
+
sid,
|
|
63
|
+
dim_x,
|
|
64
|
+
dim_y,
|
|
65
|
+
idx_tuple,
|
|
66
|
+
expected_shape=shape_hw,
|
|
67
|
+
base_shape=base_shape,
|
|
68
|
+
)
|
|
69
|
+
rgba = _composite_overlay_mask(
|
|
70
|
+
rgba,
|
|
71
|
+
ov_raw,
|
|
72
|
+
alpha=alpha,
|
|
73
|
+
is_label=_overlay_is_label_map(sid, ov_raw),
|
|
74
|
+
override_color=color,
|
|
75
|
+
outline_only=overlay_outline,
|
|
76
|
+
)
|
|
77
|
+
return rgba
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _composite_mosaic_overlays(
|
|
81
|
+
rgba: np.ndarray,
|
|
82
|
+
overlay_sid_str: str | None,
|
|
83
|
+
overlay_colors_str: str | None,
|
|
84
|
+
overlay_alpha: float,
|
|
85
|
+
overlay_alphas_str: str | None,
|
|
86
|
+
overlay_outline: bool,
|
|
87
|
+
dim_x: int,
|
|
88
|
+
dim_y: int,
|
|
89
|
+
dim_z: int,
|
|
90
|
+
idx_tuple: tuple[int, ...],
|
|
91
|
+
base_shape: tuple[int, ...],
|
|
92
|
+
mosaic_cols: int | None = None,
|
|
93
|
+
) -> np.ndarray:
|
|
94
|
+
"""Composite each overlay as a mosaic using the base mosaic's frame indices.
|
|
95
|
+
|
|
96
|
+
Missing overlay axes are resolved per frame by ``_extract_overlay_mask``.
|
|
97
|
+
That lets a mask omit, for example, an echo axis without materialising a
|
|
98
|
+
repeated full-volume copy.
|
|
99
|
+
"""
|
|
100
|
+
if not overlay_sid_str:
|
|
101
|
+
return rgba
|
|
102
|
+
sids = [sid.strip() for sid in overlay_sid_str.split(",") if sid.strip()]
|
|
103
|
+
colors_raw = (
|
|
104
|
+
[color.strip() for color in overlay_colors_str.split(",")]
|
|
105
|
+
if overlay_colors_str
|
|
106
|
+
else []
|
|
107
|
+
)
|
|
108
|
+
alphas_raw = (
|
|
109
|
+
[alpha.strip() for alpha in overlay_alphas_str.split(",")]
|
|
110
|
+
if overlay_alphas_str
|
|
111
|
+
else []
|
|
112
|
+
)
|
|
113
|
+
tile_shape = (int(base_shape[dim_y]), int(base_shape[dim_x]))
|
|
114
|
+
n_frames = int(base_shape[dim_z])
|
|
115
|
+
|
|
116
|
+
for i, sid in enumerate(sids):
|
|
117
|
+
color = _parse_hex_color(colors_raw[i]) if i < len(colors_raw) else None
|
|
118
|
+
try:
|
|
119
|
+
alpha = float(alphas_raw[i]) if i < len(alphas_raw) else overlay_alpha
|
|
120
|
+
except ValueError:
|
|
121
|
+
alpha = overlay_alpha
|
|
122
|
+
alpha = max(0.0, min(1.0, alpha))
|
|
123
|
+
|
|
124
|
+
frames = []
|
|
125
|
+
for frame in range(n_frames):
|
|
126
|
+
frame_idx = list(idx_tuple)
|
|
127
|
+
frame_idx[dim_z] = frame
|
|
128
|
+
ov_raw = _extract_overlay_mask(
|
|
129
|
+
sid,
|
|
130
|
+
dim_x,
|
|
131
|
+
dim_y,
|
|
132
|
+
tuple(frame_idx),
|
|
133
|
+
expected_shape=tile_shape,
|
|
134
|
+
base_shape=base_shape,
|
|
135
|
+
)
|
|
136
|
+
frames.append(
|
|
137
|
+
ov_raw if ov_raw is not None else np.zeros(tile_shape, dtype=np.float32)
|
|
138
|
+
)
|
|
139
|
+
grid, _, _ = _build_mosaic_grid(frames, n_frames, cols=mosaic_cols)
|
|
140
|
+
# Gaps are transparent, not NaN label values or heatmap samples.
|
|
141
|
+
grid = np.nan_to_num(grid, nan=0.0)
|
|
142
|
+
rgba = _composite_overlay_mask(
|
|
143
|
+
rgba,
|
|
144
|
+
grid,
|
|
145
|
+
alpha=alpha,
|
|
146
|
+
is_label=_overlay_is_label_map(sid, grid),
|
|
147
|
+
override_color=color,
|
|
148
|
+
outline_only=overlay_outline,
|
|
149
|
+
)
|
|
150
|
+
return rgba
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Rendering pipeline: colormaps, LUTs, slice extraction, RGBA, mosaic, RGB, preload."""
|
|
2
2
|
|
|
3
|
+
import itertools
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
5
6
|
|
|
@@ -527,6 +528,7 @@ def _extract_overlay_mask(
|
|
|
527
528
|
dim_y: int,
|
|
528
529
|
idx_tuple: tuple[int, ...],
|
|
529
530
|
expected_shape: tuple[int, int],
|
|
531
|
+
base_shape: tuple[int, ...] | None = None,
|
|
530
532
|
) -> np.ndarray | None:
|
|
531
533
|
"""Return the raw overlay slice (float32 H×W) for compositing, or None.
|
|
532
534
|
|
|
@@ -538,27 +540,43 @@ def _extract_overlay_mask(
|
|
|
538
540
|
if ov_session is None:
|
|
539
541
|
return None
|
|
540
542
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
+
ov_shape = tuple(int(size) for size in ov_session.shape)
|
|
544
|
+
main_shape = tuple(int(size) for size in (base_shape or ov_shape))
|
|
545
|
+
if len(ov_shape) > len(main_shape):
|
|
543
546
|
return None
|
|
544
547
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
548
|
+
# An overlay may omit one or more non-spatial dimensions from the base.
|
|
549
|
+
# Match its axes to equal-sized base axes, then index only those axes. This
|
|
550
|
+
# broadcasts the overlay over every omitted axis without allocating a tiled
|
|
551
|
+
# copy (important for masks next to multi-echo or multi-channel data).
|
|
552
|
+
map_key = (main_shape, dim_x, dim_y)
|
|
553
|
+
axis_maps = getattr(ov_session, "_overlay_axis_maps", None)
|
|
554
|
+
if axis_maps is None:
|
|
555
|
+
axis_maps = {}
|
|
556
|
+
ov_session._overlay_axis_maps = axis_maps
|
|
557
|
+
if map_key not in axis_maps:
|
|
558
|
+
axis_maps[map_key] = next(
|
|
559
|
+
(
|
|
560
|
+
base_axes
|
|
561
|
+
for base_axes in itertools.combinations(range(len(main_shape)), len(ov_shape))
|
|
562
|
+
if dim_x in base_axes
|
|
563
|
+
and dim_y in base_axes
|
|
564
|
+
and tuple(main_shape[axis] for axis in base_axes) == ov_shape
|
|
565
|
+
),
|
|
566
|
+
None,
|
|
567
|
+
)
|
|
568
|
+
base_axes = axis_maps[map_key]
|
|
569
|
+
if base_axes is None:
|
|
570
|
+
return None
|
|
571
|
+
ov_dim_x = base_axes.index(dim_x)
|
|
572
|
+
ov_dim_y = base_axes.index(dim_y)
|
|
573
|
+
ov_idx = [int(idx_tuple[axis]) for axis in base_axes]
|
|
574
|
+
try:
|
|
575
|
+
ov_raw = extract_slice(ov_session, ov_dim_x, ov_dim_y, ov_idx)
|
|
576
|
+
except Exception:
|
|
577
|
+
return None
|
|
578
|
+
if ov_raw.shape == expected_shape:
|
|
579
|
+
return ov_raw.astype(np.float32)
|
|
562
580
|
return None
|
|
563
581
|
|
|
564
582
|
|
|
@@ -6,7 +6,7 @@ from fastapi import Depends, Response
|
|
|
6
6
|
from fastapi.responses import JSONResponse
|
|
7
7
|
|
|
8
8
|
from arrayview._diff import _compute_diff, _diff_histogram, _render_diff_rgba
|
|
9
|
-
from arrayview._overlays import _composite_overlays
|
|
9
|
+
from arrayview._overlays import _composite_mosaic_overlays, _composite_overlays
|
|
10
10
|
from arrayview._render import (
|
|
11
11
|
LUTS,
|
|
12
12
|
_build_mosaic_grid,
|
|
@@ -178,6 +178,11 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
178
178
|
vmin_override=vmin_override,
|
|
179
179
|
vmax_override=vmax_override,
|
|
180
180
|
)
|
|
181
|
+
rgba = _composite_mosaic_overlays(
|
|
182
|
+
rgba, overlay_sid, overlay_colors, overlay_alpha, overlay_alphas,
|
|
183
|
+
overlay_outline, dim_x, dim_y, dim_z, idx_tuple, session.shape,
|
|
184
|
+
mosaic_cols,
|
|
185
|
+
)
|
|
181
186
|
if vmin_override is not None and vmax_override is not None:
|
|
182
187
|
vmin, vmax = float(vmin_override), float(vmax_override)
|
|
183
188
|
else:
|
|
@@ -251,6 +256,7 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
251
256
|
dim_y,
|
|
252
257
|
idx_tuple,
|
|
253
258
|
rgba.shape[:2],
|
|
259
|
+
session.shape,
|
|
254
260
|
)
|
|
255
261
|
_, vmin, vmax = _prepare_display(
|
|
256
262
|
session,
|
|
@@ -263,6 +269,12 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
263
269
|
)
|
|
264
270
|
if qmri_role:
|
|
265
271
|
vmin, vmax = _qmri_adjust_vmin_vmax(vmin, vmax, qmri_role)
|
|
272
|
+
if dim_z >= 0 and qmri_role:
|
|
273
|
+
rgba = _composite_mosaic_overlays(
|
|
274
|
+
rgba, overlay_sid, overlay_colors, overlay_alpha, overlay_alphas,
|
|
275
|
+
overlay_outline, dim_x, dim_y, dim_z, idx_tuple, session.shape,
|
|
276
|
+
mosaic_cols,
|
|
277
|
+
)
|
|
266
278
|
render_ms = (time.perf_counter() - render_t0) * 1000.0
|
|
267
279
|
encode_t0 = time.perf_counter()
|
|
268
280
|
img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
|
|
@@ -8,7 +8,7 @@ from starlette.websockets import WebSocketDisconnect
|
|
|
8
8
|
import arrayview._session as _session_mod
|
|
9
9
|
from arrayview._analysis import _build_metadata
|
|
10
10
|
from arrayview._lifecycle import release_session
|
|
11
|
-
from arrayview._overlays import _composite_overlays
|
|
11
|
+
from arrayview._overlays import _composite_mosaic_overlays, _composite_overlays
|
|
12
12
|
from arrayview._render import (
|
|
13
13
|
LUTS,
|
|
14
14
|
_prepare_display,
|
|
@@ -25,6 +25,7 @@ from arrayview._session import (
|
|
|
25
25
|
SESSIONS,
|
|
26
26
|
SHELL_SOCKETS,
|
|
27
27
|
_render,
|
|
28
|
+
_schedule_overlay_prefetch,
|
|
28
29
|
_schedule_prefetch,
|
|
29
30
|
_vprint,
|
|
30
31
|
wait_for_session_ready,
|
|
@@ -231,6 +232,12 @@ def register_websocket_routes(app) -> None:
|
|
|
231
232
|
vmax_override=vmax_override,
|
|
232
233
|
),
|
|
233
234
|
)
|
|
235
|
+
rgba = _composite_mosaic_overlays(
|
|
236
|
+
rgba, msg.get("overlay_sid"), msg.get("overlay_colors"),
|
|
237
|
+
float(msg.get("overlay_alpha", 0.45)), msg.get("overlay_alphas"),
|
|
238
|
+
bool(msg.get("overlay_outline", False)), dim_x, dim_y, dim_z,
|
|
239
|
+
idx_tuple, session.shape, mosaic_cols,
|
|
240
|
+
)
|
|
234
241
|
h, w = rgba.shape[:2]
|
|
235
242
|
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
236
243
|
_, vmin, vmax = _prepare_display(
|
|
@@ -340,6 +347,15 @@ def register_websocket_routes(app) -> None:
|
|
|
340
347
|
dim_y,
|
|
341
348
|
idx_tuple,
|
|
342
349
|
(h, w),
|
|
350
|
+
session.shape,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if dim_z >= 0 and qmri_role:
|
|
354
|
+
rgba = _composite_mosaic_overlays(
|
|
355
|
+
rgba, msg.get("overlay_sid"), msg.get("overlay_colors"),
|
|
356
|
+
float(msg.get("overlay_alpha", 0.45)), msg.get("overlay_alphas"),
|
|
357
|
+
bool(msg.get("overlay_outline", False)), dim_x, dim_y, dim_z,
|
|
358
|
+
idx_tuple, session.shape, mosaic_cols,
|
|
343
359
|
)
|
|
344
360
|
|
|
345
361
|
render_ms = (time.perf_counter() - render_t0) * 1000.0
|
|
@@ -414,6 +430,16 @@ def register_websocket_routes(app) -> None:
|
|
|
414
430
|
collection_axis,
|
|
415
431
|
collection_direction,
|
|
416
432
|
)
|
|
433
|
+
_schedule_overlay_prefetch(
|
|
434
|
+
session,
|
|
435
|
+
msg.get("overlay_sid"),
|
|
436
|
+
msg.get("overlay_alphas"),
|
|
437
|
+
dim_x,
|
|
438
|
+
dim_y,
|
|
439
|
+
list(idx_tuple),
|
|
440
|
+
collection_axis,
|
|
441
|
+
collection_direction,
|
|
442
|
+
)
|
|
417
443
|
previous_indices = idx_tuple
|
|
418
444
|
except Exception as _ws_exc:
|
|
419
445
|
import traceback
|
|
@@ -101,6 +101,7 @@ def _ensure_render_thread() -> None:
|
|
|
101
101
|
# Neighbor prefetch thread pool (Phase 3)
|
|
102
102
|
# ---------------------------------------------------------------------------
|
|
103
103
|
_PREFETCH_POOL = None
|
|
104
|
+
_OVERLAY_PREFETCH_POOL = None
|
|
104
105
|
_PREFETCH_LOCK = threading.Lock()
|
|
105
106
|
|
|
106
107
|
|
|
@@ -116,6 +117,19 @@ def _get_prefetch_pool():
|
|
|
116
117
|
return _PREFETCH_POOL
|
|
117
118
|
|
|
118
119
|
|
|
120
|
+
def _get_overlay_prefetch_pool():
|
|
121
|
+
"""Return the small pool reserved for collection overlay lookahead."""
|
|
122
|
+
global _OVERLAY_PREFETCH_POOL
|
|
123
|
+
with _PREFETCH_LOCK:
|
|
124
|
+
if _OVERLAY_PREFETCH_POOL is None or _OVERLAY_PREFETCH_POOL._shutdown:
|
|
125
|
+
import concurrent.futures
|
|
126
|
+
|
|
127
|
+
_OVERLAY_PREFETCH_POOL = concurrent.futures.ThreadPoolExecutor(
|
|
128
|
+
max_workers=2, thread_name_prefix="arrayview-overlay-prefetch"
|
|
129
|
+
)
|
|
130
|
+
return _OVERLAY_PREFETCH_POOL
|
|
131
|
+
|
|
132
|
+
|
|
119
133
|
def _schedule_prefetch(session, dim_x, dim_y, idx_list, slice_dim, direction):
|
|
120
134
|
"""Warm raw_cache for the next PREFETCH_NEIGHBORS slices in *direction*.
|
|
121
135
|
|
|
@@ -170,6 +184,85 @@ def _schedule_prefetch(session, dim_x, dim_y, idx_list, slice_dim, direction):
|
|
|
170
184
|
pass # pool shutting down
|
|
171
185
|
|
|
172
186
|
|
|
187
|
+
def _schedule_overlay_prefetch(
|
|
188
|
+
session,
|
|
189
|
+
overlay_sid_str,
|
|
190
|
+
overlay_alphas_str,
|
|
191
|
+
dim_x,
|
|
192
|
+
dim_y,
|
|
193
|
+
idx_list,
|
|
194
|
+
collection_axis,
|
|
195
|
+
direction,
|
|
196
|
+
):
|
|
197
|
+
"""Warm the next collection volume for visible overlays.
|
|
198
|
+
|
|
199
|
+
Overlay work uses its own bounded pool so a long mask decompression cannot
|
|
200
|
+
hold up the main image's single-worker prefetch lane. New navigation
|
|
201
|
+
supersedes overlay jobs that have not started loading a volume yet.
|
|
202
|
+
"""
|
|
203
|
+
if not overlay_sid_str:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
sids = [sid.strip() for sid in overlay_sid_str.split(",") if sid.strip()]
|
|
207
|
+
alphas = [a.strip() for a in (overlay_alphas_str or "").split(",")]
|
|
208
|
+
visible_sids = []
|
|
209
|
+
for i, sid in enumerate(sids):
|
|
210
|
+
try:
|
|
211
|
+
alpha = float(alphas[i]) if i < len(alphas) and alphas[i] else 1.0
|
|
212
|
+
except ValueError:
|
|
213
|
+
alpha = 1.0
|
|
214
|
+
if alpha > 0.0:
|
|
215
|
+
visible_sids.append(sid)
|
|
216
|
+
if not visible_sids:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
target = idx_list[collection_axis] + direction
|
|
220
|
+
if not 0 <= target < session.shape[collection_axis]:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
request_key = (collection_axis, target, tuple(visible_sids))
|
|
224
|
+
if request_key == getattr(session, "_overlay_prefetch_request", None):
|
|
225
|
+
return
|
|
226
|
+
session._overlay_prefetch_request = request_key
|
|
227
|
+
generation = getattr(session, "_overlay_prefetch_generation", 0) + 1
|
|
228
|
+
session._overlay_prefetch_generation = generation
|
|
229
|
+
|
|
230
|
+
def _warm(overlay_sid):
|
|
231
|
+
if generation != getattr(session, "_overlay_prefetch_generation", 0):
|
|
232
|
+
return
|
|
233
|
+
overlay = SESSIONS.get(overlay_sid)
|
|
234
|
+
if overlay is None or collection_axis >= len(overlay.shape):
|
|
235
|
+
return
|
|
236
|
+
overlay_stack_axes = tuple(getattr(overlay.data, "_stack_axes", ()))
|
|
237
|
+
if collection_axis not in overlay_stack_axes or target >= overlay.shape[collection_axis]:
|
|
238
|
+
return
|
|
239
|
+
overlay_idx = list(idx_list[: len(overlay.shape)])
|
|
240
|
+
overlay_idx[collection_axis] = target
|
|
241
|
+
# Sparse --overlay-dir collections represent a missing patient with a
|
|
242
|
+
# None filepath. Avoid allocating a fresh full-size zero volume merely
|
|
243
|
+
# to prefetch an overlay that will be shown empty.
|
|
244
|
+
file_matrix = getattr(overlay.data, "_file_matrix", None)
|
|
245
|
+
if file_matrix is not None:
|
|
246
|
+
modality = 0
|
|
247
|
+
if len(overlay_stack_axes) > 1:
|
|
248
|
+
modality = overlay_idx[overlay_stack_axes[1]]
|
|
249
|
+
if not file_matrix[target][modality]:
|
|
250
|
+
return
|
|
251
|
+
try:
|
|
252
|
+
from arrayview._render import extract_slice
|
|
253
|
+
|
|
254
|
+
extract_slice(overlay, dim_x, dim_y, overlay_idx)
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
pool = _get_overlay_prefetch_pool()
|
|
260
|
+
for overlay_sid in visible_sids:
|
|
261
|
+
pool.submit(_warm, overlay_sid)
|
|
262
|
+
except RuntimeError:
|
|
263
|
+
pass # pool shutting down
|
|
264
|
+
|
|
265
|
+
|
|
173
266
|
async def _render(loop: asyncio.AbstractEventLoop, func) -> object:
|
|
174
267
|
"""Await *func()* in the render thread without using concurrent.futures."""
|
|
175
268
|
_ensure_render_thread()
|
|
@@ -476,9 +569,12 @@ __all__ = [
|
|
|
476
569
|
"_render",
|
|
477
570
|
# Prefetch
|
|
478
571
|
"_PREFETCH_POOL",
|
|
572
|
+
"_OVERLAY_PREFETCH_POOL",
|
|
479
573
|
"_PREFETCH_LOCK",
|
|
480
574
|
"_get_prefetch_pool",
|
|
575
|
+
"_get_overlay_prefetch_pool",
|
|
481
576
|
"_schedule_prefetch",
|
|
577
|
+
"_schedule_overlay_prefetch",
|
|
482
578
|
# Session
|
|
483
579
|
"Session",
|
|
484
580
|
"SESSIONS",
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import json
|
|
7
|
+
import platform
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
9
10
|
import threading
|
|
@@ -219,7 +220,7 @@ def _open_browser(
|
|
|
219
220
|
port_hint = int(url.split(":")[2].split("/")[0].split("?")[0])
|
|
220
221
|
except Exception:
|
|
221
222
|
port_hint = parsed_port
|
|
222
|
-
hostname =
|
|
223
|
+
hostname = platform.node() or "<this-host>"
|
|
223
224
|
print(
|
|
224
225
|
f"[ArrayView] Plain SSH session detected.\n"
|
|
225
226
|
f"\n"
|
|
@@ -100,6 +100,14 @@ class TestHealth:
|
|
|
100
100
|
|
|
101
101
|
assert body["default_dims"] == [0, 1]
|
|
102
102
|
|
|
103
|
+
def test_metadata_prefers_trailing_plane_when_it_has_the_two_largest_axes(self, client, tmp_path):
|
|
104
|
+
path = tmp_path / "startup_dims_volume.npy"
|
|
105
|
+
np.save(path, np.zeros((48, 224, 224), dtype=np.float32))
|
|
106
|
+
|
|
107
|
+
sid = client.post("/load", json={"filepath": str(path)}).json()["sid"]
|
|
108
|
+
|
|
109
|
+
assert client.get(f"/metadata/{sid}").json()["default_dims"] == [1, 2]
|
|
110
|
+
|
|
103
111
|
def test_sessions_lists_registered_sid(self, client, sid_2d):
|
|
104
112
|
r = client.get("/sessions")
|
|
105
113
|
assert r.status_code == 200
|
|
@@ -4593,6 +4601,82 @@ class TestCropPersistence:
|
|
|
4593
4601
|
|
|
4594
4602
|
|
|
4595
4603
|
class TestMultipleOverlays:
|
|
4604
|
+
def test_http_mosaic_repeats_overlay_across_missing_dimension(self, client, tmp_path):
|
|
4605
|
+
base_path = tmp_path / "mosaic-base.npy"
|
|
4606
|
+
np.save(base_path, np.zeros((4, 3, 6, 6), dtype=np.float32))
|
|
4607
|
+
base_sid = client.post("/load", json={"filepath": str(base_path)}).json()["sid"]
|
|
4608
|
+
overlay_data = np.zeros((4, 6, 6), dtype=np.uint8)
|
|
4609
|
+
overlay_data[2, 2:4, 2:4] = 1
|
|
4610
|
+
overlay_path = tmp_path / "mosaic-overlay.npy"
|
|
4611
|
+
np.save(overlay_path, overlay_data)
|
|
4612
|
+
overlay_sid = client.post("/load", json={"filepath": str(overlay_path)}).json()["sid"]
|
|
4613
|
+
|
|
4614
|
+
response = client.get(
|
|
4615
|
+
f"/slice/{base_sid}",
|
|
4616
|
+
params={
|
|
4617
|
+
"dim_x": 3,
|
|
4618
|
+
"dim_y": 2,
|
|
4619
|
+
"dim_z": 1,
|
|
4620
|
+
"indices": "2,0,3,3",
|
|
4621
|
+
"overlay_sid": overlay_sid,
|
|
4622
|
+
"overlay_colors": "ff0000",
|
|
4623
|
+
"overlay_alpha": 1.0,
|
|
4624
|
+
},
|
|
4625
|
+
)
|
|
4626
|
+
image = np.asarray(Image.open(io.BytesIO(response.content)))
|
|
4627
|
+
for x0 in (0, 8, 16):
|
|
4628
|
+
tile = image[:, x0 : x0 + 6]
|
|
4629
|
+
assert np.max(tile[:, :, 0].astype(int) - tile[:, :, 1].astype(int)) > 20
|
|
4630
|
+
|
|
4631
|
+
def test_mosaic_overlay_broadcasts_over_missing_mosaic_dimension(self):
|
|
4632
|
+
from arrayview._overlays import _composite_mosaic_overlays
|
|
4633
|
+
from arrayview._render import mosaic_shape
|
|
4634
|
+
from arrayview._session import SESSIONS, Session
|
|
4635
|
+
|
|
4636
|
+
base = Session(np.zeros((4, 3, 6, 6), dtype=np.float32))
|
|
4637
|
+
overlay_data = np.zeros((4, 6, 6), dtype=np.uint8)
|
|
4638
|
+
overlay_data[2, 3, 4] = 1
|
|
4639
|
+
overlay = Session(overlay_data)
|
|
4640
|
+
SESSIONS[overlay.sid] = overlay
|
|
4641
|
+
try:
|
|
4642
|
+
rows, cols = mosaic_shape(3)
|
|
4643
|
+
rgba = np.zeros((rows * 6 + (rows - 1) * 2, cols * 6 + (cols - 1) * 2, 4), dtype=np.uint8)
|
|
4644
|
+
result = _composite_mosaic_overlays(
|
|
4645
|
+
rgba, overlay.sid, "ff0000", 1.0, None, False,
|
|
4646
|
+
dim_x=3, dim_y=2, dim_z=1, idx_tuple=(2, 0, 3, 3),
|
|
4647
|
+
base_shape=base.shape,
|
|
4648
|
+
)
|
|
4649
|
+
for frame in range(3):
|
|
4650
|
+
row, col = divmod(frame, cols)
|
|
4651
|
+
assert result[row * 8 + 3, col * 8 + 4, 0] == 255
|
|
4652
|
+
finally:
|
|
4653
|
+
SESSIONS.pop(overlay.sid, None)
|
|
4654
|
+
|
|
4655
|
+
def test_overlay_broadcasts_over_missing_base_dimension(self):
|
|
4656
|
+
from arrayview._render import _extract_overlay_mask
|
|
4657
|
+
from arrayview._session import SESSIONS, Session
|
|
4658
|
+
|
|
4659
|
+
base = Session(np.zeros((48, 3, 224, 224), dtype=np.float32))
|
|
4660
|
+
overlay_data = np.zeros((48, 224, 224), dtype=np.uint8)
|
|
4661
|
+
overlay_data[12, 100, 150] = 1
|
|
4662
|
+
overlay = Session(overlay_data)
|
|
4663
|
+
SESSIONS[overlay.sid] = overlay
|
|
4664
|
+
try:
|
|
4665
|
+
for channel in range(3):
|
|
4666
|
+
result = _extract_overlay_mask(
|
|
4667
|
+
overlay.sid,
|
|
4668
|
+
dim_x=2,
|
|
4669
|
+
dim_y=3,
|
|
4670
|
+
idx_tuple=(12, channel, 112, 112),
|
|
4671
|
+
expected_shape=(224, 224),
|
|
4672
|
+
base_shape=base.shape,
|
|
4673
|
+
)
|
|
4674
|
+
assert result is not None
|
|
4675
|
+
assert result[150, 100] == 1
|
|
4676
|
+
assert overlay.data is overlay_data
|
|
4677
|
+
finally:
|
|
4678
|
+
SESSIONS.pop(overlay.sid, None)
|
|
4679
|
+
|
|
4596
4680
|
def test_single_overlay_with_color(self, client, sid_2d):
|
|
4597
4681
|
"""A single binary mask overlay with explicit hex color renders without error."""
|
|
4598
4682
|
from arrayview._app import SESSIONS, Session
|