Stabilize LUT rendering and improve MIP projection handling#1203
Stabilize LUT rendering and improve MIP projection handling#1203AdvancedImagingUTSW merged 18 commits intodevelopfrom
Conversation
Bind notebook/map visibility events and add visibility checks to camera view rendering so hidden/docked tabs defer redraw work (_is_display_visible, _request_display_if_needed, _on_visibility_changed). Avoid scheduling/performing display callbacks when views are not visible and ensure pending frames are flushed only when visible. Optimize MIP projection updates by adding reusable scratch buffers (_zy_reduce_buf, _zx_reduce_buf) and switching to OpenCV fast ops (cv2.max, cv2.reduce) for XY/ZY/ZX updates. Also refine behavior when enabling/disabling MIP display (avoid unnecessary clears and trigger redraw when enabled and visible). Add unit tests for view visibility and MIP projection behavior to cover deferred rendering and projection correctness.
Add axial-to-lateral spacing handling for orthogonal MIP projections. Introduces an axial_to_lateral_ratio attribute and helper methods _compute_axial_to_lateral_ratio and _rescale_orthogonal_for_anisotropy to compute spacing from microscope/camera state and rescale Z dimension (using cv2.resize) so orthogonal views appear isotropic. Corrects projection sourcing (ZY now uses zx_mip transposed, ZX uses zy_mip transposed) and applies anisotropic scaling before flipping/downsampling. initialize_non_live_display now computes the ratio and derives Z_image_value from number_of_slices and the ratio (with safe fallbacks). Adds tests (plus a small _Getter helper) to verify correct projection selection and anisotropic scaling behavior.
Introduce a 'Multi' perspective option that composes XY, YZ, and XZ panes into a single frame. Add multi_view_gap and _compose_multi_perspective to build the composite image, adjust orthogonal rescaling logic for ZY/ZX anisotropy, and update image sizing logic to account for the composite layout. Update UI to include the new perspective value and tweak MIPTab layout grid rowspan. Add/modify tests to verify anisotropic scaling and multi-perspective composition.
There was a problem hiding this comment.
Pull request overview
This PR aims to stabilize camera/MIP rendering (avoid redraw work when views are hidden) and improve multichannel display handling by adding an explicit Single vs Overlay display mode, per-channel LUT/alpha/gamma controls, and more accurate MIP orthogonal projection scaling/composition.
Changes:
- Add visibility-aware display queuing so hidden tabs defer GUI redraw work until visible again.
- Introduce a “Display Mode” UI (Single/Overlay) and compact multichannel LUT controls (per-channel LUT, alpha, gamma, autoscale/min/max, visibility).
- Improve MIP projection handling: anisotropic Z scaling for orthogonal views, new “Multi” composed perspective, buffer reuse for faster max-reductions, and expanded regression tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/navigate/view/main_window_content/display_notebook.py |
Adds theme-based spacing helper, new DisplayModeFrame, and compact multichannel LUT editor logic. |
src/navigate/controller/sub_controllers/camera_view.py |
Adds visibility-aware redraw scheduling, overlay rendering pipeline (LUT/alpha/gamma), and MIP projection/layout updates. |
test/view/main_window_content/test_display_notebook.py |
Adds regression tests for IntensityFrame multichannel state handling with invalid/empty Tk var values. |
test/controller/sub_controllers/test_view_visibility.py |
Adds tests covering visibility gating for queued display work (BaseViewController + MIP behavior). |
test/controller/sub_controllers/test_mip_view_projection.py |
Adds tests for anisotropic scaling, multi-perspective composition, overlay composition, and channel-signature caching behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| def _space(px: int) -> int: | ||
| """Resolve spacing through the active GUI theme token map.""" | ||
| return get_theme_space_px(px, px) |
There was a problem hiding this comment.
_space() uses get_theme_space_px(px, px), which looks up tokens like space_px_5. The active spacing presets define space_0..space_9 (no space_px_*), so this will always fall back to the literal pixel value and won’t honor the theme spacing scale. Consider mapping to the existing tokens (e.g., get_theme_spacing(f"space_{n}")) or adding space_px_* entries to the theme spacing presets so the helper actually reflects the active theme.
There was a problem hiding this comment.
Good catch. Implemented: _space() now maps to theme tokens via get_theme_spacing("space_n"), so spacing follows theme.py scale instead of always falling back to raw pixels. This is included in the latest branch changes.
| if self._multichannel_overlay_mode and len(self._multichannel_channels) > 1: | ||
| self._multichannel_channel_widget["values"] = ( | ||
| self._multichannel_channels | ||
| ) | ||
| self._multichannel_channel_widget.configure(state="readonly") | ||
| active_channel = self._active_multichannel_channel.get() | ||
| if active_channel not in self._multichannel_channels: | ||
| self._active_multichannel_channel.set( | ||
| self._multichannel_channels[0] | ||
| ) | ||
| else: | ||
| self._multichannel_channel_widget["values"] = ( | ||
| self._all_channels_label, | ||
| ) | ||
| self._multichannel_channel_widget.configure(state="disabled") | ||
| self._active_multichannel_channel.set(self._all_channels_label) | ||
| finally: |
There was a problem hiding this comment.
In single-channel mode (overlay_mode=False), set_multichannel_channel_selector_mode() forces the channel combobox to the disabled "All" value. Since the controllers use lut.get_multichannel_active_channel() to decide which channel to render, this effectively makes the active channel always the first selected channel whenever multiple channels are acquired but overlay mode is off (no way for the user to pick a different channel). Consider enabling channel selection whenever there are >1 channels (and keep an "All" option for bulk edits), or decouple "apply settings to all" from the "active display channel" selection so single-channel viewing remains selectable.
There was a problem hiding this comment.
Not adopting this one by design. Product requirement for Single mode is: channel selector shows All and is inactive; only Overlay mode exposes per-channel selection.
| def _update_channel_selector_for_display_mode(self) -> None: | ||
| """Disable single-channel selectors while overlay mode is active.""" | ||
| if self.display_state != "Slice": | ||
| return | ||
| if self._has_selected_channels(): | ||
| self.view.live_frame.channel.configure(state="disabled") | ||
| else: | ||
| self.view.live_frame.channel.state(["!disabled", "readonly"]) | ||
|
|
There was a problem hiding this comment.
_update_channel_selector_for_display_mode() disables the legacy channel selector whenever self._has_selected_channels() is true, but the overlay toggle is separate from that. Combined with the LUT multichannel selector being disabled in "Single" mode, this leaves no UI path to select which channel is shown for multi-channel acquisitions when overlay mode is off (the view will stick to the first channel). Consider only disabling live_frame.channel while overlay mode is active, or updating _get_multichannel_active_channel() to consult live_frame.channel in single mode.
There was a problem hiding this comment.
Leaving current behavior intentionally. We standardized on the compact LUT UI, and in Single mode the selector remains All + disabled per product spec; channel selection is only active in Overlay mode.
| def _update_channel_selector_for_display_mode(self) -> None: | ||
| """Disable MIP legacy channel selector when compact channel picker is active.""" | ||
| if self._has_selected_channels(): | ||
| self.render_widgets["channel"].widget.state(["disabled"]) | ||
| else: | ||
| self.render_widgets["channel"].widget.state(["!disabled", "readonly"]) | ||
|
|
There was a problem hiding this comment.
_update_channel_selector_for_display_mode() disables render_widgets["channel"] whenever channels are selected, regardless of whether overlay mode is actually active. Since get_mip_image()/rendering now uses the compact LUT "active channel" (which is forced to "All" and disabled in single mode), there is currently no way to choose which channel’s MIP is shown when multiple channels are acquired but display mode is "Single". Consider disabling the legacy channel selector only in overlay mode, or ensuring the compact channel selector remains usable for selecting the active display channel in single mode.
There was a problem hiding this comment.
Same disposition as above: keeping this as-is per agreed UX. For multi-channel acquisition in Single display mode, selector remains All and inactive; Overlay mode is the channel-selectable path.
|
Implemented the CI and display updates discussed in review:\n\n- Fixed Windows py39 failures by updating stale camera-view tests to the active redraw path and hardening MIP OpenCV projection updates against frame shape/dtype mismatches.\n- Added targeted regression coverage for MIP dtype/shape compatibility and LUT-stability redraw paths.\n- Regenerated display screenshots with and updated user docs for the unified single/overlay LUT workflow, per-channel controls (including gamma range), and isotropic multi-perspective MIP behavior.\n\nLocal validation run in both conda envs ( and ) for previously failing targets is passing. |
|
Follow-up (clean formatting):\n\nImplemented the CI and display updates discussed in review.\n\n- Fixed Windows py39 failures by updating stale camera-view tests to the active redraw path and hardening MIP OpenCV projection updates against frame shape/dtype mismatches.\n- Added targeted regression coverage for MIP dtype/shape compatibility and LUT-stability redraw paths.\n- Regenerated display screenshots with docs/capture_gui.py and updated user docs for the unified single/overlay LUT workflow, per-channel controls (including gamma range), and isotropic multi-perspective MIP behavior.\n\nLocal validation in both conda environments (navigate and python39) for previously failing targets is passing. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #1203 +/- ##
===========================================
+ Coverage 63.21% 63.46% +0.25%
===========================================
Files 189 189
Lines 24975 25864 +889
===========================================
+ Hits 15788 16415 +627
- Misses 9187 9449 +262
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Introduce WidgetInputAdapter to expose standalone tk widgets with the same accessors as LabelInput (get/set/get_variable/label/master). Replace several LabelInput instances in the display notebook with explicit label + widget pairs and wrap checkbuttons with the adapter where needed to preserve API compatibility; adjust grid column configuration to maintain layout. Also reorder imports (logging) and add typing imports. Update unit tests to reflect the new adapter and widget/label structure.
Summary
Testing