Dedup viewer Qt boilerplate after the split
- One _apply_alignment_result() replaces the five copies of the cache-clear / generation-bump / Aligned-View-checkbox dance, and _on_manual_alignment_saved/_cleared collapse into a shared _manual_alignment_changed. - QSignalBlocker context managers replace every hand-rolled blockSignals(True)/set/blockSignals(False) triple (11 sites). - _make_dspin() replaces the 11 four-to-seven-line QDoubleSpinBox constructions; _axes_extent() replaces the duplicated imshow-extent formula; _is_fft_mode() replaces the repeated CH1-mode guard. - Jobs constants replace the stringly-typed background-job keys. - The two 160-line widget builders split along their section banners: _build_left_panel -> file/info/view/roi groups, the manual-alignment _build_ui -> six per-group builders + _connect_controls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+38
-2
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QFormLayout, QFrame, QGroupBox, QLabel, QScrollArea, QSizePolicy,
|
QDoubleSpinBox, QFormLayout, QFrame, QGroupBox, QLabel, QScrollArea,
|
||||||
QVBoxLayout, QWidget,
|
QSizePolicy, QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sras_format import CH1_IDX, CH3_IDX, CH4_IDX
|
from sras_format import CH1_IDX, CH3_IDX, CH4_IDX
|
||||||
@@ -52,6 +52,42 @@ _SPIN_MIN_W = 96
|
|||||||
# Small layout helpers
|
# Small layout helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Jobs:
|
||||||
|
"""Keys for SrasViewerWindow's background-job registry (_run_worker /
|
||||||
|
_job_running) and its progress dialogs — one place instead of string
|
||||||
|
literals scattered across window and dialogs."""
|
||||||
|
LOAD = "load"
|
||||||
|
COMPUTE = "compute"
|
||||||
|
DC_PRECOMPUTE = "dc_precompute"
|
||||||
|
BATCH = "batch"
|
||||||
|
ALIGN = "align"
|
||||||
|
MANUAL_ALIGN_MASKS = "manual_align_masks"
|
||||||
|
MANUAL_ALIGN_CORRELATE = "manual_align_correlate"
|
||||||
|
|
||||||
|
|
||||||
|
def _make_dspin(lo: float, hi: float, decimals: int, *, suffix: str = "",
|
||||||
|
value: float | None = None, step: float | None = None) -> QDoubleSpinBox:
|
||||||
|
"""A QDoubleSpinBox with the panel-standard construction."""
|
||||||
|
spin = QDoubleSpinBox()
|
||||||
|
spin.setRange(lo, hi)
|
||||||
|
spin.setDecimals(decimals)
|
||||||
|
if suffix:
|
||||||
|
spin.setSuffix(suffix)
|
||||||
|
if step is not None:
|
||||||
|
spin.setSingleStep(step)
|
||||||
|
if value is not None:
|
||||||
|
spin.setValue(value)
|
||||||
|
spin.setMinimumWidth(_SPIN_MIN_W)
|
||||||
|
return spin
|
||||||
|
|
||||||
|
|
||||||
|
def _axes_extent(x_axis, y_axis, dx: float, dy: float) -> list[float]:
|
||||||
|
"""Matplotlib imshow extent with half-pixel margins, Y flipped so row 0
|
||||||
|
renders at the top."""
|
||||||
|
return [x_axis[0] - dx / 2, x_axis[-1] + dx / 2,
|
||||||
|
y_axis[-1] + dy / 2, y_axis[0] - dy / 2]
|
||||||
|
|
||||||
|
|
||||||
def _wrap_label(text: str = "", css: str | None = None) -> QLabel:
|
def _wrap_label(text: str = "", css: str | None = None) -> QLabel:
|
||||||
"""A word-wrapped QLabel that reports its *wrapped* height to the layout.
|
"""A word-wrapped QLabel that reports its *wrapped* height to the layout.
|
||||||
|
|
||||||
|
|||||||
+47
-76
@@ -5,11 +5,11 @@ from typing import TYPE_CHECKING
|
|||||||
import matplotlib as mpl
|
import matplotlib as mpl
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT
|
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT
|
||||||
from PyQt6.QtCore import pyqtSignal
|
from PyQt6.QtCore import QSignalBlocker, pyqtSignal
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QButtonGroup, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox,
|
QButtonGroup, QComboBox, QDialog, QDialogButtonBox, QGroupBox,
|
||||||
QGroupBox, QHBoxLayout, QLabel, QMessageBox, QPushButton, QRadioButton,
|
QHBoxLayout, QLabel, QMessageBox, QPushButton, QRadioButton, QSpinBox,
|
||||||
QSpinBox, QVBoxLayout, QWidget,
|
QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
import sras_compute as compute
|
import sras_compute as compute
|
||||||
@@ -22,7 +22,7 @@ from sras_workers import Ch4MaskWorker, CrossCorrelateWorker
|
|||||||
|
|
||||||
from .canvases import ManualAlignOverlayCanvas
|
from .canvases import ManualAlignOverlayCanvas
|
||||||
from .common import (
|
from .common import (
|
||||||
_CSS_HINT, _CSS_MUTED, _CSS_WARN, _SPIN_MIN_W, _form, _group,
|
_CSS_HINT, _CSS_MUTED, _CSS_WARN, Jobs, _axes_extent, _form, _group, _make_dspin,
|
||||||
_scroll_panel, _wrap_label,
|
_scroll_panel, _wrap_label,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -268,8 +268,20 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
panel_l = QVBoxLayout(panel)
|
panel_l = QVBoxLayout(panel)
|
||||||
panel_l.setContentsMargins(0, 0, 0, 0)
|
panel_l.setContentsMargins(0, 0, 0, 0)
|
||||||
panel_l.setSpacing(8)
|
panel_l.setSpacing(8)
|
||||||
|
panel_l.addWidget(self._build_angle_group())
|
||||||
|
panel_l.addWidget(self._build_adjust_group())
|
||||||
|
panel_l.addWidget(self._build_step_group())
|
||||||
|
panel_l.addWidget(self._build_threshold_group(dc_threshold_mv))
|
||||||
|
panel_l.addWidget(self._build_correlate_group())
|
||||||
|
panel_l.addWidget(self._build_actions_group())
|
||||||
|
self.lbl_status = _wrap_label("", _CSS_MUTED)
|
||||||
|
panel_l.addWidget(self.lbl_status)
|
||||||
|
panel_l.addStretch()
|
||||||
|
|
||||||
# ---- Active Angle -------------------------------------------------
|
root.addWidget(_scroll_panel(panel, 320))
|
||||||
|
self._connect_controls()
|
||||||
|
|
||||||
|
def _build_angle_group(self) -> QWidget:
|
||||||
grp_angle, al = _group("Active Angle")
|
grp_angle, al = _group("Active Angle")
|
||||||
self.combo_active_angle = QComboBox()
|
self.combo_active_angle = QComboBox()
|
||||||
for a in range(self._sras.n_angles):
|
for a in range(self._sras.n_angles):
|
||||||
@@ -280,80 +292,52 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
al.addWidget(self.combo_active_angle)
|
al.addWidget(self.combo_active_angle)
|
||||||
self.lbl_active_note = _wrap_label("", _CSS_WARN)
|
self.lbl_active_note = _wrap_label("", _CSS_WARN)
|
||||||
al.addWidget(self.lbl_active_note)
|
al.addWidget(self.lbl_active_note)
|
||||||
panel_l.addWidget(grp_angle)
|
return grp_angle
|
||||||
|
|
||||||
# ---- Manual Adjustment ---------------------------------------------
|
def _build_adjust_group(self) -> QWidget:
|
||||||
self.grp_manual_adjust, mform_box = _group("Manual Adjustment")
|
self.grp_manual_adjust, mform_box = _group("Manual Adjustment")
|
||||||
mform = _form()
|
mform = _form()
|
||||||
self.spin_active_rotation_deg = QDoubleSpinBox()
|
self.spin_active_rotation_deg = _make_dspin(-3600.0, 3600.0, 3, suffix=" °")
|
||||||
self.spin_active_rotation_deg.setRange(-3600.0, 3600.0)
|
|
||||||
self.spin_active_rotation_deg.setDecimals(3)
|
|
||||||
self.spin_active_rotation_deg.setSuffix(" °")
|
|
||||||
self.spin_active_rotation_deg.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
mform.addRow("Rotation:", self.spin_active_rotation_deg)
|
mform.addRow("Rotation:", self.spin_active_rotation_deg)
|
||||||
|
|
||||||
self.spin_active_shift_x_mm = QDoubleSpinBox()
|
self.spin_active_shift_x_mm = _make_dspin(-1e5, 1e5, 4, suffix=" mm")
|
||||||
self.spin_active_shift_x_mm.setRange(-1e5, 1e5)
|
|
||||||
self.spin_active_shift_x_mm.setDecimals(4)
|
|
||||||
self.spin_active_shift_x_mm.setSuffix(" mm")
|
|
||||||
self.spin_active_shift_x_mm.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
mform.addRow("Shift X:", self.spin_active_shift_x_mm)
|
mform.addRow("Shift X:", self.spin_active_shift_x_mm)
|
||||||
|
|
||||||
self.spin_active_shift_y_mm = QDoubleSpinBox()
|
self.spin_active_shift_y_mm = _make_dspin(-1e5, 1e5, 4, suffix=" mm")
|
||||||
self.spin_active_shift_y_mm.setRange(-1e5, 1e5)
|
|
||||||
self.spin_active_shift_y_mm.setDecimals(4)
|
|
||||||
self.spin_active_shift_y_mm.setSuffix(" mm")
|
|
||||||
self.spin_active_shift_y_mm.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
mform.addRow("Shift Y:", self.spin_active_shift_y_mm)
|
mform.addRow("Shift Y:", self.spin_active_shift_y_mm)
|
||||||
mform_box.addLayout(mform)
|
mform_box.addLayout(mform)
|
||||||
panel_l.addWidget(self.grp_manual_adjust)
|
return self.grp_manual_adjust
|
||||||
|
|
||||||
# ---- Nudge Step Sizes ------------------------------------------------
|
def _build_step_group(self) -> QWidget:
|
||||||
self.grp_step_sizes, sl = _group("Nudge Step Sizes")
|
self.grp_step_sizes, sl = _group("Nudge Step Sizes")
|
||||||
sform = _form()
|
sform = _form()
|
||||||
self.spin_step_translate_mm = QDoubleSpinBox()
|
self.spin_step_translate_mm = _make_dspin(0.0001, 1000.0, 4,
|
||||||
self.spin_step_translate_mm.setRange(0.0001, 1000.0)
|
suffix=" mm", value=0.01)
|
||||||
self.spin_step_translate_mm.setDecimals(4)
|
|
||||||
self.spin_step_translate_mm.setSuffix(" mm")
|
|
||||||
self.spin_step_translate_mm.setValue(0.01)
|
|
||||||
self.spin_step_translate_mm.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
sform.addRow("Translate step:", self.spin_step_translate_mm)
|
sform.addRow("Translate step:", self.spin_step_translate_mm)
|
||||||
|
|
||||||
self.spin_step_rotate_deg = QDoubleSpinBox()
|
self.spin_step_rotate_deg = _make_dspin(0.001, 90.0, 3,
|
||||||
self.spin_step_rotate_deg.setRange(0.001, 90.0)
|
suffix=" °", value=0.1)
|
||||||
self.spin_step_rotate_deg.setDecimals(3)
|
|
||||||
self.spin_step_rotate_deg.setSuffix(" °")
|
|
||||||
self.spin_step_rotate_deg.setValue(0.1)
|
|
||||||
self.spin_step_rotate_deg.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
sform.addRow("Rotate step:", self.spin_step_rotate_deg)
|
sform.addRow("Rotate step:", self.spin_step_rotate_deg)
|
||||||
|
|
||||||
self.spin_step_multiplier = QDoubleSpinBox()
|
self.spin_step_multiplier = _make_dspin(1.0, 1000.0, 1, value=10.0)
|
||||||
self.spin_step_multiplier.setRange(1.0, 1000.0)
|
|
||||||
self.spin_step_multiplier.setDecimals(1)
|
|
||||||
self.spin_step_multiplier.setValue(10.0)
|
|
||||||
self.spin_step_multiplier.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
sform.addRow("Coarse × (Shift):", self.spin_step_multiplier)
|
sform.addRow("Coarse × (Shift):", self.spin_step_multiplier)
|
||||||
sl.addLayout(sform)
|
sl.addLayout(sform)
|
||||||
sl.addWidget(_wrap_label(
|
sl.addWidget(_wrap_label(
|
||||||
"Arrow keys nudge X/Y translation; Q/E nudge rotation (CCW/CW). "
|
"Arrow keys nudge X/Y translation; Q/E nudge rotation (CCW/CW). "
|
||||||
"Hold Shift for the coarse step. Click the image once so it has "
|
"Hold Shift for the coarse step. Click the image once so it has "
|
||||||
"keyboard focus.", _CSS_HINT))
|
"keyboard focus.", _CSS_HINT))
|
||||||
panel_l.addWidget(self.grp_step_sizes)
|
return self.grp_step_sizes
|
||||||
|
|
||||||
# ---- Mask Threshold ---------------------------------------------------
|
def _build_threshold_group(self, dc_threshold_mv: float) -> QWidget:
|
||||||
self.grp_mask_threshold, tl = _group("Mask Threshold")
|
self.grp_mask_threshold, tl = _group("Mask Threshold")
|
||||||
tform = _form()
|
tform = _form()
|
||||||
self.spin_mask_threshold_mv = QDoubleSpinBox()
|
self.spin_mask_threshold_mv = _make_dspin(-500.0, 500.0, 3,
|
||||||
self.spin_mask_threshold_mv.setRange(-500.0, 500.0)
|
suffix=" mV", value=dc_threshold_mv)
|
||||||
self.spin_mask_threshold_mv.setDecimals(3)
|
|
||||||
self.spin_mask_threshold_mv.setSuffix(" mV")
|
|
||||||
self.spin_mask_threshold_mv.setValue(dc_threshold_mv)
|
|
||||||
self.spin_mask_threshold_mv.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
tform.addRow("DC threshold:", self.spin_mask_threshold_mv)
|
tform.addRow("DC threshold:", self.spin_mask_threshold_mv)
|
||||||
tl.addLayout(tform)
|
tl.addLayout(tform)
|
||||||
panel_l.addWidget(self.grp_mask_threshold)
|
return self.grp_mask_threshold
|
||||||
|
|
||||||
# ---- Cross-Correlate (FFT) -----------------------------------------
|
def _build_correlate_group(self) -> QWidget:
|
||||||
self.grp_correlate, cl = _group("Cross-Correlate (FFT)")
|
self.grp_correlate, cl = _group("Cross-Correlate (FFT)")
|
||||||
cform = _form()
|
cform = _form()
|
||||||
self.combo_correlate_source = QComboBox()
|
self.combo_correlate_source = QComboBox()
|
||||||
@@ -361,13 +345,8 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
self.combo_correlate_source.addItem(label, sources)
|
self.combo_correlate_source.addItem(label, sources)
|
||||||
cform.addRow("Correlate on:", self.combo_correlate_source)
|
cform.addRow("Correlate on:", self.combo_correlate_source)
|
||||||
|
|
||||||
self.spin_correlate_search_deg = QDoubleSpinBox()
|
self.spin_correlate_search_deg = _make_dspin(0.0, 180.0, 1, suffix=" °",
|
||||||
self.spin_correlate_search_deg.setRange(0.0, 180.0)
|
value=6.0, step=1.0)
|
||||||
self.spin_correlate_search_deg.setSingleStep(1.0)
|
|
||||||
self.spin_correlate_search_deg.setDecimals(1)
|
|
||||||
self.spin_correlate_search_deg.setSuffix(" °")
|
|
||||||
self.spin_correlate_search_deg.setValue(6.0)
|
|
||||||
self.spin_correlate_search_deg.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
cform.addRow("Rotation search (±):", self.spin_correlate_search_deg)
|
cform.addRow("Rotation search (±):", self.spin_correlate_search_deg)
|
||||||
cl.addLayout(cform)
|
cl.addLayout(cform)
|
||||||
self.btn_auto_correlate = QPushButton("Auto Cross-Correlate (vs Reference)")
|
self.btn_auto_correlate = QPushButton("Auto Cross-Correlate (vs Reference)")
|
||||||
@@ -378,9 +357,9 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
"reported angle is only the starting point of the search, and both "
|
"reported angle is only the starting point of the search, and both "
|
||||||
"of its signs are tried. Run this first, then nudge only for small "
|
"of its signs are tried. Run this first, then nudge only for small "
|
||||||
"corrections.", _CSS_HINT))
|
"corrections.", _CSS_HINT))
|
||||||
panel_l.addWidget(self.grp_correlate)
|
return self.grp_correlate
|
||||||
|
|
||||||
# ---- Actions ------------------------------------------------------
|
def _build_actions_group(self) -> QWidget:
|
||||||
grp_actions, acl = _group("Actions")
|
grp_actions, acl = _group("Actions")
|
||||||
self.btn_auto_derotate = QPushButton("Auto De-rotate (use known angles)")
|
self.btn_auto_derotate = QPushButton("Auto De-rotate (use known angles)")
|
||||||
self.btn_save = QPushButton("Save Alignment")
|
self.btn_save = QPushButton("Save Alignment")
|
||||||
@@ -388,14 +367,9 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
self.btn_close = QPushButton("Close")
|
self.btn_close = QPushButton("Close")
|
||||||
for btn in (self.btn_auto_derotate, self.btn_save, self.btn_clear, self.btn_close):
|
for btn in (self.btn_auto_derotate, self.btn_save, self.btn_clear, self.btn_close):
|
||||||
acl.addWidget(btn)
|
acl.addWidget(btn)
|
||||||
panel_l.addWidget(grp_actions)
|
return grp_actions
|
||||||
|
|
||||||
self.lbl_status = _wrap_label("", _CSS_MUTED)
|
|
||||||
panel_l.addWidget(self.lbl_status)
|
|
||||||
panel_l.addStretch()
|
|
||||||
|
|
||||||
root.addWidget(_scroll_panel(panel, 320))
|
|
||||||
|
|
||||||
|
def _connect_controls(self):
|
||||||
self.combo_active_angle.currentIndexChanged.connect(self._on_active_angle_changed)
|
self.combo_active_angle.currentIndexChanged.connect(self._on_active_angle_changed)
|
||||||
self.spin_active_rotation_deg.editingFinished.connect(self._on_rotation_spin_edited)
|
self.spin_active_rotation_deg.editingFinished.connect(self._on_rotation_spin_edited)
|
||||||
self.spin_active_shift_x_mm.editingFinished.connect(self._on_shift_spin_edited)
|
self.spin_active_shift_x_mm.editingFinished.connect(self._on_shift_spin_edited)
|
||||||
@@ -409,9 +383,8 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
self.canvas.nudge_translate.connect(self._on_nudge_translate)
|
self.canvas.nudge_translate.connect(self._on_nudge_translate)
|
||||||
self.canvas.nudge_rotate.connect(self._on_nudge_rotate)
|
self.canvas.nudge_rotate.connect(self._on_nudge_rotate)
|
||||||
|
|
||||||
self.combo_active_angle.blockSignals(True)
|
with QSignalBlocker(self.combo_active_angle):
|
||||||
self.combo_active_angle.setCurrentIndex(self._active_angle)
|
self.combo_active_angle.setCurrentIndex(self._active_angle)
|
||||||
self.combo_active_angle.blockSignals(False)
|
|
||||||
self._on_active_angle_changed(self._active_angle)
|
self._on_active_angle_changed(self._active_angle)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -426,7 +399,7 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
return
|
return
|
||||||
self.lbl_status.setText(f"Preparing masks: 0/{len(missing)} angle(s) needed…")
|
self.lbl_status.setText(f"Preparing masks: 0/{len(missing)} angle(s) needed…")
|
||||||
started = self._parent._run_worker(
|
started = self._parent._run_worker(
|
||||||
"manual_align_masks", Ch4MaskWorker(self._sras, missing),
|
Jobs.MANUAL_ALIGN_MASKS, Ch4MaskWorker(self._sras, missing),
|
||||||
connect=(
|
connect=(
|
||||||
("angle_done", self._on_mask_angle_done),
|
("angle_done", self._on_mask_angle_done),
|
||||||
("error", lambda msg: self.lbl_status.setText(f"Mask prep error: {msg}")),
|
("error", lambda msg: self.lbl_status.setText(f"Mask prep error: {msg}")),
|
||||||
@@ -541,8 +514,7 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
dx, dy = self._preview_pitch_mm
|
dx, dy = self._preview_pitch_mm
|
||||||
x_axis = x0 + np.arange(n_cols) * dx
|
x_axis = x0 + np.arange(n_cols) * dx
|
||||||
y_axis = y0 + np.arange(n_rows) * dy
|
y_axis = y0 + np.arange(n_rows) * dy
|
||||||
extent = [x_axis[0] - dx / 2, x_axis[-1] + dx / 2,
|
extent = _axes_extent(x_axis, y_axis, dx, dy)
|
||||||
y_axis[-1] + dy / 2, y_axis[0] - dy / 2]
|
|
||||||
title = (f"Angle {self._active_angle} active "
|
title = (f"Angle {self._active_angle} active "
|
||||||
f"({self._sras.angles_deg[self._active_angle]:.1f}°)")
|
f"({self._sras.angles_deg[self._active_angle]:.1f}°)")
|
||||||
self.canvas.show_overlay(rgba, extent, title)
|
self.canvas.show_overlay(rgba, extent, title)
|
||||||
@@ -565,9 +537,8 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
for spin, val in ((self.spin_active_rotation_deg, p.rotation_deg),
|
for spin, val in ((self.spin_active_rotation_deg, p.rotation_deg),
|
||||||
(self.spin_active_shift_x_mm, p.shift_mm[0]),
|
(self.spin_active_shift_x_mm, p.shift_mm[0]),
|
||||||
(self.spin_active_shift_y_mm, p.shift_mm[1])):
|
(self.spin_active_shift_y_mm, p.shift_mm[1])):
|
||||||
spin.blockSignals(True)
|
with QSignalBlocker(spin):
|
||||||
spin.setValue(val)
|
spin.setValue(val)
|
||||||
spin.blockSignals(False)
|
|
||||||
|
|
||||||
def _on_nudge_translate(self, dir_x: int, dir_y: int, coarse: bool):
|
def _on_nudge_translate(self, dir_x: int, dir_y: int, coarse: bool):
|
||||||
if not self._masks_ready or self._active_angle == self._ref_angle_idx:
|
if not self._masks_ready or self._active_angle == self._ref_angle_idx:
|
||||||
@@ -655,7 +626,7 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
self._set_controls_enabled(False)
|
self._set_controls_enabled(False)
|
||||||
self.lbl_status.setText(f"Cross-correlating: 0/{self._correlate_total} angle(s)…")
|
self.lbl_status.setText(f"Cross-correlating: 0/{self._correlate_total} angle(s)…")
|
||||||
started = self._parent._run_worker(
|
started = self._parent._run_worker(
|
||||||
"manual_align_correlate", worker,
|
Jobs.MANUAL_ALIGN_CORRELATE, worker,
|
||||||
connect=(
|
connect=(
|
||||||
("angle_done", self._on_correlate_angle_done),
|
("angle_done", self._on_correlate_angle_done),
|
||||||
("error", self._on_correlate_error),
|
("error", self._on_correlate_error),
|
||||||
|
|||||||
+78
-99
@@ -5,11 +5,11 @@ from pathlib import Path
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT
|
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT
|
||||||
from PyQt6.QtCore import QObject, QSettings, Qt, QThread
|
from PyQt6.QtCore import QObject, QSettings, QSignalBlocker, Qt, QThread
|
||||||
from PyQt6.QtGui import QAction
|
from PyQt6.QtGui import QAction
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QFileDialog,
|
QApplication, QCheckBox, QComboBox, QDialog, QFileDialog, QFrame,
|
||||||
QFrame, QHBoxLayout, QLabel, QMainWindow, QProgressDialog, QPushButton,
|
QHBoxLayout, QLabel, QMainWindow, QProgressDialog, QPushButton,
|
||||||
QSizePolicy, QSpinBox, QSplitter, QVBoxLayout, QWidget,
|
QSizePolicy, QSpinBox, QSplitter, QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ from sras_workers import (
|
|||||||
from .canvases import ImageCanvas, WaveformCanvas
|
from .canvases import ImageCanvas, WaveformCanvas
|
||||||
from .common import (
|
from .common import (
|
||||||
CH1_DERIVED_MODES, CH_LABELS, CMAPS, VELOCITY_MODE_IDX, _CHANNEL_DISPLAY,
|
CH1_DERIVED_MODES, CH_LABELS, CMAPS, VELOCITY_MODE_IDX, _CHANNEL_DISPLAY,
|
||||||
_CSS_BUSY, _CSS_HINT, _CSS_INFO, _CSS_MUTED, _CSS_WARN, _LEFT_PANEL_W,
|
_CSS_BUSY, _axes_extent, _CSS_HINT, _CSS_INFO, _CSS_MUTED, _CSS_WARN, _LEFT_PANEL_W,
|
||||||
_RIGHT_PANEL_W, _SPIN_MIN_W, _form, _group, _scroll_panel, _wrap_label,
|
_RIGHT_PANEL_W, Jobs, _form, _group, _make_dspin, _scroll_panel, _wrap_label,
|
||||||
)
|
)
|
||||||
from .dialogs import FftOptionsDialog, ManualAlignmentDialog
|
from .dialogs import FftOptionsDialog, ManualAlignmentDialog
|
||||||
|
|
||||||
@@ -175,17 +175,23 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
panel_layout = QVBoxLayout(panel)
|
panel_layout = QVBoxLayout(panel)
|
||||||
panel_layout.setContentsMargins(0, 0, 0, 0)
|
panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
panel_layout.setSpacing(8)
|
panel_layout.setSpacing(8)
|
||||||
|
panel_layout.addWidget(self._build_file_group())
|
||||||
|
panel_layout.addWidget(self._build_info_group())
|
||||||
|
panel_layout.addWidget(self._build_view_group())
|
||||||
|
panel_layout.addWidget(self._build_roi_group())
|
||||||
|
panel_layout.addStretch()
|
||||||
|
return _scroll_panel(panel, _LEFT_PANEL_W)
|
||||||
|
|
||||||
# ---- File -------------------------------------------------------
|
def _build_file_group(self) -> QWidget:
|
||||||
grp_file, fl = _group("File")
|
grp_file, fl = _group("File")
|
||||||
self.btn_open = QPushButton("Open .sras…")
|
self.btn_open = QPushButton("Open .sras…")
|
||||||
self.btn_open.clicked.connect(self._on_open)
|
self.btn_open.clicked.connect(self._on_open)
|
||||||
self.lbl_filename = _wrap_label("No file loaded", _CSS_MUTED)
|
self.lbl_filename = _wrap_label("No file loaded", _CSS_MUTED)
|
||||||
fl.addWidget(self.btn_open)
|
fl.addWidget(self.btn_open)
|
||||||
fl.addWidget(self.lbl_filename)
|
fl.addWidget(self.lbl_filename)
|
||||||
panel_layout.addWidget(grp_file)
|
return grp_file
|
||||||
|
|
||||||
# ---- Scan info --------------------------------------------------
|
def _build_info_group(self) -> QWidget:
|
||||||
grp_info, il = _group("Scan Info")
|
grp_info, il = _group("Scan Info")
|
||||||
il.setSpacing(3)
|
il.setSpacing(3)
|
||||||
self._info = {}
|
self._info = {}
|
||||||
@@ -202,9 +208,9 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# background DC-precompute progress
|
# background DC-precompute progress
|
||||||
self.lbl_dc_precompute = _wrap_label("", _CSS_BUSY)
|
self.lbl_dc_precompute = _wrap_label("", _CSS_BUSY)
|
||||||
il.addWidget(self.lbl_dc_precompute)
|
il.addWidget(self.lbl_dc_precompute)
|
||||||
panel_layout.addWidget(grp_info)
|
return grp_info
|
||||||
|
|
||||||
# ---- View settings ----------------------------------------------
|
def _build_view_group(self) -> QWidget:
|
||||||
grp_view, vl = _group("View Settings")
|
grp_view, vl = _group("View Settings")
|
||||||
|
|
||||||
view_form = _form()
|
view_form = _form()
|
||||||
@@ -244,14 +250,9 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# DC threshold (for RF / CH1 masking)
|
# DC threshold (for RF / CH1 masking)
|
||||||
self.grp_threshold, tl = _group("RF Mask Threshold (CH1 only)")
|
self.grp_threshold, tl = _group("RF Mask Threshold (CH1 only)")
|
||||||
thr_form = _form()
|
thr_form = _form()
|
||||||
self.spin_threshold_mv = QDoubleSpinBox()
|
self.spin_threshold_mv = _make_dspin(-500.0, 500.0, 3, suffix=" mV",
|
||||||
self.spin_threshold_mv.setRange(-500.0, 500.0)
|
value=50.0, step=0.025)
|
||||||
self.spin_threshold_mv.setDecimals(3)
|
|
||||||
self.spin_threshold_mv.setSingleStep(0.025)
|
|
||||||
self.spin_threshold_mv.setSuffix(" mV")
|
|
||||||
self.spin_threshold_mv.setValue(50.0)
|
|
||||||
self.spin_threshold_mv.setEnabled(False)
|
self.spin_threshold_mv.setEnabled(False)
|
||||||
self.spin_threshold_mv.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
self.spin_threshold_mv.editingFinished.connect(self._on_threshold_changed)
|
self.spin_threshold_mv.editingFinished.connect(self._on_threshold_changed)
|
||||||
thr_form.addRow("DC threshold:", self.spin_threshold_mv)
|
thr_form.addRow("DC threshold:", self.spin_threshold_mv)
|
||||||
tl.addLayout(thr_form)
|
tl.addLayout(thr_form)
|
||||||
@@ -289,10 +290,9 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
"Save the current CH1 image (one scan row per CSV line).")
|
"Save the current CH1 image (one scan row per CSV line).")
|
||||||
self.btn_export_csv.clicked.connect(self._on_export_csv)
|
self.btn_export_csv.clicked.connect(self._on_export_csv)
|
||||||
vl.addWidget(self.btn_export_csv)
|
vl.addWidget(self.btn_export_csv)
|
||||||
|
return grp_view
|
||||||
|
|
||||||
panel_layout.addWidget(grp_view)
|
def _build_roi_group(self) -> QWidget:
|
||||||
|
|
||||||
# ---- ROI ---------------------------------------------------------
|
|
||||||
grp_roi, rl = _group("ROI (Region of Interest)")
|
grp_roi, rl = _group("ROI (Region of Interest)")
|
||||||
|
|
||||||
self.btn_draw_roi = QPushButton("Draw ROI")
|
self.btn_draw_roi = QPushButton("Draw ROI")
|
||||||
@@ -327,10 +327,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
self.lbl_roi_npix = _wrap_label("pixels inside: —", _CSS_HINT)
|
self.lbl_roi_npix = _wrap_label("pixels inside: —", _CSS_HINT)
|
||||||
for lbl in (self.lbl_roi_center, self.lbl_roi_size, self.lbl_roi_npix):
|
for lbl in (self.lbl_roi_center, self.lbl_roi_size, self.lbl_roi_npix):
|
||||||
rl.addWidget(lbl)
|
rl.addWidget(lbl)
|
||||||
|
return grp_roi
|
||||||
panel_layout.addWidget(grp_roi)
|
|
||||||
panel_layout.addStretch()
|
|
||||||
return _scroll_panel(panel, _LEFT_PANEL_W)
|
|
||||||
|
|
||||||
def _build_canvases(self) -> QWidget:
|
def _build_canvases(self) -> QWidget:
|
||||||
splitter = QSplitter(Qt.Orientation.Vertical)
|
splitter = QSplitter(Qt.Orientation.Vertical)
|
||||||
@@ -372,14 +369,9 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# Velocity settings (visible only in velocity mode)
|
# Velocity settings (visible only in velocity mode)
|
||||||
self.grp_velocity, vel_l = _group("Velocity Settings (CH1 only)")
|
self.grp_velocity, vel_l = _group("Velocity Settings (CH1 only)")
|
||||||
vel_form = _form()
|
vel_form = _form()
|
||||||
self.spin_grating_um = QDoubleSpinBox()
|
self.spin_grating_um = _make_dspin(0.1, 1000.0, 2, suffix=" µm",
|
||||||
self.spin_grating_um.setRange(0.1, 1000.0)
|
value=25, step=0.5)
|
||||||
self.spin_grating_um.setDecimals(2)
|
|
||||||
self.spin_grating_um.setSingleStep(0.5)
|
|
||||||
self.spin_grating_um.setSuffix(" µm")
|
|
||||||
self.spin_grating_um.setValue(25)
|
|
||||||
self.spin_grating_um.setEnabled(False)
|
self.spin_grating_um.setEnabled(False)
|
||||||
self.spin_grating_um.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
self.spin_grating_um.editingFinished.connect(self._on_grating_changed)
|
self.spin_grating_um.editingFinished.connect(self._on_grating_changed)
|
||||||
vel_form.addRow("Grating size:", self.spin_grating_um)
|
vel_form.addRow("Grating size:", self.spin_grating_um)
|
||||||
vel_l.addLayout(vel_form)
|
vel_l.addLayout(vel_form)
|
||||||
@@ -407,11 +399,8 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
|
|
||||||
range_form = _form()
|
range_form = _form()
|
||||||
for label, attr in (("min:", "spin_vmin"), ("max:", "spin_vmax")):
|
for label, attr in (("min:", "spin_vmin"), ("max:", "spin_vmax")):
|
||||||
spin = QDoubleSpinBox()
|
spin = _make_dspin(-1e9, 1e9, 4)
|
||||||
spin.setRange(-1e9, 1e9)
|
|
||||||
spin.setDecimals(4)
|
|
||||||
spin.setEnabled(False)
|
spin.setEnabled(False)
|
||||||
spin.setMinimumWidth(_SPIN_MIN_W)
|
|
||||||
spin.editingFinished.connect(self._on_manual_range_changed)
|
spin.editingFinished.connect(self._on_manual_range_changed)
|
||||||
setattr(self, attr, spin)
|
setattr(self, attr, spin)
|
||||||
range_form.addRow(label, spin)
|
range_form.addRow(label, spin)
|
||||||
@@ -494,7 +483,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
|
|
||||||
def _load_file(self, path: str):
|
def _load_file(self, path: str):
|
||||||
started = self._run_worker(
|
started = self._run_worker(
|
||||||
"load", LoadWorker(path),
|
Jobs.LOAD, LoadWorker(path),
|
||||||
connect=(
|
connect=(
|
||||||
("finished", self._on_load_done),
|
("finished", self._on_load_done),
|
||||||
("error", lambda msg: self.statusBar().showMessage(f"Error: {msg}")),
|
("error", lambda msg: self.statusBar().showMessage(f"Error: {msg}")),
|
||||||
@@ -530,25 +519,18 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
self._dc_generation += 1
|
self._dc_generation += 1
|
||||||
self.lbl_dc_precompute.setText("")
|
self.lbl_dc_precompute.setText("")
|
||||||
|
|
||||||
self._alignment_result = None
|
self._apply_alignment_result(None, view_checked=False)
|
||||||
self._aligned_cache = {}
|
|
||||||
self._alignment_generation += 1
|
|
||||||
self.chk_aligned_view.blockSignals(True)
|
|
||||||
self.chk_aligned_view.setChecked(False)
|
|
||||||
self.chk_aligned_view.setEnabled(False)
|
|
||||||
self.chk_aligned_view.blockSignals(False)
|
|
||||||
|
|
||||||
# Silently restore a previously-saved manual alignment, if any, so
|
# Silently restore a previously-saved manual alignment, if any, so
|
||||||
# the work survives closing and reopening the file.
|
# the work survives closing and reopening the file.
|
||||||
sidecar = load_manual_alignment(sras)
|
sidecar = load_manual_alignment(sras)
|
||||||
if sidecar is not None:
|
if sidecar is not None:
|
||||||
try:
|
try:
|
||||||
self._alignment_result = build_manual_alignment(
|
self._apply_alignment_result(
|
||||||
|
build_manual_alignment(
|
||||||
sras, sidecar.ref_angle_idx, sidecar.dc_threshold_mv,
|
sras, sidecar.ref_angle_idx, sidecar.dc_threshold_mv,
|
||||||
sidecar.per_angle)
|
sidecar.per_angle),
|
||||||
self.chk_aligned_view.blockSignals(True)
|
view_checked=True)
|
||||||
self.chk_aligned_view.setChecked(True)
|
|
||||||
self.chk_aligned_view.blockSignals(False)
|
|
||||||
self.statusBar().showMessage(
|
self.statusBar().showMessage(
|
||||||
f"Restored saved manual alignment from "
|
f"Restored saved manual alignment from "
|
||||||
f"{sidecar_path(sras.path).name}")
|
f"{sidecar_path(sras.path).name}")
|
||||||
@@ -564,17 +546,15 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
|
|
||||||
self.lbl_filename.setText(sras.path.name)
|
self.lbl_filename.setText(sras.path.name)
|
||||||
|
|
||||||
self.spin_angle.blockSignals(True)
|
with QSignalBlocker(self.spin_angle):
|
||||||
self.spin_angle.setRange(0, max(0, sras.n_angles - 1))
|
self.spin_angle.setRange(0, max(0, sras.n_angles - 1))
|
||||||
self.spin_angle.setValue(0)
|
self.spin_angle.setValue(0)
|
||||||
self.spin_angle.blockSignals(False)
|
|
||||||
|
|
||||||
# DC channels are cheap and give an instant, fluid overview of a
|
# DC channels are cheap and give an instant, fluid overview of a
|
||||||
# scan; CH1/Velocity require an FFT per pixel that can take minutes
|
# scan; CH1/Velocity require an FFT per pixel that can take minutes
|
||||||
# on a large scan, so don't default to it.
|
# on a large scan, so don't default to it.
|
||||||
self.combo_channel.blockSignals(True)
|
with QSignalBlocker(self.combo_channel):
|
||||||
self.combo_channel.setCurrentIndex(CH4_IDX)
|
self.combo_channel.setCurrentIndex(CH4_IDX)
|
||||||
self.combo_channel.blockSignals(False)
|
|
||||||
|
|
||||||
self._update_controls_enabled(True)
|
self._update_controls_enabled(True)
|
||||||
self._on_threshold_changed() # refresh ADC label with file calibration
|
self._on_threshold_changed() # refresh ADC label with file calibration
|
||||||
@@ -657,14 +637,14 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
|
|
||||||
# Batch Convert actions pick their own files, independent of
|
# Batch Convert actions pick their own files, independent of
|
||||||
# whatever's currently open — only gated on no batch already running.
|
# whatever's currently open — only gated on no batch already running.
|
||||||
can_batch = not self._job_running("batch")
|
can_batch = not self._job_running(Jobs.BATCH)
|
||||||
self._batch_dc_act.setEnabled(can_batch)
|
self._batch_dc_act.setEnabled(can_batch)
|
||||||
self._batch_fft_act.setEnabled(can_batch)
|
self._batch_fft_act.setEnabled(can_batch)
|
||||||
|
|
||||||
self._alignment_act.setEnabled(
|
self._alignment_act.setEnabled(
|
||||||
has_file and s.n_angles > 1 and not self._job_running("align"))
|
has_file and s.n_angles > 1 and not self._job_running(Jobs.ALIGN))
|
||||||
self._manual_align_act.setEnabled(
|
self._manual_align_act.setEnabled(
|
||||||
has_file and s.n_angles > 1 and not self._job_running("align"))
|
has_file and s.n_angles > 1 and not self._job_running(Jobs.ALIGN))
|
||||||
self.chk_aligned_view.setEnabled(enabled and self._alignment_result is not None)
|
self.chk_aligned_view.setEnabled(enabled and self._alignment_result is not None)
|
||||||
self._update_roi_ui()
|
self._update_roi_ui()
|
||||||
|
|
||||||
@@ -676,7 +656,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# Background subtraction changes the FFT input, so it genuinely
|
# Background subtraction changes the FFT input, so it genuinely
|
||||||
# invalidates the cached raw FFT (the cache key includes it) —
|
# invalidates the cached raw FFT (the cache key includes it) —
|
||||||
# _refresh_display() recomputes only on a miss for the new state.
|
# _refresh_display() recomputes only on a miss for the new state.
|
||||||
if self._sras is not None and self.combo_channel.currentIndex() in CH1_DERIVED_MODES:
|
if self._is_fft_mode():
|
||||||
self._refresh_display()
|
self._refresh_display()
|
||||||
|
|
||||||
def _on_grating_changed(self):
|
def _on_grating_changed(self):
|
||||||
@@ -693,7 +673,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# Threshold decides which pixels get an FFT at all, so changing it is
|
# Threshold decides which pixels get an FFT at all, so changing it is
|
||||||
# a genuine cache-key change — but the recompute reuses the cached DC4
|
# a genuine cache-key change — but the recompute reuses the cached DC4
|
||||||
# image to skip masked-out pixels.
|
# image to skip masked-out pixels.
|
||||||
if self._sras is not None and self.combo_channel.currentIndex() in CH1_DERIVED_MODES:
|
if self._is_fft_mode():
|
||||||
self._refresh_display()
|
self._refresh_display()
|
||||||
|
|
||||||
def _on_autoscale_toggled(self, checked: bool):
|
def _on_autoscale_toggled(self, checked: bool):
|
||||||
@@ -810,9 +790,8 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
|
|
||||||
def _on_draw_mode_changed(self, active: bool):
|
def _on_draw_mode_changed(self, active: bool):
|
||||||
# Keep the toggle button's visual state in sync with the canvas.
|
# Keep the toggle button's visual state in sync with the canvas.
|
||||||
self.btn_draw_roi.blockSignals(True)
|
with QSignalBlocker(self.btn_draw_roi):
|
||||||
self.btn_draw_roi.setChecked(active)
|
self.btn_draw_roi.setChecked(active)
|
||||||
self.btn_draw_roi.blockSignals(False)
|
|
||||||
|
|
||||||
def _on_clear_roi(self):
|
def _on_clear_roi(self):
|
||||||
self.image_canvas.clear_roi()
|
self.image_canvas.clear_roi()
|
||||||
@@ -859,6 +838,11 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
return None
|
return None
|
||||||
return self._sras.samples_per_frame * self._fft_pad_factor
|
return self._sras.samples_per_frame * self._fft_pad_factor
|
||||||
|
|
||||||
|
def _is_fft_mode(self) -> bool:
|
||||||
|
"""Is the selected channel an FFT-derived (CH1/Velocity) mode?"""
|
||||||
|
return (self._sras is not None
|
||||||
|
and self.combo_channel.currentIndex() in CH1_DERIVED_MODES)
|
||||||
|
|
||||||
def _scale_for_display(self, freq_mhz: np.ndarray, ch_idx: int) -> np.ndarray:
|
def _scale_for_display(self, freq_mhz: np.ndarray, ch_idx: int) -> np.ndarray:
|
||||||
"""Velocity is a pure post-multiply of the (already DC-masked)
|
"""Velocity is a pure post-multiply of the (already DC-masked)
|
||||||
cached frequency image — never worth a recompute on its own."""
|
cached frequency image — never worth a recompute on its own."""
|
||||||
@@ -947,15 +931,13 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
|
|
||||||
dx = x_axis[1] - x_axis[0] if len(x_axis) > 1 else s.pixel_x_mm
|
dx = x_axis[1] - x_axis[0] if len(x_axis) > 1 else s.pixel_x_mm
|
||||||
dy = float(y_axis[1] - y_axis[0]) if len(y_axis) > 1 else 1.0
|
dy = float(y_axis[1] - y_axis[0]) if len(y_axis) > 1 else 1.0
|
||||||
extent = [x_axis[0] - dx / 2, x_axis[-1] + dx / 2,
|
extent = _axes_extent(x_axis, y_axis, dx, dy)
|
||||||
y_axis[-1] + dy / 2, y_axis[0] - dy / 2]
|
|
||||||
|
|
||||||
if self.chk_auto.isChecked():
|
if self.chk_auto.isChecked():
|
||||||
vmin, vmax = float(display_img.min()), float(display_img.max())
|
vmin, vmax = float(display_img.min()), float(display_img.max())
|
||||||
for spin, val in ((self.spin_vmin, vmin), (self.spin_vmax, vmax)):
|
for spin, val in ((self.spin_vmin, vmin), (self.spin_vmax, vmax)):
|
||||||
spin.blockSignals(True)
|
with QSignalBlocker(spin):
|
||||||
spin.setValue(val)
|
spin.setValue(val)
|
||||||
spin.blockSignals(False)
|
|
||||||
else:
|
else:
|
||||||
vmin, vmax = self.spin_vmin.value(), self.spin_vmax.value()
|
vmin, vmax = self.spin_vmin.value(), self.spin_vmax.value()
|
||||||
|
|
||||||
@@ -988,7 +970,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _start_compute(self):
|
def _start_compute(self):
|
||||||
if self._sras is None or self._job_running("compute"):
|
if self._sras is None or self._job_running(Jobs.COMPUTE):
|
||||||
return # re-checked when the running compute finishes
|
return # re-checked when the running compute finishes
|
||||||
|
|
||||||
angle_idx = self.spin_angle.value()
|
angle_idx = self.spin_angle.value()
|
||||||
@@ -1013,7 +995,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
is_fft_mode=is_fft,
|
is_fft_mode=is_fft,
|
||||||
)
|
)
|
||||||
if not self._run_worker(
|
if not self._run_worker(
|
||||||
"compute", worker,
|
Jobs.COMPUTE, worker,
|
||||||
connect=(
|
connect=(
|
||||||
("finished", self._on_compute_done),
|
("finished", self._on_compute_done),
|
||||||
("error", lambda msg: self.statusBar().showMessage(
|
("error", lambda msg: self.statusBar().showMessage(
|
||||||
@@ -1073,7 +1055,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
|
|
||||||
worker = DcPrecomputeWorker(self._sras)
|
worker = DcPrecomputeWorker(self._sras)
|
||||||
self._run_worker(
|
self._run_worker(
|
||||||
"dc_precompute", worker,
|
Jobs.DC_PRECOMPUTE, worker,
|
||||||
connect=(
|
connect=(
|
||||||
("angle_done", lambda a, dc3, dc4, g=generation:
|
("angle_done", lambda a, dc3, dc4, g=generation:
|
||||||
self._on_dc_precompute_angle_done(g, a, dc3, dc4, n_angles)),
|
self._on_dc_precompute_angle_done(g, a, dc3, dc4, n_angles)),
|
||||||
@@ -1101,7 +1083,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# caught up and are still waiting), show it now.
|
# caught up and are still waiting), show it now.
|
||||||
current_ch = self.combo_channel.currentIndex()
|
current_ch = self.combo_channel.currentIndex()
|
||||||
if (angle_idx == self.spin_angle.value()
|
if (angle_idx == self.spin_angle.value()
|
||||||
and not self._job_running("compute")
|
and not self._job_running(Jobs.COMPUTE)
|
||||||
and current_ch in (CH3_IDX, CH4_IDX)
|
and current_ch in (CH3_IDX, CH4_IDX)
|
||||||
and (self._current_angle != angle_idx or self._current_ch != current_ch)):
|
and (self._current_angle != angle_idx or self._current_ch != current_ch)):
|
||||||
self._refresh_display()
|
self._refresh_display()
|
||||||
@@ -1171,7 +1153,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _on_batch_compute(self, mode: str):
|
def _on_batch_compute(self, mode: str):
|
||||||
if self._job_running("batch"):
|
if self._job_running(Jobs.BATCH):
|
||||||
return
|
return
|
||||||
label = "DC" if mode == "dc" else "FFT"
|
label = "DC" if mode == "dc" else "FFT"
|
||||||
paths, _ = QFileDialog.getOpenFileNames(
|
paths, _ = QFileDialog.getOpenFileNames(
|
||||||
@@ -1183,9 +1165,9 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
self._batch_errors = []
|
self._batch_errors = []
|
||||||
worker = BatchCacheWorker(paths, mode, self.chk_bg_sub.isChecked())
|
worker = BatchCacheWorker(paths, mode, self.chk_bg_sub.isChecked())
|
||||||
started = self._run_worker(
|
started = self._run_worker(
|
||||||
"batch", worker,
|
Jobs.BATCH, worker,
|
||||||
connect=(
|
connect=(
|
||||||
("progress", lambda pct: self._set_progress("batch", pct)),
|
("progress", lambda pct: self._set_progress(Jobs.BATCH, pct)),
|
||||||
("file_done", self._on_batch_file_done),
|
("file_done", self._on_batch_file_done),
|
||||||
("finished", lambda p=paths: self._on_batch_finished(p)),
|
("finished", lambda p=paths: self._on_batch_finished(p)),
|
||||||
),
|
),
|
||||||
@@ -1197,16 +1179,16 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
self._batch_dc_act.setEnabled(False)
|
self._batch_dc_act.setEnabled(False)
|
||||||
self._batch_fft_act.setEnabled(False)
|
self._batch_fft_act.setEnabled(False)
|
||||||
self._show_progress(
|
self._show_progress(
|
||||||
"batch", f"Batch computing {label} for {len(paths)} file(s)…",
|
Jobs.BATCH, f"Batch computing {label} for {len(paths)} file(s)…",
|
||||||
maximum=100)
|
maximum=100)
|
||||||
|
|
||||||
def _on_batch_file_done(self, path: str, err: str):
|
def _on_batch_file_done(self, path: str, err: str):
|
||||||
if err:
|
if err:
|
||||||
self._batch_errors.append(f"{Path(path).name} — {err}")
|
self._batch_errors.append(f"{Path(path).name} — {err}")
|
||||||
self._show_progress("batch", f"Processed {Path(path).name}…")
|
self._show_progress(Jobs.BATCH, f"Processed {Path(path).name}…")
|
||||||
|
|
||||||
def _on_batch_finished(self, paths: list[str]):
|
def _on_batch_finished(self, paths: list[str]):
|
||||||
self._close_progress("batch")
|
self._close_progress(Jobs.BATCH)
|
||||||
|
|
||||||
n_total = len(paths)
|
n_total = len(paths)
|
||||||
n_failed = len(self._batch_errors)
|
n_failed = len(self._batch_errors)
|
||||||
@@ -1240,7 +1222,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
generation = self._alignment_generation
|
generation = self._alignment_generation
|
||||||
|
|
||||||
started = self._run_worker(
|
started = self._run_worker(
|
||||||
"align", AngleAlignmentWorker(self._sras, ref_idx, threshold_mv),
|
Jobs.ALIGN, AngleAlignmentWorker(self._sras, ref_idx, threshold_mv),
|
||||||
connect=(
|
connect=(
|
||||||
("progress", lambda pct: self._set_progress("main", pct)),
|
("progress", lambda pct: self._set_progress("main", pct)),
|
||||||
("finished", lambda result, err, g=generation:
|
("finished", lambda result, err, g=generation:
|
||||||
@@ -1265,12 +1247,9 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
if error_msg:
|
if error_msg:
|
||||||
self.statusBar().showMessage(f"Angle alignment failed: {error_msg}")
|
self.statusBar().showMessage(f"Angle alignment failed: {error_msg}")
|
||||||
return
|
return
|
||||||
self._alignment_result = result
|
# No generation bump: this result *is* the current generation's.
|
||||||
self._aligned_cache = {}
|
self._apply_alignment_result(result, view_checked=True,
|
||||||
self.chk_aligned_view.setEnabled(True)
|
bump_generation=False)
|
||||||
self.chk_aligned_view.blockSignals(True)
|
|
||||||
self.chk_aligned_view.setChecked(True)
|
|
||||||
self.chk_aligned_view.blockSignals(False)
|
|
||||||
nr, nc = result.canvas_shape
|
nr, nc = result.canvas_shape
|
||||||
self.statusBar().showMessage(
|
self.statusBar().showMessage(
|
||||||
f"Angle alignment computed ({self._sras.n_angles} angles, "
|
f"Angle alignment computed ({self._sras.n_angles} angles, "
|
||||||
@@ -1320,32 +1299,32 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
def _on_manual_align_dialog_closed(self, _result_code: int):
|
def _on_manual_align_dialog_closed(self, _result_code: int):
|
||||||
self._manual_align_dialog = None
|
self._manual_align_dialog = None
|
||||||
|
|
||||||
def _on_manual_alignment_saved(self, result, sidecar_path_str: str):
|
def _apply_alignment_result(self, result, *, view_checked: bool,
|
||||||
|
bump_generation: bool = True):
|
||||||
|
"""Install (or clear, with result=None) the active alignment: reset
|
||||||
|
the aligned-image cache and set the Aligned View checkbox without
|
||||||
|
firing its change signal."""
|
||||||
self._alignment_result = result
|
self._alignment_result = result
|
||||||
self._aligned_cache = {}
|
self._aligned_cache = {}
|
||||||
|
if bump_generation:
|
||||||
self._alignment_generation += 1
|
self._alignment_generation += 1
|
||||||
self.chk_aligned_view.setEnabled(True)
|
with QSignalBlocker(self.chk_aligned_view):
|
||||||
self.chk_aligned_view.blockSignals(True)
|
self.chk_aligned_view.setChecked(view_checked)
|
||||||
self.chk_aligned_view.setChecked(True)
|
self.chk_aligned_view.setEnabled(result is not None)
|
||||||
self.chk_aligned_view.blockSignals(False)
|
|
||||||
|
def _manual_alignment_changed(self, result, message: str):
|
||||||
|
self._apply_alignment_result(result, view_checked=result is not None)
|
||||||
self._update_controls_enabled(self._sras is not None)
|
self._update_controls_enabled(self._sras is not None)
|
||||||
self.statusBar().showMessage(
|
self.statusBar().showMessage(message)
|
||||||
f"Manual alignment saved to {Path(sidecar_path_str).name}")
|
|
||||||
if self._current_image is not None:
|
if self._current_image is not None:
|
||||||
self._refresh_display()
|
self._refresh_display()
|
||||||
|
|
||||||
|
def _on_manual_alignment_saved(self, result, sidecar_path_str: str):
|
||||||
|
self._manual_alignment_changed(
|
||||||
|
result, f"Manual alignment saved to {Path(sidecar_path_str).name}")
|
||||||
|
|
||||||
def _on_manual_alignment_cleared(self):
|
def _on_manual_alignment_cleared(self):
|
||||||
self._alignment_result = None
|
self._manual_alignment_changed(None, "Manual alignment cleared.")
|
||||||
self._aligned_cache = {}
|
|
||||||
self._alignment_generation += 1
|
|
||||||
self.chk_aligned_view.blockSignals(True)
|
|
||||||
self.chk_aligned_view.setChecked(False)
|
|
||||||
self.chk_aligned_view.setEnabled(False)
|
|
||||||
self.chk_aligned_view.blockSignals(False)
|
|
||||||
self._update_controls_enabled(self._sras is not None)
|
|
||||||
self.statusBar().showMessage("Manual alignment cleared.")
|
|
||||||
if self._current_image is not None:
|
|
||||||
self._refresh_display()
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# FFT Options
|
# FFT Options
|
||||||
@@ -1369,7 +1348,7 @@ class SrasViewerWindow(QMainWindow):
|
|||||||
# Pad factor changes the FFT bin count, so it genuinely invalidates
|
# Pad factor changes the FFT bin count, so it genuinely invalidates
|
||||||
# the cached raw FFT (part of the cache key) — _refresh_display()
|
# the cached raw FFT (part of the cache key) — _refresh_display()
|
||||||
# recomputes only on a cache miss.
|
# recomputes only on a cache miss.
|
||||||
if self._sras is not None and self.combo_channel.currentIndex() in CH1_DERIVED_MODES:
|
if self._is_fft_mode():
|
||||||
self._refresh_display()
|
self._refresh_display()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user