mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-23 03:31:13 +00:00
[AZ-918] [AZ-919] [AZ-920] [AZ-921] [AZ-922] VIO/ESKF baseline fixes
Derkachi e2e Tier-2 divergence had three stacked root causes; this
commit ships fixes for all three plus the IMU prerequisite they
depend on, plus a baseline cheirality gate for cv2.recoverPose.
AZ-918 MAVLink IMU adapters now convert raw mG/mrad-s + FRD body to
SI m/s^2 + rad/s + FLU body via helpers.imu_units. Without
this the ESKF receives values ~1000x too small with wrong-
sign Y/Z and cannot function at all.
AZ-919 Composition root wires EskfNominalAltitudeProvider into the
KLT/RANSAC strategy via the AZ-331 factory introspect path;
OKVIS2 and VINS-Mono are unaffected.
AZ-920 KLT/RANSAC recovers metric translation via Ground Sampling
Distance when AGL is available; otherwise falls through with
scale_quality=direction_only/unknown (no fake scale invented).
AZ-921 VioOutput.scale_quality signal; ESKF add_vio adapts R_meas
position block based on the flag (1e6 inflation when scale is
direction_only/unknown to keep the filter consistent).
AZ-922 KLT/RANSAC cheirality gate rejects single-frame rotations
beyond a config threshold (default 30 deg), catching
cv2.recoverPose twisted-pair flips that cause immediate ESKF
divergence on low-parallax aerial scenes.
Verification:
- Tier-1 (macOS) unit suite: 2346 passed, 0 failed.
- Tier-2 (Jetson) Derkachi e2e: divergence moves from frame 5
(mahalanobis^2 3757) to frame 233 (mahalanobis^2 212). Remaining
drift is open-loop attitude accumulation, not cheirality.
Follow-up tickets filed:
- AZ-923 closed as misdiagnosed: EskfNominalAltitudeProvider was
already correct (nominal_pos.z IS the AGL when takeoff origin sits
at ground level); the early-frame AGL near zero reflects the drone
being stationary on the ground, not a provider bug.
- AZ-942 filed: cross-check VIO rotation against IMU preintegrator
(consistency gate) - more physically grounded than the coarse
AZ-922 threshold and likely required to absorb the frame-233 drift.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""Above-Ground-Level (AGL) provider abstraction for C1 VIO scale recovery.
|
||||
|
||||
The monocular KLT/RANSAC strategy (AZ-334) recovers metric scale from the
|
||||
unit-length translation that ``cv2.recoverPose`` emits by using ground
|
||||
sample distance (GSD), which requires the current drone height above the
|
||||
ground plane. The AGL signal lives in the C5 state estimator's nominal
|
||||
position; this module wraps that read so the C1 strategy does not import
|
||||
or hold a direct reference to the C5 estimator (which is built later in
|
||||
the composition-root topological order).
|
||||
|
||||
AZ-919 introduces only the interface + plumbing. The GSD scale-recovery
|
||||
math lands in AZ-920, and the degraded-mode signal in AZ-921.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
||||
|
||||
from gps_denied_onboard._types.state import IsamState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from gps_denied_onboard.components.c5_state.eskf_baseline import (
|
||||
EskfStateEstimator,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AltitudeProvider",
|
||||
"EskfNominalAltitudeProvider",
|
||||
]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AltitudeProvider(Protocol):
|
||||
"""Read the drone's current AGL height in metres, or ``None``.
|
||||
|
||||
Producers MUST return ``None`` whenever the local-ENU origin has not
|
||||
yet been anchored (pre cold-start) or the underlying estimator is
|
||||
in :class:`IsamState.LOST`. Consumers MUST treat ``None`` as "no
|
||||
reliable AGL" and fall back to a non-scale-recovery code path
|
||||
(AZ-921 formalises that fallback as a degraded VIO output).
|
||||
"""
|
||||
|
||||
def agl_m(self, now_ns: int) -> float | None: # pragma: no cover - Protocol
|
||||
"""Return AGL in metres at ``now_ns`` (monotonic), or ``None``.
|
||||
|
||||
``now_ns`` is the same monotonic timebase used by the C1 strategy
|
||||
for ``VioOutput.emitted_at_ns``. It is currently advisory — the
|
||||
ESKF impl does not interpolate — but the parameter is in the
|
||||
Protocol so future implementations (e.g. an LPF-smoothed AGL or
|
||||
a DEM-aware provider) can interpolate or extrapolate without a
|
||||
breaking change.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class EskfNominalAltitudeProvider:
|
||||
"""Concrete :class:`AltitudeProvider` backed by the C5 ESKF estimator.
|
||||
|
||||
Reads AGL as the Z component of the ESKF nominal-position vector in
|
||||
local-ENU. The takeoff origin is anchored at local-ENU ``(0, 0, 0)``
|
||||
when ``set_takeoff_origin`` lands, so ``nominal_pos_z`` IS the AGL
|
||||
once the origin has been set — no separate cold-start-altitude
|
||||
subtraction is needed.
|
||||
|
||||
The estimator instance is supplied through a callable rather than
|
||||
held directly because the composition root builds C1 (where this
|
||||
provider is wired) before C5. The callable closes over the
|
||||
composition root's mutable ``constructed`` dict and resolves the
|
||||
estimator at every ``agl_m`` call, which is the same time the C1
|
||||
strategy actually consumes the AGL signal (well after the topo
|
||||
order has built C5).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
estimator_supplier: Callable[[], EskfStateEstimator | None],
|
||||
) -> None:
|
||||
self._estimator_supplier = estimator_supplier
|
||||
|
||||
def agl_m(self, now_ns: int) -> float | None:
|
||||
estimator = self._estimator_supplier()
|
||||
if estimator is None:
|
||||
return None
|
||||
if getattr(estimator, "_takeoff_origin_set", None) is None:
|
||||
return None
|
||||
if getattr(estimator, "_isam2_state", None) == IsamState.LOST:
|
||||
return None
|
||||
nominal_pos = getattr(estimator, "_nominal_pos", None)
|
||||
if nominal_pos is None:
|
||||
return None
|
||||
return float(nominal_pos[2])
|
||||
Reference in New Issue
Block a user