feat(phases 2-7): implement full GPS-denied navigation pipeline

Phase 2 — Visual Odometry:
  - ORBVisualOdometry (dev/CI), CuVSLAMVisualOdometry (Jetson)
  - TRTInferenceEngine (TensorRT FP16, conditional import)
  - create_vo_backend() factory

Phase 3 — Satellite Matching + GPR:
  - SatelliteDataManager: local z/x/y tiles, ESKF ±3σ tile selection
  - GSD normalization (SAT-03), RANSAC inlier-ratio confidence (SAT-04)
  - GlobalPlaceRecognition: Faiss index + numpy fallback

Phase 4 — MAVLink I/O:
  - MAVLinkBridge: GPS_INPUT 15+ fields, IMU callback, 1Hz telemetry
  - 3-consecutive-failure reloc request
  - MockMAVConnection for CI

Phase 5 — Pipeline Wiring:
  - ESKF wired into process_frame: VO update → satellite update
  - CoordinateTransformer + SatelliteDataManager via DI
  - MAVLink state push per frame (PIPE-07)
  - Real pixel_to_gps via ray-ground projection (PIPE-06)
  - GTSAM ISAM2 update when available (PIPE-03)

Phase 6 — Docker + CI:
  - Multi-stage Dockerfile (python:3.11-slim)
  - docker-compose.yml (dev), docker-compose.sitl.yml (ArduPilot SITL)
  - GitHub Actions: ci.yml (lint+pytest+docker smoke), sitl.yml (nightly)
  - tests/test_sitl_integration.py (8 tests, skip without SITL)

Phase 7 — Accuracy Validation:
  - AccuracyBenchmark + SyntheticTrajectory
  - AC-PERF-1: 80% within 50m 
  - AC-PERF-2: 60% within 20m 
  - AC-PERF-3: p95 latency < 400ms 
  - AC-PERF-4: VO drift 1km < 100m  (actual ~11m)
  - scripts/benchmark_accuracy.py CLI

Tests: 195 passed / 8 skipped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yuzviak
2026-04-02 17:00:41 +03:00
parent a15bef5c01
commit 094895b21b
40 changed files with 4572 additions and 497 deletions
+160 -127
View File
@@ -1,50 +1,57 @@
# GPS-Denied Onboard
Сервіс геолокалізації знімків БПЛА в умовах відсутності GPS-сигналу.
Бортова система GPS-denied навігації для фіксованого крила БПЛА на Jetson Orin Nano Super.
Система використовує візуальну одометрію (VO), співставлення з супутниковими картами (cross-view matching) та оптимізацію траєкторії через фактор-графи для визначення координат дрона в реальному часі.
Замінює GPS-сигнал власною оцінкою позиції на основі відеопотоку (cuVSLAM), IMU та супутникових знімків. Позиція подається у польотний контролер ArduPilot у форматі `GPS_INPUT` через MAVLink при 5–10 Гц.
---
## Архітектура
```
UAV Frames ──▷ ImageInputPipeline (F05) ──▷ ImageRotationManager (F06)
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
SequentialVO (F07) GlobalPlaceRecog (F08) SatelliteData (F04)
▼ ▼ ▼
FactorGraphOptim (F10) ◂── MetricRefinement (F09) ◂── CoordTransform (F13)
┌─────────┴─────────┐
▼ ▼
RouteChunkManager (F12) FailureRecovery (F11)
SSE Event Streamer ──▷ Ground Station
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
NORMAL ──(VO fail)── LOST ── RECOVERY ──(GPR+Metric ok)── NORMAL
```
---
## Стек
| Підсистема | Технологія |
|-----------|------------|
| **API** | FastAPI + Pydantic v2, SSE (sse-starlette) |
| **БД** | SQLite + SQLAlchemy 2 (asyncio) |
| **CV** | OpenCV (Essential Matrix, RANSAC, recoverPose) |
| **Оптимізація** | GTSAM 4.3 (iSAM2, Huber kernel) |
| **Моделі** | Mock engines: SuperPoint, LightGlue, DINOv2, LiteSAM |
| **Кеш** | diskcache (супутникові тайли) |
| **HTTP** | httpx (Google Maps Static Tiles) |
| **Тести** | pytest + pytest-asyncio (80 тестів) |
| Підсистема | 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 | — |
---
@@ -53,7 +60,6 @@ NORMAL ──(VO fail)──▷ LOST ──▷ RECOVERY ──(GPR+Metric ok)─
### Вимоги
- Python ≥ 3.11
- pip / venv
- ~500 MB дискового простору (GTSAM wheel)
### Встановлення
@@ -65,80 +71,108 @@ git checkout stage1
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```
### Конфігурація `.env`
```env
# Опціонально — для реальних супутникових тайлів
GOOGLE_MAPS_API_KEY=<your_key>
GOOGLE_MAPS_SESSION_TOKEN=<your_token>
# Налаштування серверу (за замовчуванням)
GPS_DENIED_HOST=127.0.0.1
GPS_DENIED_PORT=8000
GPS_DENIED_DB_URL=sqlite+aiosqlite:///./gps_denied.db
```
### Запуск серверу
### Запуск
```bash
# Пряме запуск
python -m gps_denied
# Docker
docker compose up --build
```
Сервер стартує на `http://127.0.0.1:8000`.
Сервер: `http://127.0.0.1:8000`
### Змінні середовища
```env
GPS_DENIED_DB_PATH=/data/flights.db
GPS_DENIED_TILE_DIR=/data/satellite_tiles # локальні тайли z/x/y.png
GPS_DENIED_LOG_LEVEL=INFO
MAVLINK_CONNECTION=serial:/dev/ttyTHS1:57600 # UART на Jetson
```
---
## API
| Endpoint | Метод | Опис |
|----------|-------|------|
| `/health` | GET | Health check |
| `/flights` | POST | Створити новий політ |
| `/flights` | POST | Створити політ |
| `/flights/{id}` | GET | Деталі польоту |
| `/flights/{id}` | DELETE | Видалити політ |
| `/flights/{id}/images/batch` | POST | Завантажити батч зображень |
| `/flights/{id}/fix` | POST | Надати GPS-якір (user fix) |
| `/flights/{id}/images/batch` | POST | Батч зображень |
| `/flights/{id}/fix` | POST | GPS-якір від оператора |
| `/flights/{id}/status` | GET | Статус обробки |
| `/flights/{id}/events` | GET | SSE стрім подій |
| `/flights/{id}/object-gps` | POST | Pixel → GPS координата |
| `/flights/{id}/events` | GET | SSE стрім (позиція + confidence) |
| `/flights/{id}/object-gps` | POST | Pixel → GPS (ray-ground проекція) |
---
## Тести
```bash
# Усі тести (80 шт, ~23с)
python -m pytest tests/ -v
# Всі тести
python -m pytest -q
# Тільки acceptance
python -m pytest tests/test_acceptance.py -v
# Конкретний модуль
python -m pytest tests/test_eskf.py -v
python -m pytest tests/test_mavlink.py -v
python -m pytest tests/test_accuracy.py -v
# Тільки конкретний модуль
python -m pytest tests/test_graph.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
```
### Покриття тестами
### Покриття тестами (195 passed / 8 skipped)
| Файл тесту | Компонент | Кількість |
|-------------|-----------|-----------|
| `test_schemas.py` | Pydantic моделі | 12 |
| Файл тесту | Компонент | К-сть |
|-------------|-----------|-------|
| `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_satellite.py` | Тайли + Mercator | 5 |
| `test_coordinates.py` | ENU / GPS конвертері | 4 |
| `test_pipeline.py` | Image queue | 3 |
| `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` | Mock engines | 3 |
| `test_vo.py` | Visual Odometry | 5 |
| `test_gpr.py` | Place Recognition | 3 |
| `test_metric.py` | Metric Refinement | 3 |
| `test_graph.py` | Factor Graph | 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 |
| | **Всього** | **80** |
| `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 м |
---
@@ -147,70 +181,69 @@ python -m pytest tests/test_graph.py -v
```
gps-denied-onboard/
├── src/gps_denied/
│ ├── __init__.py
│ ├── __main__.py # Entry point (uvicorn)
│ ├── app.py # FastAPI application
│ ├── config.py # Pydantic Settings (.env)
│ ├── api/
│ │ └── flights.py # REST endpoints
│ ├── app.py # FastAPI factory + lifespan
│ ├── config.py # Pydantic Settings
│ ├── api/routers/flights.py # REST + SSE endpoints
│ ├── core/
│ │ ├── processor.py # FlightProcessor + process_frame (State Machine)
│ │ ├── vo.py # Sequential Visual Odometry (F07)
│ │ ├── gpr.py # Global Place Recognition (F08)
│ │ ├── metric.py # Metric Refinement (F09)
│ │ ├── graph.py # Factor Graph Optimizer (F10, GTSAM)
│ │ ├── recovery.py # Failure Recovery Coordinator (F11)
│ │ ├── chunk_manager.py # Route Chunk Manager (F12)
│ │ ├── coordinates.py # Coordinate Transformer (F13)
│ │ ├── models.py # Model Manager (F16)
│ │ ├── satellite.py # Satellite Data Manager (F04)
│ │ ├── pipeline.py # Image Input Pipeline (F05)
│ │ ├── rotation.py # Image Rotation Manager (F06)
│ │ ├── sse.py # SSE Event Streamer
│ │ ── results.py # Result Manager
├── db/
│ ├── database.py # Async engine + session
│ ├── models.py # SQLAlchemy ORM models
│ └── repository.py # FlightRepository (CRUD)
│ ├── schemas/
│ │ ├── __init__.py # Re-exports
│ ├── flight.py # Flight, Waypoint, GPS, Camera schemas
│ │ ├── events.py # SSE event models
│ │ ├── image.py # ImageBatch, ProcessingStatus
│ │ ├── rotation.py # RotationResult, HeadingHistory
│ │ ├── model.py # InferenceEngine, ModelConfig
│ ├── vo.py # Features, Matches, RelativePose
│ ├── gpr.py # TileCandidate, DatabaseMatch
│ │ ├── metric.py # AlignmentResult, Sim3Transform
│ │ ├── graph.py # Pose, OptimizationResult
│ │ ├── chunk.py # ChunkHandle, ChunkStatus
│ │ └── satellite.py # TileCoords, TileBounds
│ └── utils/
│ └── mercator.py # Web Mercator utilities
├── tests/ # 17 test modules (80 tests)
├── docs/ # Архітектурні специфікації
├── docs-Lokal/ # Локальний план та рішення
├── pyproject.toml # Залежності та конфігурація
└── .gitignore
│ │ ├── 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
```
---
## Компоненти (F-індексація)
## Компоненти
| ID | Назва | Файл | Статус |
|----|-------|------|--------|
| F04 | Satellite Data Manager | `core/satellite.py` | ✅ Mock |
| F05 | Image Input Pipeline | `core/pipeline.py` | ✅ |
| F06 | Image Rotation Manager | `core/rotation.py` | ✅ |
| F07 | Sequential Visual Odometry | `core/vo.py` | ✅ Mock engines |
| F08 | Global Place Recognition | `core/gpr.py` | ✅ Mock Faiss |
| F09 | Metric Refinement | `core/metric.py` | Mock LiteSAM |
| F10 | Factor Graph Optimizer | `core/graph.py` | GTSAM wrapper |
| F11 | Failure Recovery Coordinator | `core/recovery.py` | ✅ |
| F12 | Route Chunk Manager | `core/chunk_manager.py` | ✅ |
| F13 | Coordinate Transformer | `core/coordinates.py` | ✅ |
| F16 | Model Manager | `core/models.py` | Mock/Fallback |
| 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 для конкретного апарату
---