mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-22 21:46:36 +00:00
dfd41f27d4
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>
317 lines
16 KiB
Markdown
317 lines
16 KiB
Markdown
# 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)
|
||
|
||
### Встановлення
|
||
|
||
```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 допоможе.
|
||
|
||
### Покриття тестами (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)
|
||
|
||
```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 |
|
||
|
||
---
|
||
|
||
## Що залишилось (наступні кроки)
|
||
|
||
### 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 для конкретного апарату
|
||
|
||
---
|
||
|
||
## Ліцензія
|
||
|
||
Приватний репозиторій. Усі права захищено.
|