Skip to content

Remove napari live view; port overexposure indicator and alignment (with auto-align) to the pyqtgraph viewer - #627

Open
Alpaca233 wants to merge 5 commits into
masterfrom
feature/remove-napari-live-view
Open

Remove napari live view; port overexposure indicator and alignment (with auto-align) to the pyqtgraph viewer#627
Alpaca233 wants to merge 5 commits into
masterfrom
feature/remove-napari-live-view

Conversation

@Alpaca233

@Alpaca233 Alpaca233 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

The napari live viewer (NapariLiveWidget) was unreachable in practice — USE_NAPARI_FOR_LIVE_VIEW, USE_NAPARI_FOR_LIVE_CONTROL and USE_NAPARI_WELL_SELECTION were hardcoded False with no config override — which also made the alignment feature unreachable, since it required the napari viewer. This PR removes the napari live view path and ports its two napari-only features into the pyqtgraph ImageDisplayWindow, which has been the actual live viewer all along.

Mosaic View and the Multichannel Acquisition tab still use napari and are untouched.

Changes

ImageDisplayWindow (core/core.py)

  • Overexposure toggle on the status bar next to Line Profiler, off by default. Implemented as a red mask overlay item (1 where any channel is at or above the upper contrast limit), so one mechanism covers both display modes and both mono and RGB frames without touching the histogram-owned LUT or the frame data. The mask is recomputed only when the frame or the levels change.
  • Alignment reference overlay: show_alignment_reference(image) / hide_alignment_reference() add a second ImageItem in additive magenta whose levels follow the live image (per frame and on histogram changes). Color references are reduced to intensity here, since pyqtgraph ignores lookup tables on H×W×3 data.
  • current_image() returns the last received frame.

AlignmentWidget (widgets.py)

  • Takes the live display instead of a napari viewer; the napari layer-management section is replaced by the two calls above. State transitions go through one _set_state helper.
  • New Auto button (enabled while a reference is loaded): emits the reference image; the GUI registers the current live frame against it with skimage.registration.phase_cross_correlation (already used by PDAF and the stitcher), converts the shift with the shared pixel→stage helper, and moves the stage to cancel it. The user then clicks Confirm Offset as before, so the human check stays in the loop and no correlation-confidence threshold is needed — a failed registration is obvious in the overlay. Auto requires a running live view (the last frame is stale once the stage has moved).

control/utils.py

  • measure_translation_px(reference, live, live_crop_fraction) — center-crops the reference by the live display's resolution scaling (via the new crop_to_fraction, also used by StreamHandler), resamples to the live resolution (binning mismatch), handles color inputs, returns the displacement in live pixels.
  • image_delta_to_stage_delta_mm(...) — the pixel→stage conversion extracted from move_from_click_image, now shared by click-to-move, auto-align and WellplateCalibration.viewerClicked.
  • to_grayscale — shared by registration, the overlay and calculate_focus_measure.

Removed

  • NapariLiveWidget (~580 lines) and the three flags plus every gated branch in gui_hcs.py; well selector always docks normally; the alignment widget is now set up unconditionally (outside live-only mode); napari_connections is a flat list.
  • The NapariLiveWidget intensity-cap test (the LiveControlWidget twin remains).

Testing

  • New tests in tests/control/test_alignment.py (registration incl. sign conventions, display crop, binning rescale and color reference; stage mapping under both INVERTED_OBJECTIVE settings; the widget workflow against a real ImageDisplayWindow; the GUI auto-align handler and its guards; the calibration click) and tests/control/test_image_display_window.py (overexposure overlay in both display modes, for RGB frames and on histogram level changes; reference overlay add/reuse/remove, level sync and grayscale reduction).
  • Full suite: 1749 passed, 9 skipped, 1 xfailed; the one failure (fluidics_protocol/test_library_port.py) is pre-existing on master from feat(fluidics): protocol engine phases 0–1 — plumbing, demolition, engine seams, headless ProtocolRunner #625. test_HighContentScreeningGui.py: 5 passed (exit 0 with SQUID_PYTEST_HARD_EXIT=1 as in CI).
  • black clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C54w5TgbZet5DP3GyCZHTN

…lignment to the pyqtgraph viewer

- ImageDisplayWindow gains an "Overexposure" toggle on the status bar (napari's
  grayclip mapping: pixels at/above the upper contrast limit render red; off by
  default), an additive magenta reference overlay that tracks the live contrast
  range, and current_image()
- AlignmentWidget takes the live display instead of a napari viewer and gains an
  Auto button: the live frame is registered against the reference with
  skimage phase_cross_correlation and the stage is nudged to cancel the
  displacement; Confirm Offset then computes the offset exactly as before
- Pixel->stage conversion extracted into control.utils and shared by
  click-to-move and auto-align
- Remove NapariLiveWidget and the USE_NAPARI_FOR_LIVE_VIEW / _LIVE_CONTROL /
  _WELL_SELECTION flags (hardcoded False with no config override, so the napari
  live view was unreachable and the alignment feature with it)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C54w5TgbZet5DP3GyCZHTN

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Removes the unreachable napari live-view path and ports alignment and overexposure features to the pyqtgraph viewer.

Changes:

  • Adds pyqtgraph overexposure and alignment overlays.
  • Adds automatic image registration and stage alignment.
  • Removes napari live-view flags, implementation, and tests.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
software/control/_def.py Removes obsolete napari live flags.
software/control/core/core.py Adds display overlays and current-frame access.
software/control/gui_hcs.py Integrates pyqtgraph alignment and removes napari branches.
software/control/utils.py Adds registration and stage-delta helpers.
software/control/widgets.py Ports alignment workflow and removes NapariLiveWidget.
software/tests/control/test_alignment.py Tests registration and alignment workflows.
software/tests/control/test_image_display_window.py Tests new display features.
software/tests/control/test_intensity_cap.py Removes obsolete napari test coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread software/control/utils.py Outdated
Comment on lines +50 to +52
if reference.shape != moving.shape:
height, width = moving.shape
reference = cv2.resize(reference, (width, height), interpolation=cv2.INTER_AREA)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Claude Code] Fixed in b01c620 - measure_translation_px now takes live_crop_fraction (StreamHandler.display_resolution_scaling). The reference is center-cropped with the same utils.crop_image the stream handler uses, then resampled, so display cropping and binning are handled separately. Tests: test_measure_translation_px_accounts_for_display_center_crop, test_auto_align_uses_the_display_crop_fraction.

Comment thread software/control/core/core.py Outdated
item.setCompositionMode(QPainter.CompositionMode_Plus)
self._active_view().addItem(item)
self.alignment_reference_item = item
self.alignment_reference_item.setImage(image, autoLevels=False)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Claude Code] Fixed in b01c620 - AlignmentWidget reduces the reference to a 2-D intensity image (utils.to_grayscale) before overlaying, so the magenta LUT applies to BMP color references too. Test: test_color_reference_is_overlaid_as_an_intensity_image.

Comment on lines +1312 to +1316
live_image = self.imageDisplayWindow.current_image()
pixel_size_um = self.microscope.get_image_pixel_size_um()
if live_image is None or pixel_size_um is None:
QMessageBox.warning(self, "Alignment Error", "Auto align needs a live image and a known pixel size.")
return

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Claude Code] Fixed in b01c620 - _alignment_auto_align now requires liveController.is_live (warning updated) before any motion. Test: test_auto_align_refuses_to_move_without_a_current_live_frame[live stopped].

Comment thread software/control/core/core.py Outdated

def set_overexposure_indicator(self, enabled: bool):
"""Render pixels at or above the upper contrast limit in red."""
if self.show_LUT:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Claude Code] Fixed in b01c620 - for H x W x 3 frames display_image marks pixels with any channel at or above the upper contrast limit red in a copy of the frame when the indicator is on; current_image() returns the unmodified frame so registration is unaffected. Tests: test_overexposure_marks_saturated_rgb_pixels_red, test_rgb_frames_are_shown_unmodified_when_indicator_is_off.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Claude Code] Follow-up in 9a0c906 - replaced the in-data marking with a single red mask overlay item (1 where any channel is at/above the upper contrast limit), which is now the one mechanism for mono and RGB frames in both display modes; the frame data and pixel readouts are untouched.

…e frames, RGB overexposure

- measure_translation_px takes the display crop fraction: below 100% display
  resolution the live frame is the center crop of the FOV (StreamHandler), so
  the same part of the reference is compared instead of resizing the full FOV
  into the crop
- AlignmentWidget reduces color references to intensity before overlaying;
  pyqtgraph ignores lookup tables on H x W x 3 data
- Auto align requires a running live view: current_image() keeps the last
  frame after live stops, which is stale once the stage has moved
- ImageDisplayWindow marks saturated RGB pixels red in the data when the
  overexposure indicator is on (LUTs do not apply to RGB frames), and
  current_image() returns the unmodified frame

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C54w5TgbZet5DP3GyCZHTN
@Alpaca233
Alpaca233 force-pushed the feature/remove-napari-live-view branch from dd2cfd7 to b01c620 Compare September 1, 2026 01:34
Alpaca233 and others added 3 commits August 31, 2026 18:53
…, grayscale and stage-move helpers

- Overexposure indicator is now a single red mask overlay item, replacing the
  three mode-specific implementations (histogram gradient / item LUT / RGB
  data marking). Pixel readouts no longer see marked data, and the mask only
  recomputes when the frame or levels change; the reference overlay's levels
  are only reapplied when they differ
- Auto align reads the display crop fraction from LiveController; the
  QtStreamHandler wrapper does not expose it (the stub-based test encoded the
  wrong attribute path and now uses the real one)
- crop_to_fraction shared by StreamHandler and measure_translation_px;
  to_grayscale uses the codebase's RGB convention and is reused by
  calculate_focus_measure; grayscale reduction moves into
  show_alignment_reference where the LUT constraint lives
- _move_stage_by_image_delta shared by click-to-move and auto align;
  WellplateCalibration.viewerClicked uses the same helpers instead of its own
  copy of the pixel->stage math
- napari_connections is a flat list; AlignmentWidget clear reuses reset();
  shared image_display_window fixture; hoisted test imports

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C54w5TgbZet5DP3GyCZHTN
…g templates

_def.py only maps an ini key onto an existing module variable, so
use_napari_for_live_view has been silently ignored since the flag was
removed, and use_napari_for_multipoint / use_napari_for_tiled_display have
had no counterpart for longer. Also give LIVE_ONLY_MODE its own label in
_def.py instead of sitting under "Napari integration".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C54w5TgbZet5DP3GyCZHTN
…ver-exposed Pixels"

The Auto button is hidden outside the confirm step instead of greyed out.
The floating Align/Auto widget uses a fixed-size layout so it shrinks and
grows with the button, and NavigationViewer repositions it on resize so it
stays beside the Clear Scan Grid button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C54w5TgbZet5DP3GyCZHTN
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