Fix alignment rotation pivot and sign convention

Rotation was pivoting on each angle's raw scan-window bbox center, which
ties alignment to wherever the window happened to sit in microscope/global
XY space rather than to the sample itself, leaving a residual orbital drift
between angles. Pivot on each angle's own CH4-threshold mask centroid
instead (its own sample footprint), for both the automatic phase-
correlation path and the manual dialog's Auto De-rotate/live preview.

Also negate the rotation used everywhere (_theta_deg): the GR stage's
reported angle increases in the opposite rotational sense from this
module's CCW math convention, so de-rotating by the raw angles_deg delta
was making misalignment worse rather than better.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-07-31 09:36:12 -05:00
parent 3c835f4592
commit ca0c736c28
3 changed files with 172 additions and 40 deletions
+21 -4
View File
@@ -860,6 +860,7 @@ class ManualAlignmentDialog(QDialog):
self._downsample_factor = 1
self._dc4_mv: dict[int, np.ndarray] = {}
self._masks_small: dict[int, np.ndarray] = {}
self._pivot_mm: dict[int, tuple[float, float]] = {}
self._preview_layers: dict[int, np.ndarray] = {}
self._preview_origin_mm = (0.0, 0.0)
self._preview_shape = (1, 1)
@@ -1063,6 +1064,7 @@ class ManualAlignmentDialog(QDialog):
max_dim = max(max(img.shape) for img in self._dc4_mv.values())
self._downsample_factor = max(1, int(np.ceil(max_dim / self._MAX_PREVIEW_DIM)))
self._recompute_masks_small()
self._recompute_pivot_mm()
self._rebuild_preview_canvas()
self._set_controls_enabled(True)
self.lbl_status.setText("Ready.")
@@ -1079,6 +1081,19 @@ class ManualAlignmentDialog(QDialog):
for a, img in self._dc4_mv.items()
}
def _recompute_pivot_mm(self):
"""Each angle's alignment pivot: the centroid of its own full-
resolution CH4-threshold mask (its own sample footprint), not the
raw scan-window bbox center — see compute.compute_pivot_points_mm.
Recomputed alongside _recompute_masks_small whenever the mask
threshold changes, from the full-res masks (not the downsampled
preview ones) since this feeds the canonical geometry, not just the
preview."""
threshold = self.spin_mask_threshold_mv.value()
masks = {a: img >= threshold for a, img in self._dc4_mv.items()}
self._pivot_mm = compute.compute_pivot_points_mm(
self._sras, threshold, masks=masks)
# ------------------------------------------------------------------
# Preview canvas: full rebuild vs. incremental single-layer refresh
# ------------------------------------------------------------------
@@ -1096,14 +1111,14 @@ class ManualAlignmentDialog(QDialog):
dx_c, dy_c = dx_ref * factor, dy_ref * factor
origin, shape = compute.union_canvas_mm(
self._sras, self._ref_angle_idx, dx_c, dy_c, self._angle_params,
margin_frac=self._PREVIEW_MARGIN_FRAC)
self._pivot_mm, margin_frac=self._PREVIEW_MARGIN_FRAC)
self._preview_origin_mm, self._preview_shape = origin, shape
self._preview_dx_mm, self._preview_dy_mm = dx_c, dy_c
self._preview_layers = {
a: compute.reproject_mask(
self._sras, a, self._ref_angle_idx, self._masks_small[a],
self._angle_params[a].rotation_deg, self._angle_params[a].shift_mm,
dx_c, dy_c, origin, shape)
dx_c, dy_c, origin, shape, self._pivot_mm)
for a in range(self._sras.n_angles)
}
self._redraw_overlay()
@@ -1117,7 +1132,7 @@ class ManualAlignmentDialog(QDialog):
self._sras, a, self._ref_angle_idx, self._masks_small[a],
self._angle_params[a].rotation_deg, self._angle_params[a].shift_mm,
self._preview_dx_mm, self._preview_dy_mm,
self._preview_origin_mm, self._preview_shape)
self._preview_origin_mm, self._preview_shape, self._pivot_mm)
self._redraw_overlay()
def _redraw_overlay(self):
@@ -1212,6 +1227,7 @@ class ManualAlignmentDialog(QDialog):
if not self._masks_ready:
return
self._recompute_masks_small()
self._recompute_pivot_mm()
self._rebuild_preview_canvas()
# ------------------------------------------------------------------
@@ -1237,7 +1253,8 @@ class ManualAlignmentDialog(QDialog):
resolved = dict(self._angle_params) # already concrete floats
try:
path = save_manual_alignment(self._sras, self._ref_angle_idx, threshold, resolved)
result = build_manual_alignment(self._sras, self._ref_angle_idx, threshold, resolved)
result = build_manual_alignment(self._sras, self._ref_angle_idx, threshold,
resolved, self._pivot_mm)
except OSError as exc:
QMessageBox.warning(self, "Save Alignment Failed", str(exc))
return