frame_align — ASTROSTACK align op

• Data kinds: image2d × image2d → matrix

• Call: import fullseye as fs; fs.ledger.frame_align(reference, frame, model='similarity', threshold_sigma=5.0, max_stars=60, tolerance_px=2.0, max_shift_px=None, ransac_iters=500, seed=0, min_inliers=3) (to call the implementation directly, import astrostack; astrostack.frame_align(reference, frame, model='similarity', threshold_sigma=5.0, max_stars=60, tolerance_px=2.0, max_shift_px=None, ransac_iters=500, seed=0, min_inliers=3); from the registry, opsastrostack.get("frame_align"))

• Return value through the ledger: fullseye.ledger.frame_align(...) returns only the declared out type matrix (the underlying function also returns auxiliary values). When you need what was dropped, use fullseye.ledger.frame_align.raw(...) or call astrostack.frame_align directly.

• Underlying return: (matrix, info) -> matrix

Usage

Estimate the 2-D transform frame → reference from star correspondences.

The process has 3 stages, and the core of each estimate is an existing op:

1. star_detect picks up the stars in both images.

2. A coarse translation is obtained by offset voting (_vote_translation). Not using features.match_keypoints here is a decision based on measurement:

9x9 patches of a star field look almost identical to one another, so Lowe's ratio test

(default ratio=0.8) discards nearly all of them. 128x128 with 40 stars, true offset

(+0.590, -0.540) px, measured on a frame pair: match_keypoints

returned 4 correspondences, of which 0 were within 1 px of the ground truth (= not a single

usable one). On the same pair the voting method gives 26 votes → 26 correspondences → 26 inliers, estimation error

0.0155 px. A star field does not "lack features"; "all its features are the same", so

the right thing is to use the geometry of the arrangement, not descriptors.

3. Nearest-neighbour correspondences are formed with the coarse shift, mosaic.proj_match_points_ransac drops false correspondences, and fit_transform.vector_to_similarity (depending on model, vector_to_rigid / vector_to_hom_mat2d) does the fit. Neither a RANSAC loop nor Umeyama is written here.

★Not usable on repetitive structures (2026-09-08, found by poc_print_registration). inlier_ratio is "the fraction of correspondences that voted for the same answer", not "the probability that the answer is correct". In images where the same shape repeats periodically, such as halftone dots, fabrics or grids, a matching shifted by a lattice vector is also agreed on by everyone, so the agreement ratio stays at 1.00 while the answer alone is off by one lattice period (or by several).

Measured: on a pair of 256x256 halftone dots equivalent to 133 lpi at 15 degrees, shifted by (0.00, +1.30) px, it returns inlier_ratio 1.00 / rms_px 0.78 while the estimate is (+54.24, +30.12) px. On a star field (128x128, 40 stars, ground truth (+0.59, -0.54)) inlier_ratio is the same 1.00 and the estimate is correct. The agreement ratio cannot tell the 2 apart.

What can tell them apart is vote_margin —— the height of the 2nd peak of the vote divided by the 1st; 0 means a single peak, and the closer to 1, the more "there are other equally plausible answers". On the same 2 examples: halftone 0.857 / star field 0.143. On paths that might be given periodic structures, look at this rather than inlier_ratio. If you only need the translation itself, it is more straightforward to look at the correlation surface of piv_cross_correlate, where you can count for yourself how many peaks stand.

*model* "translation" uses only the median of the correspondence differences (works even with 1 star). "rigid" = rotation + translation, "similarity" = + isotropic scale, "affine" = 6 degrees of freedom. Unless the field of view is wide, "similarity" suffices (equatorial-mount tracking error is rotation and translation; atmospheric refraction enters the scale to first order).

Returns (matrix, info):

• matrix —— (3, 3) float64. Multiplying (row, col, 1) from the left gives coordinates in reference (same convention as fit_transform).

• info —— dict. n_stars_ref / n_stars_src / n_pairs / n_inliers / inlier_ratio / shift_row / shift_col / rotation_deg / scale / rms_px (residual RMS of inliers) / model.

fail-closed: if the inliers fall short of *min_inliers*, ValueError is raised. It does not silently return the identity —— mixing a frame whose registration failed into a stack as "offset 0" produces a double image with neither exception nor warning.

Raises ValueError: if not 2-D / shapes differ / *model* is not in ALIGN_MODELS / no star at all is found in either image / no correspondences can be formed / not enough inliers.

References (sample data, literature)

• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).

• Operator provenance and references — the sources of the research/methods this op family came from.

• The canonical algorithm (author, year) and its uses are named in the family usage guide above.

Runnable examples (verified samples that actually call this op)

• astro_stacking — py -3.11 examples/astro_stacking.py

• poc_exoplanet_transit — py -3.11 examples/poc_exoplanet_transit.py

• poc_print_registration — py -3.11 examples/poc_print_registration.py

Ops the type connects to (they accept matrix as input)

—

Same category (align)

align_frames


*Provenance: astrostack.py — ASTROSTACK operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.