Stop seeding Manual Alignment translation from the automatic result

The dialog was seeding rotation_deg/shift_mm from self._alignment_result
whenever it was non-None, with no way to tell whether that result came
from a user's own manual Save or from the automatic Fusion -> Angle
Alignment run. The automatic path's translation comes from FFT phase
correlation - the exact thing manual mode exists to work around - so
opening Manual Alignment after running the automatic pass silently
inherited its unreliable shifts on top of the (correct) analytic
rotation, reproducing the same scattered-scan symptom under a "manual"
label.

Manual mode should always start from identity (each angle's centroid
already coincides with every other's, per the prior pivot fix) unless
the user previously used this same dialog to save their own deliberate
translation - which is exactly what the sidecar file already records.
Seed exclusively from load_manual_alignment() and drop the
self._alignment_result branch entirely; the automatic result is never
an appropriate seed for a manual editing session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-07-31 11:07:55 -05:00
parent 8312668c02
commit 52e91d46fd
2 changed files with 23 additions and 19 deletions
+9 -6
View File
@@ -2540,12 +2540,15 @@ class SrasViewerWindow(QMainWindow):
ref_idx = 0 ref_idx = 0
threshold_mv = self.spin_threshold_mv.value() threshold_mv = self.spin_threshold_mv.value()
seed: dict[int, ManualAngleParams] = {} seed: dict[int, ManualAngleParams] = {}
if (self._alignment_result is not None # Seed only from a previously *saved manual* alignment (this dialog's
and self._alignment_result.ref_angle_idx == ref_idx): # own Save also writes this sidecar) -- never from self._alignment_result
seed = {a: ManualAngleParams(t.rotation_deg, t.shift_mm) # when it holds the automatic Fusion -> Angle Alignment's output. That
for a, t in self._alignment_result.per_angle.items()} # path's translation comes from FFT phase correlation, which is the
threshold_mv = self._alignment_result.dc_threshold_mv # very thing manual mode exists to work around; inheriting it here
else: # would silently reintroduce the same bad translations under a
# "manual" label, on top of the (correct) analytic rotation, which is
# exactly what makes manual mode look like it "still does the same
# thing" the automatic one does.
sidecar = load_manual_alignment(self._sras) sidecar = load_manual_alignment(self._sras)
if sidecar is not None and sidecar.ref_angle_idx == ref_idx: if sidecar is not None and sidecar.ref_angle_idx == ref_idx:
seed = dict(sidecar.per_angle) seed = dict(sidecar.per_angle)
+9 -8
View File
@@ -278,19 +278,20 @@ def main():
-(float(s.angles_deg[a]) - float(s.angles_deg[0]))) -(float(s.angles_deg[a]) - float(s.angles_deg[0])))
for a in range(s.n_angles))) for a in range(s.n_angles)))
# --- Open, seeding from the still-live automatic AlignmentResult -------- # --- Open: must NOT seed from the still-live automatic AlignmentResult --
# The automatic result's translation comes from FFT phase correlation --
# the very thing manual mode exists to work around -- so manual mode
# must start from identity (centroids coincide, zero shift) regardless
# of whatever the automatic run last computed. Only a previously *saved
# manual* alignment (sidecar) should ever seed this dialog.
win._on_manual_alignment() win._on_manual_alignment()
check("dialog opened", win._manual_align_dialog is not None) check("dialog opened", win._manual_align_dialog is not None)
dlg = win._manual_align_dialog dlg = win._manual_align_dialog
check("mask prep needed no background worker (already DC-cached)", check("mask prep needed no background worker (already DC-cached)",
not win._job_running("manual_align_masks")) not win._job_running("manual_align_masks"))
r = win._alignment_result # still the automatic result from the block above check("no manual sidecar yet -> dialog starts at identity, not the "
if r is not None: "automatic result",
check("seeded rotation matches the automatic result", all(dlg._angle_params[a] == compute.ManualAngleParams()
all(dlg._angle_params[a].rotation_deg == r.per_angle[a].rotation_deg
for a in range(s.n_angles)))
check("seeded shift matches the automatic result",
all(dlg._angle_params[a].shift_mm == r.per_angle[a].shift_mm
for a in range(s.n_angles))) for a in range(s.n_angles)))
# --- Reference angle is locked ------------------------------------------- # --- Reference angle is locked -------------------------------------------