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:

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_deg in 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.