Remove napari live view; port overexposure indicator and alignment (with auto-align) to the pyqtgraph viewer - #627
Remove napari live view; port overexposure indicator and alignment (with auto-align) to the pyqtgraph viewer#627Alpaca233 wants to merge 5 commits into
Conversation
…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
There was a problem hiding this comment.
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.
| if reference.shape != moving.shape: | ||
| height, width = moving.shape | ||
| reference = cv2.resize(reference, (width, height), interpolation=cv2.INTER_AREA) |
There was a problem hiding this comment.
[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.
| item.setCompositionMode(QPainter.CompositionMode_Plus) | ||
| self._active_view().addItem(item) | ||
| self.alignment_reference_item = item | ||
| self.alignment_reference_item.setImage(image, autoLevels=False) |
There was a problem hiding this comment.
[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.
| 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 |
There was a problem hiding this comment.
[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].
|
|
||
| def set_overexposure_indicator(self, enabled: bool): | ||
| """Render pixels at or above the upper contrast limit in red.""" | ||
| if self.show_LUT: |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
[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
dd2cfd7 to
b01c620
Compare
…, 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
Summary
The napari live viewer (
NapariLiveWidget) was unreachable in practice —USE_NAPARI_FOR_LIVE_VIEW,USE_NAPARI_FOR_LIVE_CONTROLandUSE_NAPARI_WELL_SELECTIONwere hardcodedFalsewith 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 pyqtgraphImageDisplayWindow, 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)show_alignment_reference(image)/hide_alignment_reference()add a secondImageItemin 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)_set_statehelper.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.pymeasure_translation_px(reference, live, live_crop_fraction)— center-crops the reference by the live display's resolution scaling (via the newcrop_to_fraction, also used byStreamHandler), 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 frommove_from_click_image, now shared by click-to-move, auto-align andWellplateCalibration.viewerClicked.to_grayscale— shared by registration, the overlay andcalculate_focus_measure.Removed
NapariLiveWidget(~580 lines) and the three flags plus every gated branch ingui_hcs.py; well selector always docks normally; the alignment widget is now set up unconditionally (outside live-only mode);napari_connectionsis a flat list.NapariLiveWidgetintensity-cap test (theLiveControlWidgettwin remains).Testing
tests/control/test_alignment.py(registration incl. sign conventions, display crop, binning rescale and color reference; stage mapping under bothINVERTED_OBJECTIVEsettings; the widget workflow against a realImageDisplayWindow; the GUI auto-align handler and its guards; the calibration click) andtests/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).fluidics_protocol/test_library_port.py) is pre-existing onmasterfrom feat(fluidics): protocol engine phases 0–1 — plumbing, demolition, engine seams, headless ProtocolRunner #625.test_HighContentScreeningGui.py: 5 passed (exit 0 withSQUID_PYTEST_HARD_EXIT=1as in CI).blackclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01C54w5TgbZet5DP3GyCZHTN