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:
+21
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user