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
+29
View File
@@ -240,6 +240,35 @@ def main():
print("\nmanual alignment (Fusion)")
check("manual alignment action enabled", win._manual_align_act.isEnabled())
# --- Alignment pivot is the mask centroid, not the raw bbox center -----
corner_mask = np.zeros(s.image_shape(0), dtype=bool)
corner_mask[0, 0] = True
expected_corner = (float(s.x_axis_mm(0)[0]), float(s.y_positions_mm(0)[0]))
centroid = compute._mask_centroid_mm(s, 0, corner_mask)
check("mask centroid of a single corner pixel is that corner exactly",
np.allclose(centroid, expected_corner), f"{centroid} vs {expected_corner}")
bbox_center = compute._bbox_center_mm(s, 0)
check("mask centroid differs from the raw scan-window bbox center",
not np.allclose(centroid, bbox_center),
f"centroid {centroid} vs bbox center {bbox_center}")
# A threshold that would yield an *empty* mask if recomputed from scratch
# (real DC values never reach 999 mV) — so this only matches expected_corner
# if compute_pivot_points_mm actually reused the pre-computed masks dict
# instead of silently recomputing (and falling back to the bbox center).
reused_pivot = compute.compute_pivot_points_mm(
s, 999.0, masks={0: corner_mask.astype(np.float32)})[0]
check("compute_pivot_points_mm reuses a pre-computed masks dict",
np.allclose(reused_pivot, expected_corner))
# --- Rotation sign convention: negative of the raw angles_deg delta ----
check("_theta_deg negates the raw angles_deg delta (GR stage's positive "
"angle is the opposite rotational sense from this module's CCW "
"math convention)",
all(np.isclose(compute._theta_deg(s, a, 0),
-(float(s.angles_deg[a]) - float(s.angles_deg[0])))
for a in range(s.n_angles)))
# --- Open, seeding from the still-live automatic AlignmentResult --------
win._on_manual_alignment()
check("dialog opened", win._manual_align_dialog is not None)