Camera-rotation correction
This page documents how the autonomous-navigation pipeline fits an in-plane camera rotation alongside the per-image translation when the mission’s reconstructed attitude carries a rotation residual large enough to be observable per-image.
Why per-instrument
Cassini ISS and New Horizons LORRI report attitude that is essentially
free of rotation residual; their offsets are 2-DoF (dv, du).
Voyager ISS and Galileo SSI carry image-to-image rotation residuals up
to a few degrees that are not consistent enough to be calibrated out
mission-wide. For those instruments the navigator must fit a small
rotation as part of the per-image solution, which is what
fit_camera_rotation enables.
The flag
Each per-camera config block under config_4N0_inst_*.yaml carries:
fit_camera_rotation: true # Voyager ISS, Galileo SSI default
max_rotation_deg: 5.0 # bound on the rotation magnitude
The orchestrator reads both fields from
InstrumentSettings and
plumbs them onto NavContext
as fit_camera_rotation and max_rotation_deg. Every technique
reads from the context — never from the obs directly — so the flag is
an image-side property that travels with the navigation, not a
technique-side opt-in.
Parameter vector
When fit_camera_rotation is True, every technique works in a 3-DoF
parameter space. The parameter vector is (dv, du, theta) with
theta in radians, bounded by ±deg_to_rad(max_rotation_deg).
Each technique’s covariance grows from 2x2 to 3x3; the
_CombinedEstimate wrapping the
ensemble’s combined output carries an optional rotation_rad field
that is populated only on 3-DoF runs.
The rotation pivot is the natural geometric centre for each technique:
BodyLimbNav,BodyTerminatorNav,RingEdgeNav: the centroid of the polyline vertices.BodyDiscCorrelateNav,RingAnnulusNav: the centroid of the predicted body / planet centres carried on the template payloads (the 3-D NCC pyramid pre-rotates each level’s template about that pivot).StarFieldFromCatalogNav: the centroid of the inlier matched-point set.StarUniqueMatchNavin 2-star mode: the centroid of the two predicted positions.
Per-technique strategy
DT-based techniques (limb, terminator, ring edge) fit rotation as the
third Levenberg-Marquardt parameter. The shared
lm_subpixel_refine() helper accepts
fit_rotation=True plus a pivot_vu and pivot_distance_px
(used to convert rotation steps into pixel-equivalent magnitudes for
the convergence test). The Jacobian against theta is computed by
central differences on the rotated-vertex DT samples; the M-estimator
information matrix at convergence is inverted via pinvh to produce
the 3x3 covariance.
Template-NCC techniques run a 3-D NCC pyramid that augments the existing translation pyramid with a rotation-sample schedule per Navigation Techniques:
Level 0 (coarsest): 11 rotation samples spanning
±max_rotation_degin 1° steps.Level 1: 5 samples in 0.5° steps centred on the level-0 winner.
Level 2: 3 samples in 0.25° steps centred on the level-1 winner.
Level 3 (full resolution): one sample at the level-2 winner; sub-deg refinement falls out of the per-level NCC peak interpolation.
Each level pre-rotates the composite template about the technique’s
pivot; the NCC kernel itself is unchanged. The technique builds a 3x3
covariance from the NCC peak’s local curvature in (dv, du, theta);
the translation block derives from the existing 2-D peak curvature and
the rotation block is the second-difference along the rotation axis at
the converged estimate.
Star techniques fit a 2-D similarity transform (rotation + translation,
no scale).
StarUniqueMatchNav
in 2-star mode rotates the catalog pair onto the detected pair via
atan2(cross, dot) of the centroid-relative vectors.
StarFieldFromCatalogNav
runs the orthogonal-Procrustes (Kabsch) SVD on the inlier set; the
det(U @ Vt) correction column keeps the result a proper rotation
(no reflection).
StarRefineNav
runs the same Procrustes on the per-star residuals when at least two
inliers survive. The 1-star path always reports rotation as
unobservable.
Rank-deficient rotation
A few technique / scene combinations carry no rotation evidence. The
canonical example is
BodyBlobNav: the
brightness-weighted centroid is rotation-invariant about itself, so a
rotation parameter is unobservable from a blob alone. The same
applies to StarUniqueMatchNav
in 1-star mode and to flat-ring-only scenes from
RingEdgeNav.
To honour the parameter-vector contract (every technique on the same
image emits the same DoF) without inventing rotation evidence, those
techniques call
embed_rotation_unobservable()
to promote the 2x2 translation covariance to a 3x3 with the rotation
diagonal carrying
ROTATION_UNOBSERVABLE_VARIANCE
(1.0e15 px², the finite-sentinel substitute for +inf that
np.linalg.eigvalsh cannot represent). The ensemble’s
pinvh-based combine sees the rotation eigenvalue as null and
gracefully drops the technique’s rotation contribution while still
fusing its translation constraint.
Ensemble combine in 3-D
ensemble() operates uniformly on
2-DoF and 3-DoF inputs; it just picks the parameter-vector dimension
that matches the inputs’ covariance shape. Mixed-DoF inputs (one 2x2
covariance and one 3x3 covariance in the same image) raise
ValueError — the orchestrator pins the DoF per image via
context.fit_camera_rotation so this assertion should never fire in
production but catches programmer errors in technique implementations.
Single-link clustering by Mahalanobis distance, the precision-weighted information-form merge, and the rank-deficiency check all extend naturally to 3-D — see Orchestrator Subsystem for the underlying math.
JSON output
The metadata curator converts rotation_rad to rotation_deg and
sigma_rotation_rad to sigma_rotation_deg for JSON output
(build_metadata_dict()); both
fields are omitted entirely when fit_camera_rotation is False, so
2-DoF runs do not litter the JSON with null fields. The rank
derivation only consults the translation sigma — max_sigma_px
compares max(sigma_dv, sigma_du) — so a low rotation sigma can
never inflate a tier above what the translation accuracy supports.
at_edge for rotation
The at_edge flag fires when the converged rotation magnitude
crosses rotation_at_edge_fraction * max_rotation_deg, where
rotation_at_edge_fraction is read from each technique’s tuning
block in config_510_techniques.yaml (default 0.95;
ROTATION_AT_EDGE_FRACTION
is the canonical value that ships in the YAML). This is a separate
condition from translation at_edge (which checks proximity to
the search-window margin); both are OR-ed together onto the
at_edge
field. A separate INFO log line surfaces the rotation magnitude and
its sigma whenever fit_camera_rotation is on, with an explicit
AT_EDGE annotation when the rotation cap is the trigger.