spindoctor.ui
Interactive manual-navigation dialog (PyQt6).
Renders a composite of the predicted scene against the source image and
lets an operator pick a translation offset by hand. An Auto button
runs the same masked-NCC pyramid that correlation-based techniques use,
so the operator can either accept the auto-pick or override it.
This module is GUI-only; no autonomous-pipeline code imports it. It is
constructed by NavTechniqueManual (which is opted out of the
auto-discovery registry) on demand.
- class ManualNavDialog(*args: Any, **kwargs: Any)[source]
Bases:
QDialogManual navigation dialog for overlaying image and combined model.
The viewport uses false color (image in red/blue, blend in green). Separate Image Stretch and Model Stretch groups each provide black/white/gamma and a display toggle. Model transparency and an optional binary mask overlay sit under model stretch.
Save-as-library-entry helper for the manual-navigation dialog.
The manual-nav dialog drives library curation: an operator picks an
offset by hand, then clicks “Save as Library Entry…” to drop a sidecar
into tests/integration/image_library/images/<class>/<image_id>.yaml.
This module owns the YAML template and the obs-introspection helper so
the dialog (which is otherwise PyQt-only) keeps a thin UI surface.
The emitted YAML carries TODO-style placeholders for every field the
dialog cannot infer from the observation (scene class, expected
technique, etc.). The library’s own structural-invariants test
(tests.integration.test_image_library) refuses to load a sidecar
that still has placeholder enum values, so the operator must edit the
file before committing it.
- class LibraryEntryDraft(image_id: str, mission: str, camera: str, filter_combo: str, exposure_time_sec: float | None, image_datetime_utc: str | None)[source]
Bases:
objectAuto-inferred fields the dialog seeds into a fresh sidecar.
- build_sidecar_yaml(*, draft: LibraryEntryDraft, image_url: str, offset_dv_px: float, offset_du_px: float, ui_version: str, operator: str | None = None, today: date | None = None, constraint_normal_angle_deg: float | None = None) → str[source]
Render the sidecar as a YAML string with placeholders for the rest.
The output is deliberately not an opaque blob: every placeholder is a clear
TODO_REPLACE_*string so the operator knows exactly what to edit before committing. Fields the dialog can infer (offset_*_px,mission,camera,filter_combo,exposure_time_sec,image_id) are filled in directly.When
constraint_normal_angle_degis set (the operator navigated with the constrained-drag mode on a rank-1 scene), the ground truth carries aconstraintblock —offset_along_normal_pxcomputed from the saved 2-D offset so the two stay consistent by construction — andexpectedpinsstatus_reason: rank_1_only.
- compute_image_url(obs: Any, config: Any = None) → str[source]
Return the best
image_urlfor the sidecar.When the obs’s absolute path lives under a known PDS3 holdings root (resolved from
PDS3_HOLDINGS_DIRfirst, thenconfig.environment.pds3_holdings_root), the URL is rewritten to the opaquepds3://<relative-path>form the regression test re-resolves at load time. Otherwise the raw absolute path is returned so the operator at least sees what was navigated.- Parameters:
obs – Observation snapshot;
obs.abspathis the file path.config – Optional
Configoverride for theenvironment.pds3_holdings_rootfallback.
- Returns:
Either
'pds3://<relative-path>'or the raw absolute path as a string. Empty string whenobscarries noabspath.
- infer_obs_metadata(obs: Any) → LibraryEntryDraft[source]
Pull the sidecar’s auto-fillable fields off an observation snapshot.
Missing or unknown fields are returned as empty strings (or
Noneforexposure_time_sec/image_datetime_utc) so the operator sees them in the YAML and can decide what to write; an empty mission/camera tripstests.integration.sidecar.load_sidecar()on validation, which is the right error.
spindoctor.ui.mosaic_viewer
Projection math for alternate 2-D display modes and the 3-D sphere view.
All projection functions operate in normalized display units. The conversion between normalized units and viewport pixels uses the linear transform:
viewport_x = normalized_x * scale + cx
viewport_y = normalized_y * scale + cy
where (cx, cy) is the projection centre in viewport pixels and scale
is pixels per normalized unit. ProjectionParams carries these values.
Normalized-unit conventions per kind
RECT – 1 unit = 1 degree. x = longitude [0, 360), y = -latitude. This projection is rendered by the existing body-sphere canvas path; these functions are used only for graticule overlays.
POLAR_N – 1 unit = stereographic equatorial radius. r = 0 is the north pole, r = 1 is the equator. 0 deg longitude appears at the top (screen-y direction).
POLAR_S – same as POLAR_N with latitude sign-flipped. r = 0 is the south pole, r = 1 is the equator.
MOLLWEIDE – 1 unit = sqrt(2). The full-globe ellipse spans x in [-2*sqrt(2), 2*sqrt(2)] and y in [-sqrt(2), sqrt(2)].
SPHERE_3D – 1 unit = sphere radius. The visible disk has r <= 1. The camera views from the +X direction; yaw/pitch define which geographic point faces the camera.
Coordinate system
Screen y increases downward (PyQt6 convention). Normalized y also increases
downward to match, so vy = yn * scale + cy with no sign flip needed in
the viewport transform.
- class ProjectionKind(*values)[source]
Bases:
EnumAvailable projection modes for the body mosaic viewer.
- MOLLWEIDE = 'mollweide'
- POLAR_N = 'polar_n'
- POLAR_S = 'polar_s'
- RECT = 'rect'
- SPHERE_3D = 'sphere_3d'
- class ProjectionParams(kind: ProjectionKind, cx: float, cy: float, scale: float, yaw_deg: float = 0.0, pitch_deg: float = 0.0)[source]
Bases:
objectViewport geometry for a single projection frame.
- Parameters:
kind – Which projection to use.
cx – Viewport X coordinate of the projection centre (pixels from left).
cy – Viewport Y coordinate of the projection centre (pixels from top).
scale – Pixels per normalized unit (see module docstring).
yaw_deg – SPHERE_3D only – longitude at the centre of the view (deg).
pitch_deg – SPHERE_3D only – latitude at the centre of the view (deg).
- kind: ProjectionKind
- display_to_lonlat(vx: ndarray, vy: ndarray, params: ProjectionParams) → tuple[ndarray, ndarray, ndarray][source]
Map viewport pixel positions to geographic coordinates.
- Parameters:
vx – Viewport X pixel array.
vy – Viewport Y pixel array.
params – Current projection parameters.
- Returns:
Tuple
(lon_deg, lat_deg, valid)wherevalidis a bool mask (False for pixels outside the valid domain of the projection).- Raises:
ValueError – If
params.kindis unknown orparams.scaleis zero / non-finite.
- fit_scale(kind: ProjectionKind, vw: int, vh: int) → float[source]
Return a scale (pixels per normalized unit) that fits the projection.
The result leaves a margin
_FIT_SCALE_MARGIN(10 % inset) around the projection so it does not touch the viewport edge at the initial zoom level.- Parameters:
kind – Projection kind.
vw – Viewport width in pixels.
vh – Viewport height in pixels.
- Returns:
Scale in pixels per normalized unit.
- Raises:
ValueError – If
vworvhis not strictly positive.
- lonlat_to_display(lon_deg: ndarray, lat_deg: ndarray, params: ProjectionParams) → tuple[ndarray, ndarray, ndarray][source]
Map geographic coordinates to viewport pixel positions.
- Parameters:
lon_deg – Longitude array (deg), any range.
lat_deg – Latitude array (deg), values in [-90, 90].
params – Current projection parameters.
- Returns:
Tuple
(vx, vy, visible)wherevxandvyare float64 arrays of viewport pixel coordinates andvisibleis a bool array (False for points not representable in this projection atparams).- Raises:
ValueError – If
params.kindis unknown orparams.scaleis zero / non-finite.
- sphere_pixel_to_lonlat(vx: ndarray, vy: ndarray, params: ProjectionParams) → tuple[ndarray, ndarray, ndarray][source]
Ray-cast orthographic sphere: viewport pixels to (lon_deg, lat_deg, hit).
Points outside the sphere disk or on the back hemisphere return
hit=False.- Parameters:
vx – Viewport X pixel array.
vy – Viewport Y pixel array.
params – Projection parameters;
kindmust beSPHERE_3D.
- Returns:
Tuple
(lon_deg, lat_deg, hit).- Raises:
ValueError – If
params.kindis notSPHERE_3Dorparams.scaleis zero / non-finite.
Software renderer for non-rectangular body-mosaic projections.
render_to_image() converts a (lon_deg, lat_deg, valid) coordinate grid
– produced by projections – into a
QImage (Format_RGB888) using nearest-neighbour texture lookup, the same
contrast-stretch pipeline as the existing rectangular render path, and optional
per-pixel metadata tinting.
The function is intentionally stateless and side-effect-free so it can be called from any paint method without lock concerns.
- render_to_image(image_ma: MaskedArray, *, lon_deg: ndarray, lat_deg: ndarray, valid: ndarray, lon_min_deg: float, lat_min_deg: float, d_lon_deg: float, d_lat_deg: float, lon_bin_to_dc: ndarray, n_full_lon: int, n_data_rows: int, n_data_cols: int, black: float, white: float, gamma: float, color_tint: ndarray | None) → QImage[source]
Render a mosaic texture onto an arbitrary projection grid.
- Parameters:
image_ma – 2-D masked array (n_data_rows, n_data_cols) with the photometrically-corrected mosaic data.
lon_deg – 2-D float64 array of longitudes (deg) for every output pixel, in [0, 360). After binning to
k, any pixel withlon_bin_to_dc[k] == -1has no data column and is off-grid (no-data whenvalidis True).lat_deg – 2-D float64 array of latitudes (deg), in [-90, 90]. Latitudes outside
[lat_min_deg, lat_min_deg + n_data_rows * d_lat_deg)map todroutside[0, n_data_rows)and are off-grid (no-data whenvalidis True).valid – 2-D bool array; False pixels are painted black (off-projection background). True pixels with no usable data (outside the file’s lat/lon extent or explicitly masked) are painted dark red.
lon_min_deg – Geographic minimum longitude of the data grid (deg).
lat_min_deg – Geographic minimum latitude of the data grid (deg).
d_lon_deg – Longitude resolution of the data grid (deg/column).
d_lat_deg – Latitude resolution of the data grid (deg/row).
lon_bin_to_dc – 1-D int32 array mapping full-circle longitude bin index to data column index (-1 = absent).
n_full_lon – Length of
lon_bin_to_dc.n_data_rows – Number of rows in
image_ma.n_data_cols – Number of columns in
image_ma.black – Stretch black point.
white – Stretch white point.
gamma – Stretch gamma (applied as power).
color_tint – Optional per-pixel RGB tint, shape (n_data_rows, n_data_cols, 3), float32 in [0, 1]. None = greyscale.
- Returns:
A QImage in Format_RGB888 matching the shape of
lon_deg/lat_deg.- Raises:
ValueError – If
d_lon_degord_lat_degis zero, ifgammais not positive, iflen(lon_bin_to_dc) != n_full_lon, or iflon_deg,lat_deg, andvaliddo not share the same shape.
Lat/lon graticule polylines for non-rectangular body-mosaic projections.
graticule_polylines() samples parallels and meridians in geographic
coordinates, projects them through the current ProjectionParams, and
returns screen-space polyline segments ready for QPainter.drawPolyline.
Polylines are split wherever consecutive samples jump discontinuously (e.g. at the limb of a sphere or a map seam), so each returned segment is a continuous visible arc.
- graticule_label_anchors(params: ProjectionParams, *, lat_step_deg: float, lon_step_deg: float) → tuple[list[tuple[float, float, str]], list[tuple[float, float, str]]][source]
Return label anchor positions for visible graticule lines.
Finds the best visible point along each parallel and meridian at which to draw a tick label. For parallels the anchor is the point closest to the screen centre along the line; for meridians similarly.
- Parameters:
params – Current projection parameters.
lat_step_deg – Spacing of parallels (deg).
lon_step_deg – Spacing of meridians (deg).
- Returns:
Tuple
(parallel_anchors, meridian_anchors). Each anchor is(vx, vy, label_text).- Raises:
TypeError – If
paramsisNone.ValueError – If
lat_step_degorlon_step_degis negative.
- graticule_polylines(params: ProjectionParams, *, lat_step_deg: float, lon_step_deg: float, samples_per_line: int = 181, show_parallels: bool = True, show_meridians: bool = True) → tuple[list[list[tuple[float, float]]], list[list[tuple[float, float]]]][source]
Compute graticule screen-space polylines for the given projection.
Each “line” in the returned lists is a list of (vx, vy) tuples that can be drawn as a connected polyline. Lines are pre-split at discontinuities (sphere limb, map seam) so callers do not need to check for jumps.
- Parameters:
params – Current projection parameters. For RECT mode the caller should use the existing straight-line overlay path instead.
lat_step_deg – Spacing of parallels (deg). Pass 0 to disable.
lon_step_deg – Spacing of meridians (deg). Pass 0 to disable.
samples_per_line – Number of sample points along each graticule line.
show_parallels – Include parallel (constant-latitude) lines.
show_meridians – Include meridian (constant-longitude) lines.
- Returns:
Tuple
(parallel_lines, meridian_lines)where each element is a list of polyline segments. A segment is a list of(vx, vy)float tuples.- Raises:
TypeError – If
paramsisNone.ValueError – If
lat_step_degorlon_step_degis negative, or ifsamples_per_lineis less than 2.