component decomposition is done

This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-11-24 14:09:23 +02:00
parent acec83018b
commit f50006d100
34 changed files with 8637 additions and 0 deletions
@@ -0,0 +1,410 @@
# 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, image: np.ndarray, satellite_tile: np.ndarray) -> Optional[RotationResult]:
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, heading: float) -> 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
```
## Component Description
### Responsibilities
- Handle UAV image rotation preprocessing for LiteSAM
- **Critical**: LiteSAM fails if images rotated >45°, requires preprocessing
- Perform 30° step rotation sweeps (12 rotations: 0°, 30°, 60°, ..., 330°)
- Track UAV heading angle across flight
- Calculate precise rotation angle from homography point correspondences
- Detect sharp turns requiring rotation sweep
- Pre-rotate images to known heading for subsequent frames
### Scope
- Image rotation operations
- UAV heading tracking and history
- Sharp turn detection
- Rotation sweep coordination with LiteSAM matching
- Precise angle calculation from homography
## 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, image: np.ndarray, satellite_tile: np.ndarray) -> Optional[RotationResult]`
**Description**: Performs 30° rotation sweep, trying LiteSAM match for each rotation.
**Called By**:
- Internal (when requires_rotation_sweep() returns True)
- Main processing loop (first frame or sharp turn)
**Input**:
```python
flight_id: str
image: np.ndarray # UAV image
satellite_tile: np.ndarray # Satellite reference tile
```
**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 = LiteSAM.align_to_satellite(rotated_image, satellite_tile)
if result.matched and result.confidence > threshold:
precise_angle = calculate_precise_angle(result.homography, angle)
update_heading(flight_id, precise_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 image
- Call G09 Metric Refinement (LiteSAM)
- Check if match found
2. If match found:
- Calculate precise angle from homography
- Update UAV heading
- Return result
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**:
- G06 Internal (to check if pre-rotation needed)
- Main processing loop (before LiteSAM)
- G11 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, heading: float) -> 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
heading: float # New heading angle (0-360)
```
**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. Persist to database (optional)
**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)
- G11 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
## Integration Tests
### Test 1: First Frame Rotation Sweep
1. First frame arrives (no heading set)
2. requires_rotation_sweep() → True
3. try_rotation_steps() → rotates 12 times
4. Match found at 60° step
5. calculate_precise_angle() → 62.3°
6. update_heading(62.3°)
7. 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(92.5°)
### 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. LiteSAM fails to match (no overlap after turn)
2. requires_rotation_sweep() → True
3. try_rotation_steps() with all 12 rotations
4. Match found → heading updated
## 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
- **G09 Metric Refinement**: For LiteSAM matching during rotation sweep
- **H07 Image Rotation Utils**: For image rotation and angle calculations
### 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
```