Files
Yuzviak 1618190105 docs: update README and next_steps with sprint 1 VO migration results
README:
- Stack table: VO row shows CuVSLAMMonoDepthVisualOdometry (Mono-Depth mode)
- Test coverage: 195+8 → 216+8 (new mono_depth tests, AnyLoc markers, GPS_INPUT encoding)
- Added test_gps_input_encoding.py row
- F07 component table: dev/prod shows Mono-Depth variants
- "Next steps" rewritten: sprint 1 complete, sprint 2 queued

next_steps.md:
- New §5.1a documenting sprint 1 execution (7 commits, 3 decisions recorded)
- §5.3 week-1 marked numpy pin / Mono-Depth class / GPS_INPUT encoding as done
- Week-2 updated with harness wiring and CuVSLAMVisualOdometry deletion tasks

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

331 lines
17 KiB
Markdown
Raw Permalink 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 / CuVSLAMMonoDepthVisualOdometry (scaled ORB fallback) | CuVSLAMMonoDepthVisualOdometry (PyCuVSLAM v15 Mono-Depth — barometer as synthetic depth) |
| **AI Inference** | MockInferenceEngine | TRTInferenceEngine (TensorRT FP16; INT8 disabled — broken for ViT on Jetson) |
| **Place Recognition** | numpy L2 fallback (AnyLoc-VLAD-DINOv2 baseline) | Faiss GPU index + DINOv2-VLAD TRT FP16 |
| **MAVLink** | MockMAVConnection | pymavlink over UART |
| **ESKF** | numpy (15-state) | numpy (15-state) |
| **Factor Graph** | Mock poses | GTSAM 4.3 ISAM2 (sprint 2 — ESKF-only sufficient for sprint 1) |
| **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`.
**Поточний статус реальних прогонів (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 допоможе.
### Покриття тестами (216 passed / 8 skipped — unit/component; EuRoC MH_0105 e2e PASS)
| Файл тесту | Компонент | К-сть |
|-------------|-----------|-------|
| `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 + Mono-Depth) | 16 |
| `test_gpr.py` | Place Recognition + AnyLoc markers | 9 |
| `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_gps_input_encoding.py` | GPS_INPUT field encoding (MAVLink #232) | 12 |
| `test_acceptance.py` | AC сценарії + perf | 6 |
| `test_accuracy.py` | Accuracy validation | 23 |
| `test_sitl_integration.py` | SITL (skip без ArduPilot) | 8 |
| | **Всього** | **216+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 / Mono-Depth (scaled ORB) | cuVSLAM Mono-Depth |
| 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 |
---
## Що залишилось (наступні кроки)
### Sprint 1 — виконано (2026-04-18)
Див. план `docs/superpowers/plans/2026-04-18-sprint1-vo-migration.md` і design doc `docs/superpowers/specs/2026-04-18-oss-stack-tech-audit-design.md`.
- [x] **numpy pin** `>=1.26,<2.0` — NumPy 2.0 silently breaks GTSAM bindings (issue #2264)
- [x] **CuVSLAMMonoDepthVisualOdometry** додано поряд з Inertial-варіантом. Dev/CI: ORB translation scaled by `depth_hint_m / 600.0`. Jetson: cuVSLAM Mono-Depth mode з barometric altitude як synthetic depth.
- [x] **GlobalPlaceRecognition** явно маркований як AnyLoc-VLAD-DINOv2 baseline з selection rationale в docstring
- [x] **GPS_INPUT encoding** покритий 12 unit-тестами проти `_eskf_to_gps_input` (degE7 lat/lon, ENU→NED velocity, ConfidenceTier → fix_type)
- [x] **E2E regression guard** на EuRoC MH_01 для Mono-Depth (ATE 0.2046м, baseline незмінний)
### Sprint 2 — далі (захищений e2e-харнесом)
1. **Wire CuVSLAMMonoDepthVisualOdometry через E2EHarness** — harness зараз хардкодить `ORBVisualOdometry()`; додати `vo_backend` параметр щоб прогнати Mono-Depth через pipeline
2. **Колапс дуплікатного коду з `CuVSLAMVisualOdometry`** (Inertial варіант) — видалити Inertial mode після Jetson validation
3. **VPAIR unblock** — xfail (1770 км ATE) блокований відсутністю raw IMU + mock satellite index. Реальні MapTiler tiles + Mono-Depth з GT-altitude розблокують.
4. **DINOv2-VLAD TRT FP16 engine** — реальний descriptor замість numpy L2 fallback. Satellite tiles через MapTiler MBTiles (offline).
5. **aero-vloc benchmark** на наших nadir кадрах — підтвердити R@1 DINOv2-VLAD перед фіксацією Faiss index design
6. **Аудит solution.md** — звірити `_docs/01_solution/solution.md` з реальним кодом (Inertial → Mono-Depth)
7. **Реструктуризація**`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 для конкретного апарату
---
## Ліцензія
Приватний репозиторій. Усі права захищено.