Make alignment pivot independent of the DC mask threshold

The pivot was still derived from the binary CH4 >= dc_threshold_mv mask,
so a threshold that happens to leave a real angle's mask empty (signal
levels vary scan to scan across a many-angle acquisition) silently fell
back to the raw scan-window bbox center — reproducing the exact "aligned
to the scan window, not the sample" scatter the centroid pivot exists to
fix, without any visible error.

Replace the binary-mask centroid with an intensity-weighted centroid of
the continuous CH4 signal, which is always well-defined regardless of the
RF-mask threshold in effect. The threshold still controls what the manual
dialog's overlay and CH1 masking show; it no longer has any bearing on
where alignment pivots.

Also bump the sidecar schema version: a file saved before today's pivot
and rotation-sign fixes stores numbers for a since-corrected transform, so
loading it unchanged would reintroduce the same scatter. Old sidecars are
now treated as absent rather than silently reused.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-07-31 09:58:15 -05:00
parent ca0c736c28
commit 8312668c02
3 changed files with 124 additions and 78 deletions
+9 -16
View File
@@ -1064,7 +1064,12 @@ 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()
# Alignment pivot: the CH4-signal-weighted centroid of each angle's
# own footprint (see compute.compute_pivot_points_mm) — computed once
# from the full-res CH4 images and deliberately independent of the
# mask threshold, so it never needs recomputing when that changes
# (unlike _masks_small, which is purely for the overlay's visuals).
self._pivot_mm = compute.compute_pivot_points_mm(self._sras, self._dc4_mv)
self._rebuild_preview_canvas()
self._set_controls_enabled(True)
self.lbl_status.setText("Ready.")
@@ -1072,7 +1077,9 @@ class ManualAlignmentDialog(QDialog):
def _recompute_masks_small(self):
"""Threshold + downsample every angle's already-in-memory full-res
CH4 mV image. Cheap (a compare + block-mean), so this re-runs in
full whenever the mask-threshold spin box changes — no re-fetch."""
full whenever the mask-threshold spin box changes — no re-fetch.
Purely for the overlay's visuals — the alignment pivot does not
depend on this threshold (see _pivot_mm / compute_pivot_points_mm)."""
threshold = self.spin_mask_threshold_mv.value()
factor = self._downsample_factor
self._masks_small = {
@@ -1081,19 +1088,6 @@ 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
# ------------------------------------------------------------------
@@ -1227,7 +1221,6 @@ class ManualAlignmentDialog(QDialog):
if not self._masks_ready:
return
self._recompute_masks_small()
self._recompute_pivot_mm()
self._rebuild_preview_canvas()
# ------------------------------------------------------------------