spindoctor.support
Shared utilities for navigation code.
This package groups small, reusable helpers. Import from submodules directly, for
example from spindoctor.support import image or
from spindoctor.support.image import shift_array.
Modules:
typesNumPy typing aliases (e.g.
NDArrayFloatType),PathLike, and protocols such asMutableStar.imageTwo-dimensional array helpers: shifting, padding, cropping, normalization, and FFT-related image operations.
correlateFourier-domain and template-matching utilities (e.g. normalized cross-correlation) built on
imageandmisc.miscMiscellaneous helpers, including sky-coordinate formatting and oops-backed utilities.
timeWall-clock helpers: ISO strings, timezone-aware datetimes, and Julian conversions.
fileYAML/JSON serialization helpers and
clean_objfor stripping NumPy scalars from nested structures.constantsCommon mathematical constants (e.g.
PI,HALFPI).exceptions
NavContractError— typed exception for internal contract violations.nav_base
NavBase, a small base class wiringConfigandPdsLoggerfor nav objects.attrdict
AttrDict, adictsubclass that supports attribute-style key access.fluxLegacy flux and filter-convolution experiments; most of the implementation is commented out but kept for reference.
filters
NavFilterSpec/NavFilterKindand the dispatcherapply_filterused across feature extraction and matching techniques.filter_combo
canonicalizefor normalizing multi-filter combos into a stable key.status_reason
NavStatusReasonenum carried on everyNavResult.noise_estimate
estimate_image_noise_sigma— robust per-image noise estimator.image_quality
saturation_maskandcosmic_ray_mask— global image-quality masks consumed by extractors.distance_transform
apply_translationandsample_dt_bilinear— chamfer-matching helpers built on top of an externally-computed distance transform.
- class AttrDict(*args: Any, **kwargs: Any)[source]
-
Implements a dictionary that allows attribute-style access to its key-value pairs.
A dictionary subclass that exposes its keys as attributes, allowing dict items to be accessed using attribute notation (dict.key) in addition to the normal dictionary lookup (dict[key]).
- Parameters:
*args – Variable length argument list passed to dict constructor.
**kwargs – Arbitrary keyword arguments passed to dict constructor.
Constants module for navigation calculations.
This module defines mathematical constants used throughout the navigation system.
- evaluate_candidate(*, image_pad: NDArray[floating[Any]], model_pad: NDArray[floating[Any]], mask_pad: NDArray[bool], corr: NDArray[floating[Any]], rc: tuple[int, int], upsample_factor: int, model_shape: tuple[int, int], image_shape: tuple[int, int], logger: PdsLogger, prior_shift: tuple[float, float] | None = None, prior_weight: float = 0.0, metric: str = 'psr', refine_image_pad: NDArray[floating[Any]] | None = None, refine_model_pad: NDArray[floating[Any]] | None = None, refine_lowpass_sigma_px: float = 0.0) dict[str, Any][source]
Evaluate a candidate for the navigation.
- Parameters:
image_pad – The padded image.
model_pad – The padded model.
mask_pad – The padded mask.
corr – The correlation matrix.
rc – The row and column of the candidate.
upsample_factor – The upsample factor.
model_shape – The shape of the model.
image_shape – The shape of the image.
prior_shift – The prior shift.
prior_weight – The prior weight.
metric – The metric to use for the navigation.
refine_image_pad – Optional padded image used only for the sub-pixel cross-power-spectrum refinement, in place of
image_pad. When the coarse peak is found on gradient-magnitude surfaces (use_gradient), pass the raw-intensity padded image here: the Sobel magnitude rectifies the signal, so its cross-power peak is non-smooth at the apex and the upsampled-DFT sub-pixel estimate is biased (largest near whole-pixel offsets), whereas raw intensity reaches theupsample_factorresolution. Defaults toimage_pad.refine_model_pad – Optional padded model for the same refinement; defaults to
model_pad.
- Returns:
A dictionary containing the navigation result.
- Raises:
ValueError – If the model is smaller than the image in either dimension (the model must be at least image-sized; pad it with
pad_top_leftfirst). This is validated here so the failure is a clear contract error rather than an opaquecrop_centererror two calls deep.
- fourier_shift(img: NDArray[floating[Any]], dy: float, dx: float) NDArray[floating[Any]][source]
Subpixel shift via Fourier shift theorem (positive = down/right).
- gradient_magnitude(arr: NDArray[floating[Any]]) NDArray[floating[Any]][source]
Sobel gradient magnitude.
Emphasizes edges over flat interior regions, which is what you want when the raw-intensity NCC has a broad plateau — e.g., a lit body disc that overflows the FOV, where interior brightness is near-uniform and the only unique-alignment signal lies at the limb.
- Parameters:
arr – 2-D input array.
- Returns:
Same-shape gradient-magnitude array, as
float64.
- int_to_signed(idx: int, size: int) int[source]
Map
[0 .. size-1]argmax index to a signed displacement coordinate.- Parameters:
idx – Peak index from correlation search in
[0, size).size – FFT / correlation length (modulus for wrapping).
- Returns:
intsigned offset equivalent toidxwhenidx < size // 2, elseidx - size(negative branch for peaks in the second half).
- masked_ncc(image: NDArray[floating[Any]], model: NDArray[floating[Any]], mask: NDArray[bool], data_mask: NDArray[bool] | None = None) tuple[NDArray[floating[Any]], NDArray[floating[Any]]][source]
Masked normalized cross-correlation surface between image and model.
Computes shift-wise NCC (Pearson r) using FFT-based sums. Also returns the unnormalized NCC numerator (mean-subtracted covariance) as a diagnostic.
The NCC surface contains values in [-1, 1] at every shift where the mask overlaps non-constant image content, and is the primary peak-selection surface: it is invariant to the image variance under the shifted mask and therefore does not suffer from the “scale bias” that causes the numerator to prefer shifts where the fixed template mask straddles bright/dark boundaries in the image (e.g., the limb of a Lambert-shaded body disc). The numerator surface scales with sqrt(image-variance-under-mask) times the NCC, which is useful as a sanity check but must not be used for peak finding when the template is dense.
- Parameters:
image – Padded image array.
model – Padded model array (same shape as image).
mask – Padded boolean mask (same shape as image). True where model pixels are valid.
data_mask – Optional padded boolean mask indicating where
imagecontains real sensor data (True) versus zero-padded margin (False). When provided, the NCC is computed in its bi-directional form so that pixel pairs where the shifted model mask straddles zero-padded image pixels do not bias the normalization. This removes the edge-ridge artifact that otherwise appears at|dV| = extfov_margin_vwhen the body model extends into the extfov margin. WhenNone, the standard (single-mask) NCC formula is used.
- Returns:
Tuple of (ncc, numerator) arrays, each with the same shape as the inputs.
nccis the Pearson-r surface used for peak localization;numeratoris the mean-subtracted cross-covariance (unnormalized) returned as a diagnostic. Whendata_maskis provided, shifts with degenerate overlap or variance are set to-infin the NCC surface.
- matched_filter_covariance(model_aligned: NDArray[floating[Any]], sigma_n: float, *, correlation_area: float = 1.0) NDArray[floating[Any]][source]
Peak-curvature (matched-filter) covariance of a 2-D translation fit.
For a template
Maligned to an imageI = M(x - s) + nthe shiftsmaximum-likelihood estimate has Fisher informationF_ab = (1 / sigma_n**2) * sum_x (dM/dx_a)(dM/dx_b)and covarianceF**-1 = sigma_n**2 * inv(sum grad M grad M^T).Two properties are essential for the reported sigma to track the true error and are the reason this replaces the previous derivation:
Unit consistency.
sigma_nand the gradients ofMmust be measured on the same intensity scale, or the covariance scales with the (arbitrary) template amplitude. The caller passes a residual noisesigma_ncomputed on the zero-mean, unit-std normalized image and model; this function must therefore receive the same normalized model sograd Mshares that scale. Passing the raw template here (whose amplitude can be thousands of DN) shrinks the covariance by the template variance and is the miscalibration this derivation corrects.Effective sample count.
correlation_areainflates the white-noise bound by the residual’s spatial correlation area so correlated model error is not counted as thousands of independent constraints (see_residual_correlation_area()).
- Parameters:
model_aligned – The aligned template on the same normalized scale as the residual from which
sigma_nwas measured.sigma_n – Residual noise standard deviation on that normalized scale.
correlation_area – Residual correlation area in pixels (
>= 1); multiplies the covariance. Default1.0reproduces the white-noise Cramer-Rao bound.
- Returns:
The
2x2translation covariance in pixels squared. A degenerate (rank-deficient) gradient structure returns a large isotropic covariance so the result is de-weighted rather than trusted.
One-scale masked NCC + top-K candidate evaluation.
- Parameters:
image – The image to navigate.
model – The model to navigate.
mask – The mask to use for the navigation.
max_peaks – The number of peaks to use for the navigation.
upsample_factor – The upsample factor to use for the navigation.
metric – The metric to use for the navigation.
prior_shift – The prior shift to use for the navigation.
prior_weight – The prior weight to use for the navigation.
nms_radius – The radius to use for the non-maximum suppression.
logger – The logger to use for the navigation.
max_offset_vu – If given, only correlation peaks whose signed (V, U) offset satisfies
|dV| <= max_offset_vu[0]and|dU| <= max_offset_vu[1]are considered candidates. Typically set to the extended-FOV margins so that offsets outside the physically-plausible range are never evaluated.data_mask – Optional boolean mask (same shape as
image) that is True where the image contains real sensor data and False inside the zero-padded extended-FOV margin. When provided, the NCC is computed in its bi-directional form so that the model extending into the padded margin does not bias the peak toward|dV| = margin_v.use_gradient – When True, replace
imageandmodelwith their Sobel gradient magnitudes before the NCC. Use this when the raw intensity surface has a broad plateau — e.g., a body that fills or overflows the FOV, where the only unique-alignment signal is at the limb.
- Returns:
A dictionary containing the navigation result.
TODO Clean this up Build class-aware effective model + mask, run coarse->fine, then evaluate K peaks at final scale. Returns dict with shift, covariance, sigma_xy, quality, consistency, spurious flag.
- Parameters:
image – The source image to navigate, unpadded.
model – The model to navigate against, padded as necessary to include more data around the edges. It does not need to be the same size as the image.
mask – The mask indicating which pixels in the model are valid. Same size as the model.
pyramid_levels – The number of pyramid levels to use. Each pyramid level divides the image and model by an additional factor of 2 (pyramid_levels=3 means to start with 1/4, then 1/2, then 1/1 downsampling).
max_peaks – The number of peaks to look for in the correlation at each pyramid level.
upsample_factor – The upsample factor to use for increased FFT resolution around a peak.
metric – The metric to use for the navigation. Can be one of ‘psr’, ‘pmr’, or ‘per’.
quality_thresh – The quality threshold to use for the navigation.
consistency_tol – The consistency tolerance to use for the navigation.
nms_radius – The radius to use for the non-maximum suppression.
prior_weight_final – The prior weight to use for the final navigation.
max_offset_vu – Maximum permitted signed offset as
(max_dV, max_dU)in full-resolution pixels. When given, correlation peaks outside this range are excluded at every pyramid level (the limit is scaled by the downsample factor at each level). Passobs.extfov_margin_vuto restrict the search to offsets that are physically reachable given the extended FOV padding.data_mask – Optional boolean mask (same shape as
image) that is True where the image contains real sensor data and False inside the zero-padded extended-FOV margin. When provided, the NCC is computed in its bi-directional form at every pyramid level so that a body model extending into the extfov margin does not bias the peak toward|dV| = margin_v. Passobs.extfov_data_sensor_mask().use_gradient –
Controls gradient-magnitude preprocessing of the image and model before the NCC.
False(default): raw-intensity NCC at every pyramid level.True: gradient-magnitude NCC at every pyramid level. Use this when the raw surface has a broad plateau — e.g., a body that fills or overflows the FOV, where only the limb carries unique-alignment signal.'auto': run both modes, pick the more confident result. A non-spurious result is preferred over a spurious one; within the same spurious bucket the higher-quality result wins.
localization_uncertainty_scale – Scales the inter-pyramid-level peak migration (
consistency, in px) into an added translation uncertainty. The peak-curvature covariance measures only the statistical (photon / residual) precision at the winning peak; a peak that walks between pyramid levels is empirically less well localized than a peak that does not, so(scale * consistency)**2is added in quadrature to the translation covariance diagonal.0.0(default) disables it and leaves the bare peak-curvature covariance.logger – The logger to use for the navigation.
- Returns:
offset: The offset.
cov: The covariance matrix.
sigma_xy: The sigma_xy.
quality: The quality of the navigation.
metric: The metric used for the navigation.
consistency: The consistency of the navigation.
spurious: True if the navigation is spurious, False otherwise.
- Return type:
A dictionary containing the navigation result
Notes
The metrics are:
PSR (Peak-to-Sidelobe Ratio): Measures peak distinctness as (peak - mean_sidelobe) / std_sidelobe, where the sidelobe region excludes the peak neighborhood.
PMR (Peak-to-Mean Ratio): Ratio of the global maximum correlation value to the mean of all correlation values; indicates how dominant the main peak is over the average background.
PER (Peak-to-Energy Ratio): Ratio of the squared peak value to the total correlation energy (sum of squares); reflects how much of the total response energy is concentrated in the main peak.
- nms_topk(corr: NDArray[floating[Any]], k: int = 5, radius: int = 5, max_offset_vu: tuple[int, int] | None = None) list[tuple[int, int, float]][source]
Non-maximum suppression to get top-k peaks.
- Parameters:
corr – 2-D correlation surface (V x U).
k – Maximum number of peaks to return.
radius – Suppression radius around each selected peak in pixels.
max_offset_vu – If given, only positions whose signed offset satisfies
|dV| < max_offset_vu[0]and|dU| < max_offset_vu[1]are eligible (strict inequality: peaks at the exact window boundary are excluded because they signal a clipped search and cannot be trusted; the pyramid driver additionally marks any final result within 1 pixel of the boundary as spurious). Signed offsets are derived from the FFT-convention wrap-around used byint_to_signed().
- Returns:
List of
(row, col, value)tuples for up tokpeaks.
- peak_to_runner_up_ratio(top_k_peaks: list[tuple[float, float, float]]) float[source]
Return the ratio of the winning peak’s quality to the runner-up’s.
top_k_peaksis[(quality, dv, du), ...]sorted by quality descending (the conventionnavigate_with_pyramid_kpeaks()uses). Returns1.0when only one peak survives non-maximum suppression – what an unambiguous correlation looks like, so a value at or above 1.0 is the “good” tail – and0.0when no peaks are present. When the runner-up quality is non-positive (rare; happens with the prior penalty) the result is the unambiguous-winner cap_MAX_PEAK_RATIOrather thanwinner / 1e-9(which scaled with the winner’s magnitude and could reach~1e9). The ordinary ratio is likewise clamped to_MAX_PEAK_RATIO.- Parameters:
top_k_peaks – Peaks sorted by quality descending.
- Returns:
Capped peak-to-runner-up quality ratio.
- upsampled_dft(X: NDArray[complexfloating[Any, Any]], up_factor: int, region_sz: tuple[int, int], offsets: tuple[int, int]) NDArray[complexfloating[Any, Any]][source]
Localized upsampled DFT.
From Guizar-Sicairos, 2008. “Efficient subpixel image registration via cross-correlation.” Optics Leters, 33(2):156-158
Chamfer-matching helpers built on top of the distance transform.
DT-based techniques (BodyLimbNav, BodyTerminatorNav, RingEdgeNav) all rely on the same primitive: given an image-side distance transform of an edge map and a model polyline, evaluate the cost of a candidate offset by sampling the DT at the shifted polyline vertices.
This module owns the polyline-shifting and DT-sampling routines so the three
techniques don’t reimplement them. Bilinear DT interpolation is used for
sub-pixel precision; the orchestrator’s NavContext carries the per-image
DT array so callers reach in once and the gradient is shared.
- apply_translation(vertices_vu: NDArray[floating[Any]], dv: float, du: float) NDArray[floating[Any]][source]
Return
vertices_vushifted by(dv, du).- Parameters:
vertices_vu –
(N, 2)array of (v, u) vertex positions.dv – Shift along the v axis (rows).
du – Shift along the u axis (columns).
- Returns:
(N, 2)shifted vertex positions; original is not modified.- Raises:
ValueError – if
vertices_vuis not(N, 2).
- sample_dt_bilinear(dt: NDArray[floating[Any]], vertices_vu: NDArray[floating[Any]]) NDArray[floating[Any]][source]
Bilinear-sample a distance transform at sub-pixel vertex positions.
Vertices outside
dtare clamped to the boundary value at the closest in-bounds pixel. The result is the DT cost contribution per vertex; the sum (or the weighted sum, with M-estimator weights) is what an LM refinement minimizes.- Parameters:
dt – 2-D distance-transform array (output of
apply_filterwithDISTANCE_TRANSFORMkind, or an externally-built DT).vertices_vu –
(N, 2)array of (v, u) sub-pixel vertex positions.
- Returns:
(N,)array of bilinear-interpolated DT values at each vertex.- Raises:
ValueError – if shape requirements are violated.
Typed exceptions shared across the navigation core.
Contract violations must not be expressed as bare assert statements:
asserts are stripped under python -O, and inside the orchestrator’s
broad plugin sandboxes an AssertionError would be swallowed as an
ordinary technique failure. Raising NavContractError instead
keeps the check active in optimized runs and lets the orchestrator treat
the violation distinctly (error-level log plus a failed NavResult
with NavStatusReason.CONTRACT_VIOLATION).
Bases:
ExceptionAn internal navigation invariant (contract) was violated.
Raised when an upstream component hands core code a value outside its documented bounds (for example a 3-DoF technique result whose rotation exceeds the ensemble’s small-angle bound). A
NavContractErroralways indicates a programming error, never bad image data, so it is never swallowed by the orchestrator’s plugin sandboxes: the sandboxes log it at error level and re-raise, andNavOrchestrator.navigateconverts it into a failedNavResultwithNavStatusReason.CONTRACT_VIOLATION.
- clean_obj(obj: Any) Any[source]
Recursively converts NumPy types in any object to Python native types.
Not a pure function: a
dictargument (and any nesteddict) is converted in place – each NumPy scalar is replaced by its Python native equivalent in the caller’s own dict – and the same object is returned. Alist/tupleargument yields a newlist(so the caller’s sequence object is not mutated), butdictvalues nested inside it are still mutated in place. Callers that must keep the original untouched should pass a deep copy.- Parameters:
obj – The object to clean, can be a dict, list, tuple or scalar value.
- Returns:
The object with all NumPy types converted to Python native types (the same dict object when a dict is passed; see the mutation note).
- dump_yaml(data: Any, stream: Any = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>) None[source]
Dumps data as YAML output after converting NumPy types to Python types.
- Parameters:
data – The data to dump as YAML.
- json_as_string(data: Any) str[source]
Dumps data as a JSON string after converting NumPy types to Python types.
- Parameters:
data – The data to dump as JSON.
Canonical form of a multi-filter optical combo string.
Most missions in this pipeline carry one or two filter names per exposure (e.g. Cassini ISS uses two filter wheels per camera; Voyager and Galileo use one). Multiple call sites need a canonical string for the same combo:
mag_offset_tablelookup keys.Test image-library sidecar
filter_combofield.Per-image log lines and metadata fields.
This module supplies the single canonicalize rule (alphabetic-sort joined
by '+') so the spelling never drifts between consumers.
- canonicalize(filters: Sequence[str | None]) str[source]
Return a canonical string representation of a filter combo.
Drops
Noneentries, sorts the remaining filter names alphabetically, joins them with'+'. Duplicate names are preserved (so['CL', 'CL']becomes'CL+CL'). An empty sequence (or a sequence containing onlyNoneentries) returns'NONE'.- Parameters:
filters – Iterable of filter name strings;
Noneentries are dropped.- Returns:
Canonical
'+'-joined sorted filter combo string, or'NONE'if no non-None filters are given.
Examples
canonicalize([])->'NONE'canonicalize(['CL1'])->'CL1'canonicalize(['CL2', 'CL1'])->'CL1+CL2'canonicalize(['CL', 'CL'])->'CL+CL'canonicalize(['F1', None, 'F2'])->'F1+F2'
Filter abstraction used by feature extractors and matching techniques.
Each NavFeature carries a preferred_filter NavFilterSpec; the
consuming technique applies the spec to both the image patch and the model
template before computing its matching metric. A small number of kinds
covers every feature type used by the v1 pipeline.
The apply_filter entry point dispatches on NavFilterKind and runs the
configured operation, with two universal short-circuits:
NavFilterKind.NONEreturns the input unchanged.A spec whose largest principal sigma is below
null_filter_threshold_sigma(per-config) is treated asNONE.
Higher-level technique code never indexes by kind itself; it just calls
apply_filter(arr, spec).
Thread safety: all functions in this module are pure / stateless; safe for concurrent use on independent inputs.
Bases:
EnumThe kind of operation a NavFilterSpec describes.
NONE: identity filter;apply_filterreturns the input unchanged.ISOTROPIC_GAUSSIAN: symmetric Gaussian blur with a singlesigma_xy.ANISOTROPIC_GAUSSIAN: possibly axis-aligned Gaussian blur with a full 2x2 covariance.align_axismay rotate the blur into a non-axis-aligned principal frame.BANDPASS_DOG: difference-of-Gaussians bandpass; subtract a heavy-blur from a light-blur of the input to suppress low-frequency content while preserving sharper detail.DISTANCE_TRANSFORM: signed distance transform of a thresholded edge map; only meaningful as a precomputed image-side quantity, not a generic operator.apply_filterraises if asked to apply it to a non-binary array.GRADIENT_OF_GAUSSIAN: gradient magnitude of a Gaussian-smoothed input; isotropic blur followed by Sobel.MORPH_DILATE: morphological dilation by a structuring element of half-width derived fromsigma_xy. Used when building search margins for edge-based matching.
Bases:
objectDescription of a filter to be applied uniformly to image and template.
All fields except
kindare optional; only the ones the chosen kind consumes need to be set. Tests assert that mismatched kind/parameter combinations raise on application, not at construction (so techniques can carry under-populated specs through identity short-circuits without extra ceremony).- Parameters:
kind – Which kind of filter operation this spec describes.
sigma_xy – Per-axis Gaussian sigma in pixels, used by ISOTROPIC_GAUSSIAN, GRADIENT_OF_GAUSSIAN, and MORPH_DILATE.
covariance_px2 – 2x2 covariance matrix used by ANISOTROPIC_GAUSSIAN.
Nonefor other kinds.bandpass_cutoffs_px –
(lo_sigma, hi_sigma)in pixels for BANDPASS_DOG. Only used by that kind.dt_half_width_px – Half-width truncation of distance values, in pixels. Used only by DISTANCE_TRANSFORM.
align_axis – Optional
(v, u)direction to align the principal axis of an anisotropic filter.Nonemeans axis-aligned (identity rotation).
- apply_filter(arr: NDArray[floating[Any]], spec: NavFilterSpec) NDArray[floating[Any]][source]
Apply
spectoarrand return the filtered array.Two universal short-circuits run before kind dispatch:
spec.kind == NavFilterKind.NONEreturnsarrunchanged.If the largest principal sigma of
specis belowspec.null_filter_threshold_sigma, the spec is too small to make a meaningful difference; the array is returned unchanged.
Otherwise the operation indicated by
spec.kindis run.- Parameters:
arr – 2-D float input array.
spec – NavFilterSpec describing the operation.
- Returns:
Filtered 2-D float array; same shape as the input.
- Raises:
ValueError – if
specis missing parameters required by its kind (e.g.ANISOTROPIC_GAUSSIANwithout a covariance matrix).TypeError – if
arris not 2-D.
- clean_sclass(sclass: str | None) str[source]
Return a clean stellar classification such as A0 or M8.
- apply_linear_gamma_stretch(data: NDArray[floating[Any]], *, black: float, white: float, gamma: float) NDArray[floating[Any]][source]
Apply black/white/gamma stretch and return a float array in
[0, 1].Uses the convention
((clip(data, black, white) - black) / (white - black)) ** gamma. Agammaof 1.0 is linear; values below 1.0 brighten the mid-tones (common for display) and values above 1.0 darken them.- Parameters:
data – Input float array (any shape).
black – Black-point; input values at or below this map to 0.
white – White-point; input values at or above this map to 1. If
white <= black(including accidental UI equality),whiteis raised silently to the next representable float aboveblackso the linear scale denominator is never zero.gamma – Exponent applied after linear normalisation; must be finite and strictly greater than zero.
- Returns:
Float array of the same shape as
datawith values in[0, 1].- Raises:
TypeError – If
black,white, orgammais not anintorfloat, or any of them is abool.ValueError – If any of
black,white, orgammais not finite; or ifgamma <= 0.
- array_unzoom(array: NDArray[NPType], factor: int | list[int] | tuple[int, ...], method: str = 'mean') NDArray[NPType][source]
Zoom down by an integer factor. Returned arrays are floating-point.
- array_zoom(a: NDArray[NPType], factor: list[int] | tuple[int, ...]) NDArray[NPType][source]
Zooms an array by the specified factor using array indexing.
- Parameters:
a – The input array to zoom.
factor – The zoom factor for each dimension of the array.
- Returns:
The zoomed array with dimensions multiplied by the corresponding factors.
- crop_center(img: NDArray[NPType], out_shape: tuple[int, int]) NDArray[NPType][source]
Center crop to out_shape (h,w).
- Parameters:
img – A 2-D array to crop.
out_shape – The target shape (height, width).
- Returns:
The center-cropped array.
- Raises:
ValueError – If img is not 2-dimensional or out_shape is larger than img.shape.
- draw_circle(img: NDArray[NPType], color: int | float | list[int | float] | tuple[int | float, ...], x0: int, y0: int, r: int, thickness: int = 1) None[source]
Draw a circle using Bresenham’s algorithm with the given thickness.
- Parameters:
img – The 2-D (or higher) array to draw on.
x0 – The middle of the circle.
y0 – The middle of the circle.
r – The radius of the circle.
color – The scalar (or higher) color to draw.
thickness – The thickness (total width) of the circle.
- draw_line(img: NDArray[NPType], color: int | float | list[int | float] | tuple[int | float, ...], x0: int | float, y0: int | float, x1: int | float, y1: int | float, thickness: float = 1.0) None[source]
Draw a line using Bresenham’s algorithm with the given thickness.
The line is drawn by drawing each point as a line perpendicular to the main line.
- Parameters:
img – The 2-D (or higher) array to draw on.
x0 – The starting point.
y0 – The starting point.
x1 – The ending point.
y1 – The ending point.
color – The scalar (or higher) color to draw.
thickness – The thickness (total width) of the line.
- draw_line_arrow(img: NDArray[NPType], color: int | float | list[int | float] | tuple[int | float, ...], x0: int | float, y0: int | float, x1: int | float, y1: int | float, thickness: float = 1.0, arrow_head_length: int = 10, arrow_head_angle: int = 45) None[source]
Draws a line with an arrow head at the end point.
- Parameters:
img – The 2-D (or higher) array to draw on.
color – The scalar (or higher) color to draw.
x0 – The x-coordinate of the starting point.
y0 – The y-coordinate of the starting point.
x1 – The x-coordinate of the ending point.
y1 – The y-coordinate of the ending point.
thickness – The thickness of the line.
arrow_head_length – The length of the arrow head lines.
arrow_head_angle – The angle in degrees between the arrow head and the main line.
- draw_rect(img: NDArray[NPType], color: bool | int | float | list[int | float] | tuple[int | float, ...], xctr: int, yctr: int, xhalfwidth: int, yhalfwidth: int, thickness: int = 1, dot_spacing: int = 1) None[source]
Draw a rectangle with the given line thickness.
- Parameters:
img – The 2-D (or higher) array to draw on.
color – The scalar (or higher) color to draw.
xctr – The horizontal (column) center of the rectangle.
yctr – The vertical (row) center of the rectangle.
xhalfwidth – The horizontal half-width, on each side of the center.
yhalfwidth – The vertical half-width, on each side of the center.
thickness – The thickness (total width) of the line.
dot_spacing – The spacing between dots in the rectangle. 1 means dots are adjacent (solid lines).
All slice bounds are clipped to the array extent so a center near or beyond the image edge cannot wrap a negative index around to the far side and paint a spurious rectangle; an entirely off-image rectangle draws nothing.
- filter_downsample(arr: NDArray[floating[Any]], amt_y: int, amt_x: int) NDArray[floating[Any]][source]
Downsamples an array by averaging blocks of pixels.
- Parameters:
arr – The input array to downsample.
amt_y – The vertical downsampling factor.
amt_x – The horizontal downsampling factor.
- Returns:
The downsampled array.
- Raises:
AssertionError – If the array dimensions are not divisible by the downsampling factors.
- filter_local_maximum(data: NDArray[NPType], maximum_boxsize: int = 3, median_boxsize: int = 11, maximum_blur: int = 0, maximum_tolerance: float = 1.0, minimum_boxsize: int = 0, gaussian_blur: float = 0.0) NDArray[NPType][source]
Filter an array to find local maxima.
- Process:
Create a mask consisting of the pixels that are local maxima within maximum_boxsize
Find the minimum value for the area around each array pixel using minimum_boxsize
Remove maxima that are not at least maximum_tolerange times the local minimum
Blur the maximum pixels mask to make each a square area of maximum_blur X maximum_blur
Compute the median-subtracted value for each array pixel using median_boxsize
Copy the median-subtracted array pixels to a new zero-filled array where the maximum mask is true
Gaussian blur this final result
- Parameters:
data – The array
maximum_boxsize – The box size to use when finding the maximum value for the area around each pixel.
median_boxsize – The box size to use when finding the median value for the area around each pixel.
maximum_blur – The amount to blur the maximum filter. If a pixel is marked as a maximum, then the pixels in a blur X blur square will also be marked as a maximum.
maximum_tolerance – The factor above the local minimum that a maximum pixel has to be in order to be included in the final result.
minimum_boxsize – The box size to use when finding the minimum value for the area around each pixel.
gaussian_blur – The amount to blur the final result.
- Returns:
The filtered array.
- filter_sub_median(data: NDArray[floating[Any]], median_boxsize: int = 11, gaussian_blur: float = 0.0, footprint: str = 'square') NDArray[floating[Any]][source]
Compute the median-subtracted value for each pixel.
- Parameters:
data – The array
median_boxsize – The box size to use when finding the median value for the area around each pixel.
gaussian_blur – The amount to blur the median value before subtracting it from the array.
footprint – The shape of footprint to use (‘square’, ‘circle’).
- Returns:
The median-subtracted array.
- gaussian_blur_cov(img: NDArray[floating[Any]], sigma: NDArray[floating[Any]]) NDArray[floating[Any]][source]
Blur by anisotropic Gaussian with covariance matrix in frequency domain.
- Parameters:
img – A 2-D array to blur.
sigma – A 2x2 covariance matrix [[Syy, Syx], [Sxy, Sxx]].
- Returns:
The blurred image.
- Raises:
ValueError – If img is not 2-dimensional or sigma is not 2x2.
- gradient_magnitude(img: NDArray[floating[Any]]) NDArray[floating[Any]][source]
Compute isotropic gradient magnitude.
- Parameters:
img – The input array.
- Returns:
The gradient magnitude at each pixel, computed as sqrt(gx^2 + gy^2).
- next_power_of_2(n: int) int[source]
Computes the smallest power of 2 that is greater than or equal to n.
- Parameters:
n – A non-negative integer.
- Returns:
The smallest power of 2 that is >= n.
0returns1(the smallest power of 2 that is >= 0).- Raises:
ValueError – If
nis negative (a power of 2 >= a negative number is ill-defined, andbin()of a negative would be misparsed).
- normalize_array(a: NDArray[floating[Any]], eps: float = 1e-12) NDArray[floating[Any]][source]
Zero-mean, unit-std normalization (safe if nearly constant).
- Parameters:
a – The input array to normalize.
eps – Minimum standard deviation threshold. If std < eps, returns zeros.
- Returns:
Normalized array with mean 0 and std 1, or zeros if nearly constant.
- pad_array(array: NDArray[NPType], margin: list[int] | tuple[int, ...], fill: Any = 0) NDArray[NPType][source]
Pad an array by the given margin at each edge.
- Parameters:
array – The N-dimensional array to pad.
margin – A list or tuple of length N of margin amounts in the same order as the array indices. Each axis is padded to its original size plus 2*N and the original values are centered on the new axis.
fill – The value used to fill the newly created array elements.
- Returns:
an all-zero margin returns the same array object (no copy); a non-zero margin returns a fresh
np.padarray.- Return type:
The array padded by the given amount. Aliasing note
- Raises:
ValueError – If the array shape and margin list have different lengths.
- pad_array_to_power_of_2(data: NDArray[NPType]) tuple[NDArray[NPType], tuple[int, ...]][source]
Zero-pad a 2-D array on all sides to be a power of 2 in each dimension.
If the original array has a side that is not a power of 2, it must be even.
Returns: A tuple containing the padded array and the amount of padding added.
- pad_top_left(array: NDArray[NPType], v_size: int, u_size: int) NDArray[NPType][source]
Place array at (0,0) inside zeros(v_size, u_size).
- Parameters:
array – A 2-D array to place.
v_size – The height of the output array.
u_size – The width of the output array.
- Returns:
A (v_size, u_size) array with the input array at the top-left corner. If the input array is larger than the target size, it will be truncated.
- require_finite_int_or_float(name: str, value: object) None[source]
Validate stretch control numeric parameters at the public API boundary.
Used by stretch helpers and the UI control-builders to reject
bool, non-numeric, and non-finite inputs with a clear error message tagged by the parameter name.- Parameters:
name – Parameter name to include in the error message (e.g.
'black').value – Candidate value to validate.
- Raises:
TypeError – When
valueis aboolor not anint/float.ValueError – When
valueis not finite.
- shift_array(array: NDArray[NPType], offset: int | list[int] | tuple[int, ...], fill: Any = 0) NDArray[NPType][source]
Shift an array by an offset, filling with zeros or a specified value.
- Parameters:
array – The N-dimensional array to shift.
offset – A list or tuple of length N of offsets in the same order as the array indices. A positive offset shifts to higher numbers on the axis.
fill – The value used to fill the newly created array elements.
- Returns:
a zero offset returns the same array object (no copy); any non-zero offset returns a fresh copy. Do not mutate the result in place assuming it is always independent of the input.
- Return type:
The array shifted by the given amount. Aliasing note
- Raises:
ValueError – If the array shape and offset list have different lengths.
- unpad_array(array: NDArray[NPType], margin: list[int] | tuple[int, ...]) NDArray[NPType][source]
Remove a padded margin from each edge.
- Parameters:
array – The N-dimensional array to unpad.
margin – A list or tuple of length N of margin amounts in the same order as the array indices. Each axis is unpadded to its original size minus 2*N and the original centered values are used on the new axis.
- Returns:
this never copies – an all-zero margin returns the same array object and a non-zero margin returns a view (slice) into the input. Mutating the result therefore mutates the input; copy first if independence is needed.
- Return type:
The array unpadded by the given amount. Aliasing note
- Raises:
ValueError – If the array shape and margin list have different lengths.
Global image-quality helpers used during orchestrator preflight.
Three image-level checks happen before any technique runs. This module owns the implementations:
saturation_mask: pixels at or above the per-instrument full-well DN.cosmic_ray_mask: single-pixel spikes above 5 sigma over a median-3x3 neighborhood.
Each helper operates on the entire image and produces a boolean mask consumed by feature extractors and the gradient computation. Image-side operations are global by design — no helper here crops to a predicted position.
- cosmic_ray_mask(image: NDArray[floating[Any]], *, image_noise_sigma: float, k_sigma: float = 5.0) NDArray[bool][source]
Boolean mask flagging single-pixel spikes above a sigma threshold.
Compares each pixel against a 3x3 median around it (reflect boundary handling); pixels whose excess over the local median exceeds
k_sigma * image_noise_sigmaare flagged. Suitable as a cheap pre-detection step that prevents single hot pixels from masquerading as star detections in pattern matching.- Parameters:
image – 2-D float input array.
image_noise_sigma – Global MAD-based noise sigma (DN units); typically from
estimate_image_noise_sigma.k_sigma – Multiplier on
image_noise_sigma(default 5).
- Returns:
Boolean mask of the same shape as
image.- Raises:
TypeError – if
imageis not 2-D.ValueError – if
image_noise_sigmais non-positive.
- saturation_mask(image: NDArray[floating[Any]], *, full_well_dn: float) NDArray[bool][source]
Boolean mask with True where pixels are at or above the saturation DN.
- Parameters:
image – 2-D float input array.
full_well_dn – Per-instrument saturation DN. Pixels at or above this value are flagged as saturated.
- Returns:
Boolean mask of the same shape as
image.- Raises:
TypeError – if
imageis not 2-D.
- current_git_version() str[source]
Return the git version of the current repo, caching the result.
The result is cached because the git version cannot change during a single program run.
- Returns:
The git describe string, or ‘GIT DESCRIBE FAILED’ if the command fails.
- dec_rad_to_dms(dec: float) str[source]
Converts declination in radians to a formatted string in degrees, minutes, and seconds.
- Parameters:
dec – Declination value in radians.
- Returns:
Formatted string in the form “+/-DDDdMMmSS.SSSs”.
- flatten_list(lst: list[Any]) list[Any][source]
Flattens a list of lists into a single list.
- Parameters:
lst – The list to flatten.
- Returns:
A flattened list.
- get_local_host_name() str[source]
Return this machine’s fully qualified domain name as a string.
The value is obtained from
socket.getfqdn()on the first call and stored in the module-level_LOCAL_HOST_NAME_CACHEso later calls return the same string without callinggetfqdnagain.- Returns:
The FQDN string on success, or the literal
'LOCAL HOST NAME FAILED'ifsocket.getfqdn()raises any exception.
- Side effects:
On the first successful call, sets
_LOCAL_HOST_NAME_CACHEto the FQDN. On the first failing call, sets_LOCAL_HOST_NAME_CACHEto'LOCAL HOST NAME FAILED'. Subsequent calls return the cached value and do not callsocket.getfqdn()again.
- log_run_environment(logger: PdsLogger, command_list: list[str]) None[source]
Log host, git, and command-line context to the given logger.
Call once at process startup on the main logger (e.g.
sd_mosaicaftersetup_logging). Per-image loggers may omit this to avoid duplicating the same block on the console when handlers mirror output toMAIN_LOGGER.- Parameters:
logger – The logger to write to.
command_list – The command-line arguments for the current run (typically
sys.argv[1:]).
- mad_std(a: NDArray[floating[Any]] | list[float]) float[source]
Median absolute deviation (MAD) standard deviation.
- Parameters:
a – Sample values; must contain at least one finite element.
- Returns:
The robust standard-deviation estimate
1.4826 * MAD.- Raises:
ValueError – If
ais empty or contains no finite values, which would otherwise return a silentNaNthat poisons every downstream covariance / confidence computation.
- ra_rad_to_hms(ra: float) str[source]
Converts right ascension in radians to a formatted string in hours, minutes, and seconds.
- Parameters:
ra – Right ascension value in radians.
- Returns:
Formatted string in the form “HHhMMmSS.SSSs”.
- Raises:
ValueError – If the right ascension value is negative.
- safe_lstrip_zero(s: str) str[source]
Strips leading zeros from a string but leaves one zero behind if that’s all there is.
- Parameters:
s – The string to strip leading zeros from.
- Returns:
The string with leading zeros stripped.
Robust per-image noise estimate over the sensor area.
The autonomous-navigation orchestrator computes image_noise_sigma once per
image and stores it on NavContext so every extractor and technique uses
the same value. This module owns the implementation.
The estimate is global – it does not require knowledge of where any feature lives in the image, and is therefore not biased by a wrong SPICE pointing prediction. It is computed from a 3x3 second-difference (Laplacian) response so that smooth scene structure (ring brightness ramps, limb shading, a bright extended disc) cancels and only pixel-to-pixel noise survives; the MAD over that response further rejects the minority of pixels sitting on sharp edges or cosmic rays. A plain MAD of the raw intensities is not used: when the scene is dominated by structure (for example rings filling the frame) it measures the bright-to-dark spread rather than the noise and overestimates sigma by orders of magnitude, which in turn pushes the edge-detection threshold above every real gradient and empties the distance transform.
- estimate_background_and_sky_sigma(image: NDArray[floating[Any]], valid_mask: NDArray[bool], *, noise_sigma: float, clip_sigma: float = 3.0) tuple[float, float][source]
Estimate the frame’s background pedestal and sky-noise sigma.
Returns two frame-global quantities body-brightness photometry needs (the BODY_BLOB centroid moment in
BodyBlobNavand the BODY_BLOB detection SNR inNavModelBodyBaseshare this estimate):Background (bias + dark) pedestal. Real raw frames carry a non-zero bias/dark pedestal, and the sim adds a
bias_dnpedestal so dark sky is distinguishable from the missing-data marker. That pedestal sits on every pixel, so without subtracting it a brightness-weighted moment is pulled toward the bbox geometric center – a bias that grows with phase as the lit signal concentrates away from center.Sky-noise sigma. Lit-pixel thresholds must reject sky noise, but the global
image_noise_sigmais a MAD over the whole sensor, which a bright body inflates (its brightness gradient widens the global spread). An inflated sigma raises the threshold and cuts the dim crescent. The sky noise estimated here, over the body-excluded sky population, is the correct floor; for a small body it equals the global sigma, so this only matters when a large body would otherwise inflate it.
Both are estimated over the valid sky region (sensor data, excluding saturation / cosmic rays) by seeding at the overall median and the global sigma, then rejecting the bright body (the high tail) and re-estimating the median and MAD of the surviving sky population. On a zero-background fixture this returns
(~0, ~0).- Parameters:
image – The extended-FOV image (native units).
valid_mask –
Truewhere the pixel is real sensor data and not saturated / a cosmic ray (the sky-candidate population).noise_sigma – Global per-pixel image noise sigma (native units); seeds the sky-population selection.
clip_sigma – High-tail rejection threshold in sigmas; pixels above
background + clip_sigma * sky_sigmaare excluded from the sky population on each iteration.
- Returns:
(background, sky_sigma)in the image’s native units;(0.0, max(noise_sigma, 1e-9))when there are no valid pixels.
- estimate_image_noise_sigma(image: NDArray[floating[Any]], sensor_mask: NDArray[bool] | None = None) float[source]
Return a robust noise sigma over the sensor pixels.
The estimate is the MAD-based standard deviation of a 3x3 second-difference (Laplacian) response, divided by 6. The second difference cancels smooth brightness structure so the value reflects pixel-to-pixel noise rather than scene content, and the MAD rejects the minority of pixels on sharp edges or cosmic rays.
- Parameters:
image – 2-D float input array.
sensor_mask – Optional boolean mask with
Truefor sensor pixels andFalsefor extfov padding. IfNone, every pixel ofimageis treated as sensor data. Only response pixels whose full 3x3 neighbourhood lies inside the mask contribute.
- Returns:
Robust noise sigma in the same DN units as
image.- Raises:
TypeError – if
imageis not 2-D.ValueError – if no sensor pixels are available.
NavStatusReason — typed enumeration of every navigation outcome reason.
A single NavStatusReason value is attached to every NavResult produced by
the orchestrator. The value tells callers, in one slot, exactly why the
result has its status — success modes, image-quality refusals, kernel
problems, ensemble disagreement, and degenerate-geometry refusals.
Located under spindoctor.support (not spindoctor.nav_orchestrator) because some values
describe failure modes that are not strictly navigation outcomes — image-load
errors, missing kernels, instrument-not-configured — and are produced by code
outside the orchestrator (image-quality classifier, kernel-loading shim,
dataset enumeration).
Bases:
StrEnumDiscrete outcome reasons set on every NavResult.
The value tells downstream consumers exactly why the navigation has its final status. Each value maps to a specific outcome:
OK: normal success.RANK_1_ONLY: success with only one observable axis (e.g. flat-ring scene with no orthogonal feature);sigma_along_unobservable_pxis set on the result.CONFLICTED_TECHNIQUES: multiple agreement groups exist after grouping and the best-vs-runner-up summed-confidence gap is belowagreement_gap; offset reported with reduced confidence.NO_SIGNAL_IN_IMAGE: image classifier flagged a blank or dark frame.IMAGE_OVEREXPOSED: image classifier saw most pixels at full-well DN.MISSING_DATA_DOMINANT: image classifier saw too many missing-data pixels.IMAGE_CORRUPT: image file failed to parse or read.KERNELS_UNAVAILABLE: SPICE coverage missing for the image ET.INSTRUMENT_NOT_CONFIGURED: no per-instrument YAML block for this camera.TITAN_UNSUPPORTED: the only navigable content in the frame is Titan, whose opaque haze hides the surface, so no shape-based or haze-limb navigation is available; the pipeline records the refusal instead of a silent empty failure.NO_FEATURES_EXTRACTED: every extractor returned an empty list.ALL_FEATURES_GATED: features extracted but every one fell below the reliability gate.NO_FEASIBLE_TECHNIQUES: features pass the gate but no technique’sis_feasiblereturned true.ALL_TECHNIQUES_SPURIOUS: every technique returnedspurious=True.FINAL_CONFIDENCE_BELOW_THRESHOLD: ensemble combined confidence sat belowmin_confidence(or below every tier’smin_confidence).FINAL_SIGMA_ABOVE_THRESHOLD: combined confidence cleared the lowest tier’smin_confidencebut the offset sigma exceeded every tier’smax_sigma_px(confident but too imprecise to earn any tier).UNOBSERVABLE_OFFSET: every input covariance shares one null direction; the precision-weighted combine cannot proceed.CONTRACT_VIOLATION: an internal navigation invariant was violated (NavContractError); a programming error upstream, not bad image data. The full traceback is in the error log.
Annotated-summary-PNG rendering shared by the autonomous and manual paths.
The autonomous pipeline (spindoctor.navigate_image_files._write_summary_png)
and the manual-navigation dialog both produce a labelled overlay PNG of
the source image with each NavModel’s annotation drawn on top. The
rendering logic lives here so both code paths produce visually
identical PNGs from the same (obs, annotations, offset_px) triple.
- grayscale_to_rgb_with_quantile_stretch(image: NDArray[floating[Any]]) NDArray[uint8][source]
Build a uint8 RGB grayscale background from a float image.
The black point is fixed at the 0.001 quantile. The white point adapts to the number of “bright” pixels in the image: the default 0.999 quantile clips the top 0.1 % of pixels, but on an image with only a handful of bright outliers (a sparse star field over dark sky, a distant body against empty sky) that fixed clip count saturates every bright pixel to 255 even though the brightest is much brighter than the rest.
The fix counts the bright outliers via a robust median + 15 * MAD threshold and clips at most half of them — so the brightest few are saturated but the remaining bright pixels keep their relative brightness ordering. When the image carries many bright pixels (a body filling the FOV, a busy ring scene) the original 0.1 % behavior dominates and nothing about the existing visualization changes.
- render_annotated_summary_rgb(obs: ObsSnapshot, annotations: Annotations, offset_px: tuple[float, float] = (0.0, 0.0)) NDArrayUint8Type[source]
Composite
obs.datawithannotations.combineatoffset_px.Builds a quantile-stretched grayscale background from the FOV image, asks the annotations layer for its FOV-shaped RGB overlay at the requested offset, and replaces every pixel where the overlay carries any non-zero color channel. When the annotations collection is empty, the returned RGB is the source-image grayscale alone — so the result is always a faithful record of what the navigator saw.
- Parameters:
obs – Observation snapshot supplying the background image.
annotations – Merged
Annotationscollection from every NavModel that contributed.offset_px –
(dv, du)offset that shifts the overlay onto the best-fit pose. The convention matches every other offset in the pipeline: predicted + offset = actual.
- Returns:
(H, W, 3)uint8 RGB array in FOV coordinates.
- dt_delta_str(start_time: datetime, end_time: datetime) str[source]
Returns the difference between two datetime objects as a string representation.
- Parameters:
start_time – The starting datetime.
end_time – The ending datetime.
- Returns:
String representation of the time difference.
- et_to_utc(et: float, digits: int = 3) str[source]
Returns the UTC time for a given ET time.
- Parameters:
et – The SPICE ET time (equivalent to TDB).
digits – The number of digits to include after the decimal point.
- Returns:
The UTC time as a string.
- now_dt() datetime[source]
Returns the current time as a datetime object with timezone information.
- Returns:
Current time as a timezone-aware datetime object.
- now_iso() str[source]
Returns the current time as an ISO 8601 formatted string with timezone information.
- Returns:
Current time as an ISO 8601 formatted string.
- utc_to_et(utc: str) float[source]
Returns the ET time (TDB seconds) for a given UTC time string.
- Parameters:
utc – The UTC time as an ISO 8601 formatted string (e.g., “2008-01-01 12:00:00” or “2008-01-01T12:00:00”).
- Returns:
The SPICE ET time (equivalent to TDB) in seconds as a float.
Shared type aliases and protocols used across the nav package.
This module defines the numpy-array aliases (NDArrayBoolType,
NDArrayFloatType, NDArrayIntType, NDArrayUint8Type,
NDArrayUint32Type, NDArrayType), the generic NPType type
variable, the PathLike union accepted by I/O helpers, and the
MutableStar protocol describing the in-memory star-record shape
used by the star-catalog reduction code.
Centralising these aliases keeps every import site aligned on a single spelling for the heavily-used numpy types and lets a downstream module narrow them in one place.