mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-22 22:26:38 +00:00
dd9835c0cd
- ruff --fix: removed trailing whitespace (W293), sorted imports (I001) - Manual: broke long lines (E501) in eskf, rotation, vo, gpr, metric, pipeline, rotation tests - Removed unused imports (F401) in models.py, schemas/__init__.py - pyproject.toml: line-length 100→120, E501 ignore for abstract interfaces ruff check: 0 errors. pytest: 195 passed / 8 skipped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
3.0 KiB
Python
92 lines
3.0 KiB
Python
"""Tests for Image Rotation Manager (F06)."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from gps_denied.core.rotation import IImageMatcher, ImageRotationManager
|
|
from gps_denied.schemas import GPSPoint
|
|
from gps_denied.schemas.rotation import RotationResult
|
|
from gps_denied.schemas.satellite import TileBounds
|
|
|
|
|
|
@pytest.fixture
|
|
def rotation_manager():
|
|
return ImageRotationManager()
|
|
|
|
|
|
def test_rotate_image_360(rotation_manager):
|
|
img = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
|
|
# Just draw a white rectangle to test rotation
|
|
img[10:40, 10:40] = [255, 255, 255]
|
|
|
|
r90 = rotation_manager.rotate_image_360(img, 90.0)
|
|
assert r90.shape == (100, 100, 3)
|
|
|
|
# Top left corner should move
|
|
assert not np.array_equal(img, r90)
|
|
|
|
|
|
def test_heading_management(rotation_manager):
|
|
fid = "flight_1"
|
|
now = datetime.now(timezone.utc)
|
|
|
|
assert rotation_manager.get_current_heading(fid) is None
|
|
|
|
rotation_manager.update_heading(fid, 1, 370.0, now) # should modulo to 10
|
|
assert rotation_manager.get_current_heading(fid) == 10.0
|
|
|
|
rotation_manager.update_heading(fid, 2, 90.0, now)
|
|
assert rotation_manager.get_current_heading(fid) == 90.0
|
|
|
|
|
|
def test_detect_sharp_turn(rotation_manager):
|
|
fid = "flight_2"
|
|
now = datetime.now(timezone.utc)
|
|
|
|
assert rotation_manager.detect_sharp_turn(fid, 90.0) is False # no history
|
|
|
|
rotation_manager.update_heading(fid, 1, 90.0, now)
|
|
|
|
assert rotation_manager.detect_sharp_turn(fid, 100.0) is False # delta 10
|
|
assert rotation_manager.detect_sharp_turn(fid, 180.0) is True # delta 90
|
|
assert rotation_manager.detect_sharp_turn(fid, 350.0) is True # delta 100
|
|
assert rotation_manager.detect_sharp_turn(fid, 80.0) is False # delta 10 (wraparound)
|
|
|
|
# Wraparound test explicitly
|
|
rotation_manager.update_heading(fid, 2, 350.0, now)
|
|
assert rotation_manager.detect_sharp_turn(fid, 10.0) is False # delta 20
|
|
|
|
|
|
class MockMatcher(IImageMatcher):
|
|
def align_to_satellite(
|
|
self, uav_image: np.ndarray, satellite_tile: np.ndarray,
|
|
tile_bounds: TileBounds,
|
|
) -> RotationResult:
|
|
# Mock that only matches when angle was originally 90
|
|
# By checking internal state or just returning generic true for test
|
|
return RotationResult(matched=True, initial_angle=90.0, precise_angle=90.0, confidence=0.99)
|
|
|
|
|
|
def test_try_rotation_steps(rotation_manager):
|
|
fid = "flight_3"
|
|
img = np.zeros((10, 10, 3), dtype=np.uint8)
|
|
sat = np.zeros((10, 10, 3), dtype=np.uint8)
|
|
|
|
nw = GPSPoint(lat=10.0, lon=10.0)
|
|
se = GPSPoint(lat=9.0, lon=11.0)
|
|
tb = TileBounds(nw=nw, ne=nw, sw=se, se=se, center=nw, gsd=0.5)
|
|
|
|
matcher = MockMatcher()
|
|
|
|
# Should perform sweep and mock matcher says matched=True immediately in the loop
|
|
res = rotation_manager.try_rotation_steps(fid, 1, img, sat, tb, datetime.now(timezone.utc), matcher)
|
|
|
|
assert res is not None
|
|
assert res.matched is True
|
|
# The first step is 0 degrees, the mock matcher returns matched=True.
|
|
# Therefore the first matched angle is 0.
|
|
assert res.initial_angle == 0.0
|