mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 10:11: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:
@@ -802,6 +802,7 @@ def _run_replay_loop(config: Config, runtime: RuntimeRoot) -> int:
|
||||
EstimatorDegradedError,
|
||||
EstimatorFatalError,
|
||||
)
|
||||
from gps_denied_onboard.helpers.imu_units import mavlink_imu_to_si_flu
|
||||
from gps_denied_onboard.logging import get_logger
|
||||
from gps_denied_onboard.replay_input.csv_ground_truth import (
|
||||
load_csv_ground_truth,
|
||||
@@ -993,9 +994,10 @@ def _run_replay_loop(config: Config, runtime: RuntimeRoot) -> int:
|
||||
Branches on ``using_csv`` so the closure stays a single
|
||||
definition. Both branches respect the same ``pending_imu``
|
||||
buffer + ``imu_anchor_ns`` / ``imu_eof`` state and produce
|
||||
:class:`ImuSample` instances with identical numeric semantics
|
||||
— the tlog branch matches :meth:`TlogReplayFcAdapter._handle_imu`
|
||||
for byte-for-byte compatibility with the legacy path.
|
||||
:class:`ImuSample` instances in SI/FLU per the ImuSample
|
||||
contract — the tlog branch routes raw MAVLink fields through
|
||||
:func:`mavlink_imu_to_si_flu` so it matches the CSV branch
|
||||
and :meth:`TlogReplayFcAdapter._handle_imu` (AZ-918).
|
||||
"""
|
||||
nonlocal imu_anchor_ns, imu_eof, csv_imu_idx
|
||||
while not imu_eof:
|
||||
@@ -1018,10 +1020,18 @@ def _run_replay_loop(config: Config, runtime: RuntimeRoot) -> int:
|
||||
ts_ns = int(getattr(msg, "time_usec", 0)) * 1000
|
||||
if ts_ns == 0:
|
||||
continue
|
||||
accel_si_flu, gyro_si_flu = mavlink_imu_to_si_flu(
|
||||
xacc=float(msg.xacc),
|
||||
yacc=float(msg.yacc),
|
||||
zacc=float(msg.zacc),
|
||||
xgyro=float(msg.xgyro),
|
||||
ygyro=float(msg.ygyro),
|
||||
zgyro=float(msg.zgyro),
|
||||
)
|
||||
sample = ImuSample(
|
||||
ts_ns=ts_ns,
|
||||
accel_xyz=(float(msg.xacc), float(msg.yacc), float(msg.zacc)),
|
||||
gyro_xyz=(float(msg.xgyro), float(msg.ygyro), float(msg.zgyro)),
|
||||
accel_xyz=accel_si_flu,
|
||||
gyro_xyz=gyro_si_flu,
|
||||
)
|
||||
if imu_anchor_ns is None:
|
||||
imu_anchor_ns = sample.ts_ns
|
||||
|
||||
@@ -58,6 +58,7 @@ from typing import TYPE_CHECKING, Any, Final
|
||||
from gps_denied_onboard._types.calibration import CameraCalibration
|
||||
from gps_denied_onboard.clock.wall_clock import WallClock
|
||||
from gps_denied_onboard.fdr_client.client import make_fdr_client
|
||||
from gps_denied_onboard.helpers.altitude_provider import EskfNominalAltitudeProvider
|
||||
from gps_denied_onboard.helpers.feature_extractor import OpenCvOrbExtractor
|
||||
from gps_denied_onboard.helpers.imu_preintegrator import (
|
||||
ImuPreintegrator,
|
||||
@@ -305,7 +306,18 @@ def _require(constructed: Mapping[str, Any], key: str, component_slug: str) -> A
|
||||
|
||||
def _c1_vio_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
fdr_client = _require(constructed, "c13_fdr", "c1_vio")
|
||||
return build_vio_strategy(config, fdr_client=fdr_client)
|
||||
# AZ-919: AGL is read from the C5 ESKF estimator. C5 is built AFTER C1
|
||||
# in the topological order, so the provider holds a callable that
|
||||
# resolves the estimator from ``constructed`` at call time (i.e. when
|
||||
# the strategy actually consumes AGL during ``process_frame``).
|
||||
altitude_provider = EskfNominalAltitudeProvider(
|
||||
estimator_supplier=lambda: constructed.get("c5_state"),
|
||||
)
|
||||
return build_vio_strategy(
|
||||
config,
|
||||
fdr_client=fdr_client,
|
||||
altitude_provider=altitude_provider,
|
||||
)
|
||||
|
||||
|
||||
def _c2_vpr_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
|
||||
@@ -14,6 +14,7 @@ verifiable via ``sys.modules``).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -26,6 +27,7 @@ if TYPE_CHECKING:
|
||||
)
|
||||
from gps_denied_onboard.components.c13_fdr import FdrClient
|
||||
from gps_denied_onboard.config.schema import Config
|
||||
from gps_denied_onboard.helpers.altitude_provider import AltitudeProvider
|
||||
|
||||
__all__ = ["build_vio_strategy"]
|
||||
|
||||
@@ -74,6 +76,7 @@ def build_vio_strategy(
|
||||
config: "Config",
|
||||
*,
|
||||
fdr_client: "FdrClient",
|
||||
altitude_provider: "AltitudeProvider | None" = None,
|
||||
) -> "VioStrategy":
|
||||
"""Construct the :class:`VioStrategy` impl selected by config.
|
||||
|
||||
@@ -82,11 +85,18 @@ def build_vio_strategy(
|
||||
:class:`StrategyNotAvailableError` BEFORE any import.
|
||||
3. Lazily imports the concrete strategy module.
|
||||
4. Constructs and returns the strategy instance, passing
|
||||
``config`` and ``fdr_client``.
|
||||
``config`` and ``fdr_client`` and (if the strategy's
|
||||
``__init__`` accepts it) ``altitude_provider`` — AZ-919.
|
||||
|
||||
Raises :class:`StrategyNotAvailableError` when the compile-time
|
||||
flag is OFF (canonical Tier-0 path) or when the concrete strategy
|
||||
module has not been built yet (AZ-332 / AZ-333 / AZ-334 pending).
|
||||
|
||||
The ``altitude_provider`` kwarg is introspected against the
|
||||
selected strategy's ``__init__`` signature so OKVIS2 / VINS-Mono
|
||||
do not need to grow a parameter they do not yet consume; the
|
||||
KLT/RANSAC strategy accepts it (per AZ-919 AC-3) and will use it
|
||||
for GSD-based scale recovery in AZ-920.
|
||||
"""
|
||||
block = _c1_config(config)
|
||||
strategy = block.strategy
|
||||
@@ -111,4 +121,7 @@ def build_vio_strategy(
|
||||
"yet (AZ-332 / AZ-333 / AZ-334 pending)."
|
||||
) from exc
|
||||
strategy_cls = getattr(module, class_name)
|
||||
return strategy_cls(config, fdr_client=fdr_client)
|
||||
kwargs: dict[str, object] = {"fdr_client": fdr_client}
|
||||
if "altitude_provider" in inspect.signature(strategy_cls).parameters:
|
||||
kwargs["altitude_provider"] = altitude_provider
|
||||
return strategy_cls(config, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user