mirror of
https://github.com/azaion/gps-denied-desktop.git
synced 2026-04-22 23:56:35 +00:00
initial structure implemented
docs -> _docs
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Feature: Image Rotation Core
|
||||
|
||||
## Description
|
||||
Pure image rotation operations without state. Provides utility functions to rotate single images and batches of images by specified angles around their center. This is the foundation for rotation sweeps and pre-rotation before matching.
|
||||
|
||||
## Component APIs Implemented
|
||||
- `rotate_image_360(image: np.ndarray, angle: float) -> np.ndarray`
|
||||
- `rotate_chunk_360(chunk_images: List[np.ndarray], angle: float) -> List[np.ndarray]`
|
||||
|
||||
## External Tools and Services
|
||||
- **opencv-python**: `cv2.warpAffine` for rotation transformation
|
||||
- **numpy**: Matrix operations for rotation matrix construction
|
||||
|
||||
## Internal Methods
|
||||
- `_build_rotation_matrix(center: Tuple[float, float], angle: float) -> np.ndarray`: Constructs 2x3 affine rotation matrix
|
||||
- `_get_image_center(image: np.ndarray) -> Tuple[float, float]`: Calculates image center coordinates
|
||||
|
||||
## Unit Tests
|
||||
|
||||
### rotate_image_360
|
||||
1. **Rotate 0°**: Input image equals output image (identity)
|
||||
2. **Rotate 90°**: Image rotated 90° clockwise, dimensions preserved
|
||||
3. **Rotate 180°**: Image inverted correctly
|
||||
4. **Rotate 270°**: Image rotated 270° clockwise
|
||||
5. **Rotate 45°**: Diagonal rotation with black fill at corners
|
||||
6. **Rotate 360°**: Equivalent to 0° rotation
|
||||
7. **Negative angle**: -90° equivalent to 270°
|
||||
8. **Large angle normalization**: 450° equivalent to 90°
|
||||
|
||||
### rotate_chunk_360
|
||||
1. **Empty chunk**: Returns empty list
|
||||
2. **Single image chunk**: Equivalent to rotate_image_360
|
||||
3. **Multiple images**: All images rotated by same angle
|
||||
4. **Image independence**: Original chunk images unchanged
|
||||
5. **Consistent dimensions**: All output images have same dimensions as input
|
||||
|
||||
## Integration Tests
|
||||
None - this feature is stateless and has no external dependencies beyond opencv/numpy.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Feature: Heading Management
|
||||
|
||||
## Description
|
||||
Manages UAV heading state per flight. Tracks current heading, maintains heading history, detects sharp turns, and determines when rotation sweeps are required. This is the stateful core of the rotation manager.
|
||||
|
||||
## Component APIs Implemented
|
||||
- `get_current_heading(flight_id: str) -> Optional[float]`
|
||||
- `update_heading(flight_id: str, frame_id: int, heading: float, timestamp: datetime) -> bool`
|
||||
- `detect_sharp_turn(flight_id: str, new_heading: float) -> bool`
|
||||
- `requires_rotation_sweep(flight_id: str) -> bool`
|
||||
|
||||
## External Tools and Services
|
||||
None - pure Python state management.
|
||||
|
||||
## Internal Methods
|
||||
- `_normalize_angle(angle: float) -> float`: Normalizes angle to 0-360 range
|
||||
- `_calculate_angle_delta(angle1: float, angle2: float) -> float`: Calculates smallest delta between two angles (handles wraparound)
|
||||
- `_get_flight_state(flight_id: str) -> HeadingHistory`: Gets or creates heading state for flight
|
||||
- `_add_to_history(flight_id: str, heading: float)`: Adds heading to circular history buffer
|
||||
- `_set_sweep_required(flight_id: str, required: bool)`: Sets sweep required flag (used after tracking loss)
|
||||
|
||||
## Unit Tests
|
||||
|
||||
### get_current_heading
|
||||
1. **New flight**: Returns None (no heading set)
|
||||
2. **After update**: Returns last updated heading
|
||||
3. **Multiple flights**: Each flight has independent heading
|
||||
|
||||
### update_heading
|
||||
1. **First heading**: Sets initial heading, returns True
|
||||
2. **Update heading**: Overwrites previous heading
|
||||
3. **Angle normalization**: 370° stored as 10°
|
||||
4. **Negative normalization**: -30° stored as 330°
|
||||
5. **History tracking**: Heading added to history list
|
||||
6. **History limit**: Only last 10 headings kept
|
||||
|
||||
### detect_sharp_turn
|
||||
1. **No current heading**: Returns False (can't detect turn)
|
||||
2. **Small turn (15°)**: 60° → 75° returns False
|
||||
3. **Sharp turn (60°)**: 60° → 120° returns True
|
||||
4. **Exactly 45°**: Returns False (threshold is >45)
|
||||
5. **Exactly 46°**: Returns True
|
||||
6. **Wraparound small**: 350° → 20° returns False (30° delta)
|
||||
7. **Wraparound sharp**: 350° → 60° returns True (70° delta)
|
||||
8. **180° turn**: 0° → 180° returns True
|
||||
|
||||
### requires_rotation_sweep
|
||||
1. **First frame (no heading)**: Returns True
|
||||
2. **Heading known, no flags**: Returns False
|
||||
3. **Tracking loss flag set**: Returns True
|
||||
4. **Sharp turn detected recently**: Returns True
|
||||
5. **After successful match**: Returns False
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### Test 1: Heading Lifecycle
|
||||
1. Create new flight
|
||||
2. get_current_heading → None
|
||||
3. requires_rotation_sweep → True
|
||||
4. update_heading(heading=45°)
|
||||
5. get_current_heading → 45°
|
||||
6. requires_rotation_sweep → False
|
||||
|
||||
### Test 2: Sharp Turn Flow
|
||||
1. update_heading(heading=90°)
|
||||
2. detect_sharp_turn(new_heading=100°) → False
|
||||
3. detect_sharp_turn(new_heading=180°) → True
|
||||
4. Set sweep required flag
|
||||
5. requires_rotation_sweep → True
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# Feature: Rotation Sweep Orchestration
|
||||
|
||||
## Description
|
||||
Coordinates rotation sweeps by rotating images at 30° steps and delegating matching to an injected matcher (F09 Metric Refinement). Calculates precise angles from homography matrices after successful matches. This feature ties together image rotation and heading management with external matching.
|
||||
|
||||
## Component APIs Implemented
|
||||
- `try_rotation_steps(flight_id: str, frame_id: int, image: np.ndarray, satellite_tile: np.ndarray, tile_bounds: TileBounds, timestamp: datetime, matcher: IImageMatcher) -> Optional[RotationResult]`
|
||||
- `try_chunk_rotation_steps(chunk_images: List[np.ndarray], satellite_tile: np.ndarray, tile_bounds: TileBounds, matcher: IImageMatcher) -> Optional[RotationResult]`
|
||||
- `calculate_precise_angle(homography: np.ndarray, initial_angle: float) -> float`
|
||||
|
||||
## External Tools and Services
|
||||
- **H07 Image Rotation Utils**: Angle extraction from homography
|
||||
- **IImageMatcher (injected)**: F09 Metric Refinement for align_to_satellite and align_chunk_to_satellite
|
||||
|
||||
## Internal Methods
|
||||
- `_get_rotation_steps() -> List[float]`: Returns [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330]
|
||||
- `_extract_rotation_from_homography(homography: np.ndarray) -> float`: Extracts rotation component from 3x3 homography
|
||||
- `_combine_angles(initial_angle: float, delta_angle: float) -> float`: Combines step angle with homography delta, normalizes result
|
||||
- `_select_best_result(results: List[Tuple[float, AlignmentResult]]) -> Tuple[float, AlignmentResult]`: Selects highest confidence match if multiple found
|
||||
|
||||
## Unit Tests
|
||||
|
||||
### calculate_precise_angle
|
||||
1. **Identity homography**: Returns initial_angle unchanged
|
||||
2. **Small rotation delta**: initial=60°, homography shows +2.5° → returns 62.5°
|
||||
3. **Negative delta**: initial=30°, homography shows -3° → returns 27°
|
||||
4. **Large delta normalization**: initial=350°, delta=+20° → returns 10°
|
||||
5. **Invalid homography (singular)**: Returns initial_angle as fallback
|
||||
6. **Near-zero homography**: Returns initial_angle as fallback
|
||||
|
||||
### try_rotation_steps (with mock matcher)
|
||||
1. **Match at 0°**: First rotation matches, returns RotationResult with initial_angle=0
|
||||
2. **Match at 60°**: Third rotation matches, returns RotationResult with initial_angle=60
|
||||
3. **Match at 330°**: Last rotation matches, returns RotationResult with initial_angle=330
|
||||
4. **No match**: All 12 rotations fail, returns None
|
||||
5. **Multiple matches**: Returns highest confidence result
|
||||
6. **Heading updated**: After match, flight heading is updated
|
||||
7. **Confidence threshold**: Match below threshold rejected
|
||||
|
||||
### try_chunk_rotation_steps (with mock matcher)
|
||||
1. **Match at 0°**: First rotation matches chunk
|
||||
2. **Match at 120°**: Returns RotationResult with initial_angle=120
|
||||
3. **No match**: All 12 rotations fail, returns None
|
||||
4. **Chunk consistency**: All images rotated by same angle before matching
|
||||
5. **Does not update heading**: Chunk matching doesn't affect flight state
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### Test 1: First Frame Rotation Sweep
|
||||
1. Create flight with no heading
|
||||
2. Call try_rotation_steps with image, satellite tile, mock matcher
|
||||
3. Mock matcher returns match at 60° rotation
|
||||
4. Verify RotationResult.initial_angle = 60
|
||||
5. Verify RotationResult.precise_angle refined from homography
|
||||
6. Verify flight heading updated to precise_angle
|
||||
|
||||
### Test 2: Full Sweep No Match
|
||||
1. Call try_rotation_steps with mock matcher that never matches
|
||||
2. Verify all 12 rotations attempted (0°, 30°, ..., 330°)
|
||||
3. Verify returns None
|
||||
4. Verify flight heading unchanged
|
||||
|
||||
### Test 3: Chunk Rotation Sweep
|
||||
1. Create chunk with 10 images
|
||||
2. Call try_chunk_rotation_steps with mock matcher
|
||||
3. Mock matcher returns match at 90° rotation
|
||||
4. Verify all 10 images were rotated before matching call
|
||||
5. Verify RotationResult returned with correct angles
|
||||
|
||||
### Test 4: Precise Angle Calculation
|
||||
1. Perform rotation sweep, match at 60° step
|
||||
2. Homography indicates +2.3° additional rotation
|
||||
3. Verify precise_angle = 62.3°
|
||||
4. Verify heading updated to 62.3° (not 60°)
|
||||
|
||||
+554
@@ -0,0 +1,554 @@
|
||||
# Image Rotation Manager
|
||||
|
||||
## Interface Definition
|
||||
|
||||
**Interface Name**: `IImageRotationManager`
|
||||
|
||||
### Interface Methods
|
||||
|
||||
```python
|
||||
class IImageRotationManager(ABC):
|
||||
@abstractmethod
|
||||
def rotate_image_360(self, image: np.ndarray, angle: float) -> np.ndarray:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def try_rotation_steps(self, flight_id: str, frame_id: int, image: np.ndarray, satellite_tile: np.ndarray, tile_bounds: TileBounds, timestamp: datetime, matcher: IImageMatcher) -> Optional[RotationResult]:
|
||||
"""
|
||||
Performs rotation sweep.
|
||||
'matcher' is an injected dependency (usually F09) to avoid direct coupling.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def calculate_precise_angle(self, homography: np.ndarray, initial_angle: float) -> float:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_heading(self, flight_id: str) -> Optional[float]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_heading(self, flight_id: str, frame_id: int, heading: float, timestamp: datetime) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def detect_sharp_turn(self, flight_id: str, new_heading: float) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def requires_rotation_sweep(self, flight_id: str) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def rotate_chunk_360(self, chunk_images: List[np.ndarray], angle: float) -> List[np.ndarray]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def try_chunk_rotation_steps(self, chunk_images: List[np.ndarray], satellite_tile: np.ndarray, tile_bounds: TileBounds, matcher: IImageMatcher) -> Optional[RotationResult]:
|
||||
pass
|
||||
```
|
||||
|
||||
## Component Description
|
||||
|
||||
### Responsibilities
|
||||
- Image rotation utility.
|
||||
- Heading tracking.
|
||||
- Coordination of rotation sweeps.
|
||||
|
||||
### Decoupling Fix
|
||||
- **Problem**: F06 previously depended directly on `F09 Metric Refinement`.
|
||||
- **Fix**: Methods `try_rotation_steps` and `try_chunk_rotation_steps` now accept a `matcher` argument conforming to `IImageMatcher`.
|
||||
- **IImageMatcher Interface**:
|
||||
```python
|
||||
class IImageMatcher(ABC):
|
||||
def align_to_satellite(self, uav_image, satellite_tile, tile_bounds) -> AlignmentResult: pass
|
||||
def align_chunk_to_satellite(self, chunk_images, satellite_tile, tile_bounds) -> ChunkAlignmentResult: pass
|
||||
```
|
||||
- **Runtime**: F02.2 injects F09 instance when calling F06 methods.
|
||||
|
||||
### Scope
|
||||
- Image rotation operations (pure rotation, no matching)
|
||||
- UAV heading tracking and history
|
||||
- Sharp turn detection
|
||||
- Rotation sweep coordination (rotates images, delegates matching to F09 Metric Refinement)
|
||||
- Precise angle calculation from homography (extracted from F09 results)
|
||||
- **Chunk-level rotation (all images rotated by same angle)**
|
||||
|
||||
## API Methods
|
||||
|
||||
### `rotate_image_360(image: np.ndarray, angle: float) -> np.ndarray`
|
||||
|
||||
**Description**: Rotates an image by specified angle around center.
|
||||
|
||||
**Called By**:
|
||||
- Internal (during rotation sweep)
|
||||
- H07 Image Rotation Utils (may delegate to)
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
image: np.ndarray # Input image (H×W×3)
|
||||
angle: float # Rotation angle in degrees (0-360)
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
np.ndarray # Rotated image (same dimensions)
|
||||
```
|
||||
|
||||
**Processing Details**:
|
||||
- Rotation around image center
|
||||
- Preserves image dimensions
|
||||
- Fills borders with black or extrapolation
|
||||
|
||||
**Error Conditions**:
|
||||
- None (always returns rotated image)
|
||||
|
||||
**Test Cases**:
|
||||
1. **Rotate 90°**: Image rotated correctly
|
||||
2. **Rotate 0°**: Image unchanged
|
||||
3. **Rotate 180°**: Image inverted
|
||||
4. **Rotate 45°**: Diagonal rotation
|
||||
|
||||
---
|
||||
|
||||
### `try_rotation_steps(flight_id: str, frame_id: int, image: np.ndarray, satellite_tile: np.ndarray, tile_bounds: TileBounds, timestamp: datetime, matcher: IImageMatcher) -> Optional[RotationResult]`
|
||||
|
||||
**Description**: Performs 30° rotation sweep, rotating image at each step and delegating matching to F09 Metric Refinement.
|
||||
|
||||
**Called By**:
|
||||
- Internal (when requires_rotation_sweep() returns True)
|
||||
- Main processing loop (first frame or sharp turn)
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
flight_id: str
|
||||
frame_id: int # Frame identifier for heading persistence
|
||||
image: np.ndarray # UAV image
|
||||
satellite_tile: np.ndarray # Satellite reference tile
|
||||
tile_bounds: TileBounds # GPS bounds and GSD of satellite tile (passed to F09)
|
||||
timestamp: datetime # Timestamp for heading persistence
|
||||
matcher: IImageMatcher # Injected matcher (F09)
|
||||
```
|
||||
|
||||
**About tile_bounds**: `TileBounds` contains the GPS bounding box of the satellite tile:
|
||||
- `nw`, `ne`, `sw`, `se`: GPS coordinates of tile corners
|
||||
- `center`: GPS coordinate of tile center
|
||||
- `gsd`: Ground Sampling Distance (meters/pixel)
|
||||
|
||||
The caller (F02 Flight Processor) obtains tile_bounds by calling `F04.compute_tile_bounds(tile_coords)` before calling this method. F06 passes tile_bounds to F09.align_to_satellite() which uses it to convert pixel coordinates to GPS.
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
RotationResult:
|
||||
matched: bool
|
||||
initial_angle: float # Best matching step angle (0, 30, 60, ...)
|
||||
precise_angle: float # Refined angle from homography
|
||||
confidence: float
|
||||
homography: np.ndarray
|
||||
```
|
||||
|
||||
**Algorithm**:
|
||||
```
|
||||
For angle in [0°, 30°, 60°, 90°, 120°, 150°, 180°, 210°, 240°, 270°, 300°, 330°]:
|
||||
rotated_image = rotate_image_360(image, angle)
|
||||
result = matcher.align_to_satellite(rotated_image, satellite_tile, tile_bounds)
|
||||
if result.matched and result.confidence > threshold:
|
||||
precise_angle = calculate_precise_angle(result.homography, angle)
|
||||
update_heading(flight_id, frame_id, precise_angle, timestamp)
|
||||
return RotationResult(matched=True, initial_angle=angle, precise_angle=precise_angle, ...)
|
||||
return None # No match found
|
||||
```
|
||||
|
||||
**Processing Flow**:
|
||||
1. For each 30° step:
|
||||
- Rotate image via rotate_image_360()
|
||||
- Call matcher.align_to_satellite(rotated_image, satellite_tile, tile_bounds)
|
||||
- Check if match found
|
||||
2. If match found:
|
||||
- Calculate precise angle from homography via calculate_precise_angle()
|
||||
- Update UAV heading via update_heading()
|
||||
- Return RotationResult
|
||||
3. If no match:
|
||||
- Return None (triggers progressive search expansion)
|
||||
|
||||
**Error Conditions**:
|
||||
- Returns `None`: No match found in any rotation
|
||||
- This is expected behavior (leads to progressive search)
|
||||
|
||||
**Test Cases**:
|
||||
1. **Match at 60°**: Finds match, returns result
|
||||
2. **Match at 0°**: No rotation needed, finds match
|
||||
3. **No match**: All 12 rotations tried, returns None
|
||||
4. **Multiple matches**: Returns best confidence
|
||||
|
||||
---
|
||||
|
||||
### `calculate_precise_angle(homography: np.ndarray, initial_angle: float) -> float`
|
||||
|
||||
**Description**: Calculates precise rotation angle from homography matrix point shifts.
|
||||
|
||||
**Called By**:
|
||||
- Internal (after LiteSAM match in rotation sweep)
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
homography: np.ndarray # 3×3 homography matrix from LiteSAM
|
||||
initial_angle: float # 30° step angle that matched
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
float: Precise rotation angle (e.g., 62.3° refined from 60° step)
|
||||
```
|
||||
|
||||
**Algorithm**:
|
||||
1. Extract rotation component from homography
|
||||
2. Calculate angle from rotation matrix
|
||||
3. Refine initial_angle with delta from homography
|
||||
|
||||
**Uses**: H07 Image Rotation Utils for angle calculation
|
||||
|
||||
**Error Conditions**:
|
||||
- Falls back to initial_angle if calculation fails
|
||||
|
||||
**Test Cases**:
|
||||
1. **Refine 60°**: Returns 62.5° (small delta)
|
||||
2. **Refine 0°**: Returns 3.2° (small rotation)
|
||||
3. **Invalid homography**: Returns initial_angle
|
||||
|
||||
---
|
||||
|
||||
### `get_current_heading(flight_id: str) -> Optional[float]`
|
||||
|
||||
**Description**: Gets current UAV heading angle for a flight.
|
||||
|
||||
**Called By**:
|
||||
- F06 Internal (to check if pre-rotation needed)
|
||||
- Main processing loop (before LiteSAM)
|
||||
- F11 Failure Recovery Coordinator (logging)
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
flight_id: str
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
Optional[float]: Heading angle in degrees (0-360), or None if not initialized
|
||||
```
|
||||
|
||||
**Error Conditions**:
|
||||
- Returns `None`: First frame, heading not yet determined
|
||||
|
||||
**Test Cases**:
|
||||
1. **After first frame**: Returns heading angle
|
||||
2. **Before first frame**: Returns None
|
||||
3. **During flight**: Returns current heading
|
||||
|
||||
---
|
||||
|
||||
### `update_heading(flight_id: str, frame_id: int, heading: float, timestamp: datetime) -> bool`
|
||||
|
||||
**Description**: Updates UAV heading angle after successful match.
|
||||
|
||||
**Called By**:
|
||||
- Internal (after rotation sweep match)
|
||||
- Internal (after normal LiteSAM match with small rotation delta)
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
flight_id: str
|
||||
frame_id: int # Frame identifier for database persistence
|
||||
heading: float # New heading angle (0-360)
|
||||
timestamp: datetime # Timestamp for database persistence
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
bool: True if updated successfully
|
||||
```
|
||||
|
||||
**Processing Flow**:
|
||||
1. Normalize angle to 0-360 range
|
||||
2. Add to heading history (last 10 headings)
|
||||
3. Update current_heading for flight
|
||||
4. Return True (caller F02 is responsible for persistence via F03)
|
||||
|
||||
**Note**: Heading persistence is the caller's responsibility (F02 Flight Processor calls F03.save_heading() after receiving the updated heading).
|
||||
|
||||
**Test Cases**:
|
||||
1. **Update heading**: Sets new heading
|
||||
2. **Angle normalization**: 370° → 10°
|
||||
3. **History tracking**: Maintains last 10 headings
|
||||
|
||||
---
|
||||
|
||||
### `detect_sharp_turn(flight_id: str, new_heading: float) -> bool`
|
||||
|
||||
**Description**: Detects if UAV made a sharp turn (>45° heading change).
|
||||
|
||||
**Called By**:
|
||||
- Internal (before deciding if rotation sweep needed)
|
||||
- Main processing loop
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
flight_id: str
|
||||
new_heading: float # Proposed new heading
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
bool: True if sharp turn detected (>45° change)
|
||||
```
|
||||
|
||||
**Algorithm**:
|
||||
```python
|
||||
current = get_current_heading(flight_id)
|
||||
if current is None:
|
||||
return False
|
||||
delta = abs(new_heading - current)
|
||||
if delta > 180: # Handle wraparound
|
||||
delta = 360 - delta
|
||||
return delta > 45
|
||||
```
|
||||
|
||||
**Test Cases**:
|
||||
1. **Small turn**: 60° → 75° → False (15° delta)
|
||||
2. **Sharp turn**: 60° → 120° → True (60° delta)
|
||||
3. **Wraparound**: 350° → 20° → False (30° delta)
|
||||
4. **180° turn**: 0° → 180° → True
|
||||
|
||||
---
|
||||
|
||||
### `requires_rotation_sweep(flight_id: str) -> bool`
|
||||
|
||||
**Description**: Determines if rotation sweep is needed for current frame.
|
||||
|
||||
**Called By**:
|
||||
- Main processing loop (before each frame)
|
||||
- F11 Failure Recovery Coordinator (after tracking loss)
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
flight_id: str
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
bool: True if rotation sweep required
|
||||
```
|
||||
|
||||
**Conditions for sweep**:
|
||||
1. **First frame**: heading not initialized
|
||||
2. **Sharp turn detected**: >45° heading change from VO
|
||||
3. **Tracking loss**: LiteSAM failed to match in previous frame
|
||||
4. **User flag**: Manual trigger (rare)
|
||||
|
||||
**Test Cases**:
|
||||
1. **First frame**: Returns True
|
||||
2. **Second frame, no turn**: Returns False
|
||||
3. **Sharp turn detected**: Returns True
|
||||
4. **Tracking loss**: Returns True
|
||||
|
||||
---
|
||||
|
||||
### `rotate_chunk_360(chunk_images: List[np.ndarray], angle: float) -> List[np.ndarray]`
|
||||
|
||||
**Description**: Rotates all images in a chunk by the same angle.
|
||||
|
||||
**Called By**:
|
||||
- Internal (during try_chunk_rotation_steps)
|
||||
- F11 Failure Recovery Coordinator (chunk rotation sweeps)
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
chunk_images: List[np.ndarray] # 5-20 images from chunk
|
||||
angle: float # Rotation angle in degrees (0-360)
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
List[np.ndarray] # Rotated images (same dimensions)
|
||||
```
|
||||
|
||||
**Processing Flow**:
|
||||
1. For each image in chunk:
|
||||
- rotate_image_360(image, angle) → rotated_image
|
||||
2. Return list of rotated images
|
||||
|
||||
**Performance**:
|
||||
- Rotation time: ~20ms × N images
|
||||
- For 10 images: ~200ms total
|
||||
|
||||
**Test Cases**:
|
||||
1. **Rotate chunk**: All images rotated correctly
|
||||
2. **Angle consistency**: All images rotated by same angle
|
||||
3. **Image preservation**: Original images unchanged
|
||||
|
||||
---
|
||||
|
||||
### `try_chunk_rotation_steps(chunk_images: List[np.ndarray], satellite_tile: np.ndarray, tile_bounds: TileBounds, matcher: IImageMatcher) -> Optional[RotationResult]`
|
||||
|
||||
**Description**: Performs 30° rotation sweep on entire chunk, rotating all images at each step and delegating matching to F09 Metric Refinement.
|
||||
|
||||
**Called By**:
|
||||
- F11 Failure Recovery Coordinator (chunk matching with rotation)
|
||||
|
||||
**Input**:
|
||||
```python
|
||||
chunk_images: List[np.ndarray] # Chunk images
|
||||
satellite_tile: np.ndarray # Reference satellite tile
|
||||
tile_bounds: TileBounds # GPS bounds and GSD of satellite tile (for F09)
|
||||
matcher: IImageMatcher # Injected matcher
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```python
|
||||
RotationResult:
|
||||
matched: bool
|
||||
initial_angle: float # Best matching step angle (0, 30, 60, ...)
|
||||
precise_angle: float # Refined angle from homography
|
||||
confidence: float
|
||||
homography: np.ndarray
|
||||
```
|
||||
|
||||
**Algorithm**:
|
||||
```
|
||||
For angle in [0°, 30°, 60°, 90°, 120°, 150°, 180°, 210°, 240°, 270°, 300°, 330°]:
|
||||
rotated_chunk = rotate_chunk_360(chunk_images, angle)
|
||||
result = matcher.align_chunk_to_satellite(rotated_chunk, satellite_tile, tile_bounds)
|
||||
if result.matched and result.confidence > threshold:
|
||||
precise_angle = calculate_precise_angle(result.homography, angle)
|
||||
return RotationResult(matched=True, initial_angle=angle, precise_angle=precise_angle, ...)
|
||||
return None # No match found
|
||||
```
|
||||
|
||||
**Processing Flow**:
|
||||
1. For each 30° step:
|
||||
- Rotate all chunk images via rotate_chunk_360()
|
||||
- Call matcher.align_chunk_to_satellite(rotated_chunk, satellite_tile, tile_bounds)
|
||||
- Check if match found
|
||||
2. If match found:
|
||||
- Calculate precise angle from homography via calculate_precise_angle()
|
||||
- Return RotationResult
|
||||
3. If no match:
|
||||
- Return None
|
||||
|
||||
**Performance**:
|
||||
- 12 rotations × chunk matching via F09 (~60ms) = ~720ms
|
||||
- Acceptable for chunk matching (async operation)
|
||||
|
||||
**Test Cases**:
|
||||
1. **Match at 60°**: Finds match, returns result
|
||||
2. **Match at 0°**: No rotation needed, finds match
|
||||
3. **No match**: All 12 rotations tried, returns None
|
||||
4. **Multiple matches**: Returns best confidence
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### Test 1: First Frame Rotation Sweep
|
||||
1. First frame arrives (no heading set)
|
||||
2. requires_rotation_sweep() → True
|
||||
3. try_rotation_steps(flight_id, frame_id=1, image, satellite_tile, tile_bounds, timestamp=now()) → rotates 12 times
|
||||
4. F09 Metric Refinement called for each rotation
|
||||
5. Match found at 60° step
|
||||
6. calculate_precise_angle() → 62.3°
|
||||
7. update_heading(flight_id, frame_id=1, heading=62.3°, timestamp=now())
|
||||
8. Subsequent frames use 62.3° heading
|
||||
|
||||
### Test 2: Normal Frame Processing
|
||||
1. Heading known (90°)
|
||||
2. requires_rotation_sweep() → False
|
||||
3. Pre-rotate image to 90°
|
||||
4. LiteSAM match succeeds with small delta (+2.5°)
|
||||
5. update_heading(flight_id, frame_id=237, heading=92.5°, timestamp=now())
|
||||
|
||||
### Test 3: Sharp Turn Detection
|
||||
1. UAV heading 45°
|
||||
2. Next frame shows 120° heading (from VO estimate)
|
||||
3. detect_sharp_turn() → True (75° delta)
|
||||
4. requires_rotation_sweep() → True
|
||||
5. Perform rotation sweep → find match at 120° step
|
||||
|
||||
### Test 4: Tracking Loss Recovery
|
||||
1. F09 Metric Refinement fails to match (no overlap after turn)
|
||||
2. requires_rotation_sweep() → True
|
||||
3. try_rotation_steps(flight_id, frame_id, image, satellite_tile, tile_bounds, timestamp) with all 12 rotations
|
||||
4. F09 called for each rotation step
|
||||
5. Match found → heading updated
|
||||
|
||||
### Test 5: Chunk Rotation Sweeps
|
||||
1. Build chunk with 10 images (unknown orientation)
|
||||
2. try_chunk_rotation_steps(chunk_images, satellite_tile, tile_bounds) with all 12 rotations
|
||||
3. F09 Metric Refinement called for each rotation
|
||||
4. Match found at 120° step
|
||||
5. Precise angle calculated (122.5°)
|
||||
6. Verify all images rotated consistently
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### Performance
|
||||
- **rotate_image_360**: < 20ms per rotation
|
||||
- **try_rotation_steps**: < 1.2 seconds (12 rotations × 100ms LiteSAM)
|
||||
- **calculate_precise_angle**: < 10ms
|
||||
- **get_current_heading**: < 1ms
|
||||
- **update_heading**: < 5ms
|
||||
|
||||
### Accuracy
|
||||
- **Angle precision**: ±0.5° for precise angle calculation
|
||||
- **Sharp turn detection**: 100% accuracy for >45° turns
|
||||
|
||||
### Reliability
|
||||
- Rotation sweep always completes all 12 steps
|
||||
- Graceful handling of no-match scenarios
|
||||
- Heading history preserved across failures
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Internal Components
|
||||
- **H07 Image Rotation Utils**: For image rotation and angle calculations
|
||||
- **Injected Matcher (F09)**.
|
||||
|
||||
**Note**:
|
||||
- `TileBounds` data model is imported from F09 Metric Refinement.
|
||||
- F06 does NOT call F04 directly. The caller (F02 or F11) provides satellite tiles and tile_bounds.
|
||||
- F06 does NOT persist heading to database. The caller (F02) is responsible for calling F03.save_heading().
|
||||
- Chunk rotation orchestration (calling try_chunk_rotation_steps in recovery flow) is done by F11 Failure Recovery Coordinator.
|
||||
|
||||
### External Dependencies
|
||||
- **opencv-python**: Image rotation (`cv2.warpAffine`)
|
||||
- **numpy**: Matrix operations
|
||||
|
||||
## Data Models
|
||||
|
||||
### RotationResult
|
||||
```python
|
||||
class RotationResult(BaseModel):
|
||||
matched: bool
|
||||
initial_angle: float # 30° step angle (0, 30, 60, ...)
|
||||
precise_angle: float # Refined angle from homography
|
||||
confidence: float
|
||||
homography: np.ndarray
|
||||
inlier_count: int
|
||||
```
|
||||
|
||||
### HeadingHistory
|
||||
```python
|
||||
class HeadingHistory(BaseModel):
|
||||
flight_id: str
|
||||
current_heading: float
|
||||
heading_history: List[float] # Last 10 headings
|
||||
last_update: datetime
|
||||
sharp_turns: int # Count of sharp turns detected
|
||||
```
|
||||
|
||||
### RotationConfig
|
||||
```python
|
||||
class RotationConfig(BaseModel):
|
||||
step_angle: float = 30.0 # Degrees
|
||||
sharp_turn_threshold: float = 45.0 # Degrees
|
||||
confidence_threshold: float = 0.7 # For accepting match
|
||||
history_size: int = 10 # Number of headings to track
|
||||
```
|
||||
Reference in New Issue
Block a user