Body Navigation Model
Overview
The catalog-driven body navigation model renders one solar-system body’s predicted appearance from SPICE prediction and emits up to four navigation features per body: a polyline along the lit limb, a polyline along the terminator, a pixel template of the lit disc, and a centroid / bounding-box description for under-resolved or irregular bodies. The orchestrator builds one instance per body whose extended-FOV bounding box overlaps the observation; a Saturn fly-by image that catches Mimas, Tethys, and Dione in the same frame produces three instances, each free to emit any subset of the four feature types its silhouette geometry justifies.
A simulated-image sibling (NavModelBodySimulated)
renders a body from operator-supplied ellipsoid parameters instead of SPICE prediction; both
classes share the silhouette / annotation helpers on
NavModelBodyBase. Per-body shape, albedo, and SPICE
ephemeris-residual quantities feed in from
shape_for_body(), which overlays an operator-curated YAML on top
of a hard-coded BODY_SHAPE_TABLE and a
DEFAULT_BODY_SHAPE fallback.
Theory
The body model is a per-body silhouette renderer whose output is a small number of navigation features — geometric primitives that downstream techniques know how to align against the image. A body in the field of view contributes between zero and four such features depending on its predicted geometry.
Bounding-box construction
The model starts from the predicted bounding box reported by the per-image inventory query
(u_min_unclipped, u_max_unclipped, v_min_unclipped, v_max_unclipped) — the
half-open extent of pixels intersected by the predicted body silhouette before it is clipped
into the sensor. Inventory bounding boxes are sometimes a half-pixel too small; the model
inflates each axis by a fixed fraction of its current extent before clipping into the
extended-FOV grid. The 5 % default fraction keeps anti-aliased limb pixels from being lost
on the boundary while remaining small enough that the rendered postage-stamp does not balloon
on an in-FOV body.
After inflation the bounding box is clipped against the per-instrument extended-FOV margins.
A bounding box that collapses to a single point along either axis is bumped by one pixel so
the downstream meshgrid is non-degenerate. The model also computes a boolean
guaranteed_visible_in_fov quantity: True when the inflated, clipped bounding box lies
fully inside the sensor area minus the per-instrument margin. The flag is recorded on
self._metadata for the curator and is consumed by the disc-emission gates downstream.
Anti-aliased silhouette extraction
The model lays an oversampled grid of pixel centres inside the clipped bounding box. The per-axis oversample factor is
and analogously for the v axis, using the inventory’s u_pixel_size and v_pixel_size —
the body’s predicted angular extent in pixels. A 100-pixel body at the default settings
(oversample_edge_limit = 512, oversample_maximum = 2) yields oversample_u = 2. A
sub-pixel body (predicted extent 1 px) would saturate the formula at oversample_u = 2 per
the maximum. The cap exists to bound memory on field-filling targets where a higher
oversample would balloon the backplane query.
At each oversampled grid point an incidence-angle backplane is queried from the SPICE prediction. The discrete silhouette mask comes from the pixels where the incidence query returns a finite value (i.e. the line of sight intersects the body). The oversampled mask is filter-downsampled to the extended-FOV grid so that anti-aliased limb pixels at the silhouette boundary remain represented as fractional values rather than being lost to round-off.
From the discrete silhouette mask the model derives:
a limb mask of valid silhouette pixels with at least one off-body neighbour;
a terminator mask of lit silhouette pixels with at least one neighbouring dark pixel (incidence at or above 90 degrees);
a lit-and-in-FOV count used by the body-disc gates;
a predicted brightness image that is either the binary silhouette or the per-pixel Lambert cosine of the incidence angle, optionally scaled by the per-body geometric albedo.
The brightness image is the template downstream correlation techniques use; the limb and terminator masks are walked to produce per-vertex polylines.
Polyline sampling
Each polyline vertex is a pixel that survived the discrete-mask construction. At every vertex the model records:
The vertex position in the extended-FOV pixel frame.
The outward-pointing unit normal, estimated from the mask gradient: a body-side neighbour contributes a +1 in the outward direction, an off-body neighbour contributes a -1, and the resulting two-component vector is normalised.
The local incidence angle at the vertex (radians).
The local kilometres-per-pixel scale at the vertex, queried from the SPICE backplane.
The km/px scale at the limb sets the sensitivity that converts physical km uncertainties into pixel sigmas. An empty mask collapses the sampler to zero-length arrays so the downstream emission gates skip the corresponding feature without special-case handling.
Per-vertex position covariance
The per-vertex normal sigma — the prior standard deviation of how far the true limb may lie from the predicted vertex along the outward normal — is the quadrature sum of four physical uncertainties for the lit limb:
The first two terms are intrinsic to the body shape (RMS deviation from the best-fit ellipsoid; characteristic crater scale). The third term is the photometric softness of the limb at this vertex; \(\sigma_{\mathrm{soft}}(i)\) is the optical PSF sigma converted to kilometres at the vertex (PSF-sigma-px times km-per-pixel-at-vertex), and \(f_{\mathrm{inc}}(i)\) is a dimensionless incidence-angle penalty that grows as the local incidence approaches 90 degrees and the limb fades photometrically. The penalty is
with the clip / cap angles and the maximum \(f_{\mathrm{cap}}\) set as project-wide
constants in nav.feature.constants. The last quadrature term is the SPK ephemeris
uncertainty projected to the limb plane. Dividing through by km/px gives the per-vertex pixel
sigma the polyline carries on its geometry payload.
The terminator polyline carries the same form plus an additional albedo-variation term and a photometric-softness term, both proportional to \(\sigma_{\mathrm{soft}}^{2}\), because the terminator is photometrically rather than geometrically defined and is therefore sensitive to disc albedo variation in a way the lit limb is not.
Per-vertex tangent sigma is set to a small constant matching the polyline-sampling resolution; the DT-based fit treats motion along the polyline as essentially unobservable by construction.
Visible-lit and overflow fractions
Two scalars derived from the discrete masks gate the disc-template emission:
The visible-lit fraction measures the portion of the whole predicted disc (lit + dark together) that is both lit and inside the sensor FOV — not the lit hemisphere alone, which would always score near unity for a fully-in-frame body and lose discriminating power for the disc gate. The overflow fraction measures the portion of the predicted disc that is outside the sensor FOV.
Lit-weighted predicted centroid
The blob path’s predicted centroid is the brightness-weighted moment of the rendered model inside the body silhouette:
Predicting the lit-weighted centroid up front means the navigation offset the technique recovers is just the spacecraft pointing error, not pointing error plus the systematic phase bias. At zero phase the rendered model is uniform and the formula collapses to the geometric centre.
Sub-solar direction
The vector from the geometric centre to the lit-weighted centroid points along the projected
body-to-Sun direction, so the blob feature also carries its unit form as
sub_solar_dir_vu. Body Blob Centroid (BodyBlobNav) orients its high-phase
crescent coarse-acquisition template along this direction (a filled disc cannot match a
crescent). Near full phase the two centroids coincide and the direction is meaningless, so it
is reported as (0, 0) and the technique falls back to a disc template. The derivation is
model-agnostic – it reads only the rendered appearance – so a SPICE-backed body model
populates the same field without extra plumbing.
Phase-and-irregularity coupling
The blob feature carries a dimensionless coupling that combines the body’s fractional shape uncertainty with a phase factor:
where \(R_{\mathrm{body}}\) is the predicted body radius (half the predicted disc diameter, in km, derived from the local km/px scale) and \(\phi\) is the phase angle. The phase factor evaluates to 1 at full phase, 2 at 90 degrees, and 3 at full crescent — even at zero phase the body’s rotational orientation is unknown, so a residual-scale centroid bias is always present. The blob feature’s confidence formula consumes \(\kappa\) directly so a high-phase, irregular scene can be down-weighted without re-deriving the formula at the technique layer.
Feature emission gates
The model decides which of the four features to emit by computing two scalar quantities from the silhouette extraction:
The limb uncertainty \(\sigma_{\mathrm{ellipsoid}} / (\mathrm{km\ per\ px})\), which measures how far the limb fit can be expected to wander given the body’s intrinsic ellipsoid-fit residual. When this scalar exceeds the documented cap the limb fit is information-limited; the gate rejects the
LIMB_ARCand falls back to the brightness-weighted-centroid path.The visible-lit fraction and overflow fraction above. The disc gate fires only when
LIMB_ARCwas emitted, the visible-lit fraction is at or above its minimum, and the overflow fraction is at or below its maximum.
The terminator polyline gates fire when the surviving vertex count is at or above the configured minimum and the phase factor \(\sin\phi\) is at or above its minimum.
Restrictions and assumptions
The model assumes:
SPICE has predicted the body’s pose and ephemeris well enough that the bounding box is approximately correct (the bounding box is inflated by a small slop fraction before clipping into the extfov to absorb half-pixel jitter from the inventory query).
The body is approximately ellipsoidal at the level of the per-vertex sigma budget; bodies whose ellipsoid-fit residual exceeds the per-pixel scale are silently downgraded from limb arcs to blobs.
The optical PSF is well approximated by a single Gaussian sigma, queried from the observation.
The reported phase angle is in \([0, 180]\) degrees; values outside this range are clamped before being recorded on the blob feature flags.
Bodies whose predicted bounding box has zero overlap with the extended FOV produce no instance and contribute no features. Bodies whose silhouette is entirely in shadow, or whose lit fraction falls below the photometric thresholds, emit only the geometric features (limb arc when its uncertainty allows; otherwise nothing).
Sources of uncertainty
The per-vertex sigma values consumed by downstream techniques reflect the four physical
quantities enumerated above plus the photometric softness budget. They do not capture pose
errors that bias the entire silhouette uniformly (those manifest as a global translation that
the DT fit recovers); they do not account for unmodelled atmospheric haze or lens flare; and
they do not propagate the SPICE pointing uncertainty itself (that is handled separately by
the search-window margin). The predicted lit-weighted centroid attached to the blob feature
collapses to the geometric centre at zero phase but carries a phase-and-irregularity factor
on its flags that grows for high-phase, non-ellipsoidal bodies; the enclosing
NavFeature adds a corresponding photon-noise-limited centroid
sigma in quadrature to a shape-irregularity sigma so the blob covariance reflects both noise
and shape modelling error.
Configuration
The model’s runtime knobs are split across two YAML files: the rendering / extraction
parameters live under bodies in src/nav/config_files/config_040_bodies.yaml (consumed
by the model itself plus its annotation helpers and the reprojection pipeline); per-body
shape, albedo, and SPK-residual values live under body_shape in
src/nav/config_files/config_220_body_shape.yaml (consumed via the
shape_for_body() lookup chain). Module-level Python
constants in nav.nav_model.nav_model_body set the emission-gate thresholds that are
not exposed to YAML.
bodies block
Every key under bodies is listed below. Several keys are not consumed by
NavModelBody itself; the second column names the
module that does consume each key. The grep
grep -c '^ [a-z_]\+:' src/nav/config_files/config_040_bodies.yaml returns 30 keys, which
matches the 30 bullets here.
min_bounding_box_area— int, default9px². Recorded on the model’s metadata assize_ok; sub-threshold bounding boxes are flagged for reviewer awareness. Does not by itself suppress feature emission. Consumed byNavModelBody.min_emission_ring_body— int, default20px. Reserved for the per-body ring-emission gate that decides whether to emit RING_EDGE features near a body. Reserved as an unused key so the per-instrument override story is uniform once a consumer is wired in.oversample_edge_limit— int, default512px. Cap on the per-axis oversample factor: the floor ofoversample_edge_limit / max(1, ceil(bbox_extent_px)). Larger values produce a smoother anti-aliased limb at the cost of a larger backplane query. Consumed byNavModelBody.oversample_maximum— int, default2(dimensionless). Hard cap on the per-axis oversample factor independent of bounding-box size. Saturates small-body renders so large field-fillers do not exhaust memory. Consumed byNavModelBody.curvature_threshold_frac— float, default0.02(dimensionless). Reserved for the curvature-classification heuristic that distinguishes “limb fits a circle” from “limb is effectively a straight line”; not consumed by the currentNavModelBody.curvature_threshold_pixels— int, default20px. Reserved alongsidecurvature_threshold_fracfor the same heuristic.limb_incidence_threshold— float, default1.53589...rad (= 88 degrees). Reserved for a limb-vertex shadow filter that drops vertices whose local incidence angle is past the threshold. Not consumed by the current model.limb_incidence_frac— float, default0.4(dimensionless). Reserved for the same shadow filter.surface_bumpiness— dict[str, float], per-body table (km). Reserved for a per-body surface-roughness estimate that would feed into the per-vertex sigma formula; not consumed (the equivalent quantity comes fromcrater_scale_km).geometric_albedo— dict[str, float], per-body table (dimensionless). Per-body geometric albedo applied to the rendered brightness image whenuse_albedois enabled. Entries default to bright icy moons (0.6-1.0); Phoebe (0.08), Iapetus (0.275), and Saturn (0.342) are the notable dark exceptions. Consumed byNavModelBody.use_lambert— bool, defaulttrue(dimensionless). When true the predicted brightness image is the per-pixel Lambert cosine of the incidence angle plus a small floor (so dark-but-visible silhouette pixels stay distinguishable from background); when false, the silhouette is rendered as a flat binary mask. Consumed byNavModelBody.use_albedo— bool, defaultfalse(dimensionless). When true and the body has an entry ingeometric_albedo, the rendered brightness is multiplied by that albedo. Distinguishes bright icy moons from dark bodies in multi-body composites. Consumed byNavModelBody.min_reproj_seed_area— int, default40000px². Threshold on per-image body extent below which a body is not seeded into a body mosaic. Consumed byBodyMosaic.min_reproj_candidate_area— int, default2500px². Threshold on per-image body extent below which a body is not added as a candidate to an existing body mosaic. Consumed byBodyMosaic.reproj_lon_resolution— float, default0.01745...rad (= 1 degree). Longitude step for the body-mosaic reprojection grid. Consumed byBodyMosaic.reproj_lat_resolution— float, default0.01745...rad (= 1 degree). Latitude step for the body-mosaic reprojection grid. Consumed byBodyMosaic.reproj_latlon_type— str, defaultcentric(one ofcentric/graphic). Latitude / longitude convention used by the body-mosaic reprojection. Consumed byBodyMosaic.reproj_lon_direction— str, defaulteast(one ofeast/west). Longitude positive direction for the body-mosaic reprojection. Consumed byBodyMosaic.min_text_area— float, default0.003(dimensionless fraction of frame area). Below this fraction the body is too small to label and the annotation pipeline skips its label. Consumed byNavModelBodyBase.label_mask_enlarge— int, default10px. Pixels around a body to avoid for label placement. Consumed byNavModelBodyBase.label_limb_color— list[int], default[255, 0, 0](RGB). Color of the limb outline drawn on the summary PNG. Consumed byNavModelBodyBase.label_font— str, defaultliberation2/LiberationMono-Bold.ttf. Font used for body labels. Consumed byNavModelBodyBase.label_font_size— int, default18px. Body label font size. Consumed byNavModelBodyBase.label_font_color— list[int], default[255, 0, 0](RGB). Body label font color. Consumed byNavModelBodyBase.label_horiz_gap— int, default7px. Horizontal gap between the limb and the head of the label arrow. Consumed byNavModelBodyBase.label_vert_gap— int, default5px. Vertical gap between the limb and the head of the label arrow. Consumed byNavModelBodyBase.label_scan_v— int, default1px. Granularity in V when scanning a limb to find places to put labels. Consumed byNavModelBodyBase.label_grid_v— int, default10px. Coarse V grid for label placement when no per-limb candidate is suitable. Consumed byNavModelBodyBase.label_grid_u— int, default10px. Coarse U grid for label placement. Consumed byNavModelBodyBase.outline_thicken— int, default0px. Number of dilation passes applied to the limb outline before drawing. Consumed byNavModelBodyBase.
Body-shape catalogue
src/nav/config_files/config_220_body_shape.yaml carries the per-body
BodyShape overrides under a body_shape mapping keyed by
upper-case SPICE body name. Each entry may set any subset of the
BodyShape fields; null or missing fields fall through to
the hard-coded BODY_SHAPE_TABLE baseline (Saturn-moon,
irregular-moon, gas-giant, or default profile depending on the body) per
load_body_shape().
Module-level emission constants
The emission-gate thresholds are Python module-level constants in
nav.nav_model.nav_model_body and are not exposed as YAML knobs. Tests and downstream
tools read the canonical values via these symbols.
BODY_POSITION_SLOP_FRAC— float,0.05(dimensionless). Inflation factor applied to the inventory bounding box before clipping into the extended FOV.LIMB_ARC_MAX_UNCERTAINTY_PX— float,3.0px. Cap on the limb normal-sigma at whichLIMB_ARCremains useful. Above this value the per-vertex normal uncertainty is too large for the DT-based limb fit; the extractor switches toBODY_BLOB.BODY_BLOB_MIN_DIAMETER_PX— float,8.0px. Minimum predicted disc diameter at whichBODY_BLOBis emitted. Below this diameter the brightness-weighted centroid cannot pin the body to better than ~1 px; the per-body shape table can override this floor upward but not downward.BODY_DISC_MIN_VISIBLE_LIT_FRACTION— float,0.4(dimensionless). Minimum lit-and-in-FOV fraction forBODY_DISCemission. Below 40 % the disc match is too asymmetric to be useful.BODY_DISC_MAX_OVERFLOW_FRACTION— float,0.3(dimensionless). Maximum overflow fraction forBODY_DISCemission. A body whose disc is more than 30 % off-frame loses too much template support for a sharp correlation peak.TERMINATOR_MIN_VERTICES— int,8(count). Minimum surviving terminator vertex count forTERMINATOR_ARCemission.TERMINATOR_MIN_PHASE_FACTOR— float,0.05(dimensionless). Minimum \(\sin\phi\) forTERMINATOR_ARCemission. Below \(\sin\phi \approx 0.05\) (phase below ~3 degrees) the terminator is too close to the limb to be photometrically distinguishable.
Per-instrument overrides
The bodies block is global: per-instrument YAML (config_4N0_inst_*.yaml) does not
override any of the keys above. Instrument-specific behaviour enters through the
observation snapshot — the optical PSF sigma read from
star_psf() and the extended-FOV margin set by
InstrumentSettings — rather than through
this config block.
Implementation
Source files:
src/nav/nav_model/nav_model_body.py—NavModelBody, the polyline-sampling helper, the per-feature constructors, and the module-level emission constants.src/nav/nav_model/nav_model_body_base.py—NavModelBodyBase, the abstract shared base carrying the limb-mask helper and the body-label annotation pipeline.src/nav/nav_model/body_shape.py—BodyShape,BODY_SHAPE_TABLE,DEFAULT_BODY_SHAPE,load_body_shape(), andshape_for_body().
Public class NavModelBody, base
NavModelBodyBase. The class registers itself in
NavModel._registry via __init_subclass__ so that
build_models_for_obs() discovers it. Public surface
(autodocumented at nav.nav_model):
instances_for_obs()— class method that returns one instance per body whoseinventory_body_in_extfov()predicate fires. The inventory is queried once per observation against the planet plus its configured satellites; bodies with no inventory entry, or whose entry fails the in-extfov predicate, contribute nothing.create_model()— populates the model state by calling the silhouette renderer, the polyline samplers, and the geometry-summary logger.to_features()— runs the four emission gates and constructs zero or moreNavFeatureinstances.to_annotations()— delegates to the shared annotation helper on the base class to render body silhouette and labels onto the summary PNG.name,obs,metadata— inherited read-only properties exposing the model’s name (body:<NAME>), its source observation, and the per-image metadata dict.
BodyShape dataclass
BodyShape is a frozen dataclass whose fields drive the
covariance and emission gates:
ellipsoid_rms_residual_km— RMS deviation of the body silhouette from the best-fit ellipsoid (km). Primary contribution to \(\sigma_{\mathrm{ellipsoid}}\) in the per-vertex sigma quadrature sum.crater_scale_km— characteristic crater / topographic scale (km).albedo_variation— fractional disc-brightness variation in \([0, 1]\). Drives the terminator-arc reliability formula.spice_orbital_residual_km— SPK ephemeris uncertainty in km.min_blob_diameter_px— predicted disc diameter (px) at which the extractor stops emittingLIMB_ARCand switches toBODY_BLOB.shape_class_hint— coarse classification (regular/irregular/highly_irregular/unknown); used in human-readable logs and reviewer-facing diagnostics.
The lookup chain is shape_for_body(), which calls
load_body_shape() and returns the merged
BodyShape. Priority order:
Operator-curated YAML (
config_220_body_shape.yaml) — each non-null field overrides the baseline.Hard-coded
BODY_SHAPE_TABLEprofile for the body (Saturn-moon, irregular-moon, or gas-giant).DEFAULT_BODY_SHAPEfor entirely unknown bodies.
Annotation helpers
NavModelBodyBase is the abstract shared base.
Two helpers live there:
_compute_limb_mask_from_body_mask— computes the limb mask from the body mask via discrete neighbour shifts. Used by the simulated body model._create_annotations— builds the body-labelAnnotationscollection for the summary PNG. Consumes thelabel_*,min_text_area, andoutline_thickenkeys documented above.
Per-image metadata
create_model() populates
metadata with the following entries for the curator
to surface in the per-image JSON sidecar:
start_time/end_time/elapsed_time_sec— wall-clock timing for the model build.body_name— upper-case SPICE body name.sub_solar_lon_deg/sub_solar_lat_deg— sub-solar coordinates of the body at midtime.sub_observer_lon_deg/sub_observer_lat_deg— sub-observer coordinates.phase_angle_deg— centre-pixel phase angle.bbox_area_px/size_ok— predicted bounding-box area and themin_bounding_box_areatest result.guaranteed_visible_in_fov— bool; True iff the inflated bounding box lies fully inside the sensor minus the per-instrument extfov margin.km_per_pixel_at_limb— mean km/px scale across the limb polyline.predicted_diameter_px— predicted body diameter in pixels.visible_lit_fraction/overflow_fraction— fractions defined in Theory above.
Call path
Call path traced through
create_model() and
to_features():
create_model()opens a logged section, clearsself._metadata, recordsstart_time, and invokes the private render helper.The render helper looks up the inventory entry, queries five sub-solar / sub-observer / phase-angle backplanes for the geometry summary, and clips an inflated bounding box into the extended FOV. The inflation factor is
BODY_POSITION_SLOP_FRACof the inventory extent.The render helper builds an oversampled meshgrid + backplane around the clipped bounding box, queries the incidence-angle backplane, downsamples the mask to extfov resolution, and derives the limb / terminator / body / lit masks via discrete neighbour shifts.
A predicted brightness image is built from the Lambert cosine when
use_lambertis true and the body is at least partly lit; otherwise the silhouette is rendered as a binary mask with a small offset so empty body pixels remain non-zero. Whenuse_albedois true and the body has ageometric_albedoentry, the brightness image is multiplied by the albedo.Discrete masks are walked by the private polyline-sampler helper to produce per-vertex polylines (vertex position, outward normal, incidence, km-per-pixel). The lit-and-in-FOV pixels are counted to compute
visible_lit_fractionandoverflow_fraction.to_features()resolves the per-bodyBodyShapeviashape_for_body()and computes the limb-uncertainty scalar. Three branches follow, in order:When the limb polyline survived and its uncertainty is at or below
LIMB_ARC_MAX_UNCERTAINTY_PX, aLIMB_ARCfeature is emitted. The downstream disc gate then has a chance to fire alongside the limb arc whenvisible_lit_fractionandoverflow_fractionallow.When the limb arc was rejected and the predicted disc diameter is at least
max(BODY_BLOB_MIN_DIAMETER_PX,min_blob_diameter_px), aBODY_BLOBfeature is emitted instead. The blob feature carries the lit-weighted predicted centroid and the phase-and-irregularity factor \(\kappa\) on itsBodyBlobFlags.Otherwise no body feature is emitted (the body is too small to fit and too unresolved to centroid).
Independent of the limb / blob branch, a
TERMINATOR_ARCfeature is emitted whenever the terminator polyline meetsTERMINATOR_MIN_VERTICESand \(\sin\phi \ge\)TERMINATOR_MIN_PHASE_FACTOR.
The per-feature constructors live in module-level helpers that use the
BodyShape parameters and the optical-PSF sigma to
populate the per-vertex sigma arrays per the formula in the Theory section.
Examples
The named scenes in tests/integration/image_library/images/ exercise the four feature
types. Numbers below are taken from the operator-curated YAML sidecars and the integration
tests.
body_full_fov(Cassini ISS NAC, imageN1572105349_1)Dione fills the FOV — predicted disc diameter approximately 155 px, mostly lit with a sliver of terminator,
overflow_fraction = 0.0,visible_lit_fractionapproximately0.97. The model emits a singleBODY_DISCfeature carrying the rendered template; theLIMB_ARCis rejected by the downstream reliability gate that consumes it (the textbook full-disc, fully-lit limb saturates the model-side reliability formula’s incidence-factor penalty). Operator-verified offset is \((\Delta v, \Delta u) = (8.68, -17.37)\) px.body_partial_overflow(Cassini ISS NAC, imageN1484593951_2)Rhea visible in the upper right with
overflow_fraction \approx 0.22. The model emits aLIMB_ARCfeature plus aBODY_DISCfeature (the overflow fraction sits belowBODY_DISC_MAX_OVERFLOW_FRACTION) and aTERMINATOR_ARCfeature. Operator-verified offset is \((\Delta v, \Delta u) = (11.0, 29.5)\) px.below_resolution_body(Cassini ISS NAC, imageN1777325846_1)Mimas is approximately 20 px in diameter in the lower left, at phase angle 72 degrees. The predicted disc diameter is well above
BODY_BLOB_MIN_DIAMETER_PXbut the per-pixel ellipsoid uncertainty exceedsLIMB_ARC_MAX_UNCERTAINTY_PX, so the model emits aBODY_BLOBfeature instead of a LIMB_ARC. The blob feature carries the lit-weighted centroid and a phase-irregularity factor populated from the per-bodyBodyShape. Operator-verified offset is \((\Delta v, \Delta u) = (6.08, -1.53)\) px.high_phase_terminator(Cassini ISS NAC, imageN1597846115_2)A high-phase terminator arc with no other features in the FOV. The model emits a
LIMB_ARCfeature (the lit limb survives) and aTERMINATOR_ARCfeature (the terminator polyline meets the minimum vertex count and the phase factor sits above the minimum). Operator-verified offset is \((\Delta v, \Delta u) = (5.19, 1.30)\) px.multi_body(Cassini ISS NAC, imageN1487595731_1)Dione and Rhea both visible and overlapping at phase angle approximately 90 degrees. The orchestrator instantiates two
NavModelBodyinstances, one per body, each emitting its own combination of features per the gates above; the downstream techniques receive the union and fuse a single offset. Operator-verified offset is \((\Delta v, \Delta u) = (7.03, -18.42)\) px.