feat: throttled live preview during Record + Z-Stack acquisitions - #592
Draft
hongquanli wants to merge 100 commits into
Draft
feat: throttled live preview during Record + Z-Stack acquisitions#592hongquanli wants to merge 100 commits into
hongquanli wants to merge 100 commits into
Conversation
Implements set_frame_rate override on SimulatedCamera to cap the frame rate in continuous acquisition mode. When set_frame_rate is called, the streaming thread's frame cadence gate now honors both exposure time and the target frame period, using whichever is larger. The method returns the effective achievable FPS clamped by the total frame time (exposure + strobe). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Lift shared acquisition mechanics out of MultiPointWorker into a new MultiPointWorkerBase so a future RecordZStackWorker sibling can reuse them: camera/stage/channel-apply, single-frame capture (acquire_camera_image), the camera frame callback (_image_callback) with CaptureInfo handoff + job creation/dispatch + backpressure gating + ready-for-next-trigger event, job-result summarize/finish, move_to_z_level, _select_config, _sleep, _frame_wait_timeout_s, wait_till_operation_is_completed, update_use_piezo. MultiPointWorker now subclasses MultiPointWorkerBase, calls super().__init__() for the shared handles/state, then builds the real job runners / backpressure controller and sets all MultiPoint-specific state (NZ/deltaZ/z_range/ z_stacking_config/use_piezo/selected_configurations/scan coords/time-point/ AF + per-channel-offset). Orchestration (run, run_single_time_point, run_coordinate_acquisition, acquire_at_position) and the z-stack/offset/AF helpers stay in MultiPointWorker unchanged. _emit_plate_layout gets a base no-op stub overridden by MultiPointWorker's real implementation so the lifted frame callback stays self-contained. Method bodies moved verbatim; net MultiPointWorker state is identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….py (no behavior change) Move the experiment-ID timestamping + directory creation logic from MultiPointController.start_new_experiment into a free function create_experiment_dir(base_path, experiment_id) -> (resolved_id, dir_path) in control/core/acquisition_setup.py so a future RecordZStackController can reuse it without duplicating code. Pre-warmed JobRunner methods left in MultiPointController — they mutate instance state and do not factor out cleanly as free functions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rt/write race) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Appends ContinuousFrameSource (wraps AbstractCamera into start/stop source) and StreamingCapture (orchestrates source, router, stop-condition, writer) to streaming_capture.py. run() accepts an optional timeout parameter so Task D can bound wall-clock wait without hanging on a stalled camera. Adds fake-source test confirming 5-frame emit + downsampling + finalize. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…le); document stop/finalize assumption Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add RecordZStackAcquisitionParameters dataclass and pure helper functions frame_count, zstack_plane_count, and zstack_offsets_um for z-stack and recording acquisition planning. These helpers handle frame counting, z-plane calculation with epsilon tolerance, and offset list generation. Includes full test coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address code review: add ValueError tests for invalid z-stack inputs (z_max<z_min, step<=0) and add a comment explaining the floor epsilon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add RecordZStackWorker(MultiPointWorkerBase) orchestrating per-FOV record + z-stack acquisition: - record() streams a continuous high-fps capture to a per-FOV recording .ome.zarr via the C3 StreamingCapture primitive - zstack() reuses the inherited triggered-capture + SaveZarrJob dispatch path (builds its own JobRunner + BackpressureController like MultiPointWorker), managing camera streaming/callback lifecycle locally - establish_reference() uses laser AF when available, falls back to current stage Z - run() loops time points -> regions -> FOVs, abort- and dt-aware Smoke test (simulated microscope, 2 wells x 2 FOV x 2 t, both phases) verifies recording dataset count/shape and z-stack per-FOV zarr shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d fix - Add RecordZStackController to record_zstack_controller.py: builds params via setters, pre-warms a JobRunner subprocess (mirrors MultiPointController), resolves a timestamped experiment dir via create_experiment_dir, constructs RecordZStackWorker, and spawns it on a daemon thread. Exposes request_abort(), join(), close(), and per-param setters for the widget. - Fix ZarrWriter._get_loop(): after calling get_event_loop() check that the returned loop is not already closed; if it is, fall through to new_event_loop() so recordings on daemon threads across multiple time-points don't raise 'Event loop is closed'. - Add test_record_zstack_controller_smoke: controller-driven test covering Nt=2 with dt_s=0.1, both recording and z-stack phases, shape assertions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task E1: widget skeleton (Option-A single-column layout with Output, Wells & FOV, Time-lapse/Focus, Recording phase, Z-Stack phase, Start groups), pure _validate_record_zstack_params() helper, validate(), build_parameters(), and _add_zstack_channel_row(). 18 tests pass (13 pure-helper, 5 widget via qtbot). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ow table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… labels - Recording group: add exposure/gain/illumination spinboxes and a '⟳ Live' button that copies currentConfiguration into the recording channel dropdown + inline editors - Z-stack table expanded to 5 columns (channel, exposure, gain, illumination, actions); each row has per-channel spinboxes, a '⟳' copy-from-live button, and a '✕' remove button - '+ Add channel' combo+button below the z-stack table - Added _remove_zstack_channel_row(), _set_zstack_row_values(), _get_zstack_row_values(), _copy_recording_from_live(), _copy_zstack_row_from_live(), _on_zstack_add_channel_clicked() - build_parameters() now reads inline editor values for both the recording channel and each z-stack row; uses model_copy(deep=True) to avoid mutating source channel objects - 7 new qtbot tests added (26 total, all passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cstring Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RDING) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Completes E4: gui_hcs.py:1148 connects acquisition_finished -> recordZStackWidget.acquisition_is_finished; these slots were authored with E4 but omitted from its commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the recordZStackController sentinel; avoids a latent AttributeError if recordZStackWidget is referenced when ENABLE_RECORDING is False. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… lifecycle CRITICAL 1: recording captured dark frames — turn illumination on around cap.run() (off in finally), since set_microscope_mode only energizes illumination when live and the CONTINUOUS stream does not gate it. CRITICAL 2: z-stack used the wrong acquire_camera_image branch — set liveController.set_trigger_mode(SOFTWARE) (capturing/restoring the previous mode) so camera mode and liveController.trigger_mode agree. IMPORTANT 6: zstack() now calls camera.stop_streaming() in the finally, mirroring ContinuousFrameSource.stop(), so the next FOV's recording starts on a stopped camera. IMPORTANT 9a: hoist stop-live to run() start (once, capturing was_live) and restart live once in run()'s finally; removed per-FOV stop-live from record(). MEDIUM: precompute zstack offsets and frame shape once in __init__. SIMPLIFICATION: resolve recording_channel to a local in record(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g, non-blocking enqueue, abort sealing, partial warning) - ContinuousFrameSource.start: set CONTINUOUS mode before set_frame_rate so the toupcam PRECISE_FRAMERATE hint survives the mode switch (IMPORTANT 5). - RecordingWriter: track _started, set only after thread.start(); finalize/abort skip the join when not started so a failing initialize() propagates its real error instead of a 'cannot join thread' crash (IMPORTANT 7). - StreamingCapture._on_frame: re-check stop condition before routing so no frame is enqueued at/past t-index T into a (T,...)-shaped dataset (IMPORTANT 8). - RecordingWriter.enqueue: put_nowait + drop-on-full (no 0.5s block on the camera hot thread); default queue maxsize 64 -> 256 (MEDIUM). - StreamingCapture: track _aborted; run() finally calls writer.abort() on abort (seals zarr incomplete) else finalize() (MEDIUM). - StreamingCapture.run: WARNING when emitted < expected (CountStop.expected()); trailing zarr planes are blank fill (MEDIUM). - Document that the post-stop _emitted read is safe (MINOR). - Tests: start-error path, OOB gating, abort->writer.abort, complete->finalize, partial warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n fixes - IMPORTANT 3+4: fix _get_selected_well_count to resolve lazily via scanCoordinates.get_selected_wells() instead of a stale cached well_selection_widget; handles None (glass-slide → count=1) and empty dict (no wells selected → count=0 → validation rejects) - IMPORTANT 9b: reject recording configs where frame_count(fps, duration_s) < 1 in _validate_record_zstack_params with clear message - MEDIUM: replace _abort_requested bool with threading.Event (_abort_event); set/clear/is_set are thread-safe; abort_requested_fn passed to worker uses lambda: self._abort_event.is_set() - REUSE: add run_acquisition(params) direct-params path; toggle_acquisition now calls run_acquisition(self.build_parameters()) — no 15-setter fan-out; setters kept as shims for test_record_zstack_worker.py compatibility - SIMPLIFICATION: remove unreachable None guard in _on_zstack_add_channel_clicked; log warnings in _get_zstack_row_values instead of silently substituting defaults - Tests: 39 pass (8 new); cover frame_count=0, glass-slide/None well count, lazy well-selector resolution, and abort Event lifecycle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… base, extract compute_pixel_size_um, drop redundant _job_runners re-assignments, fix misleading __class__ comment - REUSE#1: move _wait_for_outstanding_callback_images verbatim to MultiPointWorkerBase; remove subclass overrides from MultiPointWorker and RecordZStackWorker (both inherit the shared implementation) - REUSE#2: add compute_pixel_size_um(objective_store, camera) to acquisition_setup.py; call from both worker __init__s - REUSE#5: deferred — ZarrWriterInfo constructions differ materially (is_hcs, use_6d_fov) between MultiPointWorker and RecordZStackWorker - SIMP#1: drop redundant self._job_runners = [] in MultiPointWorker and self._job_runners/_backpressure/_abort_on_failed_job/ _first_job_dispatched in RecordZStackWorker (base __init__ sets them) - B1: correct misleading comment — __class__ in a method body always refers to the defining class, not the runtime type; type(self) is what produces per-subclass logger names Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ix-batch5) - Add signal_acquisition_started(bool) to RecordZStackMultiPointWidget, emitting True on valid start and False in acquisition_is_finished(), mirroring the pattern used by WellplateMultiPointWidget and FlexibleMultiPointWidget. - Connect the signal to gui_hcs.toggleAcquisitionStart inside the ENABLE_RECORDING guard so other tabs, click-to-move, and live-scan-grid are locked during acquisition. - Update toggle_acquisition docstring to reflect build_parameters() call path. - Add comment in record_zstack_controller explaining the abort_event clear→start window. - Add dropped_count property to RecordingWriter and a summary WARNING log in StreamingCapture.run so slow-disk dropped frames are diagnosable without grepping. - Add 5 new tests covering the signal and dropped-count behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code review found a race: toggle_acquisition emitted True AFTER run_acquisition() returned, but run_acquisition spawns a worker thread that on a fast/one-frame acquisition could finish (acquisition_is_finished -> emit False) before the GUI thread reached the emit(True) line. The GUI would then process False then True, permanently locking all tabs. Fix mirrors WellplateMultiPointWidget (which emits True via _set_ui_acquisition_running before run_acquisition): - Emit True before calling run_acquisition. - Wrap run_acquisition in try/except; on failure un-check the button and emit False to unlock the UI (the worker never started, so acquisition_finished will not fire). - Document the emit(False) paths in acquisition_is_finished docstring. - Add 2 tests: emit-ordering (True before run) and run-raises (True then False). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntroller The widget already uses the clean run_acquisition(params) path, so the 15 one-line setter methods, the corresponding duplicate per-field instance attributes, and the legacy params=None fallback are all dead code. Remove them and update the controller test (test_record_zstack_controller_smoke) to build RecordZStackAcquisitionParameters directly and pass it to run_acquisition(params), matching how the widget works. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…offset moves to its own row Live-testing feedback: FPS/Duration should come first, and the Bottom-Z offset field (visible only when Laser AF is on) shouldn't share a row with Nz/dz/FPS/Dur since it shifts that row's width when it appears/disappears.
…yout Live-testing feedback: the offset row should line up with Nz, not start flush-left. Switched the recording-controls block from two independent QHBoxLayouts to one QGridLayout so the offset shares Nz's columns and aligns exactly regardless of font/platform metrics.
…acquisition # Conflicts: # software/control/gui_hcs.py
) master (PR #584) removed the duck-typed emit_selected_channels() / display_progress_bar() calls from gui_hcs.onTabChanged and toggleAcquisitionStart — channels are now emitted by each multipoint widget at acquisition start. RecordZStackMultiPointWidget's no-op stubs (added in 9b5be65 to satisfy the old contract) and their regression test are now unreachable dead code; remove them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…into feat/multi-plane-recording
Live-testing feedback across several rounds landed on: FPS/Duration always lead row 1; Nz/dz/offset append to row 1 too as long as at most 2 of them are visible, but move to their own row 2 (as a group) only when all 3 are shown at once (Nz>1 AND Laser AF on) -- that's too many fields for one row. When on row 2, Nz/dz get temporarily widened to match FPS/Dur's widths so the columns align exactly (both rows share the same left margin, so equal leading widths are sufficient -- no grid needed). Otherwise they use their own compact widths sized to the actual label/spinbox content, fixing an earlier pass that forced the wider shared width unconditionally and left visible dead space in the common single-row case. Also: dz now shows 1 decimal instead of 2, and both rows share one uniform inter-item spacing instead of a larger explicit gap between field groups.
…ee-run max, not the triggered frame time The recording phase reads the camera's achievable rate via set_frame_rate(). On the ToupTek the PRECISE_FRAMERATE option can't be read (E_UNEXPECTED), so set_frame_rate() fell back to 1000/get_total_frame_time(). get_total_frame_time() is the *sequential* software/hardware-trigger period (readout + trigger_delay + exposure) — ~2x the true continuous free-run period — so recordings were capped at roughly half the camera's real rate: a 20 or 30 fps request clamped to ~14 fps (and dropping exposure didn't help, since trigger_delay grows to keep the total pinned). Add ToupcamCamera._continuous_max_framerate() = 1000/max(readout, exposure), where readout = strobe_time_us (the exposure-independent sensor readout period), and return it from set_frame_rate()'s fallbacks. record()'s existing "honor the requested fps unless the camera can't reach it" logic then works: requests up to the readout ceiling are honored; higher requests clamp to the true max. Triggered-mode timing is untouched — get_strobe_time / get_total_frame_time / _calculate_strobe_info are unchanged; only set_frame_rate (continuous-only) and the new helper change. Verified on a real ITR3CMOS26000KMA: 25 fps x 5 s -> 125 frames (honored); 30 fps x 4 s -> 112 frames at 28.04 fps (the true readout max), where the old code produced ~56 at the bogus 14 fps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cleanup from the /simplify review (no behavior change): - Remove the unreachable `if frame_ms > 0` guard and the `1000/get_total_frame_time()` fallback. frame_ms is always positive (this model's sensor resolutions all map to a positive readout period), and that fallback returned the exact triggered-mode value the helper exists to reject — a misleading dead branch, not a real safety net. - De-duplicate the ~2x rationale: set_frame_rate's Returns now points to _continuous_max_framerate instead of repeating it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hecked phases Recording phase now starts checked (was unchecked) since it's the more commonly used mode. Also, unchecked phase groups (Recording, Z-Stack) now collapse their fields entirely instead of Qt's default checkable-QGroupBox behavior of graying them out while still showing and reserving space for them.
…dividers QGroupBox (even flat) renders a sunken box border under the Fusion style, inconsistent with WellplateMultiPointWidget's borderless-section convention. Switch Output/Wells+FOV/Recording/Z-Stack to plain QFrame with a bold QCheckBox header for the checkable sections, and add a thin sunken HLine divider between each of the widget's 4 sections so the boundaries stay visually clear.
…ettings - _build_start_group returned a QGroupBox, the one section still showing the sunken-box look the other 4 sections were switched away from; make it a plain QFrame like the rest. - Move the Save/Load Settings buttons out of the output group and into the start group (just above Start Acquisition), and hide them by default — drag-and-drop of an acquisition YAML already covers the same functionality without permanently using UI space.
… YAML load; fix double-indented content Code review of the QGroupBox->QFrame refactor found two regressions: - _apply_yaml_settings blocks checkbox_recording/checkbox_zstack's toggled signal while calling setChecked(), so the collapse-when-unchecked wiring in _build_recording_group/_build_zstack_group never fired on load, leaving each section's visible/hidden state stale relative to its checkbox. Store the content vbox/grid layout as attributes and resync visibility directly in the finally block, matching the existing checkbox_time/checkbox_xy pattern. Added a regression test. - Both sections' content layout set its own (4, 2, 4, 2) margins on top of the new outer_vbox's identical margins, double-indenting the channel table/fields relative to the checkbox header and the other 3 sections. Zero the inner layout's margins since the outer frame margin already applies.
recording_channel_table's fixed height was computed from rowHeight(0) before the row's cell widgets (combo/spinboxes/button, all taller than plain text) were placed, then padded by 8px to guess enough room for them — leaving visible empty space below the row once they were actually added. Move the height calculation after the cell widgets are set and call resizeRowsToContents() first, so the fixed height matches the row's real final height.
…d wrong channel - Recording/Z-Stack channel table headers were rendering bold (default header font weight); explicitly set them to normal weight. - _copy_recording_from_live and _copy_zstack_row_from_live read liveController.currentConfiguration — whatever channel happens to be active in the Live tab — instead of the settings for the channel actually selected in the recording row / z-stack row being refreshed. This silently switched the recording row's channel selection to match Live's active channel, and overwrote z-stack rows with the wrong channel's values whenever Live wasn't showing that row's channel. Both now look up settings via _channel_settings() for their own channel instead.
…RY header/test fixes Code review of e57dbb1d found: - _copy_recording_from_live/_copy_zstack_row_from_live switched to _channel_settings(name), which silently falls back to a hardcoded (50, 0, 50) triple when the channel isn't found (e.g. the objective changed elsewhere and the row's stale channel selection no longer exists) — a quiet data-reset the old currentConfiguration-based code never had. Use _find_channel() directly and leave the row untouched (with a warning) on a miss instead. Added a regression test. - test_copy_from_live_uses_selected_channels_own_settings didn't actually exercise the button's click handler, since selecting the channel via setCurrentText() already auto-seeds the same target values through _on_recording_channel_changed. Overwrite the spinboxes before clicking so the assertions can only pass if the click handler itself re-applies them. - Factored the duplicated 3-line header-unbold snippet (recording + z-stack tables) into a shared _set_header_not_bold() helper. - recording_channel_table's tightened fixed-height padding used a bare "+2" magic number; use 2 * frameWidth() (the table's actual top+bottom border) instead, so it stays correct if the frame style ever changes.
…into feat/multi-plane-recording
…trigger live can't break recording
Starting a recording while Live ran in SOFTWARE-trigger mode aborted the run on
real hardware. _probe_frame_shape() sized the recording dataset by sending a
software trigger, but Live's last trigger was still outstanding
(get_ready_for_trigger() == False), so send_trigger() was rejected ("trigger too
early"). The probe then fell back to get_resolution() — the raw binned sensor
size (3104x2084) — while the camera delivers cropped 2084x2084 frames, so every
write_frame failed with a dimension-alignment error and the store sealed
incomplete, fail-fasting the whole acquisition.
Probe in CONTINUOUS mode instead — the same mode recording uses — and read one
free-run frame (no send_trigger, so trigger readiness can't block it); the probed
shape is then exactly what recording writes. On a genuine probe failure, abort
with a clear error instead of sizing from get_resolution() and grinding out a
blank, incomplete recording.
Also fix SimulatedCamera.read_camera_frame() to wait for a frame (matching the
real camera / AbstractCamera contract) rather than returning None before the
first CONTINUOUS frame — otherwise the continuous probe gets no frame in sim.
Verified on a real ITR3CMOS26000KMA: the continuous probe returns (2084, 2084)
(matching delivery) vs get_resolution()'s (3104, 2084), and get_ready_for_trigger()
is False right after a software trigger (reproducing the reject). New tests cover
the probe using CONTINUOUS/never triggering and aborting (not get_resolution) on
no frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…acquisition # Conflicts: # software/tests/control/test_HighContentScreeningGui.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…STACK_DISPLAY_FPS Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ings Review findings (sub-threshold but real): the _def.py comment said z-stack planes 'always display' while the next sentence says 0 disables both phases; and _on_frame's 'route + enqueue only' prose predated the preview tap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… enabled Previously the button was always clickable, and clicking with both phases disabled just surfaced validate()'s "At least one phase must be enabled" error via a dialog after the fact. Disable the button proactively instead, kept in sync via checkbox_recording/checkbox_zstack toggled signals and resynced after YAML load (same signal-blocking issue as the earlier Recording/Z-Stack section-visibility fix).
RecordZStackMultiPointWidget was the only multipoint tab setting QFrame.Panel | QFrame.Raised on itself — WellplateMultiPointWidget and FlexibleMultiPointWidget use the default QFrame.NoFrame. The raised panel gave this tab a visibly different embossed background/border compared to the others.
…ts own gray fill The raised-panel removal (9907f81) wasn't the actual cause: WellplateMultiPointWidget also sets QFrame.Panel | QFrame.Raised, but it doesn't use a QScrollArea. This widget's QScrollArea viewport and content QWidget both auto-fill an opaque palette-Window background, visibly darker than the tab pane under Fusion. Disable auto-fill on both so the pane's own background shows through, matching every other multipoint tab.
…into feat/multi-plane-recording
feat: multi-plane recording — N z-planes per FOV (stacks on #564)
…into feat/record-zstack-live-preview # Conflicts: # software/tests/core/test_streaming_capture.py
…um rename #579's multi-plane recording (now merged into feat/record-zstack-acquisition) renamed RecordZStackAcquisitionParameters.recording_z_offset_um to recording_bottom_z_offset_um and added recording_Nz/recording_dz_um. The git merge of feat/record-zstack-acquisition didn't touch these two already-passing tests textually, so they kept constructing the params with the old keyword and failed after the merge.
Alpaca233
force-pushed
the
feat/record-zstack-acquisition
branch
from
July 27, 2026 21:30
0afae24 to
d07b875
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #564 (merges after it; rebase onto master if #564 lands squashed).
Shows frames in the live display during Record + Z-Stack acquisitions, which were previously display-blind:
StreamingCapture(display_fn+display_fps), first frame always shown, then atRECORD_ZSTACK_DISPLAY_FPS(default 3 fps,_def.py/INI;0disables preview for both phases).signal_new_imagecallback (previously wired to a no-op) now emits each captured plane.QtRecordZStackController.image_to_display, connected to the same display slots asmultipointController.image_to_displayin every display config (no double-fire — verified againstupdateNapariConnections's napariLiveWidget skip).streaming_capture.py/record_zstack_worker.py/record_zstack_controller.pygain only plain callables/floats;Signalappears only ingui_hcs.py. Headless path unchanged (defaultsNone/0).Testing
QT_API=pyqt5).Design:
AI-docs/Squid/to-do/2026-07-06-record-zstack-live-preview-design.md🤖 Generated with Claude Code