Initial commit

This commit is contained in:
Denys Zaitsev
2026-04-03 23:25:54 +03:00
parent 531a1301d5
commit d7e1066c60
3843 changed files with 1554468 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
import pytest
import numpy as np
from unittest.mock import Mock, patch
from f09_local_geospatial_anchoring import LocalGeospatialAnchoring, LiteSAMConfig
from f04_satellite_data_manager import TileBounds
from f02_1_flight_lifecycle_manager import GPSPoint
@pytest.fixture
def anchoring():
return LocalGeospatialAnchoring()
@pytest.fixture
def dummy_uav_image():
return np.zeros((480, 640, 3), dtype=np.uint8)
@pytest.fixture
def dummy_sat_tile():
return np.zeros((512, 512, 3), dtype=np.uint8)
@pytest.fixture
def mock_tile_bounds():
return TileBounds(
nw=GPSPoint(lat=48.1, lon=37.0),
ne=GPSPoint(lat=48.1, lon=37.1),
sw=GPSPoint(lat=48.0, lon=37.0),
se=GPSPoint(lat=48.0, lon=37.1),
center=GPSPoint(lat=48.05, lon=37.05),
gsd=0.3
)
class TestLocalGeospatialAnchoring:
# --- Feature 09.01: Single Image Alignment ---
def test_pixel_to_gps(self, anchoring, mock_tile_bounds):
# Top Left (NW)
gps_nw = anchoring._pixel_to_gps(0, 0, mock_tile_bounds, 512, 512)
assert np.isclose(gps_nw.lat, 48.1)
assert np.isclose(gps_nw.lon, 37.0)
# Center
gps_center = anchoring._pixel_to_gps(256, 256, mock_tile_bounds, 512, 512)
assert np.isclose(gps_center.lat, 48.05)
assert np.isclose(gps_center.lon, 37.05)
# Bottom Right (SE)
gps_se = anchoring._pixel_to_gps(512, 512, mock_tile_bounds, 512, 512)
assert np.isclose(gps_se.lat, 48.0)
assert np.isclose(gps_se.lon, 37.1)
def test_compute_reprojection_error(self, anchoring):
H = np.eye(3) # Identity matrix
src = np.array([[10.0, 10.0], [20.0, 20.0]])
dst = np.array([[11.0, 10.0], [20.0, 22.0]])
# Errors: pt1 -> 1px (x), pt2 -> 2px (y). Mean = 1.5
err = anchoring._compute_reprojection_error(H, src, dst)
assert err == 1.5
def test_compute_homography_success(self, anchoring, dummy_uav_image, dummy_sat_tile):
res = anchoring.compute_homography(dummy_uav_image, dummy_sat_tile)
assert res is not None
H, stats = res
assert H.shape == (3, 3)
assert stats["inlier_count"] >= anchoring.config.min_inliers
assert stats["mre"] >= 0.0
def test_compute_homography_failure(self, anchoring, dummy_uav_image, dummy_sat_tile):
# Force poor RANSAC output via mock
with patch.object(anchoring, '_estimate_homography_ransac', return_value=(None, np.array([]))):
res = anchoring.compute_homography(dummy_uav_image, dummy_sat_tile)
assert res is None
def test_extract_gps_from_alignment(self, anchoring, mock_tile_bounds):
# Pure translation homography mapping UAV center (320, 240) to sat center (256, 256)
H = np.array([
[1.0, 0.0, 256.0 - 320.0],
[0.0, 1.0, 256.0 - 240.0],
[0.0, 0.0, 1.0]
])
gps = anchoring.extract_gps_from_alignment(H, mock_tile_bounds, (320, 240))
# Should map exactly to the tile center GPS
assert np.isclose(gps.lat, 48.05)
assert np.isclose(gps.lon, 37.05)
def test_compute_match_confidence(self, anchoring):
# High confidence criteria
assert anchoring.compute_match_confidence(0.7, 60, 0.3, 1.0) > 0.8
# Medium confidence criteria
conf_med = anchoring.compute_match_confidence(0.5, 40, 1.0, 0.8)
assert 0.5 <= conf_med <= 0.8
# Low confidence criteria (High error or low inliers)
assert anchoring.compute_match_confidence(0.3, 20, 1.5, 0.5) < 0.5
assert anchoring.compute_match_confidence(0.9, 10, 0.2, 1.0) == 0.0 # Below min inliers
def test_align_to_satellite(self, anchoring, dummy_uav_image, dummy_sat_tile, mock_tile_bounds):
result = anchoring.align_to_satellite(dummy_uav_image, dummy_sat_tile, mock_tile_bounds)
assert result is not None
assert result.matched is True
assert result.homography.shape == (3, 3)
assert -90 <= result.gps_center.lat <= 90
assert -180 <= result.gps_center.lon <= 180
assert 0.0 <= result.confidence <= 1.0
# --- Feature 09.02: Chunk Alignment ---
def test_extract_chunk_features(self, anchoring, dummy_uav_image):
features = anchoring._extract_chunk_features([dummy_uav_image, dummy_uav_image])
assert len(features) == 2
assert features[0].shape == (100, 256)
def test_aggregate_features(self, anchoring):
feat1 = np.ones((100, 256))
feat2 = np.ones((100, 256)) * 3
agg = anchoring._aggregate_features([feat1, feat2])
assert np.allclose(agg, np.ones((100, 256)) * 2)
def test_aggregate_correspondences(self, anchoring):
src1 = np.array([[1, 1], [2, 2]])
dst1 = np.array([[3, 3], [4, 4]])
src2 = np.array([[5, 5]])
dst2 = np.array([[6, 6]])
src_agg, dst_agg = anchoring._aggregate_correspondences([(src1, dst1), (src2, dst2)])
assert len(src_agg) == 3
assert len(dst_agg) == 3
assert np.array_equal(src_agg[2], [5, 5])
def test_compute_sim3_transform(self, anchoring, mock_tile_bounds):
# 90 deg rotation, scale 2.0, translation 10, 20
H = np.array([
[0.0, -2.0, 10.0],
[2.0, 0.0, 20.0],
[0.0, 0.0, 1.0]
])
sim3 = anchoring._compute_sim3_transform(H, mock_tile_bounds)
assert np.isclose(sim3.scale, 2.0)
assert np.isclose(sim3.translation[0], 10.0)
assert np.isclose(sim3.translation[1], 20.0)
assert np.isclose(sim3.rotation[0, 0], 0.0, atol=1e-5)
assert np.isclose(sim3.rotation[1, 0], 1.0, atol=1e-5)
def test_get_chunk_center_gps(self, anchoring, dummy_uav_image, mock_tile_bounds):
H = np.array([
[1.0, 0.0, 256.0 - 320.0],
[0.0, 1.0, 256.0 - 240.0],
[0.0, 0.0, 1.0]
])
chunk = [dummy_uav_image, dummy_uav_image, dummy_uav_image]
gps = anchoring._get_chunk_center_gps(H, mock_tile_bounds, chunk)
assert np.isclose(gps.lat, 48.05)
assert np.isclose(gps.lon, 37.05)
def test_validate_chunk_match(self, anchoring):
assert anchoring._validate_chunk_match(50, 0.8) is True
assert anchoring._validate_chunk_match(20, 0.8) is False # < 30 inliers
assert anchoring._validate_chunk_match(50, 0.5) is False # < 0.7 confidence
def test_align_chunk_to_satellite_success(self, anchoring, dummy_uav_image, dummy_sat_tile, mock_tile_bounds):
with patch.object(anchoring, 'compute_match_confidence', return_value=0.9):
chunk = [dummy_uav_image] * 5
res = anchoring.align_chunk_to_satellite(chunk, dummy_sat_tile, mock_tile_bounds)
assert res is not None
assert res.matched is True
assert res.inlier_count >= 30
assert res.transform is not None
def test_align_chunk_to_satellite_failure(self, anchoring, dummy_uav_image, dummy_sat_tile, mock_tile_bounds):
with patch.object(anchoring, '_estimate_chunk_homography', return_value=(None, {})):
chunk = [dummy_uav_image] * 5
res = anchoring.align_chunk_to_satellite(chunk, dummy_sat_tile, mock_tile_bounds)
assert res is None
def test_chunk_vs_single_image_robustness(self, anchoring, dummy_uav_image, dummy_sat_tile, mock_tile_bounds):
# Single image failure
with patch.object(anchoring, '_estimate_homography_ransac') as mock_ransac:
mock_ransac.return_value = (np.eye(3), np.array([[1]]*10 + [[0]]*90))
single_res = anchoring.align_to_satellite(dummy_uav_image, dummy_sat_tile, mock_tile_bounds)
assert single_res is None
# Chunk success
with patch.object(anchoring, '_estimate_homography_ransac') as mock_ransac:
mock_ransac.return_value = (np.eye(3), np.array([[1]]*50 + [[0]]*450))
with patch.object(anchoring, 'compute_match_confidence', return_value=0.8):
chunk = [dummy_uav_image] * 5
chunk_res = anchoring.align_chunk_to_satellite(chunk, dummy_sat_tile, mock_tile_bounds)
assert chunk_res is not None