Files
gps-denied-onboard/README.md
T
Yuzviak 1bf8b2a684 docs: record EuRoC MH_01 real-run baseline across all doc surfaces
Updates README, testing/README, next_steps.md, and ADR 0001 with the
first real EuRoC MH_01 e2e run (100 frames, ~30s wall-time, ATE RMSE
~10.9 km → xfail). Places the EuRoC result alongside the prior VPAIR
baseline (~1770 km) so future-reader can see both failure modes at a
glance:

- VPAIR diverges because no raw IMU → ESKF never engages
- EuRoC diverges because indoor scene has no satellite anchor, so
  VO+ESKF drift without an external correction

Also records the branching policy (rename ``euroc_mh01`` →
``euroc_machine_hall``; empty URL due to DSpace UI gate; manual
fetch via DOI 10.3929/ethz-b-000690084).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:52:06 +03:00

295 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GPS-Denied Onboard
Бортова система GPS-denied навігації для фіксованого крила БПЛА на Jetson Orin Nano Super.
Замінює GPS-сигнал власною оцінкою позиції на основі відеопотоку (cuVSLAM), IMU та супутникових знімків. Позиція подається у польотний контролер ArduPilot у форматі `GPS_INPUT` через MAVLink при 510 Гц.
---
## Архітектура
```
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)
### Встановлення
```bash
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]"
```
### Запуск
```bash
# Прямий запуск
uvicorn gps_denied.app:app --host 0.0.0.0 --port 8000
# Docker
docker compose up --build
```
Сервер: `http://127.0.0.1:8000`
### Змінні середовища
```env
# Основні
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 |
---
## Тести
```bash
# Всі тести
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`.
**Поточний статус реальних прогонів:**
- **VPAIR sample** (200 кадрів fixed-wing 300-400 м над Bonn/Eifel): пайплайн завершується, ATE RMSE ~1770 км → xfail. Причина: VPAIR не ship raw IMU → ESKF-шлях не активний; VO-самостійно розходиться.
- **EuRoC MH_01** (перші 100 кадрів indoor MAV, ASL формат): пайплайн завершується за ~30 с, ATE RMSE ~10.9 км → xfail. Raw IMU є і ESKF активний, але satellite-anchoring для indoor сцени не релевантний, тому VO+ESKF без якоря дрейфує. Експерименти з більшою довжиною/тюнінгом — після стабілізації GPR+ESKF.
- **MARS-LVIG** — тести скіпаються (датасет не завантажений локально).
### Покриття тестами (195 passed / 8 skipped — unit/component; e2e — окремо)
| Файл тесту | Компонент | К-сть |
|-------------|-----------|-------|
| `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)
```bash
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 |
---
## Що залишилось (on-device)
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 для конкретного апарату
---
## Ліцензія
Приватний репозиторій. Усі права захищено.