Honest, geometrically-correct dither measurement — scatter, path, step distribution, directional rose, with walking-noise and clustering diagnostics. Meridian-flip aware.
Dither Analysis measures how well your mount actually dithered during an imaging session, from the alignment transforms produced by registration. There are three entry points:
alignment_transforms.sasd during integration). Fastest workflow: analyse an old session without re-registering.load_transforms(transforms, filenames, ref_shape) from inside other SASpro modules (e.g. stacking suite post-registration).After loading, you get four plots on the right (scatter, path, step histogram, directional rose), a detailed statistics panel on the left including warning cards for walking noise and clustering, and the ability to click any point on the plots to find its frame in the list — or right-drag a rectangle to select many frames at once for culling.
alignment_transforms.sasd for reuse later.Click 📄 Load Previous Transform File (.sasd) and pick an alignment_transforms.sasd from any earlier SASpro stacking session. The file contains all per-frame transforms plus the reference shape; parsing is instant. The tool then runs the full analysis without needing to re-register anything — the fastest way to review a session's dither quality.
The pixel scale (for the arcsec display mode) is picked up in priority order:
.sasd directory or its parent, then reading its header.REF_PATH line stored inside the .sasd itself.Once found, the pixel scale is cached under a hash of the .sasd file so re-opening it is instant on subsequent sessions.
Every frame's dither offset plotted as a point, color-coded by frame index (plasma colormap — dark purple for early frames, bright yellow for late ones). The red + marks the origin (reference frame's center). A tight, roughly-circular cloud is what you want; a line is walking noise; clumps are clustering.
The scatter points connected in time order. Flip-transition steps are drawn in orange, every other step in dark blue, so the single giant step across a meridian flip is immediately visible and visually separable from true dither steps. A green ★ marks frame 0. The path tells a story the scatter can't: whether your mount dithered randomly, drifted smoothly in one direction, or did something unexpected like repeatedly returning to the same quadrant.
Distribution of inter-frame step sizes, in the current display unit. Flip-transition steps are excluded so the histogram shows only real dither-step magnitudes. A dashed yellow line marks the mean. Bimodal distributions reveal clustering workflows ("I dither every N frames"), which is normal and expected if you set it up that way; a tight unimodal distribution means every frame dithered to a fresh position.
Polar histogram of step directions in 16 angular bins, with a yellow arrow pointing at the circular mean direction. A roughly-uniform rose means random dither (good); a strong preferred direction means walking noise (bad). When RA/Dec mode is active, the rose uses true sky axes (N up, E right); in pixel mode, the directions are relative to the sensor's pixel grid.
The plots and the file list are two-way linked for frame culling:
The intended workflow: right-drag a rectangle around a bad cluster on the scatter plot, confirm the file list shows the frames you expected, then right-click → Move to folder to shunt them to a "rejected" directory for later review.
Three buttons at the top of the right panel switch the plot units:
.sasd). Multiplies pixel values by arcsec-per-pixel. The physical scale is what matters for comparing your dither to your seeing or your pixel size.Walking noise is the single most important thing dither analysis exists to catch. If your dither offsets fall along a near-linear track instead of spreading in two dimensions, fixed-pattern noise does not stack out — amp glow, column bias, hot pixel residuals, and dark-current gradient structure all survive integration as residual gradient streaks along the walking axis. The whole point of dithering is to randomize sub-pixel pointing so the fixed-pattern contributions average down with √N; a linear walk destroys that randomization along one direction while preserving it along the perpendicular, producing an obviously-asymmetric noise floor in the stack.
The tool measures three independent metrics and raises the walking flag only when the evidence is strong (see §8.3 for the exact formulas and thresholds). The warning card lists which specific metrics fired, with their values and thresholds, so you can tell whether the diagnosis came from clean geometric linearity or from temporal drift corroborated by directional consistency.
Clustering is less serious than walking but still worth knowing about: when multiple consecutive frames share the same pointing position before the mount dithers again (common for "dither every N frames" workflows), those frames share fixed-pattern noise and are not fully separated by sigma rejection. The tool finds the valley in the step-size distribution — the gap between within-cluster noise-level steps and the actual dither steps — and counts cluster sizes.
A soft tag ("dither pattern detected, X frames per position, normal for every-N workflows") appears when clustering is intentional and benign. A hard warning card appears only when mean cluster size exceeds 3, the worst cluster is 5+ frames, and more than 30% of frames are in problem-sized clusters — the point at which the within-cluster noise commonality starts to be visible in the stack.
The math behind the dither offsets, the flip detection, the walking-noise metrics, and the WCS conversions.
Each frame's 2×3 affine alignment transform Mi is applied to the reference frame's center point:
where (cx, cy) = (W/2, H/2) are the center coordinates. The dither offset for frame i is the displacement of its mapped center relative to frame 0's mapped center:
This gives true pointing displacement regardless of whether Mi contains a rotation (meridian flip) or just a translation. The center point is specifically chosen because it's the fixed point of pure rotation — a 180° rotation about the center leaves the center unchanged, so a meridian-flipped frame whose pointing matches the reference will register as near-zero dither offset instead of a thousand-pixel apparent excursion.
Per-frame derived quantities:
is the radial offset of frame i (used for the RMS and max-radius stats), and the inter-frame step magnitude is:
which measures how far the mount moved between consecutive frames (the direct dither-step size).
The rotation angle of each frame's transform comes from the matrix's upper-left 2×2 block via standard atan2:
For a similarity transform M = s · R(θ) + t, this expression recovers θ exactly. A frame is classified as "flipped" relative to the reference angle θref when the signed angular difference exceeds 90°:
The modular arithmetic puts Δθ in the range (-180°, 180°] so the comparison respects the circular nature of angles — a jump from 179° to -179° is a 2° difference, not a 358° one.
Flip transitions are the steps between consecutive frames where the flip state changes. For step i (between frames i and i+1), the step is a flip transition if exactly one of frames i and i+1 is on the flipped side. These transition steps are excluded from the step-magnitude statistics, the step histogram, and the directional rose — because they're not dither, they're a meridian flip. The frame positions themselves remain valid data and show up normally in the scatter and path plots.
Three independent metrics are computed from the full set of dither offsets and compared to thresholds.
Metric 1: Scatter linearity. The ratio of the first to second singular value of the mean-centered point cloud (equivalent to the square root of the eigenvalue ratio of the covariance matrix):
where P is the N×2 matrix of offset points. A perfect random 2D scatter has L ≈ 1 (both singular values roughly equal). A straight-line drift sends σ₂ to zero and L to infinity. Threshold: L > 4.0 fires.
Metric 2: Temporal drift correlation. The Pearson correlation coefficient between the frame index i and the radial offset ri:
Values near +1 mean the pointing drifts monotonically outward from the reference with time — the classic signature of a mount with polar alignment error. Values near -1 would mean monotonic return to origin (rare but possible). Values near 0 indicate no temporal structure (healthy random dither). Threshold: |r| > 0.85 fires.
Metric 3: Directional consistency. The fraction of inter-frame step directions that lie within 45° of the circular mean direction:
A uniformly-distributed random dither has C ≈ 90°/360° = 0.25. A walking pattern where every step goes roughly the same direction approaches C ≈ 1.0. Threshold: C > 0.65 fires.
Combined logic. The walking flag is raised when:
Temporal drift alone is insufficient because it can fire on legitimate outward-exploring dither patterns with small frame counts — a mount that happened to dither in a slow spiral during a short run has r > 0.85 without being pathological. Requiring geometric evidence to accompany temporal evidence rejects that false positive.
The step-size threshold between "within-cluster drift" and "real dither step" is found by searching for the largest gap in the sorted step distribution, clipped to the bottom 85% (so one unusually large outlier doesn't pull the threshold):
1. Sort all valid step magnitudes.
2. Compute consecutive differences (gaps) between sorted values.
3. Find the largest gap within the lower 85% of sorted steps.
4. The threshold is the midpoint of that gap.
5. Clamp to [0.5, 5.0] pixels and no larger than half the maximum step.
Steps below the threshold are treated as "same cluster"; steps above are "new dither position." The tool then walks the step sequence, counting consecutive runs of same-cluster steps as cluster sizes. The reported metrics are the number of distinct clusters, their mean size, max size, and the fraction of frames in clusters of 3+ members.
The hard clustering warning requires all three of: mean cluster size > 3, max cluster size > 4, and clustered fraction > 30%. This threshold set is deliberately conservative — a 2-frame cluster is normal in most dither workflows, and firing on normal patterns would be noise.
Four fallback paths, in priority order:
PIXSCALE, SCALE, PLATESCL, SECPIX, SECPIX1 are common header keys written by capture software (SGP, N.I.N.A., PixInsight). Values are sanity-checked to be between 0.01 and 100 arcsec/pixel.With the solved WCS header's CD matrix available (plate solve succeeded), pixel offsets are converted to on-sky offsets:
and then to arcseconds, with the standard cos(Dec) correction on the RA component:
Without the cos(Dec) correction, dither near the celestial poles would be reported as hugely larger than near the equator for the same mount motion, because lines of RA converge at the poles. The correction factor cos(Decc) is taken at the frame center's declination (CRVAL2 from the solved WCS), which is accurate to well under 1% across any reasonable field of view.
The sign flip on RA for the plot — plot_x = -ΔRA — exists because RA increases west on the sky (looking out from Earth), but the plot convention is East = right. Flipping the sign puts East on the right where it belongs.
The position angle of North in the image, used to orient the directional rose with real sky labels, comes from:
This is the angle (CCW from pixel +X) at which the celestial declination axis points in pixel space. The rose's compass labels (N, E, S, W) and the arrow direction are all derived from this, so the rose shows true sky directions regardless of your camera's rotator position.
Fixed-pattern noise (FPN) components — amp glow, column bias, hot pixels, dark current structure — are correlated between consecutive subframes taken at the same mount pointing. Shot noise and read noise are uncorrelated. When you stack N subframes, uncorrelated noise averages down as 1/√N while correlated FPN does not reduce at all — your stack ends up FPN-limited no matter how many subs you add.
Dithering breaks the FPN correlation by moving the sky signal across different pixels between subs. Sigma rejection or median stacking at the stack-combine step then throws away the stationary FPN while preserving the moving sky. In the limit of truly random 2D dither with enough frames to populate every nearby pixel, residual FPN scales as:
Walking noise is exactly the failure of this: when dither offsets are collinear, the FPN at one pixel never gets replaced by sky at a different pixel along the perpendicular axis, so sigma rejection has nothing to reject against — the FPN stays visible in the stack as a streak along the walking direction. The three walking metrics (§8.3) are chosen to be sensitive to this failure mode with low false-positive rate.
The scatter and path plots store the exact pixel coordinates of every point in their current axes. Mouse clicks are converted from pixel coordinates to axes coordinates via Matplotlib's transData, then the Euclidean display-space distance from the click to every point is computed; the nearest point within 12 display pixels is selected. The 12-pixel radius prevents accidental selection from mild near-misses and is comfortable for touch input as well as mouse.
Rectangle selection uses Matplotlib's RectangleSelector on both axes, triggered by right mouse button (so left-click single-point selection still works). The resulting selection is converted to a set of frame indices and posted back to the file list, which blocks its own selection-changed signal during the bulk update (to avoid highlight flicker from each individual item change) and manually re-triggers the highlight on the plot once the full new selection is in place.