Skip to content

Stabilize LUT rendering and improve MIP projection handling#1203

Merged
AdvancedImagingUTSW merged 18 commits intodevelopfrom
demo
Mar 13, 2026
Merged

Stabilize LUT rendering and improve MIP projection handling#1203
AdvancedImagingUTSW merged 18 commits intodevelopfrom
demo

Conversation

@AdvancedImagingUTSW
Copy link
Collaborator

Summary

  • keep camera views from flashing and mis-rendering by tracking visibility before scheduling redraws and suppressing gauge updates when widgets are temporarily empty
  • update the MIP controller to honor anisotropic spacing, add a multi-perspective layout, reuse buffers for faster reductions, and guard display work behind visibility checks
  • add regression tests covering view visibility callbacks and MIP projection scaling/composition

Testing

  • Not run (not requested)

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.
Copilot AI review requested due to automatic review settings March 10, 2026 21:49
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +55 to +57
def _space(px: int) -> int:
"""Resolve spacing through the active GUI theme token map."""
return get_theme_space_px(px, px)
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1013 to +1029
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:
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1841 to +1849
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"])

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +2530 to +2536
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"])

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@AdvancedImagingUTSW
Copy link
Collaborator Author

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.

@AdvancedImagingUTSW
Copy link
Collaborator Author

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
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 72.96748% with 266 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.46%. Comparing base (0d0510d) to head (f642e43).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
...navigate/controller/sub_controllers/camera_view.py 69.40% 201 Missing ⚠️
...igate/view/main_window_content/display_notebook.py 83.01% 45 Missing ⚠️
...ate/view/custom_widgets/LabelInputWidgetFactory.py 67.74% 20 Missing ⚠️
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     
Flag Coverage Δ
unittests 63.46% <72.96%> (+0.25%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@AdvancedImagingUTSW AdvancedImagingUTSW merged commit 7f79199 into develop Mar 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants