mirror of
https://github.com/azaion/gps-denied-desktop.git
synced 2026-04-23 01:46:37 +00:00
component 07
This commit is contained in:
@@ -0,0 +1,129 @@
|
|||||||
|
import numpy as np
|
||||||
|
import math
|
||||||
|
|
||||||
|
class GeoTracker:
|
||||||
|
def __init__(self, focal_length_mm, sensor_width_mm, image_width_px, image_height_px):
|
||||||
|
# Intrinsics
|
||||||
|
self.fx = (focal_length_mm / sensor_width_mm) * image_width_px
|
||||||
|
self.fy = self.fx
|
||||||
|
self.cx = image_width_px / 2.0
|
||||||
|
self.cy = image_height_px / 2.0
|
||||||
|
self.image_w = image_width_px
|
||||||
|
self.image_h = image_height_px
|
||||||
|
|
||||||
|
# Standard Camera Matrix
|
||||||
|
self.K = np.array([
|
||||||
|
[self.fx, 0, self.cx],
|
||||||
|
[0, self.fy, self.cy],
|
||||||
|
[0, 0, 1]
|
||||||
|
])
|
||||||
|
self.K_inv = np.linalg.inv(self.K)
|
||||||
|
|
||||||
|
def _get_rotation_matrix(self, yaw, pitch, roll):
|
||||||
|
# Converts NED Euler angles to Rotation Matrix (Body to NED)
|
||||||
|
# Using ZYX convention
|
||||||
|
y, p, r = np.radians(yaw), np.radians(pitch), np.radians(roll)
|
||||||
|
|
||||||
|
Rz = np.array([[np.cos(y), -np.sin(y), 0], [np.sin(y), np.cos(y), 0], [0, 0, 1]])
|
||||||
|
Ry = np.array([[np.cos(p), 0, np.sin(p)], [0, 1, 0], [-np.sin(p), 0, np.cos(p)]])
|
||||||
|
Rx = np.array([[1, 0, 0], [0, np.cos(r), -np.sin(r)], [0, np.sin(r), np.cos(r)]])
|
||||||
|
|
||||||
|
R_body_to_ned = Rz @ Ry @ Rx
|
||||||
|
|
||||||
|
# Camera Frame Alignment (Camera Z is Forward, NED X is North)
|
||||||
|
# Standard assumption: Camera is mounted Forward-Right-Down relative to Body
|
||||||
|
# But usually for drones, Camera Z (Optical) aligns with Body X (Forward) or Body Z (Down)
|
||||||
|
# Assuming standard "Forward Looking" camera:
|
||||||
|
# Cam Z -> Body X, Cam X -> Body Y, Cam Y -> Body Z
|
||||||
|
T_cam_to_body = np.array([
|
||||||
|
[0, 0, 1],
|
||||||
|
[1, 0, 0],
|
||||||
|
[0, 1, 0]
|
||||||
|
])
|
||||||
|
|
||||||
|
return R_body_to_ned @ T_cam_to_body
|
||||||
|
|
||||||
|
def pixel_to_ned_ground(self, u, v, altitude, R_cam_to_ned):
|
||||||
|
"""
|
||||||
|
Ray-plane intersection.
|
||||||
|
Projects pixel (u,v) to the ground plane (z=0) in NED frame.
|
||||||
|
"""
|
||||||
|
# 1. Pixel to Normalized Camera Ray
|
||||||
|
pixel_homo = np.array([u, v, 1.0])
|
||||||
|
ray_cam = self.K_inv @ pixel_homo
|
||||||
|
|
||||||
|
# 2. Rotate Ray to NED Frame
|
||||||
|
ray_ned = R_cam_to_ned @ ray_cam
|
||||||
|
|
||||||
|
# 3. Intersect with Ground (Z = 0)
|
||||||
|
# Camera is at [0, 0, -altitude] in NED
|
||||||
|
# Ray equation: P = O + t * D
|
||||||
|
# We want P.z = 0
|
||||||
|
# 0 = -altitude + t * ray_ned.z
|
||||||
|
# t = altitude / ray_ned.z
|
||||||
|
|
||||||
|
if ray_ned[2] <= 0: # Ray pointing up or parallel to horizon
|
||||||
|
return None
|
||||||
|
|
||||||
|
t = altitude / ray_ned[2]
|
||||||
|
|
||||||
|
# P_ground relative to camera position (which is 0,0 in horizontal)
|
||||||
|
north = t * ray_ned[0]
|
||||||
|
east = t * ray_ned[1]
|
||||||
|
|
||||||
|
return np.array([north, east])
|
||||||
|
|
||||||
|
def calculate_movement(self, kpts0, kpts1, altitude, yaw, start_lat, start_lon):
|
||||||
|
"""
|
||||||
|
Calculates the drone's movement based on optical flow on the ground.
|
||||||
|
Returns dictionary with shift in meters and new lat/lon.
|
||||||
|
"""
|
||||||
|
# Rotation Matrix (Assuming simplified Pitch=-90 for Nadir or 0 for Forward)
|
||||||
|
# For a downward facing drone, Pitch is usually -90.
|
||||||
|
# For this example, we assume the camera is stabilized or we use the passed yaw.
|
||||||
|
# We will assume a gimbal pitch of -90 (looking down) if not specified,
|
||||||
|
# or -45 if looking forward.
|
||||||
|
# Let's assume standard Nadir view for mapping (Pitch = -90).
|
||||||
|
R = self._get_rotation_matrix(yaw, -90, 0)
|
||||||
|
|
||||||
|
shifts_n = []
|
||||||
|
shifts_e = []
|
||||||
|
|
||||||
|
# Process a subset of points to save time
|
||||||
|
step = max(1, len(kpts0) // 50)
|
||||||
|
|
||||||
|
for i in range(0, len(kpts0), step):
|
||||||
|
p0 = self.pixel_to_ned_ground(kpts0[i][0], kpts0[i][1], altitude, R)
|
||||||
|
p1 = self.pixel_to_ned_ground(kpts1[i][0], kpts1[i][1], altitude, R)
|
||||||
|
|
||||||
|
if p0 is not None and p1 is not None:
|
||||||
|
# If the image moved LEFT, the drone moved RIGHT.
|
||||||
|
# If pixel 0 is at (10,10) and pixel 1 is at (5,5), the world moved "back" relative to camera.
|
||||||
|
# Drone movement = P1_world - P0_world?
|
||||||
|
# NO. If we are tracking static ground, P0 and P1 correspond to the SAME physical point.
|
||||||
|
# So the Ground Point relative to Camera 0 is X.
|
||||||
|
# The Ground Point relative to Camera 1 is Y.
|
||||||
|
# Camera Movement = X - Y.
|
||||||
|
diff = p0 - p1
|
||||||
|
shifts_n.append(diff[0])
|
||||||
|
shifts_e.append(diff[1])
|
||||||
|
|
||||||
|
if not shifts_n:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Robust average (Median) to reject outliers
|
||||||
|
avg_n = np.median(shifts_n)
|
||||||
|
avg_e = np.median(shifts_e)
|
||||||
|
|
||||||
|
# Convert Meters to Lat/Lon
|
||||||
|
# Earth Radius approx 6,378,137m
|
||||||
|
d_lat = (avg_n / 6378137.0) * (180 / math.pi)
|
||||||
|
d_lon = (avg_e / (6378137.0 * math.cos(math.radians(start_lat)))) * (180 / math.pi)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'move_north_m': avg_n,
|
||||||
|
'move_east_m': avg_e,
|
||||||
|
'new_lat': start_lat + d_lat,
|
||||||
|
'new_lon': start_lon + d_lon,
|
||||||
|
'ground_points': len(shifts_n)
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from lightglue import viz2d
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
import time
|
||||||
|
|
||||||
|
# --- IMPORTS ---
|
||||||
|
# Assumes your matcher class is in 'lightglue_onnx.py'
|
||||||
|
from lightglue_onnx import LightGlueMatcher
|
||||||
|
from geolocator import GeoTracker
|
||||||
|
|
||||||
|
# --- CONFIGURATION ---
|
||||||
|
IMAGE_0_PATH = "gps-denied-ai-sandbox/docs/00_problem/input_data/AD000001.jpg"
|
||||||
|
IMAGE_1_PATH = "gps-denied-ai-sandbox/docs/00_problem/input_data/AD000002.jpg"
|
||||||
|
MODEL_PATH = "superpoint_lightglue_end2end_fused_gpu.onnx"
|
||||||
|
|
||||||
|
# FLIGHT DATA (Frame 0 Telemetry)
|
||||||
|
ALTITUDE = 100.0 # Meters
|
||||||
|
HEADING = 0.0 # Degrees (0 = North)
|
||||||
|
START_LAT = 48.275292
|
||||||
|
START_LON = 37.385220
|
||||||
|
|
||||||
|
# CAMERA SPECS
|
||||||
|
FOCAL_MM = 25.0
|
||||||
|
SENSOR_MM = 23.5
|
||||||
|
W_PX = 1920
|
||||||
|
H_PX = 1280
|
||||||
|
|
||||||
|
# Matcher Settings
|
||||||
|
PROCESS_DIM = 1024 # Size to resize images for processing
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1. SETUP
|
||||||
|
print("1. Initializing Systems...")
|
||||||
|
matcher = LightGlueMatcher(MODEL_PATH, max_dimension=PROCESS_DIM)
|
||||||
|
geo = GeoTracker(FOCAL_MM, SENSOR_MM, W_PX, H_PX)
|
||||||
|
|
||||||
|
# 2. MATCHING
|
||||||
|
print(f"2. Matching Images...")
|
||||||
|
start_t = time.time()
|
||||||
|
|
||||||
|
# YOUR MATCHER RETURNS: F, mkpts0, mkpts1
|
||||||
|
# It already handles scaling internally!
|
||||||
|
F, kpts0, kpts1 = matcher.match(IMAGE_0_PATH, IMAGE_1_PATH)
|
||||||
|
|
||||||
|
dt = time.time() - start_t
|
||||||
|
|
||||||
|
# Check if matching failed (Your matcher returns empty arrays if <8 matches)
|
||||||
|
if len(kpts0) == 0:
|
||||||
|
print("[!] Not enough matches found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" -> Success! Found {len(kpts0)} matches in {dt:.2f}s")
|
||||||
|
|
||||||
|
# 3. GEOLOCATION CALCULATION
|
||||||
|
print("3. Calculating Coordinates...")
|
||||||
|
nav_result = geo.calculate_movement(
|
||||||
|
kpts0, kpts1,
|
||||||
|
altitude=ALTITUDE,
|
||||||
|
yaw=HEADING,
|
||||||
|
start_lat=START_LAT,
|
||||||
|
start_lon=START_LON
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. REPORTING
|
||||||
|
title = "Calculation Failed"
|
||||||
|
|
||||||
|
if nav_result:
|
||||||
|
print("\n" + "="*45)
|
||||||
|
print(" 🛰️ VISUAL ODOMETRY RESULT")
|
||||||
|
print("="*45)
|
||||||
|
print(f" Start Pos: {START_LAT:.6f}, {START_LON:.6f}")
|
||||||
|
print("-" * 45)
|
||||||
|
print(f" Detected Shift: North {nav_result['move_north_m']:.2f} m")
|
||||||
|
print(f" East {nav_result['move_east_m']:.2f} m")
|
||||||
|
print("-" * 45)
|
||||||
|
print(f" NEW LATITUDE: {nav_result['new_lat']:.8f}") # <--- RESULT
|
||||||
|
print(f" NEW LONGITUDE: {nav_result['new_lon']:.8f}") # <--- RESULT
|
||||||
|
print("="*45 + "\n")
|
||||||
|
|
||||||
|
title = (f"Shift: N {nav_result['move_north_m']:.1f}m, E {nav_result['move_east_m']:.1f}m\n"
|
||||||
|
f"Lat: {nav_result['new_lat']:.6f}, Lon: {nav_result['new_lon']:.6f}")
|
||||||
|
else:
|
||||||
|
print("[!] Math failed (likely bad geometry or points at infinity).")
|
||||||
|
|
||||||
|
# 5. VISUALIZATION
|
||||||
|
# We must load images manually because your matcher doesn't return them
|
||||||
|
img0 = cv2.imread(IMAGE_0_PATH)
|
||||||
|
img1 = cv2.imread(IMAGE_1_PATH)
|
||||||
|
# Convert BGR to RGB for matplotlib
|
||||||
|
img0 = cv2.cvtColor(img0, cv2.COLOR_BGR2RGB)
|
||||||
|
img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)
|
||||||
|
|
||||||
|
viz2d.plot_images([img0, img1])
|
||||||
|
viz2d.plot_matches(kpts0, kpts1, color='lime')
|
||||||
|
plt.suptitle(title, fontsize=14, backgroundcolor='white')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import onnxruntime as ort
|
||||||
|
import os
|
||||||
|
|
||||||
|
class LightGlueMatcher:
|
||||||
|
def __init__(self, onnx_model_path, max_dimension=512):
|
||||||
|
"""
|
||||||
|
Initializes the ONNX runtime session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
onnx_model_path (str): Path to the .onnx model file.
|
||||||
|
max_dimension (int): Maximum edge length for resizing.
|
||||||
|
"""
|
||||||
|
self.max_dim = max_dimension
|
||||||
|
|
||||||
|
if not os.path.exists(onnx_model_path):
|
||||||
|
raise FileNotFoundError(f"Model not found at: {onnx_model_path}")
|
||||||
|
|
||||||
|
# Setup ONNX Runtime
|
||||||
|
if 'CUDAExecutionProvider' in ort.get_available_providers():
|
||||||
|
self.providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
|
||||||
|
print("LightGlueMatcher: Using CUDA (GPU).")
|
||||||
|
else:
|
||||||
|
self.providers = ['CPUExecutionProvider']
|
||||||
|
print("LightGlueMatcher: Using CPU.")
|
||||||
|
|
||||||
|
sess_options = ort.SessionOptions()
|
||||||
|
self.session = ort.InferenceSession(onnx_model_path, sess_options, providers=self.providers)
|
||||||
|
|
||||||
|
# Cache input/output names
|
||||||
|
self.input_names = [inp.name for inp in self.session.get_inputs()]
|
||||||
|
self.output_names = [out.name for out in self.session.get_outputs()]
|
||||||
|
|
||||||
|
def _preprocess(self, img_input):
|
||||||
|
"""
|
||||||
|
Internal helper: Resize and normalize image for ONNX.
|
||||||
|
Handles both file paths and numpy arrays.
|
||||||
|
"""
|
||||||
|
# Load image if input is a path
|
||||||
|
if isinstance(img_input, str):
|
||||||
|
img_raw = cv2.imread(img_input, cv2.IMREAD_GRAYSCALE)
|
||||||
|
if img_raw is None:
|
||||||
|
raise FileNotFoundError(f"Could not load image at {img_input}")
|
||||||
|
elif isinstance(img_input, np.ndarray):
|
||||||
|
if len(img_input.shape) == 3:
|
||||||
|
img_raw = cv2.cvtColor(img_input, cv2.COLOR_BGR2GRAY)
|
||||||
|
else:
|
||||||
|
img_raw = img_input
|
||||||
|
else:
|
||||||
|
raise ValueError("Input must be an image path or numpy array.")
|
||||||
|
|
||||||
|
h, w = img_raw.shape
|
||||||
|
scale = self.max_dim / max(h, w)
|
||||||
|
|
||||||
|
# Resize logic
|
||||||
|
if scale < 1:
|
||||||
|
h_new, w_new = int(round(h * scale)), int(round(w * scale))
|
||||||
|
img_resized = cv2.resize(img_raw, (w_new, h_new), interpolation=cv2.INTER_AREA)
|
||||||
|
else:
|
||||||
|
scale = 1.0
|
||||||
|
img_resized = img_raw
|
||||||
|
|
||||||
|
# Normalize and reshape (1, 1, H, W)
|
||||||
|
img_normalized = img_resized.astype(np.float32) / 255.0
|
||||||
|
img_tensor = img_normalized[None, None]
|
||||||
|
|
||||||
|
return img_tensor, scale
|
||||||
|
|
||||||
|
def match(self, img0_input, img1_input):
|
||||||
|
"""
|
||||||
|
Matches two images and returns the Fundamental Matrix and Keypoints.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
F (np.ndarray): The 3x3 Fundamental Matrix (or None if not enough matches).
|
||||||
|
mkpts0 (np.ndarray): Matched keypoints in Image 0 (N, 2).
|
||||||
|
mkpts1 (np.ndarray): Matched keypoints in Image 1 (N, 2).
|
||||||
|
"""
|
||||||
|
# Preprocess
|
||||||
|
tensor0, scale0 = self._preprocess(img0_input)
|
||||||
|
tensor1, scale1 = self._preprocess(img1_input)
|
||||||
|
|
||||||
|
# Inference
|
||||||
|
inputs = {
|
||||||
|
self.input_names[0]: tensor0,
|
||||||
|
self.input_names[1]: tensor1
|
||||||
|
}
|
||||||
|
outputs = self.session.run(None, inputs)
|
||||||
|
outputs_dict = dict(zip(self.output_names, outputs))
|
||||||
|
|
||||||
|
# Extract Raw Matches
|
||||||
|
kpts0 = outputs_dict['kpts0'].squeeze(0)
|
||||||
|
kpts1 = outputs_dict['kpts1'].squeeze(0)
|
||||||
|
matches0 = outputs_dict['matches0']
|
||||||
|
|
||||||
|
# Filter and Rescale Keypoints
|
||||||
|
if len(matches0) < 8:
|
||||||
|
print("Not enough matches to compute matrix.")
|
||||||
|
return None, np.array([]), np.array([])
|
||||||
|
|
||||||
|
mkpts0 = kpts0[matches0[:, 0]] / scale0
|
||||||
|
mkpts1 = kpts1[matches0[:, 1]] / scale1
|
||||||
|
|
||||||
|
# Calculate Fundamental Matrix (F) using RANSAC
|
||||||
|
# This is usually "The Matrix" required for relative pose estimation
|
||||||
|
F, mask = cv2.findFundamentalMat(mkpts0, mkpts1, cv2.FM_RANSAC, 3, 0.99)
|
||||||
|
|
||||||
|
return F, mkpts0, mkpts1
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
coloredlogs==15.0.1
|
||||||
|
contourpy==1.3.3
|
||||||
|
cycler==0.12.1
|
||||||
|
filelock==3.20.0
|
||||||
|
flatbuffers==25.9.23
|
||||||
|
fonttools==4.60.1
|
||||||
|
fsspec==2025.10.0
|
||||||
|
humanfriendly==10.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
kiwisolver==1.4.9
|
||||||
|
kornia==0.8.2
|
||||||
|
kornia_rs==0.1.10
|
||||||
|
-e git+https://github.com/cvg/LightGlue/@746fac2c042e05d1865315b1413419f1c1e7ba55#egg=lightglue
|
||||||
|
MarkupSafe==3.0.3
|
||||||
|
matplotlib==3.10.7
|
||||||
|
mpmath==1.3.0
|
||||||
|
networkx==3.5
|
||||||
|
numpy==2.2.6
|
||||||
|
nvidia-cublas-cu12==12.8.4.1
|
||||||
|
nvidia-cuda-cupti-cu12==12.8.90
|
||||||
|
nvidia-cuda-nvrtc-cu12==12.8.93
|
||||||
|
nvidia-cuda-runtime-cu12==12.8.90
|
||||||
|
nvidia-cudnn-cu12==9.10.2.21
|
||||||
|
nvidia-cufft-cu12==11.3.3.83
|
||||||
|
nvidia-cufile-cu12==1.13.1.3
|
||||||
|
nvidia-curand-cu12==10.3.9.90
|
||||||
|
nvidia-cusolver-cu12==11.7.3.90
|
||||||
|
nvidia-cusparse-cu12==12.5.8.93
|
||||||
|
nvidia-cusparselt-cu12==0.7.1
|
||||||
|
nvidia-nccl-cu12==2.27.5
|
||||||
|
nvidia-nvjitlink-cu12==12.8.93
|
||||||
|
nvidia-nvshmem-cu12==3.3.20
|
||||||
|
nvidia-nvtx-cu12==12.8.90
|
||||||
|
onnxruntime-gpu==1.23.2
|
||||||
|
opencv-python==4.12.0.88
|
||||||
|
packaging==25.0
|
||||||
|
pillow==12.0.0
|
||||||
|
protobuf==6.33.0
|
||||||
|
pyparsing==3.2.5
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
setuptools==80.9.0
|
||||||
|
six==1.17.0
|
||||||
|
sympy==1.14.0
|
||||||
|
torch==2.9.0
|
||||||
|
torchvision==0.24.0
|
||||||
|
triton==3.5.0
|
||||||
|
typing_extensions==4.15.0
|
||||||
Reference in New Issue
Block a user