initial structure implemented

docs -> _docs
This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-12-01 14:20:56 +02:00
parent 9134c5db06
commit abc26d5c20
360 changed files with 3881 additions and 101 deletions
View File
@@ -0,0 +1,5 @@
from .base import ConfigurationManagerBase
from .configuration_manager import ConfigurationManager
__all__ = ["ConfigurationManagerBase", "ConfigurationManager"]
+35
View File
@@ -0,0 +1,35 @@
from abc import ABC, abstractmethod
from typing import Optional, Any
from models.config import SystemConfig
class ConfigurationManagerBase(ABC):
@abstractmethod
async def load_config(self, path: str) -> SystemConfig:
pass
@abstractmethod
async def save_config(self, config: SystemConfig, path: str) -> bool:
pass
@abstractmethod
def get_config(self) -> SystemConfig:
pass
@abstractmethod
def get_value(self, key: str, default: Any = None) -> Any:
pass
@abstractmethod
async def update_value(self, key: str, value: Any) -> bool:
pass
@abstractmethod
async def validate_config(self, config: SystemConfig) -> list[str]:
pass
@abstractmethod
async def reload_config(self) -> bool:
pass
@@ -0,0 +1,28 @@
from typing import Any
from .base import ConfigurationManagerBase
from models.config import SystemConfig
class ConfigurationManager(ConfigurationManagerBase):
async def load_config(self, path: str) -> SystemConfig:
raise NotImplementedError
async def save_config(self, config: SystemConfig, path: str) -> bool:
raise NotImplementedError
def get_config(self) -> SystemConfig:
raise NotImplementedError
def get_value(self, key: str, default: Any = None) -> Any:
raise NotImplementedError
async def update_value(self, key: str, value: Any) -> bool:
raise NotImplementedError
async def validate_config(self, config: SystemConfig) -> list[str]:
raise NotImplementedError
async def reload_config(self) -> bool:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import CoordinateTransformerBase
from .coordinate_transformer import CoordinateTransformer
__all__ = ["CoordinateTransformerBase", "CoordinateTransformer"]
+56
View File
@@ -0,0 +1,56 @@
from abc import ABC, abstractmethod
import numpy as np
from models.core import GPSPoint
from models.satellite import TileBounds
class CoordinateTransformerBase(ABC):
@abstractmethod
def gps_to_local(
self, gps: GPSPoint, origin: GPSPoint
) -> tuple[float, float]:
pass
@abstractmethod
def local_to_gps(
self, local: tuple[float, float], origin: GPSPoint
) -> GPSPoint:
pass
@abstractmethod
def pixel_to_gps(
self,
pixel: tuple[float, float],
homography: np.ndarray,
tile_bounds: TileBounds,
) -> GPSPoint:
pass
@abstractmethod
def gps_to_pixel(
self,
gps: GPSPoint,
homography: np.ndarray,
tile_bounds: TileBounds,
) -> tuple[float, float]:
pass
@abstractmethod
def compute_distance_meters(
self, gps1: GPSPoint, gps2: GPSPoint
) -> float:
pass
@abstractmethod
def compute_bearing(
self, from_gps: GPSPoint, to_gps: GPSPoint
) -> float:
pass
@abstractmethod
def offset_gps(
self, gps: GPSPoint, distance_m: float, bearing_deg: float
) -> GPSPoint:
pass
@@ -0,0 +1,49 @@
import numpy as np
from .base import CoordinateTransformerBase
from models.core import GPSPoint
from models.satellite import TileBounds
class CoordinateTransformer(CoordinateTransformerBase):
def gps_to_local(
self, gps: GPSPoint, origin: GPSPoint
) -> tuple[float, float]:
raise NotImplementedError
def local_to_gps(
self, local: tuple[float, float], origin: GPSPoint
) -> GPSPoint:
raise NotImplementedError
def pixel_to_gps(
self,
pixel: tuple[float, float],
homography: np.ndarray,
tile_bounds: TileBounds,
) -> GPSPoint:
raise NotImplementedError
def gps_to_pixel(
self,
gps: GPSPoint,
homography: np.ndarray,
tile_bounds: TileBounds,
) -> tuple[float, float]:
raise NotImplementedError
def compute_distance_meters(
self, gps1: GPSPoint, gps2: GPSPoint
) -> float:
raise NotImplementedError
def compute_bearing(
self, from_gps: GPSPoint, to_gps: GPSPoint
) -> float:
raise NotImplementedError
def offset_gps(
self, gps: GPSPoint, distance_m: float, bearing_deg: float
) -> GPSPoint:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import FactorGraphOptimizerBase
from .factor_graph_optimizer import FactorGraphOptimizer
__all__ = ["FactorGraphOptimizerBase", "FactorGraphOptimizer"]
+58
View File
@@ -0,0 +1,58 @@
from abc import ABC, abstractmethod
from typing import Optional
import numpy as np
from models.core import Pose, GPSPoint
from models.results import OptimizationResult, RefinedFrameResult
from models.processing import RelativePose
class FactorGraphOptimizerBase(ABC):
@abstractmethod
async def initialize_graph(self, flight_id: str) -> bool:
pass
@abstractmethod
async def add_odometry_factor(
self,
flight_id: str,
from_frame: int,
to_frame: int,
relative_pose: RelativePose,
) -> bool:
pass
@abstractmethod
async def add_gps_prior(
self,
flight_id: str,
frame_id: int,
gps: GPSPoint,
covariance: np.ndarray,
) -> bool:
pass
@abstractmethod
async def optimize(
self, flight_id: str, max_iterations: int = 100
) -> OptimizationResult:
pass
@abstractmethod
async def get_optimized_poses(
self, flight_id: str
) -> list[RefinedFrameResult]:
pass
@abstractmethod
async def get_pose(
self, flight_id: str, frame_id: int
) -> Optional[Pose]:
pass
@abstractmethod
async def marginalize_old_poses(
self, flight_id: str, keep_last_n: int
) -> bool:
pass
@@ -0,0 +1,51 @@
from typing import Optional
import numpy as np
from .base import FactorGraphOptimizerBase
from models.core import Pose, GPSPoint
from models.results import OptimizationResult, RefinedFrameResult
from models.processing import RelativePose
class FactorGraphOptimizer(FactorGraphOptimizerBase):
async def initialize_graph(self, flight_id: str) -> bool:
raise NotImplementedError
async def add_odometry_factor(
self,
flight_id: str,
from_frame: int,
to_frame: int,
relative_pose: RelativePose,
) -> bool:
raise NotImplementedError
async def add_gps_prior(
self,
flight_id: str,
frame_id: int,
gps: GPSPoint,
covariance: np.ndarray,
) -> bool:
raise NotImplementedError
async def optimize(
self, flight_id: str, max_iterations: int = 100
) -> OptimizationResult:
raise NotImplementedError
async def get_optimized_poses(
self, flight_id: str
) -> list[RefinedFrameResult]:
raise NotImplementedError
async def get_pose(
self, flight_id: str, frame_id: int
) -> Optional[Pose]:
raise NotImplementedError
async def marginalize_old_poses(
self, flight_id: str, keep_last_n: int
) -> bool:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import FailureRecoveryCoordinatorBase
from .failure_recovery_coordinator import FailureRecoveryCoordinator
__all__ = ["FailureRecoveryCoordinatorBase", "FailureRecoveryCoordinator"]
@@ -0,0 +1,68 @@
from abc import ABC, abstractmethod
from typing import Optional
import numpy as np
from models.recovery import (
SearchSession,
ConfidenceAssessment,
UserAnchor,
UserInputRequest,
)
from models.core import GPSPoint
from models.satellite import TileCandidate
class FailureRecoveryCoordinatorBase(ABC):
@abstractmethod
async def start_search_session(
self,
flight_id: str,
frame_id: int,
estimated_center: GPSPoint,
) -> SearchSession:
pass
@abstractmethod
async def expand_search(
self, session_id: str
) -> Optional[list[TileCandidate]]:
pass
@abstractmethod
async def assess_confidence(
self, flight_id: str, frame_id: int
) -> ConfidenceAssessment:
pass
@abstractmethod
async def request_user_input(
self,
flight_id: str,
frame_id: int,
uav_image: np.ndarray,
candidates: list[TileCandidate],
) -> UserInputRequest:
pass
@abstractmethod
async def process_user_anchor(
self, flight_id: str, anchor: UserAnchor
) -> bool:
pass
@abstractmethod
async def is_recovery_needed(
self, confidence: ConfidenceAssessment
) -> bool:
pass
@abstractmethod
async def get_active_session(
self, flight_id: str
) -> Optional[SearchSession]:
pass
@abstractmethod
async def cancel_session(self, session_id: str) -> bool:
pass
@@ -0,0 +1,60 @@
from typing import Optional
import numpy as np
from .base import FailureRecoveryCoordinatorBase
from models.recovery import (
SearchSession,
ConfidenceAssessment,
UserAnchor,
UserInputRequest,
)
from models.core import GPSPoint
from models.satellite import TileCandidate
class FailureRecoveryCoordinator(FailureRecoveryCoordinatorBase):
async def start_search_session(
self,
flight_id: str,
frame_id: int,
estimated_center: GPSPoint,
) -> SearchSession:
raise NotImplementedError
async def expand_search(
self, session_id: str
) -> Optional[list[TileCandidate]]:
raise NotImplementedError
async def assess_confidence(
self, flight_id: str, frame_id: int
) -> ConfidenceAssessment:
raise NotImplementedError
async def request_user_input(
self,
flight_id: str,
frame_id: int,
uav_image: np.ndarray,
candidates: list[TileCandidate],
) -> UserInputRequest:
raise NotImplementedError
async def process_user_anchor(
self, flight_id: str, anchor: UserAnchor
) -> bool:
raise NotImplementedError
async def is_recovery_needed(
self, confidence: ConfidenceAssessment
) -> bool:
raise NotImplementedError
async def get_active_session(
self, flight_id: str
) -> Optional[SearchSession]:
raise NotImplementedError
async def cancel_session(self, session_id: str) -> bool:
raise NotImplementedError
+5
View File
@@ -0,0 +1,5 @@
from .base import FlightAPIBase
from .flight_api import FlightAPI
__all__ = ["FlightAPIBase", "FlightAPI"]
+69
View File
@@ -0,0 +1,69 @@
from abc import ABC, abstractmethod
from typing import AsyncIterator
from fastapi import UploadFile
from models.api import (
FlightCreateRequest,
FlightResponse,
FlightDetailResponse,
FlightStatusResponse,
DeleteResponse,
UpdateResponse,
BatchResponse,
UserFixRequest,
UserFixResponse,
ObjectGPSResponse,
)
from models.core import GPSPoint
class FlightAPIBase(ABC):
@abstractmethod
async def create_flight(self, request: FlightCreateRequest) -> FlightResponse:
pass
@abstractmethod
async def get_flight(self, flight_id: str) -> FlightDetailResponse:
pass
@abstractmethod
async def get_flight_status(self, flight_id: str) -> FlightStatusResponse:
pass
@abstractmethod
async def delete_flight(self, flight_id: str) -> DeleteResponse:
pass
@abstractmethod
async def update_waypoints(
self, flight_id: str, waypoints: list[GPSPoint]
) -> UpdateResponse:
pass
@abstractmethod
async def upload_batch(
self,
flight_id: str,
files: list[UploadFile],
start_sequence: int,
end_sequence: int,
batch_number: int,
) -> BatchResponse:
pass
@abstractmethod
async def submit_user_fix(
self, flight_id: str, request: UserFixRequest
) -> UserFixResponse:
pass
@abstractmethod
async def get_object_gps(
self, flight_id: str, frame_id: int, pixel: tuple[float, float]
) -> ObjectGPSResponse:
pass
@abstractmethod
def stream_events(self, flight_id: str) -> AsyncIterator[dict]:
pass
+61
View File
@@ -0,0 +1,61 @@
from typing import AsyncIterator
from fastapi import UploadFile
from .base import FlightAPIBase
from models.api import (
FlightCreateRequest,
FlightResponse,
FlightDetailResponse,
FlightStatusResponse,
DeleteResponse,
UpdateResponse,
BatchResponse,
UserFixRequest,
UserFixResponse,
ObjectGPSResponse,
)
from models.core import GPSPoint
class FlightAPI(FlightAPIBase):
async def create_flight(self, request: FlightCreateRequest) -> FlightResponse:
raise NotImplementedError
async def get_flight(self, flight_id: str) -> FlightDetailResponse:
raise NotImplementedError
async def get_flight_status(self, flight_id: str) -> FlightStatusResponse:
raise NotImplementedError
async def delete_flight(self, flight_id: str) -> DeleteResponse:
raise NotImplementedError
async def update_waypoints(
self, flight_id: str, waypoints: list[GPSPoint]
) -> UpdateResponse:
raise NotImplementedError
async def upload_batch(
self,
flight_id: str,
files: list[UploadFile],
start_sequence: int,
end_sequence: int,
batch_number: int,
) -> BatchResponse:
raise NotImplementedError
async def submit_user_fix(
self, flight_id: str, request: UserFixRequest
) -> UserFixResponse:
raise NotImplementedError
async def get_object_gps(
self, flight_id: str, frame_id: int, pixel: tuple[float, float]
) -> ObjectGPSResponse:
raise NotImplementedError
async def stream_events(self, flight_id: str) -> AsyncIterator[dict]:
raise NotImplementedError
yield
+5
View File
@@ -0,0 +1,5 @@
from .base import FlightDatabaseBase
from .flight_database import FlightDatabase
__all__ = ["FlightDatabaseBase", "FlightDatabase"]
+70
View File
@@ -0,0 +1,70 @@
from abc import ABC, abstractmethod
from typing import Optional
from models.flight import Flight, FlightState, Waypoint
from models.results import FrameResult
from models.chunks import ChunkHandle
from models.core import Pose
class FlightDatabaseBase(ABC):
@abstractmethod
async def create_flight(self, flight: Flight) -> str:
pass
@abstractmethod
async def get_flight(self, flight_id: str) -> Optional[Flight]:
pass
@abstractmethod
async def delete_flight(self, flight_id: str) -> bool:
pass
@abstractmethod
async def update_flight_state(self, state: FlightState) -> bool:
pass
@abstractmethod
async def get_flight_state(self, flight_id: str) -> Optional[FlightState]:
pass
@abstractmethod
async def save_frame_result(self, flight_id: str, result: FrameResult) -> bool:
pass
@abstractmethod
async def get_frame_result(
self, flight_id: str, frame_id: int
) -> Optional[FrameResult]:
pass
@abstractmethod
async def get_all_frame_results(self, flight_id: str) -> list[FrameResult]:
pass
@abstractmethod
async def save_chunk(self, chunk: ChunkHandle) -> bool:
pass
@abstractmethod
async def get_chunk(self, chunk_id: str) -> Optional[ChunkHandle]:
pass
@abstractmethod
async def get_flight_chunks(self, flight_id: str) -> list[ChunkHandle]:
pass
@abstractmethod
async def save_pose(self, flight_id: str, pose: Pose) -> bool:
pass
@abstractmethod
async def get_poses(self, flight_id: str) -> list[Pose]:
pass
@abstractmethod
async def update_waypoints(
self, flight_id: str, waypoints: list[Waypoint]
) -> bool:
pass
@@ -0,0 +1,56 @@
from typing import Optional
from .base import FlightDatabaseBase
from models.flight import Flight, FlightState, Waypoint
from models.results import FrameResult
from models.chunks import ChunkHandle
from models.core import Pose
class FlightDatabase(FlightDatabaseBase):
async def create_flight(self, flight: Flight) -> str:
raise NotImplementedError
async def get_flight(self, flight_id: str) -> Optional[Flight]:
raise NotImplementedError
async def delete_flight(self, flight_id: str) -> bool:
raise NotImplementedError
async def update_flight_state(self, state: FlightState) -> bool:
raise NotImplementedError
async def get_flight_state(self, flight_id: str) -> Optional[FlightState]:
raise NotImplementedError
async def save_frame_result(self, flight_id: str, result: FrameResult) -> bool:
raise NotImplementedError
async def get_frame_result(
self, flight_id: str, frame_id: int
) -> Optional[FrameResult]:
raise NotImplementedError
async def get_all_frame_results(self, flight_id: str) -> list[FrameResult]:
raise NotImplementedError
async def save_chunk(self, chunk: ChunkHandle) -> bool:
raise NotImplementedError
async def get_chunk(self, chunk_id: str) -> Optional[ChunkHandle]:
raise NotImplementedError
async def get_flight_chunks(self, flight_id: str) -> list[ChunkHandle]:
raise NotImplementedError
async def save_pose(self, flight_id: str, pose: Pose) -> bool:
raise NotImplementedError
async def get_poses(self, flight_id: str) -> list[Pose]:
raise NotImplementedError
async def update_waypoints(
self, flight_id: str, waypoints: list[Waypoint]
) -> bool:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import FlightLifecycleManagerBase
from .flight_lifecycle_manager import FlightLifecycleManager
__all__ = ["FlightLifecycleManagerBase", "FlightLifecycleManager"]
@@ -0,0 +1,46 @@
from abc import ABC, abstractmethod
from typing import Optional
from models.flight import Flight, Waypoint
from models.core import GPSPoint
class FlightLifecycleManagerBase(ABC):
@abstractmethod
async def create_flight(
self,
name: str,
description: str,
start_gps: GPSPoint,
rough_waypoints: list[GPSPoint],
camera_params: dict,
altitude: float,
) -> Flight:
pass
@abstractmethod
async def delete_flight(self, flight_id: str) -> bool:
pass
@abstractmethod
async def update_waypoints(
self, flight_id: str, waypoints: list[Waypoint]
) -> bool:
pass
@abstractmethod
async def get_flight(self, flight_id: str) -> Optional[Flight]:
pass
@abstractmethod
async def start_processing(self, flight_id: str) -> bool:
pass
@abstractmethod
async def stop_processing(self, flight_id: str) -> bool:
pass
@abstractmethod
async def get_processing_status(self, flight_id: str) -> dict:
pass
@@ -0,0 +1,39 @@
from typing import Optional
from .base import FlightLifecycleManagerBase
from models.flight import Flight, Waypoint
from models.core import GPSPoint
class FlightLifecycleManager(FlightLifecycleManagerBase):
async def create_flight(
self,
name: str,
description: str,
start_gps: GPSPoint,
rough_waypoints: list[GPSPoint],
camera_params: dict,
altitude: float,
) -> Flight:
raise NotImplementedError
async def delete_flight(self, flight_id: str) -> bool:
raise NotImplementedError
async def update_waypoints(
self, flight_id: str, waypoints: list[Waypoint]
) -> bool:
raise NotImplementedError
async def get_flight(self, flight_id: str) -> Optional[Flight]:
raise NotImplementedError
async def start_processing(self, flight_id: str) -> bool:
raise NotImplementedError
async def stop_processing(self, flight_id: str) -> bool:
raise NotImplementedError
async def get_processing_status(self, flight_id: str) -> dict:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import FlightProcessingEngineBase
from .flight_processing_engine import FlightProcessingEngine
__all__ = ["FlightProcessingEngineBase", "FlightProcessingEngine"]
@@ -0,0 +1,41 @@
from abc import ABC, abstractmethod
from typing import Optional
import numpy as np
from models.images import ImageData
from models.results import FrameResult
from models.processing import RelativePose
from models.recovery import UserAnchor
class FlightProcessingEngineBase(ABC):
@abstractmethod
async def process_frame(
self, flight_id: str, image: ImageData
) -> Optional[FrameResult]:
pass
@abstractmethod
async def get_relative_pose(
self, prev_image: np.ndarray, curr_image: np.ndarray
) -> RelativePose:
pass
@abstractmethod
async def apply_user_anchor(
self, flight_id: str, frame_id: int, anchor: UserAnchor
) -> bool:
pass
@abstractmethod
async def is_blocked(self, flight_id: str) -> bool:
pass
@abstractmethod
async def resume_processing(self, flight_id: str) -> bool:
pass
@abstractmethod
async def get_current_chunk_id(self, flight_id: str) -> Optional[str]:
pass
@@ -0,0 +1,35 @@
from typing import Optional
import numpy as np
from .base import FlightProcessingEngineBase
from models.images import ImageData
from models.results import FrameResult
from models.processing import RelativePose
from models.recovery import UserAnchor
class FlightProcessingEngine(FlightProcessingEngineBase):
async def process_frame(
self, flight_id: str, image: ImageData
) -> Optional[FrameResult]:
raise NotImplementedError
async def get_relative_pose(
self, prev_image: np.ndarray, curr_image: np.ndarray
) -> RelativePose:
raise NotImplementedError
async def apply_user_anchor(
self, flight_id: str, frame_id: int, anchor: UserAnchor
) -> bool:
raise NotImplementedError
async def is_blocked(self, flight_id: str) -> bool:
raise NotImplementedError
async def resume_processing(self, flight_id: str) -> bool:
raise NotImplementedError
async def get_current_chunk_id(self, flight_id: str) -> Optional[str]:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import GlobalPlaceRecognitionBase
from .global_place_recognition import GlobalPlaceRecognition
__all__ = ["GlobalPlaceRecognitionBase", "GlobalPlaceRecognition"]
@@ -0,0 +1,45 @@
from abc import ABC, abstractmethod
import numpy as np
from models.core import GPSPoint
from models.satellite import TileCandidate
from models.processing import AlignmentResult
class GlobalPlaceRecognitionBase(ABC):
@abstractmethod
async def extract_global_descriptor(self, image: np.ndarray) -> np.ndarray:
pass
@abstractmethod
async def search_candidates(
self,
descriptor: np.ndarray,
search_center: GPSPoint,
search_radius: float,
top_k: int = 10,
) -> list[TileCandidate]:
pass
@abstractmethod
async def verify_candidate(
self, uav_image: np.ndarray, satellite_image: np.ndarray
) -> AlignmentResult:
pass
@abstractmethod
async def index_tile(
self, tile_image: np.ndarray, tile_id: str, gps_center: GPSPoint
) -> bool:
pass
@abstractmethod
async def build_index_for_area(
self, nw: GPSPoint, se: GPSPoint, zoom: int
) -> int:
pass
@abstractmethod
async def get_indexed_tile_count(self) -> int:
pass
@@ -0,0 +1,39 @@
import numpy as np
from .base import GlobalPlaceRecognitionBase
from models.core import GPSPoint
from models.satellite import TileCandidate
from models.processing import AlignmentResult
class GlobalPlaceRecognition(GlobalPlaceRecognitionBase):
async def extract_global_descriptor(self, image: np.ndarray) -> np.ndarray:
raise NotImplementedError
async def search_candidates(
self,
descriptor: np.ndarray,
search_center: GPSPoint,
search_radius: float,
top_k: int = 10,
) -> list[TileCandidate]:
raise NotImplementedError
async def verify_candidate(
self, uav_image: np.ndarray, satellite_image: np.ndarray
) -> AlignmentResult:
raise NotImplementedError
async def index_tile(
self, tile_image: np.ndarray, tile_id: str, gps_center: GPSPoint
) -> bool:
raise NotImplementedError
async def build_index_for_area(
self, nw: GPSPoint, se: GPSPoint, zoom: int
) -> int:
raise NotImplementedError
async def get_indexed_tile_count(self) -> int:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import ImageInputPipelineBase
from .image_input_pipeline import ImageInputPipeline
__all__ = ["ImageInputPipelineBase", "ImageInputPipeline"]
+34
View File
@@ -0,0 +1,34 @@
from abc import ABC, abstractmethod
from typing import Optional, AsyncIterator
from models.images import ImageData, ImageBatch, ProcessingStatus
from models.core import ValidationResult
class ImageInputPipelineBase(ABC):
@abstractmethod
async def receive_batch(
self, flight_id: str, batch: ImageBatch
) -> ValidationResult:
pass
@abstractmethod
async def get_next_image(self, flight_id: str) -> Optional[ImageData]:
pass
@abstractmethod
def stream_images(self, flight_id: str) -> AsyncIterator[ImageData]:
pass
@abstractmethod
async def get_status(self, flight_id: str) -> ProcessingStatus:
pass
@abstractmethod
async def clear_queue(self, flight_id: str) -> bool:
pass
@abstractmethod
async def get_queue_size(self, flight_id: str) -> int:
pass
@@ -0,0 +1,29 @@
from typing import Optional, AsyncIterator
from .base import ImageInputPipelineBase
from models.images import ImageData, ImageBatch, ProcessingStatus
from models.core import ValidationResult
class ImageInputPipeline(ImageInputPipelineBase):
async def receive_batch(
self, flight_id: str, batch: ImageBatch
) -> ValidationResult:
raise NotImplementedError
async def get_next_image(self, flight_id: str) -> Optional[ImageData]:
raise NotImplementedError
async def stream_images(self, flight_id: str) -> AsyncIterator[ImageData]:
raise NotImplementedError
yield
async def get_status(self, flight_id: str) -> ProcessingStatus:
raise NotImplementedError
async def clear_queue(self, flight_id: str) -> bool:
raise NotImplementedError
async def get_queue_size(self, flight_id: str) -> int:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import ImageRotationManagerBase
from .image_rotation_manager import ImageRotationManager
__all__ = ["ImageRotationManagerBase", "ImageRotationManager"]
+41
View File
@@ -0,0 +1,41 @@
from abc import ABC, abstractmethod
from typing import Optional
import numpy as np
from models.processing import RotationResult
from models.flight import HeadingRecord
class ImageRotationManagerBase(ABC):
@abstractmethod
async def estimate_rotation(
self, uav_image: np.ndarray, satellite_image: np.ndarray
) -> RotationResult:
pass
@abstractmethod
async def get_rotation_for_frame(
self, flight_id: str, frame_id: int
) -> Optional[float]:
pass
@abstractmethod
async def update_heading_history(
self, flight_id: str, record: HeadingRecord
) -> None:
pass
@abstractmethod
async def predict_heading(self, flight_id: str) -> Optional[float]:
pass
@abstractmethod
async def is_sharp_turn(
self, flight_id: str, current_heading: float
) -> bool:
pass
@abstractmethod
def rotate_image(self, image: np.ndarray, angle: float) -> np.ndarray:
pass
@@ -0,0 +1,35 @@
from typing import Optional
import numpy as np
from .base import ImageRotationManagerBase
from models.processing import RotationResult
from models.flight import HeadingRecord
class ImageRotationManager(ImageRotationManagerBase):
async def estimate_rotation(
self, uav_image: np.ndarray, satellite_image: np.ndarray
) -> RotationResult:
raise NotImplementedError
async def get_rotation_for_frame(
self, flight_id: str, frame_id: int
) -> Optional[float]:
raise NotImplementedError
async def update_heading_history(
self, flight_id: str, record: HeadingRecord
) -> None:
raise NotImplementedError
async def predict_heading(self, flight_id: str) -> Optional[float]:
raise NotImplementedError
async def is_sharp_turn(
self, flight_id: str, current_heading: float
) -> bool:
raise NotImplementedError
def rotate_image(self, image: np.ndarray, angle: float) -> np.ndarray:
raise NotImplementedError
+5
View File
@@ -0,0 +1,5 @@
from .base import MetricRefinementBase
from .metric_refinement import MetricRefinement
__all__ = ["MetricRefinementBase", "MetricRefinement"]
+43
View File
@@ -0,0 +1,43 @@
from abc import ABC, abstractmethod
import numpy as np
from models.core import GPSPoint
from models.processing import AlignmentResult
class MetricRefinementBase(ABC):
@abstractmethod
async def refine_alignment(
self,
uav_image: np.ndarray,
satellite_image: np.ndarray,
initial_homography: np.ndarray,
) -> AlignmentResult:
pass
@abstractmethod
async def compute_precise_gps(
self,
uav_center_pixel: tuple[float, float],
homography: np.ndarray,
tile_bounds: dict,
) -> GPSPoint:
pass
@abstractmethod
async def estimate_reprojection_error(
self,
correspondences: np.ndarray,
homography: np.ndarray,
) -> float:
pass
@abstractmethod
async def filter_outliers(
self,
correspondences: np.ndarray,
homography: np.ndarray,
threshold: float = 3.0,
) -> np.ndarray:
pass
@@ -0,0 +1,39 @@
import numpy as np
from .base import MetricRefinementBase
from models.core import GPSPoint
from models.processing import AlignmentResult
class MetricRefinement(MetricRefinementBase):
async def refine_alignment(
self,
uav_image: np.ndarray,
satellite_image: np.ndarray,
initial_homography: np.ndarray,
) -> AlignmentResult:
raise NotImplementedError
async def compute_precise_gps(
self,
uav_center_pixel: tuple[float, float],
homography: np.ndarray,
tile_bounds: dict,
) -> GPSPoint:
raise NotImplementedError
async def estimate_reprojection_error(
self,
correspondences: np.ndarray,
homography: np.ndarray,
) -> float:
raise NotImplementedError
async def filter_outliers(
self,
correspondences: np.ndarray,
homography: np.ndarray,
threshold: float = 3.0,
) -> np.ndarray:
raise NotImplementedError
+5
View File
@@ -0,0 +1,5 @@
from .base import ModelManagerBase
from .model_manager import ModelManager
__all__ = ["ModelManagerBase", "ModelManager"]
+40
View File
@@ -0,0 +1,40 @@
from abc import ABC, abstractmethod
from typing import Optional, Any
import numpy as np
from models.config import ModelConfig
class ModelManagerBase(ABC):
@abstractmethod
async def load_model(self, config: ModelConfig) -> bool:
pass
@abstractmethod
async def unload_model(self, model_name: str) -> bool:
pass
@abstractmethod
async def get_model(self, model_name: str) -> Optional[Any]:
pass
@abstractmethod
async def run_inference(
self, model_name: str, inputs: dict[str, np.ndarray]
) -> dict[str, np.ndarray]:
pass
@abstractmethod
async def warmup_model(
self, model_name: str, iterations: int = 3
) -> bool:
pass
@abstractmethod
async def get_loaded_models(self) -> list[str]:
pass
@abstractmethod
async def get_model_info(self, model_name: str) -> Optional[dict]:
pass
+33
View File
@@ -0,0 +1,33 @@
from typing import Optional, Any
import numpy as np
from .base import ModelManagerBase
from models.config import ModelConfig
class ModelManager(ModelManagerBase):
async def load_model(self, config: ModelConfig) -> bool:
raise NotImplementedError
async def unload_model(self, model_name: str) -> bool:
raise NotImplementedError
async def get_model(self, model_name: str) -> Optional[Any]:
raise NotImplementedError
async def run_inference(
self, model_name: str, inputs: dict[str, np.ndarray]
) -> dict[str, np.ndarray]:
raise NotImplementedError
async def warmup_model(
self, model_name: str, iterations: int = 3
) -> bool:
raise NotImplementedError
async def get_loaded_models(self) -> list[str]:
raise NotImplementedError
async def get_model_info(self, model_name: str) -> Optional[dict]:
raise NotImplementedError
+5
View File
@@ -0,0 +1,5 @@
from .base import ResultManagerBase
from .result_manager import ResultManager
__all__ = ["ResultManagerBase", "ResultManager"]
+49
View File
@@ -0,0 +1,49 @@
from abc import ABC, abstractmethod
from typing import Optional
from models.results import FrameResult, FlightResults, RefinedFrameResult
from models.core import GPSPoint
class ResultManagerBase(ABC):
@abstractmethod
async def save_frame_result(
self, flight_id: str, result: FrameResult
) -> bool:
pass
@abstractmethod
async def get_frame_result(
self, flight_id: str, frame_id: int
) -> Optional[FrameResult]:
pass
@abstractmethod
async def get_flight_results(self, flight_id: str) -> FlightResults:
pass
@abstractmethod
async def update_refined_result(
self, flight_id: str, result: RefinedFrameResult
) -> bool:
pass
@abstractmethod
async def get_object_gps(
self,
flight_id: str,
frame_id: int,
pixel: tuple[float, float],
) -> GPSPoint:
pass
@abstractmethod
async def export_results(
self, flight_id: str, format: str = "json"
) -> bytes:
pass
@abstractmethod
async def get_statistics(self, flight_id: str) -> dict:
pass
@@ -0,0 +1,42 @@
from typing import Optional
from .base import ResultManagerBase
from models.results import FrameResult, FlightResults, RefinedFrameResult
from models.core import GPSPoint
class ResultManager(ResultManagerBase):
async def save_frame_result(
self, flight_id: str, result: FrameResult
) -> bool:
raise NotImplementedError
async def get_frame_result(
self, flight_id: str, frame_id: int
) -> Optional[FrameResult]:
raise NotImplementedError
async def get_flight_results(self, flight_id: str) -> FlightResults:
raise NotImplementedError
async def update_refined_result(
self, flight_id: str, result: RefinedFrameResult
) -> bool:
raise NotImplementedError
async def get_object_gps(
self,
flight_id: str,
frame_id: int,
pixel: tuple[float, float],
) -> GPSPoint:
raise NotImplementedError
async def export_results(
self, flight_id: str, format: str = "json"
) -> bytes:
raise NotImplementedError
async def get_statistics(self, flight_id: str) -> dict:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import RouteChunkManagerBase
from .route_chunk_manager import RouteChunkManager
__all__ = ["RouteChunkManagerBase", "RouteChunkManager"]
+65
View File
@@ -0,0 +1,65 @@
from abc import ABC, abstractmethod
from typing import Optional
from models.chunks import ChunkHandle, ChunkBounds, Sim3Transform
from models.core import GPSPoint
from models.processing import ChunkAlignmentResult
class RouteChunkManagerBase(ABC):
@abstractmethod
async def create_chunk(
self, flight_id: str, start_frame_id: int
) -> ChunkHandle:
pass
@abstractmethod
async def add_frame_to_chunk(
self, chunk_id: str, frame_id: int
) -> bool:
pass
@abstractmethod
async def finalize_chunk(self, chunk_id: str) -> bool:
pass
@abstractmethod
async def get_chunk(self, chunk_id: str) -> Optional[ChunkHandle]:
pass
@abstractmethod
async def get_active_chunk(
self, flight_id: str
) -> Optional[ChunkHandle]:
pass
@abstractmethod
async def get_flight_chunks(
self, flight_id: str
) -> list[ChunkHandle]:
pass
@abstractmethod
async def estimate_chunk_bounds(
self, chunk_id: str
) -> ChunkBounds:
pass
@abstractmethod
async def anchor_chunk(
self, chunk_id: str, frame_id: int, gps: GPSPoint
) -> bool:
pass
@abstractmethod
async def match_chunk_to_satellite(
self, chunk_id: str
) -> Optional[ChunkAlignmentResult]:
pass
@abstractmethod
async def apply_transform_to_chunk(
self, chunk_id: str, transform: Sim3Transform
) -> bool:
pass
@@ -0,0 +1,55 @@
from typing import Optional
from .base import RouteChunkManagerBase
from models.chunks import ChunkHandle, ChunkBounds, Sim3Transform
from models.core import GPSPoint
from models.processing import ChunkAlignmentResult
class RouteChunkManager(RouteChunkManagerBase):
async def create_chunk(
self, flight_id: str, start_frame_id: int
) -> ChunkHandle:
raise NotImplementedError
async def add_frame_to_chunk(
self, chunk_id: str, frame_id: int
) -> bool:
raise NotImplementedError
async def finalize_chunk(self, chunk_id: str) -> bool:
raise NotImplementedError
async def get_chunk(self, chunk_id: str) -> Optional[ChunkHandle]:
raise NotImplementedError
async def get_active_chunk(
self, flight_id: str
) -> Optional[ChunkHandle]:
raise NotImplementedError
async def get_flight_chunks(
self, flight_id: str
) -> list[ChunkHandle]:
raise NotImplementedError
async def estimate_chunk_bounds(
self, chunk_id: str
) -> ChunkBounds:
raise NotImplementedError
async def anchor_chunk(
self, chunk_id: str, frame_id: int, gps: GPSPoint
) -> bool:
raise NotImplementedError
async def match_chunk_to_satellite(
self, chunk_id: str
) -> Optional[ChunkAlignmentResult]:
raise NotImplementedError
async def apply_transform_to_chunk(
self, chunk_id: str, transform: Sim3Transform
) -> bool:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import SatelliteDataManagerBase
from .satellite_data_manager import SatelliteDataManager
__all__ = ["SatelliteDataManagerBase", "SatelliteDataManager"]
+45
View File
@@ -0,0 +1,45 @@
from abc import ABC, abstractmethod
from typing import Optional
import numpy as np
from models.core import GPSPoint
from models.satellite import TileCoords, TileBounds
class SatelliteDataManagerBase(ABC):
@abstractmethod
async def get_tile(
self, gps: GPSPoint, zoom: int
) -> Optional[tuple[np.ndarray, TileBounds]]:
pass
@abstractmethod
async def get_tile_by_coords(
self, coords: TileCoords
) -> Optional[tuple[np.ndarray, TileBounds]]:
pass
@abstractmethod
async def get_tiles_in_radius(
self, center: GPSPoint, radius_meters: float, zoom: int
) -> list[tuple[np.ndarray, TileBounds]]:
pass
@abstractmethod
async def get_tile_bounds(self, coords: TileCoords) -> TileBounds:
pass
@abstractmethod
async def prefetch_area(
self, nw: GPSPoint, se: GPSPoint, zoom: int
) -> int:
pass
@abstractmethod
def gps_to_tile_coords(self, gps: GPSPoint, zoom: int) -> TileCoords:
pass
@abstractmethod
def tile_coords_to_gps(self, coords: TileCoords) -> GPSPoint:
pass
@@ -0,0 +1,38 @@
from typing import Optional
import numpy as np
from .base import SatelliteDataManagerBase
from models.core import GPSPoint
from models.satellite import TileCoords, TileBounds
class SatelliteDataManager(SatelliteDataManagerBase):
async def get_tile(
self, gps: GPSPoint, zoom: int
) -> Optional[tuple[np.ndarray, TileBounds]]:
raise NotImplementedError
async def get_tile_by_coords(
self, coords: TileCoords
) -> Optional[tuple[np.ndarray, TileBounds]]:
raise NotImplementedError
async def get_tiles_in_radius(
self, center: GPSPoint, radius_meters: float, zoom: int
) -> list[tuple[np.ndarray, TileBounds]]:
raise NotImplementedError
async def get_tile_bounds(self, coords: TileCoords) -> TileBounds:
raise NotImplementedError
async def prefetch_area(
self, nw: GPSPoint, se: GPSPoint, zoom: int
) -> int:
raise NotImplementedError
def gps_to_tile_coords(self, gps: GPSPoint, zoom: int) -> TileCoords:
raise NotImplementedError
def tile_coords_to_gps(self, coords: TileCoords) -> GPSPoint:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import SequentialVisualOdometryBase
from .sequential_visual_odometry import SequentialVisualOdometry
__all__ = ["SequentialVisualOdometryBase", "SequentialVisualOdometry"]
@@ -0,0 +1,39 @@
from abc import ABC, abstractmethod
import numpy as np
from models.processing import RelativePose, Matches
class SequentialVisualOdometryBase(ABC):
@abstractmethod
async def compute_relative_pose(
self, prev_image: np.ndarray, curr_image: np.ndarray
) -> RelativePose:
pass
@abstractmethod
async def extract_keypoints(
self, image: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
pass
@abstractmethod
async def match_features(
self,
keypoints1: np.ndarray,
descriptors1: np.ndarray,
keypoints2: np.ndarray,
descriptors2: np.ndarray,
) -> Matches:
pass
@abstractmethod
async def estimate_motion(
self, matches: Matches, camera_matrix: np.ndarray
) -> RelativePose:
pass
@abstractmethod
def is_tracking_good(self, pose: RelativePose) -> bool:
pass
@@ -0,0 +1,34 @@
import numpy as np
from .base import SequentialVisualOdometryBase
from models.processing import RelativePose, Matches
class SequentialVisualOdometry(SequentialVisualOdometryBase):
async def compute_relative_pose(
self, prev_image: np.ndarray, curr_image: np.ndarray
) -> RelativePose:
raise NotImplementedError
async def extract_keypoints(
self, image: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
raise NotImplementedError
async def match_features(
self,
keypoints1: np.ndarray,
descriptors1: np.ndarray,
keypoints2: np.ndarray,
descriptors2: np.ndarray,
) -> Matches:
raise NotImplementedError
async def estimate_motion(
self, matches: Matches, camera_matrix: np.ndarray
) -> RelativePose:
raise NotImplementedError
def is_tracking_good(self, pose: RelativePose) -> bool:
raise NotImplementedError
@@ -0,0 +1,5 @@
from .base import SSEEventStreamerBase
from .sse_event_streamer import SSEEventStreamer
__all__ = ["SSEEventStreamerBase", "SSEEventStreamer"]
+44
View File
@@ -0,0 +1,44 @@
from abc import ABC, abstractmethod
from typing import AsyncIterator
from models.results import FrameResult
from models.recovery import UserInputRequest
class SSEEventStreamerBase(ABC):
@abstractmethod
async def emit_frame_result(
self, flight_id: str, result: FrameResult
) -> None:
pass
@abstractmethod
async def emit_status_update(
self, flight_id: str, status: dict
) -> None:
pass
@abstractmethod
async def emit_user_input_request(
self, flight_id: str, request: UserInputRequest
) -> None:
pass
@abstractmethod
async def emit_error(
self, flight_id: str, error: str
) -> None:
pass
@abstractmethod
def subscribe(self, flight_id: str) -> AsyncIterator[dict]:
pass
@abstractmethod
async def unsubscribe(self, flight_id: str, subscriber_id: str) -> None:
pass
@abstractmethod
async def get_subscriber_count(self, flight_id: str) -> int:
pass
@@ -0,0 +1,38 @@
from typing import AsyncIterator
from .base import SSEEventStreamerBase
from models.results import FrameResult
from models.recovery import UserInputRequest
class SSEEventStreamer(SSEEventStreamerBase):
async def emit_frame_result(
self, flight_id: str, result: FrameResult
) -> None:
raise NotImplementedError
async def emit_status_update(
self, flight_id: str, status: dict
) -> None:
raise NotImplementedError
async def emit_user_input_request(
self, flight_id: str, request: UserInputRequest
) -> None:
raise NotImplementedError
async def emit_error(
self, flight_id: str, error: str
) -> None:
raise NotImplementedError
async def subscribe(self, flight_id: str) -> AsyncIterator[dict]:
raise NotImplementedError
yield
async def unsubscribe(self, flight_id: str, subscriber_id: str) -> None:
raise NotImplementedError
async def get_subscriber_count(self, flight_id: str) -> int:
raise NotImplementedError