Files
gps-denied-onboard/README.md
T
Yuzviak dfd41f27d4 chore: pin numpy<2.0 and align plan with tech-audit research
Research doc (2026-04-18 OSS stack audit) flagged NumPy 2.0 as silently
breaking GTSAM Python bindings (issue #2264). Pin numpy>=1.26,<2.0 and
constrain opencv-python-headless<4.11 (knock-on: 4.11+ requires numpy≥2).

Verified after downgrade:
  - 196 passed / 8 skipped unit/component
  - EuRoC MH_01 e2e PASS (no regression on 0.205m ESKF ATE baseline)

Plan updates in next_steps.md §5:
  - cuVSLAM strategy clarified: Mono-Depth (barometer as synthetic depth),
    not Mono-Inertial (needs stereo hardware we don't have)
  - DINOv2-VLAD (AnyLoc) for GPR + FP16 TRT (INT8 broken for ViT on Jetson)
  - GTSAM: documented that 4.2 stable is not on PyPI (only 4.3a0), so
    deferred to post-sprint-1 ESKF-only path stays the right call
  - VPAIR xfail root cause: no raw IMU + mock satellite index (verified
    with scale=1.0 and scale=45.0 runs — ATE stays at ~1236m ESKF /
    ~1770km GPS regardless of scale)
  - Flight controller H743 vs F405 check flagged as critical blocker

README "next steps" section rewritten to match the research-aligned plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:50:12 +03:00

16 KiB
Raw Blame History

GPS-Denied Onboard

Бортова система GPS-denied навігації для фіксованого крила БПЛА на Jetson Orin Nano Super.

Замінює GPS-сигнал власною оцінкою позиції на основі відеопотоку (cuVSLAM), IMU та супутникових знімків. Позиція подається у польотний контролер ArduPilot у форматі GPS_INPUT через MAVLink при 5–10 Гц.


Архітектура

IMU (MAVLink RAW_IMU) ──────────────────────────────────────────▶ ESKF.predict()
                                                                        │
ADTI 20L V1 ──▶ ImageInputPipeline ──▶ ImageRotationManager            │
                                              │                         │
                              ┌───────────────┼───────────────┐         │
                              ▼               ▼               ▼         │
                   cuVSLAM/ORB VO    GlobalPlaceRecog    SatelliteData  │
                       (F07)            (F08/Faiss)         (F04)       │
                              │               │               │         │
                              ▼               ▼               ▼         │
                         ESKF.update_vo()   GSD norm    MetricRefinement│
                              │                              (F09)       │
                              └──────────────────────▶ ESKF.update_sat()│
                                                                        │
                                                         ESKF state ◀──┘
                                                              │
                                              ┌───────────────┼──────────────┐
                                              ▼               ▼              ▼
                                      MAVLinkBridge    FactorGraph     SSE Stream
                                      GPS_INPUT 5-10Hz  (GTSAM ISAM2)  → Ground Station
                                      → ArduPilot FC

State Machine (process_frame):

NORMAL ──(VO fail)──▶ LOST ──▶ RECOVERY ──(GPR+Metric ok)──▶ NORMAL

Стек

Підсистема Dev/CI Jetson (production)
Visual Odometry ORBVisualOdometry (OpenCV) CuVSLAMVisualOdometry (PyCuVSLAM v15)
AI Inference MockInferenceEngine TRTInferenceEngine (TensorRT FP16)
Place Recognition numpy L2 fallback Faiss GPU index
MAVLink MockMAVConnection pymavlink over UART
ESKF numpy (15-state) numpy (15-state)
Factor Graph Mock poses GTSAM 4.3 ISAM2
API FastAPI + Pydantic v2 + SSE FastAPI + Pydantic v2 + SSE
БД SQLite + SQLAlchemy 2 async SQLite
Тести pytest + pytest-asyncio

Швидкий старт

Вимоги

  • Python ≥ 3.11
  • ~500 MB дискового простору (GTSAM wheel)

Встановлення

git clone https://github.com/azaion/gps-denied-onboard.git
cd gps-denied-onboard
git checkout stage1

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Запуск

# Прямий запуск
uvicorn gps_denied.app:app --host 0.0.0.0 --port 8000

# Docker
docker compose up --build

Сервер: http://127.0.0.1:8000

Змінні середовища

# Основні
DB_URL=sqlite+aiosqlite:///./flight_data.db
SATELLITE_TILE_DIR=.satellite_tiles
MAVLINK_CONNECTION=serial:/dev/ttyTHS1:57600  # або tcp:host:port
MAVLINK_OUTPUT_HZ=5.0
MAVLINK_TELEMETRY_HZ=1.0

# ESKF тюнінг (опціонально)
ESKF_VO_POSITION_NOISE=0.3
ESKF_SATELLITE_MAX_AGE=30.0
ESKF_MAHALANOBIS_THRESHOLD=16.27

# API
API_HOST=127.0.0.1
API_PORT=8000

# Моделі
MODEL_WEIGHTS_DIR=weights

Повний список: src/gps_denied/config.py (40+ параметрів з prefix DB_, API_, TILES_, MODEL_, MAVLINK_, SATELLITE_, ESKF_, RECOVERY_, ROTATION_).


API

Endpoint Метод Опис
/health GET Health check
/flights POST Створити політ
/flights/{id} GET Деталі польоту
/flights/{flight_id} DELETE Видалити політ
/flights/{flight_id}/images/batch POST Батч зображень
/flights/{flight_id}/user-fix POST GPS-якір від оператора → ESKF update
/flights/{flight_id}/status GET Статус обробки
/flights/{flight_id}/stream GET SSE стрім (позиція + confidence)
/flights/{flight_id}/frames/{frame_id}/object-to-gps POST Pixel → GPS (ray-ground проекція)
/flights/{flight_id}/waypoints/{waypoint_id} PUT Оновити waypoint
/flights/{flight_id}/waypoints/batch PUT Batch update waypoints

Тести

# Всі тести
python -m pytest -q

# Конкретний модуль
python -m pytest tests/test_eskf.py -v
python -m pytest tests/test_mavlink.py -v
python -m pytest tests/test_accuracy.py -v

# SITL (потребує ArduPilot SITL)
docker compose -f docker-compose.sitl.yml up -d
ARDUPILOT_SITL_HOST=localhost pytest tests/test_sitl_integration.py -v

# E2E пайплайн на публічних UAV-датасетах (EuRoC / VPAIR / MARS-LVIG)
pytest tests/e2e/ -q                    # unit + skip-when-absent (швидко)
pytest tests/e2e/ -m "e2e and not e2e_slow" -v   # CI-tier з завантаженим датасетом
pytest tests/e2e/ -m e2e_slow -v        # nightly-tier (VPAIR sample, MARS-LVIG stress)

# EuRoC Machine Hall bundle — 12.6 GB, DOI 10.3929/ethz-b-000690084
# Завантажити вручну (DSpace UI без прямого URL), розпакувати внутрішній
# MH_0N_easy.zip у datasets/euroc/MH_0N/, щоб існував mav0/
# SHA256 зашитий у DATASET_REGISTRY ("euroc_machine_hall") для верифікації

# VPAIR sample (fixed-wing, downward, 300-400 м) — form-gated на Zenodo
# Розпакувати так, щоб datasets/vpair/sample/poses_query.txt існував
# SHA256 зашитий у DATASET_REGISTRY ("vpair_sample") для верифікації

# Для автоматизованих entry (коли з'являться) — той самий CLI:
python scripts/download_dataset.py <dataset_name>

E2E-харнес гонить FlightProcessor як black-box через спільний DatasetAdapter (src/gps_denied/testing/). Датасети лежать у ./datasets/ (gitignored), тести пропускаються (не фейляться) коли датасету немає. Детально — у локальному design doc .planning/brainstorms/2026-04-16-e2e-datasets-design.md та плані 2026-04-16-e2e-datasets-plan.md.

Поточний статус реальних прогонів (2026-04-18):

Датасет Кадри ESKF ATE RMSE GPS ATE Статус
EuRoC MH_01 (easy) 100 0.205 м xfail (indoor) PASS
EuRoC MH_02 (easy) 100 0.131 м xfail (indoor) PASS
EuRoC MH_03 (medium) 100 0.008 м xfail (indoor) PASS
EuRoC MH_04 (difficult) 100 0.009 м xfail (indoor) PASS
EuRoC MH_05 (difficult) 100 0.007 м xfail (indoor) PASS
VPAIR sample (fixed-wing, outdoor) 200 ~1770 км xfail xfail
MARS-LVIG (rotary, RTK) skip (датасет відсутній)

EuRoC: vo_success=99/100, eskf_initialized=100/100. GPS estimate xfail — indoor, satellite tiles не релевантні. VPAIR: ESKF не активний (немає raw IMU), VO без якоря розходиться. Outdoor — потенційно satellite matching допоможе.

Покриття тестами (70 e2e passed / 1 skipped / 2 xfailed; 195 passed / 8 skipped — unit/component)

Файл тесту Компонент К-сть
test_schemas.py Pydantic схеми 12
test_database.py SQLAlchemy CRUD 9
test_api_flights.py REST endpoints 5
test_health.py Health check 1
test_eskf.py ESKF 15-state 17
test_coordinates.py ENU/GPS/pixel 4
test_satellite.py Тайли + Mercator 8
test_pipeline.py Image queue 5
test_rotation.py 360° ротації 4
test_models.py Model Manager + TRT 6
test_vo.py VO (ORB + cuVSLAM) 8
test_gpr.py Place Recognition (Faiss) 7
test_metric.py Metric Refinement + GSD 6
test_graph.py Factor Graph (GTSAM) 4
test_chunk_manager.py Chunk lifecycle 3
test_recovery.py Recovery coordinator 2
test_processor_full.py State Machine 4
test_processor_pipe.py PIPE wiring (Phase 5) 13
test_mavlink.py MAVLink I/O bridge 19
test_acceptance.py AC сценарії + perf 6
test_accuracy.py Accuracy validation 23
test_sitl_integration.py SITL (skip без ArduPilot) 8
Всього 195+8

Benchmark валідації (Phase 7)

python scripts/benchmark_accuracy.py --frames 50

Результати на синтетичній траєкторії (20 м/с, 0.7 fps, шум VO 0.3 м, супутник кожні 5 кадрів):

Критерій Результат Ліміт
80% кадрів ≤ 50 м 100% ≥ 80%
60% кадрів ≤ 20 м 100% ≥ 60%
p95 затримка ~9 мс < 400 мс
VO дрейф за 1 км ~11 м < 100 м

Структура проєкту

gps-denied-onboard/
├── src/gps_denied/
│   ├── app.py                     # FastAPI factory + lifespan
│   ├── config.py                  # Pydantic Settings
│   ├── api/routers/flights.py     # REST + SSE endpoints
│   ├── core/
│   │   ├── eskf.py                # 15-state ESKF (IMU+VO+satellite fusion)
│   │   ├── processor.py           # FlightProcessor + process_frame
│   │   ├── vo.py                  # ORBVisualOdometry / CuVSLAMVisualOdometry
│   │   ├── mavlink.py             # MAVLinkBridge → GPS_INPUT → ArduPilot
│   │   ├── satellite.py           # SatelliteDataManager (local z/x/y tiles)
│   │   ├── gpr.py                 # GlobalPlaceRecognition (Faiss/numpy)
│   │   ├── metric.py              # MetricRefinement (LiteSAM/XFeat + GSD)
│   │   ├── graph.py               # FactorGraphOptimizer (GTSAM ISAM2)
│   │   ├── coordinates.py         # CoordinateTransformer (ENU↔GPS↔pixel)
│   │   ├── models.py              # ModelManager + TRTInferenceEngine
│   │   ├── benchmark.py           # AccuracyBenchmark + SyntheticTrajectory
│   │   ├── pipeline.py            # ImageInputPipeline
│   │   ├── rotation.py            # ImageRotationManager
│   │   ├── recovery.py            # FailureRecoveryCoordinator
│   │   └── chunk_manager.py       # RouteChunkManager
│   ├── schemas/                   # Pydantic схеми (eskf, mavlink, vo, ...)
│   ├── db/                        # SQLAlchemy ORM + async repository
│   └── utils/mercator.py          # Web Mercator tile utilities
├── tests/                         # 22 test модулі
├── scripts/
│   └── benchmark_accuracy.py      # CLI валідація точності
├── Dockerfile                     # Multi-stage Python 3.11 image
├── docker-compose.yml             # Local dev
├── docker-compose.sitl.yml        # ArduPilot SITL harness
├── .github/workflows/
│   ├── ci.yml                     # lint + pytest + docker smoke (кожен push)
│   └── sitl.yml                   # SITL integration (нічний / ручний)
└── pyproject.toml

Компоненти

ID Назва Файл Dev Jetson
F04 Satellite Data Manager core/satellite.py local tiles local tiles
F05 Image Input Pipeline core/pipeline.py
F06 Image Rotation Manager core/rotation.py
F07 Sequential Visual Odometry core/vo.py ORB cuVSLAM
F08 Global Place Recognition core/gpr.py numpy Faiss GPU
F09 Metric Refinement core/metric.py Mock LiteSAM/XFeat TRT
F10 Factor Graph Optimizer core/graph.py Mock GTSAM ISAM2
F11 Failure Recovery core/recovery.py
F12 Route Chunk Manager core/chunk_manager.py
F13 Coordinate Transformer core/coordinates.py
F16 Model Manager core/models.py Mock TRT engines
F17 ESKF Sensor Fusion core/eskf.py
F18 MAVLink I/O Bridge core/mavlink.py Mock pymavlink

Що залишилось (наступні кроки)

Dev pipeline (захищений e2e-харнесом)

  1. cuVSLAM Mono-Depth — замінити ORBVisualOdometry на cuVSLAM Mono + барометричний depth (scale = altitude / focal_length). Mono-Inertial потребує stereo hardware (нема). Mono-Depth — правильний шлях для одиничної nadir-камери. Research: docs/superpowers/specs/2026-04-18-oss-stack-tech-audit-design.md.
  2. DINOv2-VLAD (AnyLoc) для GPR — замінити numpy L2 fallback. Satellite tiles через MapTiler MBTiles (offline). Fixed FP16, без INT8 (broken для ViT на Jetson).
  3. VPAIR unblock — xfail (1770 км ATE) блокований відсутністю raw IMU + mock satellite index. Реальні MapTiler tiles АБО cuVSLAM Mono-Depth з GT-altitude розблокують.
  4. Аудит solution.md — звірити імплементацію з _docs/01_solution/solution.md.
  5. Реструктуризаціяsrc/gps_denied/*src/* (зайвий неймспейс).

Критичні блокери (перевірити до наступного коду)

  • Flight Controller processor: H743 / F405 (silently ignores GPS_INPUT). Запитати постачальника.
  • IMU rate через MAVLink: за замовчуванням ArduPilot 50 Hz, для Mono-Inertial потрібно ≥100 Hz (SR2_RAW_SENS). Для Mono-Depth не критично.

On-device (Jetson Orin Nano Super)

  1. Офлайн завантаження тайлів для зони місії → {tile_dir}/z/x/y.png
  2. Конвертація моделей: LiteSAM/XFeat PyTorch → ONNX → TRT FP16
  3. Запуск SITL: docker compose -f docker-compose.sitl.yml up
  4. Польотні дані: записати GPS + відео → порівняти ESKF-траєкторію з ground truth
  5. Калібрування: camera intrinsics + IMU noise density для конкретного апарату

Ліцензія

Приватний репозиторій. Усі права захищено.