fix issues

This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-11-30 01:43:23 +02:00
parent 1082316660
commit 310cf78ee7
17 changed files with 584 additions and 240 deletions
@@ -311,8 +311,9 @@ BatchResponse:
1. Validate flight_id exists
2. Validate batch size (10-50 images)
3. Validate sequence numbers (strict sequential)
4. Pass to F05 Image Input Pipeline
5. Return immediately (processing is async)
4. Call F02 Flight Processor → queue_images(flight_id, batch)
5. F02 delegates to F05 Image Input Pipeline
6. Return immediately (processing is async)
**Error Conditions**:
- `400 Bad Request`: Invalid batch size, out-of-sequence images
@@ -355,9 +356,10 @@ UserFixResponse:
**Processing Flow**:
1. Validate flight_id exists and is blocked
2. Pass to F11 Failure Recovery Coordinator
3. Coordinator applies anchor to Factor Graph
4. Resume processing pipeline
2. Call F02 Flight Processor → handle_user_fix(flight_id, fix_data)
3. F02 delegates to F11 Failure Recovery Coordinator
4. Coordinator applies anchor to Factor Graph
5. Resume processing pipeline
**Error Conditions**:
- `400 Bad Request`: Invalid fix data
@@ -400,8 +402,9 @@ ObjectGPSResponse:
**Processing Flow**:
1. Validate flight_id and frame_id exist
2. Validate frame has been processed (has pose in Factor Graph)
3. Call F13.image_object_to_gps(pixel, frame_id)
4. Return GPS with accuracy estimate
3. Call F02 Flight Processor → convert_object_to_gps(flight_id, frame_id, pixel)
4. F02 delegates to F13.image_object_to_gps(flight_id, frame_id, pixel)
5. Return GPS with accuracy estimate
**Error Conditions**:
- `400 Bad Request`: Invalid pixel coordinates
@@ -479,6 +482,12 @@ SSE Stream with events:
- flight_completed
```
**Processing Flow**:
1. Validate flight_id exists
2. Call F02 Flight Processor → create_client_stream(flight_id, client_id)
3. F02 delegates to F15 SSE Event Streamer → create_stream()
4. Return SSE stream to client
**Event Format**:
```json
{
@@ -575,11 +584,9 @@ SSE Stream with events:
## Dependencies
### Internal Components
- **F02 Flight Processor**: For all flight operations and processing orchestration
- **F05 Image Input Pipeline**: For batch processing
- **F11 Failure Recovery Coordinator**: For user fixes
- **F15 SSE Event Streamer**: For real-time streaming
- **F03 Flight Database**: For persistence (via F02)
- **F02 Flight Processor**: For ALL operations (flight CRUD, image batching, user fixes, SSE streams, object-to-GPS conversion). F01 is a thin REST layer that delegates all business logic to F02.
**Note**: F01 does NOT directly call F05, F11, F13, or F15. All operations are routed through F02 to maintain a single coordinator pattern.
### External Dependencies
- **FastAPI**: Web framework
@@ -59,6 +59,27 @@ class IFlightProcessor(ABC):
def validate_flight_continuity(self, waypoints: List[Waypoint]) -> ValidationResult:
pass
# API Delegation Methods (called by F01)
@abstractmethod
def queue_images(self, flight_id: str, batch: ImageBatch) -> BatchQueueResult:
"""Delegates to F05 Image Input Pipeline."""
pass
@abstractmethod
def handle_user_fix(self, flight_id: str, fix_data: UserFixRequest) -> UserFixResult:
"""Delegates to F11 Failure Recovery Coordinator."""
pass
@abstractmethod
def create_client_stream(self, flight_id: str, client_id: str) -> StreamConnection:
"""Delegates to F15 SSE Event Streamer."""
pass
@abstractmethod
def convert_object_to_gps(self, flight_id: str, frame_id: int, pixel: Tuple[float, float]) -> GPSPoint:
"""Delegates to F13 Coordinate Transformer."""
pass
# Processing Orchestration
@abstractmethod
def process_frame(self, flight_id: str, frame_id: int) -> FrameResult:
@@ -576,6 +597,129 @@ ValidationResult:
---
## API Delegation Methods
These methods are called by F01 Flight API and delegate to specialized components. This maintains F02 as the single coordinator for all operations.
### `queue_images(flight_id: str, batch: ImageBatch) -> BatchQueueResult`
**Description**: Queues image batch for processing. Delegates to F05 Image Input Pipeline.
**Called By**:
- F01 Flight API (upload_image_batch endpoint)
**Input**:
```python
flight_id: str
batch: ImageBatch
```
**Output**:
```python
BatchQueueResult:
accepted: bool
sequences: List[int]
next_expected: int
message: Optional[str]
```
**Processing Flow**:
1. Validate flight exists and is in valid state
2. Delegate to F05 Image Input Pipeline → queue_batch()
3. Return result to F01
---
### `handle_user_fix(flight_id: str, fix_data: UserFixRequest) -> UserFixResult`
**Description**: Handles user-provided GPS fix. Delegates to F11 Failure Recovery Coordinator.
**Called By**:
- F01 Flight API (submit_user_fix endpoint)
**Input**:
```python
flight_id: str
fix_data: UserFixRequest:
frame_id: int
uav_pixel: Tuple[float, float]
satellite_gps: GPSPoint
```
**Output**:
```python
UserFixResult:
accepted: bool
processing_resumed: bool
message: Optional[str]
```
**Processing Flow**:
1. Validate flight exists and is blocked
2. Delegate to F11 Failure Recovery Coordinator → apply_user_anchor()
3. F11 emits UserFixApplied event (F02 subscribes and resumes processing)
4. Return result to F01
---
### `create_client_stream(flight_id: str, client_id: str) -> StreamConnection`
**Description**: Creates SSE stream for client. Delegates to F15 SSE Event Streamer.
**Called By**:
- F01 Flight API (create_sse_stream endpoint)
**Input**:
```python
flight_id: str
client_id: str
```
**Output**:
```python
StreamConnection:
stream_id: str
flight_id: str
client_id: str
last_event_id: Optional[str]
```
**Processing Flow**:
1. Validate flight exists
2. Delegate to F15 SSE Event Streamer → create_stream()
3. Return StreamConnection to F01
---
### `convert_object_to_gps(flight_id: str, frame_id: int, pixel: Tuple[float, float]) -> GPSPoint`
**Description**: Converts object pixel to GPS. Delegates to F13 Coordinate Transformer.
**Called By**:
- F01 Flight API (convert_object_to_gps endpoint)
**Input**:
```python
flight_id: str
frame_id: int
pixel: Tuple[float, float]
```
**Output**:
```python
GPSPoint:
lat: float
lon: float
```
**Processing Flow**:
1. Validate flight and frame exist
2. Validate frame has been processed (has pose)
3. Delegate to F13 Coordinate Transformer → image_object_to_gps(flight_id, frame_id, pixel)
4. Return GPSPoint to F01
---
## Processing Orchestration Methods
### `process_frame(flight_id: str, frame_id: int) -> FrameResult`
@@ -676,6 +676,13 @@ Optional[Dict]: Metadata dictionary or None
## Chunk State Operations
**Necessity**: These methods are **required** for crash recovery. Without them:
- Chunk state would be lost on system restart
- Processing would need to start from scratch
- Background chunk matching progress would be lost
F12 Route Chunk Manager delegates persistence to F03 to maintain separation of concerns (F12 manages chunk logic, F03 handles storage).
### `save_chunk_state(flight_id: str, chunk: ChunkHandle) -> bool`
**Description**: Saves chunk state to database for crash recovery.
@@ -401,8 +401,8 @@ ProcessingStatus:
## Dependencies
### Internal Components
- **H08 Batch Validator**: For validation logic
- **F03 Flight Database**: For metadata persistence and flight state information
- **H08 Batch Validator**: For batch validation (naming convention, sequence continuity, format, dimensions)
### External Dependencies
- **opencv-python**: Image I/O
@@ -495,6 +495,7 @@ return None # No match found
## Dependencies
### Internal Components
- **F04 Satellite Data Manager**: For tile fetching (`fetch_tile`) and tile bounds computation (`compute_tile_bounds`)
- **F09 Metric Refinement**: For matching during rotation sweep (align_to_satellite, align_chunk_to_satellite). F06 rotates images, F09 performs the actual matching.
- **H07 Image Rotation Utils**: For image rotation and angle calculations
- **F12 Route Chunk Manager**: For chunk image retrieval
@@ -23,12 +23,10 @@ class ISequentialVO(ABC):
@abstractmethod
def estimate_motion(self, matches: Matches, camera_params: CameraParameters) -> Optional[Motion]:
pass
@abstractmethod
def compute_relative_pose_in_chunk(self, prev_image: np.ndarray, curr_image: np.ndarray, chunk_id: str) -> Optional[RelativePose]:
pass
```
**Note**: F07 is chunk-agnostic. It only computes relative poses between images. The caller (F02 Flight Processor) determines which chunk the frames belong to and routes factors to the appropriate subgraph via F12 → F10.
## Component Description
### Responsibilities
@@ -38,14 +36,13 @@ class ISequentialVO(ABC):
- Estimate relative pose (translation + rotation) between frames
- Return relative pose factors for Factor Graph Optimizer
- Detect tracking loss (low inlier count)
- **Chunk-aware VO operations (factors added to chunk subgraph)**
### Scope
- Frame-to-frame visual odometry
- Feature-based motion estimation
- Handles low overlap and challenging agricultural environments
- Provides relative measurements for trajectory optimization
- **Chunk-scoped operations (Atlas multi-map architecture)**
- **Chunk-agnostic**: F07 doesn't know about chunks. Caller (F02) routes results to appropriate chunk subgraph.
## API Methods
@@ -224,49 +221,6 @@ Motion:
2. **Low inliers**: May return None
3. **Degenerate motion**: Handles pure rotation
---
### `compute_relative_pose_in_chunk(prev_image: np.ndarray, curr_image: np.ndarray, chunk_id: str) -> Optional[RelativePose]`
**Description**: Computes relative camera pose between consecutive frames within a chunk context.
**Called By**:
- F02 Flight Processor (chunk-aware processing)
**Input**:
```python
prev_image: np.ndarray # Previous frame (t-1)
curr_image: np.ndarray # Current frame (t)
chunk_id: str # Chunk identifier for context
```
**Output**:
```python
RelativePose:
translation: np.ndarray # (x, y, z) in meters
rotation: np.ndarray # 3×3 rotation matrix or quaternion
confidence: float # 0.0 to 1.0
inlier_count: int
total_matches: int
tracking_good: bool
chunk_id: str # Chunk context
```
**Processing Flow**:
1. Same as compute_relative_pose() (SuperPoint + LightGlue)
2. Return RelativePose with chunk_id context
3. Factor will be added to chunk's subgraph (not global graph)
**Chunk Context**:
- VO operations are chunk-scoped
- Factors added to chunk's subgraph via F10.add_relative_factor_to_chunk()
- Chunk isolation ensures independent optimization
**Test Cases**:
1. **Chunk-aware VO**: Returns RelativePose with chunk_id
2. **Chunk isolation**: Factors isolated to chunk
3. **Multiple chunks**: VO operations don't interfere between chunks
## Integration Tests
### Test 1: Normal Flight Sequence
@@ -377,11 +377,11 @@ Optional[np.ndarray]: 3×3 homography matrix or None
## Dependencies
### Internal Components
- **F12 Route Chunk Manager**: For chunk image retrieval and chunk operations
- **F16 Model Manager**: For LiteSAM model
- **H01 Camera Model**: For projection operations
- **H02 GSD Calculator**: For coordinate transformations
- **H05 Performance Monitor**: For timing
- **F12 Route Chunk Manager**: For chunk image retrieval
**Note**: tile_bounds is passed as parameter from caller (F02 Flight Processor gets it from F04 Satellite Data Manager)
@@ -8,64 +8,67 @@
```python
class IFactorGraphOptimizer(ABC):
# All methods take flight_id to support concurrent flights
# F10 maintains Dict[str, FactorGraph] keyed by flight_id internally
@abstractmethod
def add_relative_factor(self, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool:
def add_relative_factor(self, flight_id: str, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool:
pass
@abstractmethod
def add_absolute_factor(self, frame_id: int, gps: GPSPoint, covariance: np.ndarray, is_user_anchor: bool) -> bool:
def add_absolute_factor(self, flight_id: str, frame_id: int, gps: GPSPoint, covariance: np.ndarray, is_user_anchor: bool) -> bool:
pass
@abstractmethod
def add_altitude_prior(self, frame_id: int, altitude: float, covariance: float) -> bool:
def add_altitude_prior(self, flight_id: str, frame_id: int, altitude: float, covariance: float) -> bool:
pass
@abstractmethod
def optimize(self, iterations: int) -> OptimizationResult:
def optimize(self, flight_id: str, iterations: int) -> OptimizationResult:
pass
@abstractmethod
def get_trajectory(self) -> Dict[int, Pose]:
def get_trajectory(self, flight_id: str) -> Dict[int, Pose]:
pass
@abstractmethod
def get_marginal_covariance(self, frame_id: int) -> np.ndarray:
def get_marginal_covariance(self, flight_id: str, frame_id: int) -> np.ndarray:
pass
# Chunk operations - F10 only manages factor graph subgraphs
# F12 owns chunk metadata (status, is_active, etc.)
@abstractmethod
def create_chunk_subgraph(self, flight_id: str, chunk_id: str, start_frame_id: int) -> bool:
pass
@abstractmethod
def create_new_chunk(self, chunk_id: str, start_frame_id: int) -> ChunkHandle:
def add_relative_factor_to_chunk(self, flight_id: str, chunk_id: str, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool:
pass
@abstractmethod
def get_chunk_for_frame(self, frame_id: int) -> Optional[ChunkHandle]:
def add_chunk_anchor(self, flight_id: str, chunk_id: str, frame_id: int, gps: GPSPoint, covariance: np.ndarray) -> bool:
pass
@abstractmethod
def add_relative_factor_to_chunk(self, chunk_id: str, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool:
def merge_chunk_subgraphs(self, flight_id: str, source_chunk_id: str, target_chunk_id: str, transform: Sim3Transform) -> bool:
"""Merges source_chunk INTO target_chunk. Source chunk subgraph is merged into target."""
pass
@abstractmethod
def add_chunk_anchor(self, chunk_id: str, frame_id: int, gps: GPSPoint, covariance: np.ndarray) -> bool:
def get_chunk_trajectory(self, flight_id: str, chunk_id: str) -> Dict[int, Pose]:
pass
@abstractmethod
def merge_chunks(self, chunk_id_1: str, chunk_id_2: str, transform: Sim3Transform) -> bool:
def optimize_chunk(self, flight_id: str, chunk_id: str, iterations: int) -> OptimizationResult:
pass
@abstractmethod
def get_chunk_trajectory(self, chunk_id: str) -> Dict[int, Pose]:
def optimize_global(self, flight_id: str, iterations: int) -> OptimizationResult:
pass
@abstractmethod
def get_all_chunks(self) -> List[ChunkHandle]:
pass
@abstractmethod
def optimize_chunk(self, chunk_id: str, iterations: int) -> OptimizationResult:
pass
@abstractmethod
def optimize_global(self, iterations: int) -> OptimizationResult:
def delete_flight_graph(self, flight_id: str) -> bool:
"""Cleanup factor graph when flight is deleted."""
pass
```
@@ -84,16 +87,20 @@ class IFactorGraphOptimizer(ABC):
### Chunk Responsibility Clarification
**F10 provides low-level factor graph operations only**:
- `create_new_chunk()`: Creates subgraph in factor graph
- `create_chunk_subgraph()`: Creates subgraph in factor graph (returns bool, not ChunkHandle)
- `add_relative_factor_to_chunk()`: Adds factors to chunk's subgraph
- `add_chunk_anchor()`: Adds GPS anchor to chunk
- `merge_chunks()`: Applies Sim(3) transform and merges subgraphs
- `add_chunk_anchor()`: Adds GPS anchor to chunk's subgraph
- `merge_chunk_subgraphs()`: Applies Sim(3) transform and merges subgraphs
- `optimize_chunk()`, `optimize_global()`: Runs optimization
**F12 is the source of truth for chunk state** (see F12 spec):
- Chunk lifecycle management (active, anchored, merged status)
**F10 does NOT own chunk metadata** - only factor graph data structures.
**F12 is the source of truth for ALL chunk state** (see F12 spec):
- ChunkHandle with all metadata (is_active, has_anchor, matching_status)
- Chunk lifecycle management
- Chunk readiness determination
- High-level chunk queries
- F12 calls F10 for factor graph operations
**F11 coordinates recovery** (see F11 spec):
- Triggers chunk creation via F12
@@ -155,16 +162,19 @@ F07 returns unit translation vectors due to monocular scale ambiguity. F10 resol
**Explicit Flow**:
```python
# In add_relative_factor():
# altitude comes from F05 Image Input Pipeline (extracted from EXIF metadata)
# altitude comes from F17 Configuration Manager (predefined operational altitude)
# focal_length, sensor_width from F17 Configuration Manager
gsd = H02.compute_gsd(altitude, focal_length, sensor_width, image_width)
config = F17.get_flight_config(flight_id)
altitude = config.altitude # Predefined altitude, NOT from EXIF
gsd = H02.compute_gsd(altitude, config.camera_params.focal_length,
config.camera_params.sensor_width,
config.camera_params.resolution_width)
expected_displacement = frame_spacing * gsd # ~100m typical at 300m altitude
scaled_translation = relative_pose.translation * expected_displacement
# Add scaled_translation to factor graph
```
**Note**: Altitude is passed through the processing chain:
- F05 extracts altitude from EXIF → F02 includes in FrameData → F10 receives with add_relative_factor()
**Note**: Altitude comes from F17 Configuration Manager (predefined operational altitude), NOT from EXIF metadata. The problem statement specifies images don't have GPS metadata.
**Output**:
```python
@@ -95,7 +95,11 @@ class IFailureRecoveryCoordinator(ABC):
F11 emits events instead of directly calling F02's status update methods. This decouples recovery logic from flight state management.
**Events Emitted**:
### Internal Events (Component-to-Component)
F11 emits events for internal component communication. These are NOT directly sent to clients.
**Events Emitted (internal)**:
- `RecoveryStarted`: When tracking loss detected and recovery begins
- `RecoverySucceeded`: When recovery finds a match (single-image or chunk)
- `RecoveryFailed`: When all recovery strategies exhausted
@@ -104,12 +108,27 @@ F11 emits events instead of directly calling F02's status update methods. This d
- `ChunkCreated`: When new chunk created on tracking loss
- `ChunkAnchored`: When chunk successfully matched and anchored
- `ChunkMerged`: When chunk merged into main trajectory (includes flight_id, chunk_id, merged_frames)
- `ChunkMatchingFailed`: When chunk matching exhausts all candidates and fails
**Event Listeners** (F02 Flight Processor subscribes to these):
- On `RecoveryStarted`: Update status to "recovering"
- On `RecoveryFailed`: Update status to "blocked"
- On `RecoverySucceeded`: Update status to "processing"
- On `UserInputNeeded`: Update status to "blocked", blocked=True
- On `ChunkMatchingFailed`: Re-queue chunk for user input or continue building
### External SSE Events (to Clients)
F11 does NOT directly send events to clients. External events are routed through F14 Result Manager which manages SSE streaming via F15:
- When F11 emits `UserInputNeeded` → F02 receives → F02 calls F14.publish_user_input_request() → F14 sends via F15 SSE
- When F11 emits `ChunkMerged` → F02 receives → F02 calls F14.update_results_after_chunk_merge() → F14 sends via F15 SSE
- When F11 emits `RecoveryFailed` → F02 receives → F02 calls F14.publish_processing_blocked() → F14 sends via F15 SSE
This separation ensures:
1. F11 is decoupled from SSE implementation
2. F14 controls all client-facing communication
3. Consistent event format for clients
### Scope
- Confidence monitoring
@@ -516,13 +535,13 @@ bool: True if merge successful
1. Get chunk frames via F12.get_chunk_frames(chunk_id) → merged_frames
2. Get chunk anchor frame (middle frame or best frame)
3. Call F12.mark_chunk_anchored() with GPS (F12 coordinates with F10)
4. **Resolve target chunk**:
- Query F12.get_merge_target(chunk_id) → returns target_chunk_id
- Target selection logic (inside F12):
- If chunk has temporal predecessor (previous chunk by frame_id order): merge to predecessor
- If no predecessor: merge to main trajectory (chunk_id="main")
- F12 maintains chunk ordering based on first frame_id in each chunk
5. Call F12.merge_chunks(chunk_id, target_chunk_id, transform) (F12 coordinates with F10)
4. **Determine merge target**:
- Target is typically the temporal predecessor (previous chunk by frame_id order)
- If no predecessor: merge to main trajectory (target_chunk_id="main")
- F11 determines target based on chunk frame_id ordering
5. Call F12.merge_chunks(target_chunk_id, chunk_id, transform)
- Note: `merge_chunks(target, source)` merges source INTO target
- chunk_id (source) is merged into target_chunk_id
6. F12 handles chunk state updates (deactivation, status updates)
7. F10 optimizes merged graph globally (via F12.merge_chunks())
8. **Emit ChunkMerged event** with flight_id and merged_frames
@@ -580,10 +599,25 @@ while flight_active:
- Reduces user input requests
- **Lifecycle**: Starts when flight becomes active, stops when flight completed
**Chunk Failure Flow**:
When chunk matching fails after trying all candidates:
1. Emit `ChunkMatchingFailed` event with chunk_id and flight_id
2. F02 receives event and decides next action:
- **Option A**: Wait for more frames and retry later (chunk may gain more distinctive features)
- **Option B**: Create user input request for the chunk
3. If user input requested:
- F02 calls F14.publish_user_input_request() with chunk context
- Client receives via SSE
- User provides GPS anchor for one frame in chunk
- F02 receives user fix via handle_user_fix()
- F11.apply_user_anchor() anchors the chunk
- Processing resumes
**Test Cases**:
1. **Background matching**: Unanchored chunks matched asynchronously
2. **Chunk merging**: Chunks merged when matches found
3. **Non-blocking**: Frame processing continues during matching
4. **Chunk matching fails**: ChunkMatchingFailed emitted, user input requested if needed
## Integration Tests
@@ -53,12 +53,8 @@ class IRouteChunkManager(ABC):
pass
@abstractmethod
def merge_chunks(self, chunk_id_1: str, chunk_id_2: str, transform: Sim3Transform) -> bool:
pass
@abstractmethod
def get_merge_target(self, chunk_id: str) -> str:
"""Returns target chunk_id for merging. Returns 'main' for main trajectory."""
def merge_chunks(self, target_chunk_id: str, source_chunk_id: str, transform: Sim3Transform) -> bool:
"""Merges source_chunk INTO target_chunk. Result is stored in target_chunk."""
pass
@abstractmethod
@@ -468,17 +464,17 @@ bool: True if deactivated successfully
---
### `merge_chunks(chunk_id_1: str, chunk_id_2: str, transform: Sim3Transform) -> bool`
### `merge_chunks(target_chunk_id: str, source_chunk_id: str, transform: Sim3Transform) -> bool`
**Description**: Coordinates chunk merging by validating chunks, calling F10 for factor graph merge, and updating chunk states.
**Description**: Merges source_chunk INTO target_chunk. The resulting merged chunk is target_chunk. Source chunk is deactivated after merge.
**Called By**:
- F11 Failure Recovery Coordinator (after successful chunk matching)
**Input**:
```python
chunk_id_1: str # Source chunk (typically newer, to be merged)
chunk_id_2: str # Target chunk (typically older, merged into)
target_chunk_id: str # Target chunk (receives the merge, typically older/main)
source_chunk_id: str # Source chunk (being merged in, typically newer)
transform: Sim3Transform:
translation: np.ndarray # (3,)
rotation: np.ndarray # (3, 3) or quaternion
@@ -492,34 +488,33 @@ bool: True if merge successful
**Processing Flow**:
1. Verify both chunks exist
2. Verify chunk_id_1 is anchored (has_anchor=True)
2. Verify source_chunk_id is anchored (has_anchor=True)
3. Validate chunks can be merged (not already merged, not same chunk)
4. **Merge direction**: chunk_id_1 (newer, source) merges INTO chunk_id_2 (older, target)
5. Call F10.merge_chunks(chunk_id_1, chunk_id_2, transform)
6. Update chunk_id_1 state:
4. Call F10.merge_chunk_subgraphs(flight_id, source_chunk_id, target_chunk_id, transform)
5. Update source_chunk_id state:
- Set is_active=False
- Set matching_status="merged"
- Call deactivate_chunk(chunk_id_1)
7. Update chunk_id_2 state (if needed)
8. Persist chunk state via F03 Flight Database.save_chunk_state()
9. Return True
- Call deactivate_chunk(source_chunk_id)
6. target_chunk remains active (now contains merged frames)
7. Persist chunk state via F03 Flight Database.save_chunk_state()
8. Return True
**Merge Convention**:
- `merge_chunks(target, source)` → source is merged INTO target
- Result is stored in target_chunk
- source_chunk is deactivated after merge
- Example: `merge_chunks("main", "chunk_3")` merges chunk_3 into main trajectory
**Validation**:
- Both chunks must exist
- chunk_id_1 must be anchored
- chunk_id_1 must not already be merged
- chunk_id_1 and chunk_id_2 must be different
**Merge Direction**:
- **chunk_id_1**: Source chunk (newer, recently anchored)
- **chunk_id_2**: Target chunk (older, main trajectory or previous chunk)
- Newer chunks merge INTO older chunks to maintain chronological consistency
- source_chunk must be anchored
- source_chunk must not already be merged
- target_chunk and source_chunk must be different
**Test Cases**:
1. **Merge anchored chunks**: Chunks merged successfully, chunk_id_1 deactivated
2. **Merge unanchored chunk**: Returns False (validation fails)
3. **Merge already merged chunk**: Returns False (validation fails)
4. **State updates**: chunk_id_1 marked as merged and deactivated
1. **Merge anchored chunk**: source_chunk merged into target_chunk
2. **Source deactivated**: source_chunk marked as merged and deactivated
3. **Target unchanged**: target_chunk remains active with new frames
---
@@ -35,7 +35,7 @@ class ICoordinateTransformer(ABC):
pass
@abstractmethod
def image_object_to_gps(self, object_pixel: Tuple[float, float], frame_id: int) -> GPSPoint:
def image_object_to_gps(self, flight_id: str, frame_id: int, object_pixel: Tuple[float, float]) -> GPSPoint:
pass
@abstractmethod
@@ -273,18 +273,19 @@ Tuple[float, float]: (x, y) pixel coordinates
---
### `image_object_to_gps(object_pixel: Tuple[float, float], frame_id: int) -> GPSPoint`
### `image_object_to_gps(flight_id: str, frame_id: int, object_pixel: Tuple[float, float]) -> GPSPoint`
**Description**: **Critical method** - Converts object pixel coordinates to GPS. Used for external object detection integration.
**Called By**:
- F01 Flight API (via `POST /flights/{flightId}/frames/{frameId}/object-to-gps` endpoint)
- F02 Flight Processor (via convert_object_to_gps delegation from F01)
- F14 Result Manager (converts objects to GPS for output)
**Input**:
```python
object_pixel: Tuple[float, float] # Pixel coordinates from object detector
flight_id: str # Flight identifier (needed for ENU origin and factor graph)
frame_id: int # Frame containing object
object_pixel: Tuple[float, float] # Pixel coordinates from object detector
```
**Output**:
@@ -293,15 +294,17 @@ GPSPoint: GPS coordinates of object center
```
**Processing Flow**:
1. Get frame_pose from F10 Factor Graph
2. Get camera_params from F17 Configuration Manager
3. Get altitude from configuration
1. Get frame_pose from F10 Factor Graph Optimizer.get_trajectory(flight_id)[frame_id]
2. Get camera_params from F17 Configuration Manager.get_flight_config(flight_id)
3. Get altitude from F17 Configuration Manager.get_flight_config(flight_id).altitude
4. Call pixel_to_gps(object_pixel, frame_pose, camera_params, altitude)
5. Return GPS
5. Use enu_to_gps(flight_id, enu_point) for final GPS conversion
6. Return GPS
**User Story**:
- External system detects object in UAV image at pixel (1024, 768)
- Calls image_object_to_gps(frame_id=237, object_pixel=(1024, 768))
- Calls F02.convert_object_to_gps(flight_id="abc", frame_id=237, pixel=(1024, 768))
- F02 delegates to F13.image_object_to_gps(flight_id="abc", frame_id=237, object_pixel=(1024, 768))
- Returns GPSPoint(lat=48.123, lon=37.456)
- Object GPS can be used for navigation, targeting, etc.
@@ -155,8 +155,9 @@ frame_ids: List[int] # Frames with updated poses
**Processing Flow**:
1. For each frame_id:
- Get refined pose from F10 Factor Graph Optimizer ENU pose
- Convert ENU pose to GPS via F13 Coordinate Transformer.enu_to_gps(flight_id, enu_pose)
- Get refined pose from F10 Factor Graph Optimizer.get_trajectory(flight_id) → Pose object with ENU position
- Extract ENU tuple: `enu_tuple = (pose.position[0], pose.position[1], pose.position[2])`
- Convert ENU to GPS via F13 Coordinate Transformer.enu_to_gps(flight_id, enu_tuple) → GPSPoint
- Update result with refined=True via F03 Flight Database.save_frame_result()
- Update waypoint via F03 Flight Database.update_waypoint()
- Call F15 SSE Event Streamer.send_refinement()
@@ -205,8 +206,9 @@ merged_frames: List[int] # Frames whose poses changed due to chunk merge
**Processing Flow**:
1. For each frame_id in merged_frames:
- Get updated pose from F10 Factor Graph Optimizer.get_trajectory() → ENU pose
- Convert ENU pose to GPS via F13 Coordinate Transformer.enu_to_gps(flight_id, enu_pose)
- Get updated pose from F10 Factor Graph Optimizer.get_trajectory(flight_id) → Pose object with ENU position
- Extract ENU tuple: `enu_tuple = (pose.position[0], pose.position[1], pose.position[2])`
- Convert ENU to GPS via F13 Coordinate Transformer.enu_to_gps(flight_id, enu_tuple) → GPSPoint
- Update frame result via F03 Flight Database.save_frame_result()
- Update waypoint via F03 Flight Database.update_waypoint()
- Send refinement event via F15 SSE Event Streamer.send_refinement()
@@ -28,9 +28,19 @@ class ISSEEventStreamer(ABC):
def send_refinement(self, flight_id: str, frame_id: int, updated_result: FrameResult) -> bool:
pass
@abstractmethod
def send_heartbeat(self, flight_id: str) -> bool:
"""Sends heartbeat/keepalive to all clients subscribed to flight."""
pass
@abstractmethod
def close_stream(self, flight_id: str, client_id: str) -> bool:
pass
@abstractmethod
def get_active_connections(self, flight_id: str) -> int:
"""Returns count of active SSE connections for a flight."""
pass
```
## Component Description
@@ -170,12 +180,50 @@ StreamConnection:
---
### `send_heartbeat(flight_id: str) -> bool`
**Description**: Sends heartbeat/keepalive ping to all clients subscribed to a flight. Keeps connections alive and helps detect stale connections.
**Called By**:
- Background heartbeat task (every 30 seconds)
- F02 Flight Processor (periodically during processing)
**Event Format**:
```
:heartbeat
```
**Behavior**:
- Sends SSE comment (`:heartbeat`) which doesn't trigger event handlers
- Keeps TCP connection alive
- Client can use to detect connection health
**Test Cases**:
1. Send heartbeat → all clients receive ping
2. Client timeout → connection marked stale
---
### `close_stream(flight_id: str, client_id: str) -> bool`
**Description**: Closes SSE connection.
**Called By**: F01 REST API (on client disconnect)
---
### `get_active_connections(flight_id: str) -> int`
**Description**: Returns count of active SSE connections for a flight.
**Called By**:
- F02 Flight Processor (monitoring)
- Admin tools
**Test Cases**:
1. No connections → returns 0
2. 5 clients connected → returns 5
## Integration Tests
### Test 1: Real-Time Streaming
@@ -27,6 +27,21 @@ class IConfigurationManager(ABC):
@abstractmethod
def update_config(self, section: str, key: str, value: Any) -> bool:
pass
@abstractmethod
def get_operational_altitude(self, flight_id: str) -> float:
"""Returns predefined operational altitude for the flight (NOT from EXIF)."""
pass
@abstractmethod
def get_frame_spacing(self, flight_id: str) -> float:
"""Returns expected distance between consecutive frames in meters."""
pass
@abstractmethod
def save_flight_config(self, flight_id: str, config: FlightConfig) -> bool:
"""Persists flight-specific configuration."""
pass
```
## Component Description
@@ -136,6 +151,62 @@ FlightConfig:
1. Update value → succeeds
2. Invalid key → fails
---
### `get_operational_altitude(flight_id: str) -> float`
**Description**: Returns predefined operational altitude for the flight in meters. This is the altitude provided during flight creation, NOT extracted from EXIF metadata (images don't have GPS/altitude metadata per problem constraints).
**Called By**:
- F10 Factor Graph Optimizer (for scale resolution)
- H02 GSD Calculator (for GSD computation)
- F09 Metric Refinement (for alignment)
**Input**: `flight_id: str`
**Output**: `float` - Altitude in meters (typically 100-500m)
**Test Cases**:
1. Get existing flight altitude → returns value
2. Non-existent flight → raises error
---
### `get_frame_spacing(flight_id: str) -> float`
**Description**: Returns expected distance between consecutive frames in meters. Used for scale estimation in visual odometry.
**Called By**:
- F10 Factor Graph Optimizer (for expected displacement calculation)
**Input**: `flight_id: str`
**Output**: `float` - Expected frame spacing in meters (typically ~100m)
**Test Cases**:
1. Get frame spacing → returns expected distance
---
### `save_flight_config(flight_id: str, config: FlightConfig) -> bool`
**Description**: Persists flight-specific configuration when a flight is created.
**Called By**:
- F02 Flight Processor (during flight creation)
**Input**:
```python
flight_id: str
config: FlightConfig
```
**Output**: `bool` - True if saved successfully
**Test Cases**:
1. Save valid config → succeeds
2. Invalid flight_id → fails
## Data Models
### SystemConfig
+48 -3
View File
@@ -189,20 +189,27 @@
| Client | F01 | `POST /flights` | Create flight |
| F01 | F02 | `create_flight()` | Initialize flight state |
| F02 | F16 | `get_flight_config()` | Get camera params, altitude |
| F02 | F12 | `set_enu_origin(start_gps)` | Set ENU coordinate origin |
| F02 | F13 | `set_enu_origin(flight_id, start_gps)` | Set ENU coordinate origin |
| F02 | F04 | `prefetch_route_corridor()` | Prefetch tiles |
| F04 | Satellite Provider | `GET /api/satellite/tiles/batch` | HTTP batch download |
| F04 | H06 | `compute_tile_bounds()` | Tile coordinate calculations |
| F02 | F03 | `insert_flight()` | Persist flight data |
### SSE Stream Creation
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| Client | F01 | `GET .../stream` | Open SSE connection |
| F01 | F14 | `create_stream()` | Establish SSE channel |
| F01 | F02 | `create_client_stream()` | Route through coordinator |
| F02 | F15 | `create_stream()` | Establish SSE channel |
### Image Upload
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| Client | F01 | `POST .../images/batch` | Upload 10-50 images |
| F01 | F05 | `queue_batch()` | Queue for processing |
| F01 | F02 | `queue_images()` | Route through coordinator |
| F02 | F05 | `queue_batch()` | Queue for processing |
| F05 | H08 | `validate_batch()` | Validate sequence, format |
| F05 | F03 | `save_image_metadata()` | Persist image metadata |
@@ -433,3 +440,41 @@ F02 Flight Processor handles multiple concerns:
**Event Recovery**:
- Events are fire-and-forget (no persistence)
- Subscribers rebuild state from F03 on restart
### Error Propagation Strategy
**Principle**: Errors propagate upward through the component hierarchy. Lower-level components throw exceptions or return error results; higher-level coordinators handle recovery.
**Error Propagation Chain**:
```
H01-H08 (Helpers) → F07/F08/F09 (Visual Processing) → F11 (Recovery) → F02 (Coordinator) → F01 (API) → Client
Events → F14 → F15 → SSE → Client
```
**Error Categories**:
1. **Recoverable** (F11 handles):
- Tracking loss → Progressive search
- Low confidence → Rotation sweep
- Chunk matching fails → User input request
2. **Propagated to Client** (via SSE):
- User input needed → `user_input_needed` event
- Processing blocked → `processing_blocked` event
- Flight completed → `flight_completed` event
3. **Fatal** (F02 handles, returns to F01):
- Database connection lost → HTTP 503
- Model loading failed → HTTP 500
- Invalid configuration → HTTP 400
**User Fix Flow**:
```
Client ---> F01 (POST /user-fix) ---> F02.handle_user_fix() ---> F11.apply_user_anchor()
F10.add_chunk_anchor()
F02 receives UserFixApplied event
F14.publish_result() ---> F15 ---> SSE ---> Client
```
@@ -27,11 +27,25 @@ class IFaissIndexManager(ABC):
@abstractmethod
def load_index(self, path: str) -> FaissIndex:
pass
@abstractmethod
def is_gpu_available(self) -> bool:
pass
@abstractmethod
def set_device(self, device: str) -> bool:
"""Set device: 'gpu' or 'cpu'."""
pass
```
## Component Description
Manages Faiss indices for AnyLoc retrieval (IVF, HNSW options).
Manages Faiss indices for DINOv2 descriptor similarity search. H04 builds indexes from UAV image descriptors for:
1. **Loop closure detection**: Find when UAV revisits previously seen areas within the same flight
2. **Chunk-to-chunk matching**: Match disconnected chunks to each other
3. **Flight-to-flight matching**: Match current flight to previous flights in same area
**Index Source**: Descriptors are computed from UAV images using F08's DINOv2 encoder, NOT from satellite images. The index enables finding similar UAV viewpoints.
## API Methods
@@ -76,9 +90,18 @@ Manages Faiss indices for AnyLoc retrieval (IVF, HNSW options).
**External**: faiss-gpu or faiss-cpu
## GPU/CPU Fallback
H04 supports automatic fallback from GPU to CPU:
- `is_gpu_available()`: Returns True if faiss-gpu is available and CUDA works
- `set_device("gpu")`: Use GPU acceleration (faster for large indexes)
- `set_device("cpu")`: Use CPU (fallback when GPU unavailable)
## Test Cases
1. Build index with 10,000 descriptors → succeeds
2. Search query → returns top-k matches
1. Build index with 10,000 UAV image descriptors → succeeds
2. Search query UAV descriptor → returns top-k similar UAV frames
3. Save/load index → index restored correctly
4. GPU unavailable → automatically falls back to CPU
5. Add descriptors incrementally → index grows correctly
+90 -90
View File
@@ -185,7 +185,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────────
│ F02 Flight Processor │
│ │
│ ┌─────────────┐ │
@@ -265,15 +265,15 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
│ F06 Image Rotation Manager │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ try_rotation_steps(image, satellite_tile, tile_bounds) │ │
│ └──────────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┴───────────────────────────┐ │
│ │ For angle in [0°, 30°, 60°, ... 330°]: │ │
┌───────────────────────────────────────────────────────────────────────────
│ F06 Image Rotation Manager
│ ┌─────────────────────────────────────────────────────────────┐
│ │ try_rotation_steps(image, satellite_tile, tile_bounds) │
│ └──────────────────────────────┬──────────────────────────────┘
│ │
│ ┌───────────────────────┴───────────────────────────┐ │
│ │ For angle in [0°, 30°, 60°, ... 330°]: │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ H07 rotate_image(image, angle) │ │ │
@@ -300,11 +300,11 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ Return RotationResult │ │
│ │ or None │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
│ ┌────────────┴────────────┐
│ │ Return RotationResult │
│ │ or None │
│ └─────────────────────────┘
└──────────────────────────────────────────────────────────────────────────
```
**Output**: RotationResult with precise heading angle
@@ -319,7 +319,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────────
│ F11 Failure Recovery Coordinator │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
@@ -337,34 +337,34 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
│ │ ├─ F06 requires_rotation_sweep() → trigger sweep │ │
│ │ ├─ F08 retrieve_candidate_tiles() (DINOv2) │ │
│ │ └─ Progressive tile search (1→4→9→16→25): │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ For grid_size in [1, 4, 9, 16, 25]: │ │ │
│ │ │ ├─ F04 expand_search_grid() │ │ │
│ │ │ ├─ For each tile: │ │ │
│ │ │ │ ├─ F04 compute_tile_bounds() │ │ │
│ │ │ │ └─ F09 align_to_satellite(img, tile, bounds)│ │ │
│ │ │ │ │ │ │
│ │ │ └─ If match found: BREAK │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ For grid_size in [1, 4, 9, 16, 25]: │ │ │
│ │ │ ├─ F04 expand_search_grid() │ │ │
│ │ │ ├─ For each tile: │ │ │
│ │ │ │ ├─ F04 compute_tile_bounds() │ │ │
│ │ │ │ └─ F09 align_to_satellite(img, tile, bounds)│ │ │
│ │ │ │ │ │ │
│ │ │ └─ If match found: BREAK │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌────────────────────┴────────────────────┐ │
│ │ Single-image match found? │ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ EMIT RecoverySucceeded│ │ Continue chunk building │ │
│ │ Resume normal flow │ │ → Flow 7 (Chunk Building) │ │
│ └─────────────────────┘ │ → Flow 8 (Chunk Matching) │ │
│ (Background) │ │
└─────────────────────────────┘ │
│ ┌─────────────────────┐ ┌────────────────────────────┐
│ │ EMIT RecoverySucceeded│ │ Continue chunk building │
│ │ Resume normal flow │ │ → Flow 7 (Chunk Building) │
│ └─────────────────────┘ │ → Flow 8 (Chunk Matching) │
│ │ (Background) │
────────────────────────────┘
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐
│ │ If all strategies exhausted: │
│ │ → Flow 10 (User Input Recovery) │
│ └─────────────────────────────────────┘
│ ┌─────────────────────────────────────┐ │
│ │ If all strategies exhausted: │ │
│ │ → Flow 10 (User Input Recovery) │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
```
@@ -376,42 +376,42 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
│ F12 Route Chunk Manager │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ create_chunk(flight_id, start_frame_id) │ │
│ │ ├─ F10 create_new_chunk() ← Factor graph subgraph │ │
│ │ ├─ Initialize chunk state (unanchored, active) │ │
│ │ └─ F03 save_chunk_state() │ │
│ └──────────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┴───────────────────────────┐ │
│ │ For each frame in chunk: │ │
┌───────────────────────────────────────────────────────────────────────────
│ F12 Route Chunk Manager
│ ┌─────────────────────────────────────────────────────────────┐
│ │ create_chunk(flight_id, start_frame_id) │
│ │ ├─ F10 create_new_chunk() ← Factor graph subgraph │
│ │ ├─ Initialize chunk state (unanchored, active) │
│ │ └─ F03 save_chunk_state() │
│ └──────────────────────────────┬──────────────────────────────┘
│ │
│ ┌───────────────────────┴───────────────────────────┐ │
│ │ For each frame in chunk: │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ F07 compute_relative_pose_in_chunk()│ │ │
│ │ └───────────────────┬─────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ F12 add_frame_to_chunk() │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ F12 add_frame_to_chunk() │ │ │
│ │ │ └─ F10 add_relative_factor_to_chunk│ │ │
│ │ └───────────────────┬─────────────────┘ │ │
│ │ └───────────────────┬─────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ F10 optimize_chunk() (local) │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ is_chunk_ready_for_matching()? │ │
│ │ ├─ Min 5 frames │ │
│ │ ├─ Max 20 frames │ │
│ │ └─ Internal consistency (good VO inlier counts) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────────────────────────────────┐
│ │ is_chunk_ready_for_matching()? │
│ │ ├─ Min 5 frames │
│ │ ├─ Max 20 frames │
│ │ └─ Internal consistency (good VO inlier counts) │
│ └─────────────────────────────────────────────────────────────┘
└──────────────────────────────────────────────────────────────────────────
```
---
@@ -424,7 +424,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────────
│ F11 process_unanchored_chunks() (Background Task) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
@@ -472,7 +472,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
┌───────────────────────────────────────────────────────────────────────-──┐
│ F11 merge_chunk_to_trajectory() │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
@@ -485,7 +485,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
│ └──────────────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 3. Resolve target: F12 get_merge_target(chunk_id) │ │
│ │ 3. Determine target chunk (predecessor or "main") │ │
│ │ └─ Returns target_chunk_id (predecessor or "main") │ │
│ └──────────────────────────────┬──────────────────────────────┘ │
│ ▼ │
@@ -501,7 +501,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
│ └──────────────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 6. EMIT ChunkMerged event (flight_id, chunk_id, merged_frames)│ │
│ │ 6. EMIT ChunkMerged event (flight_id,chunk_id,merged_frames)│
│ │ └─ F14 subscribes → update_results_after_chunk_merge() │ │
│ │ ├─ F10 get_trajectory() → ENU poses │ │
│ │ ├─ F13 enu_to_gps() for each frame │ │
@@ -523,7 +523,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────────
│ F11 create_user_input_request() │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
@@ -537,7 +537,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
└──────────────────────────────────────────────────────────────────────────┘
▼ Client receives SSE event
┌──────────────────────────────────────────────────────────────────────────┐
┌──────────────────────────────────────────────────────────────────────────
│ Client UI │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
@@ -550,7 +550,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
│ │ POST /flights/{flightId}/user-fix │ │
│ │ body: { frame_id, uav_pixel, satellite_gps } │ │
│ └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
└──────────────────────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────────────────────┐
@@ -577,7 +577,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────-────────────┐
│ F10 Background Optimization │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
@@ -650,7 +650,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
**Sequence**:
```
┌─────────────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────────
│ F02 Flight Processor │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
@@ -741,7 +741,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ EXTERNAL
│ EXTERNAL │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Client │ │ Satellite │ │ External │ │
│ │ (UI) │ │ Provider │ │ Detector │ │
@@ -750,7 +750,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
│ REST/SSE │ HTTP │ REST
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ API LAYER
│ API LAYER │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ F01 Flight API │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
@@ -758,23 +758,23 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
┌─────────────────────────────────────────────────────────────────────────────┐
│ ORCHESTRATION LAYER
│ ORCHESTRATION LAYER │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ F02 Flight Processor │ │
│ │ (Central coordinator, event subscriber, background task manager) │ │
│ │ F02 Flight Processor │ │
│ │ (Central coordinator, event subscriber, background task manager) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
├─────────────────────────────────────────────────────────────┐
▼ ▼
┌─────────────────────────────────────────────┐ ┌────────────────────────────┐
│ DATA MANAGEMENT LAYER │ │ RECOVERY LAYER │
│ DATA MANAGEMENT LAYER │ │ RECOVERY LAYER │
│ ┌─────────────┐ ┌─────────────┐ │ │ ┌───────────────────────┐ │
│ │ F04 │ │ F05 │ │ │ │ F11 │ │
│ │ Satellite │ │ Image │ │ │ │ Failure Recovery │ │
│ │ Data │ │ Input │ │ │ │ (Event emitter) │ │
│ └─────────────┘ └─────────────┘ │ │ └───────────────────────┘ │
│ │ │ │
│ │ │ │ │
│ ┌─────────────┐ ┌─────────────┐ │ │ ▼ │
│ │ F12 │ │ F03 │ │ │ ┌───────────────────────┐ │
│ │Route Chunk │ │ Flight │ │ │ │ F12 │ │
@@ -784,7 +784,7 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
┌─────────────────────────────────────────────────────────────────────────────┐
│ VISUAL PROCESSING LAYER
│ VISUAL PROCESSING LAYER │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ F06 │ │ F07 │ │ F08 │ │ F09 │ │
│ │ Rotation │ │ Sequential VO │ │ Global │ │ Metric │ │
@@ -796,16 +796,16 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
┌─────────────────────────────────────────────────────────────────────────────┐
│ STATE ESTIMATION LAYER
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ F10 Factor Graph Optimizer │ │
│ │ (GTSAM, iSAM2, Robust kernels, Chunk subgraphs, Sim(3) merging) │ │
│ STATE ESTIMATION LAYER │
│ ┌──────────────────────────────────────────────────────────────────k──┐ │
│ │ F10 Factor Graph Optimizer │ │
│ │ (GTSAM, iSAM2, Robust kernels, Chunk subgraphs, Sim(3) merging) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ OUTPUT LAYER
│ OUTPUT LAYER │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ F13 │ │ F14 │ │ F15 │ │
│ │ Coordinate │ │ Result │ │ SSE │ │
@@ -815,14 +815,14 @@ ASTRAL-Next is a GPS-denied UAV visual localization system using tri-layer match
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────────────-
│ INFRASTRUCTURE LAYER │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────────────────┐│
│ │ F16 │ │ F17 │ │ HELPERS ││
│ │ Model │ │Configuration │ │ H01-H08 (Camera, GSD, Kernels, ││
│ │ Manager │ │ Manager │ │ Faiss, Monitor, Mercator, etc) ││
│ └───────────────┘ └───────────────┘ └───────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────────────────-┐│
│ │ F16 │ │ F17 │ │ HELPERS ││
│ │ Model │ │Configuration │ │ H01-H08 (Camera, GSD, Kernels, ││
│ │ Manager │ │ Manager │ │ Faiss, Monitor, Mercator, etc) ││
│ └───────────────┘ └───────────────┘ └───────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────
```
---