visualizzazione mappa corretta, controllare misure e rotazioni
This commit is contained in:
parent
1ee934505a
commit
03ff5ec672
BIN
GitUlitity_icona.ico
Normal file
BIN
GitUlitity_icona.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
10
config.py
10
config.py
@ -150,7 +150,7 @@ MAP_SERVICE_PROVIDER = "osm" # Name of the service to use (must match map_servi
|
|||||||
# MAP_API_KEY = None # Add this if using a service that requires a key (e.g., Google)
|
# MAP_API_KEY = None # Add this if using a service that requires a key (e.g., Google)
|
||||||
MAP_CACHE_DIRECTORY = "map_cache" # Root directory for cached tiles
|
MAP_CACHE_DIRECTORY = "map_cache" # Root directory for cached tiles
|
||||||
ENABLE_ONLINE_MAP_FETCHING = True # Allow downloading tiles if not in cache
|
ENABLE_ONLINE_MAP_FETCHING = True # Allow downloading tiles if not in cache
|
||||||
DEFAULT_MAP_ZOOM_LEVEL = 12 # Initial zoom level for the test map (adjust as needed)
|
DEFAULT_MAP_ZOOM_LEVEL = 14 # Initial zoom level for the test map (adjust as needed) 12 original, 13 little more big,
|
||||||
# Color for placeholder tiles when offline/download fails (RGB tuple)
|
# Color for placeholder tiles when offline/download fails (RGB tuple)
|
||||||
OFFLINE_MAP_PLACEHOLDER_COLOR = (200, 200, 200) # Light grey
|
OFFLINE_MAP_PLACEHOLDER_COLOR = (200, 200, 200) # Light grey
|
||||||
MAX_MAP_DISPLAY_WIDTH = 1024
|
MAX_MAP_DISPLAY_WIDTH = 1024
|
||||||
@ -158,8 +158,12 @@ MAX_MAP_DISPLAY_HEIGHT = 1024
|
|||||||
|
|
||||||
|
|
||||||
# SAR Georeferencing Defaults (Now explicitly used for map testing if ENABLE_MAP_OVERLAY is True)
|
# SAR Georeferencing Defaults (Now explicitly used for map testing if ENABLE_MAP_OVERLAY is True)
|
||||||
SAR_CENTER_LAT = 40.7128 # Example: New York City Latitude (Degrees)
|
# SAR Georeferencing Defaults
|
||||||
SAR_CENTER_LON = -74.0060 # Example: New York City Longitude (Degrees)
|
# NOTE: Setting LAT/LON to 0.0 signals the MapIntegrationManager *NOT*
|
||||||
|
# to display an initial default map area on startup.
|
||||||
|
# The map will only appear after the first valid GeoInfo is received.
|
||||||
|
SAR_CENTER_LAT = 0.0 #40.7128 # Example: New York City Latitude (Degrees)
|
||||||
|
SAR_CENTER_LON = 0.0 #-74.0060 # Example: New York City Longitude (Degrees)
|
||||||
SAR_IMAGE_SIZE_KM = (
|
SAR_IMAGE_SIZE_KM = (
|
||||||
50.0 # Example: Width/Height of the area to show on the map in Kilometers
|
50.0 # Example: Width/Height of the area to show on the map in Kilometers
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
import queue # For type hinting
|
import queue # For type hinting
|
||||||
import math
|
import math
|
||||||
from typing import Optional, Dict, Any, Tuple
|
from typing import Optional, Dict, Any, Tuple, List
|
||||||
|
|
||||||
# Third-party imports
|
# Third-party imports
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -31,6 +31,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pyproj = None
|
pyproj = None
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
|
||||||
# Local application imports
|
# Local application imports
|
||||||
import config
|
import config
|
||||||
from app_state import AppState
|
from app_state import AppState
|
||||||
@ -143,79 +145,105 @@ class MapIntegrationManager:
|
|||||||
|
|
||||||
|
|
||||||
def _display_initial_map_area_thread(self):
|
def _display_initial_map_area_thread(self):
|
||||||
"""
|
"""
|
||||||
(Runs in background thread) Calculates the initial map area based on default
|
(Runs in background thread) Calculates the initial map area based on default
|
||||||
config settings and queues the result for display on the main thread.
|
config settings and queues the result for display on the main thread,
|
||||||
Moved from App._display_initial_map_area.
|
*unless* the default coordinates in config are set to (0,0) which signals
|
||||||
"""
|
to skip the initial display.
|
||||||
log_prefix = f"{self._log_prefix} InitialMap"
|
"""
|
||||||
# Check dependencies initialized in __init__
|
log_prefix = f"{self._log_prefix} InitialMap"
|
||||||
if not (self._map_tile_manager and self._map_display_window):
|
|
||||||
# This check might be redundant if __init__ raises exceptions, but keep for safety
|
|
||||||
logging.error(f"{log_prefix} Map components not initialized. Aborting thread.")
|
|
||||||
# Queue None to signal failure to the main thread?
|
|
||||||
put_queue(self._tkinter_queue, ('SHOW_MAP', None), "tkinter", self._app)
|
|
||||||
return
|
|
||||||
# Check shutdown flag early
|
|
||||||
if self._app_state.shutting_down:
|
|
||||||
logging.info(f"{log_prefix} Shutdown detected. Aborting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.info(f"{log_prefix} Calculating initial map area...")
|
# Check if default lat/lon are set to 0.0 to prevent initial display
|
||||||
map_image_pil: Optional[Image.Image] = None
|
if config.SAR_CENTER_LAT == 0.0 and config.SAR_CENTER_LON == 0.0:
|
||||||
try:
|
# ... (codice per saltare e aggiornare lo stato, come prima) ...
|
||||||
# Use default center/size from config for the initial view
|
# ... (assicurati che questa parte sia corretta come nella risposta precedente) ...
|
||||||
bbox = get_bounding_box_from_center_size(
|
logging.info(f"{log_prefix} Initial map display skipped based on config defaults (0,0). Waiting for valid GeoInfo.")
|
||||||
config.SAR_CENTER_LAT, config.SAR_CENTER_LON, config.SAR_IMAGE_SIZE_KM
|
if not self._app_state.shutting_down:
|
||||||
)
|
status_msg = "Status Unavailable" # Default
|
||||||
if bbox is None:
|
try:
|
||||||
raise MapCalculationError("Failed to calculate initial bounding box.")
|
if self._app_state.test_mode_active:
|
||||||
|
status_msg = "Ready (Test Mode)"
|
||||||
|
elif config.USE_LOCAL_IMAGES:
|
||||||
|
status_msg = "Ready (Local Mode)"
|
||||||
|
else:
|
||||||
|
socket_ok = False
|
||||||
|
listening_info = "Error: No Network Socket"
|
||||||
|
if hasattr(self._app, 'udp_socket') and self._app.udp_socket:
|
||||||
|
if hasattr(self._app, 'local_ip') and hasattr(self._app, 'local_port'):
|
||||||
|
listening_info = f"Listening UDP {self._app.local_ip}:{self._app.local_port}"
|
||||||
|
socket_ok = True
|
||||||
|
else:
|
||||||
|
listening_info = "Listening UDP (IP/Port Unknown)"
|
||||||
|
socket_ok = True
|
||||||
|
status_msg = listening_info
|
||||||
|
status_msg += " | Map Ready (Waiting for GeoData)"
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"{log_prefix} Unexpected error determining status message:")
|
||||||
|
status_msg = "Error Getting Status | Map Ready (Waiting for GeoData)"
|
||||||
|
self._app.set_status(status_msg)
|
||||||
|
return # Esce dal thread
|
||||||
|
|
||||||
zoom = config.DEFAULT_MAP_ZOOM_LEVEL
|
# Se le coordinate di default *non* sono (0,0), procedi
|
||||||
tile_ranges = get_tile_ranges_for_bbox(bbox, zoom)
|
logging.info(f"{log_prefix} Calculating initial map area based on non-zero config defaults...")
|
||||||
if tile_ranges is None:
|
|
||||||
raise MapCalculationError("Failed to calculate initial tile ranges.")
|
|
||||||
|
|
||||||
# --- Check shutdown again before potentially long tile stitching ---
|
# Check dependencies
|
||||||
|
if not (self._map_tile_manager and self._map_display_window):
|
||||||
|
logging.error(f"{log_prefix} Map components not initialized. Aborting thread.")
|
||||||
|
put_queue(self._tkinter_queue, ('SHOW_MAP', None), "tkinter", self._app)
|
||||||
|
return
|
||||||
if self._app_state.shutting_down:
|
if self._app_state.shutting_down:
|
||||||
logging.info(f"{log_prefix} Shutdown detected before stitching.")
|
logging.info(f"{log_prefix} Shutdown detected. Aborting.")
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.info(f"{log_prefix} Stitching initial map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})...")
|
map_image_pil: Optional[Image.Image] = None
|
||||||
map_image_pil = self._map_tile_manager.stitch_map_image(
|
try:
|
||||||
zoom, tile_ranges[0], tile_ranges[1]
|
# --- MODIFICA QUI: Definisci 'zoom' PRIMA di usarlo ---
|
||||||
) # stitch_map_image uses placeholders internally if needed
|
zoom = config.DEFAULT_MAP_ZOOM_LEVEL
|
||||||
|
logging.debug(f"{log_prefix} Using default zoom level: {zoom}")
|
||||||
|
# --- FINE MODIFICA ---
|
||||||
|
|
||||||
# --- Check shutdown again after stitching ---
|
# Usa default center/size da config
|
||||||
if self._app_state.shutting_down:
|
bbox = get_bounding_box_from_center_size(
|
||||||
logging.info(f"{log_prefix} Shutdown detected after stitching.")
|
config.SAR_CENTER_LAT, config.SAR_CENTER_LON, config.SAR_IMAGE_SIZE_KM
|
||||||
# Don't queue result if shutting down
|
)
|
||||||
return
|
if bbox is None:
|
||||||
|
raise MapCalculationError("Failed to calculate initial bounding box.")
|
||||||
|
|
||||||
if map_image_pil:
|
# Calcola i tile ranges USANDO la variabile zoom definita sopra
|
||||||
logging.info(f"{log_prefix} Initial map area stitched successfully.")
|
tile_ranges = get_tile_ranges_for_bbox(bbox, zoom)
|
||||||
else:
|
if tile_ranges is None:
|
||||||
# This case should be less likely if stitch_map_image uses placeholders
|
raise MapCalculationError("Failed to calculate initial tile ranges.")
|
||||||
logging.error(f"{log_prefix} Failed to stitch initial map area (returned None even with placeholders).")
|
|
||||||
|
|
||||||
except ImportError as e:
|
if self._app_state.shutting_down:
|
||||||
# Should be caught by __init__, but handle defensively
|
logging.info(f"{log_prefix} Shutdown detected before stitching.")
|
||||||
logging.critical(f"{log_prefix} Missing library during map calculation: {e}")
|
return
|
||||||
map_image_pil = None # Ensure None is queued on error
|
|
||||||
except MapCalculationError as e:
|
# Ora puoi usare 'zoom' nel messaggio di log
|
||||||
logging.error(f"{log_prefix} Calculation error: {e}")
|
logging.info(f"{log_prefix} Stitching initial map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})...")
|
||||||
map_image_pil = None # Ensure None is queued on error
|
map_image_pil = self._map_tile_manager.stitch_map_image(
|
||||||
except Exception as e:
|
zoom, tile_ranges[0], tile_ranges[1]
|
||||||
logging.exception(f"{log_prefix} Unexpected error calculating initial map:")
|
)
|
||||||
map_image_pil = None # Ensure None is queued on error
|
|
||||||
finally:
|
if self._app_state.shutting_down:
|
||||||
# Always queue the result (PIL image or None) for the main thread to handle display
|
logging.info(f"{log_prefix} Shutdown detected after stitching.")
|
||||||
# Check shutdown one last time before queueing
|
return
|
||||||
if not self._app_state.shutting_down:
|
|
||||||
logging.debug(f"{log_prefix} Queueing SHOW_MAP command for main thread.")
|
if map_image_pil:
|
||||||
# The payload is the PIL image (or None)
|
logging.info(f"{log_prefix} Initial map area stitched successfully.")
|
||||||
put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_pil), "tkinter", self._app)
|
else:
|
||||||
logging.debug(f"{log_prefix} Initial map display thread finished.")
|
logging.error(f"{log_prefix} Failed to stitch initial map area.")
|
||||||
|
|
||||||
|
except (ImportError, MapCalculationError) as e:
|
||||||
|
logging.error(f"{log_prefix} Calculation error: {e}")
|
||||||
|
map_image_pil = None
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"{log_prefix} Unexpected error calculating initial map:")
|
||||||
|
map_image_pil = None
|
||||||
|
finally:
|
||||||
|
if not self._app_state.shutting_down:
|
||||||
|
logging.debug(f"{log_prefix} Queueing SHOW_MAP command for initial map.")
|
||||||
|
put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_pil), "tkinter", self._app)
|
||||||
|
logging.debug(f"{log_prefix} Initial map display thread finished.")
|
||||||
|
|
||||||
|
|
||||||
def update_map_overlay(self, sar_normalized_uint8: np.ndarray, geo_info_radians: Dict[str, Any]):
|
def update_map_overlay(self, sar_normalized_uint8: np.ndarray, geo_info_radians: Dict[str, Any]):
|
||||||
@ -276,8 +304,12 @@ class MapIntegrationManager:
|
|||||||
# Calculate size in KM, using default from config as fallback
|
# Calculate size in KM, using default from config as fallback
|
||||||
if scale_x > 0 and width_px > 0:
|
if scale_x > 0 and width_px > 0:
|
||||||
size_km = (scale_x * width_px) / 1000.0
|
size_km = (scale_x * width_px) / 1000.0
|
||||||
|
logging.info(f"{log_prefix} Calculated approximate size based on scale_x * width_px: {size_km:.2f} km")
|
||||||
else:
|
else:
|
||||||
logging.warning(f"{log_prefix} Using default SAR size for map due to invalid scale/width in GeoInfo.")
|
logging.error(
|
||||||
|
f"{log_prefix} Invalid scale_x ({scale_x}) or width_px ({width_px}) in received GeoInfo. "
|
||||||
|
f"Cannot determine map size from data. Using fallback default size: {config.SAR_IMAGE_SIZE_KM} km."
|
||||||
|
)
|
||||||
size_km = config.SAR_IMAGE_SIZE_KM
|
size_km = config.SAR_IMAGE_SIZE_KM
|
||||||
# Get zoom level from config
|
# Get zoom level from config
|
||||||
zoom = config.DEFAULT_MAP_ZOOM_LEVEL
|
zoom = config.DEFAULT_MAP_ZOOM_LEVEL
|
||||||
@ -433,5 +465,222 @@ class MapIntegrationManager:
|
|||||||
|
|
||||||
logging.info(f"{log_prefix} Map integration shutdown complete.")
|
logging.info(f"{log_prefix} Map integration shutdown complete.")
|
||||||
|
|
||||||
|
def _calculate_sar_corners_geo(
|
||||||
|
self, geo_info: Dict[str, Any]
|
||||||
|
) -> Optional[List[Tuple[float, float]]]:
|
||||||
|
"""
|
||||||
|
Calculates the geographic coordinates (latitude, longitude in degrees)
|
||||||
|
of the four corners of the SAR image based on its georeferencing info.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
geo_info (Dict[str, Any]): The georeferencing dictionary from AppState.
|
||||||
|
Expects keys like 'lat', 'lon', 'orientation' (radians),
|
||||||
|
'ref_x', 'ref_y', 'scale_x', 'scale_y', 'width_px', 'height_px'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[List[Tuple[float, float]]]: A list of four (lon, lat) tuples in degrees
|
||||||
|
representing the corners (e.g., TL, TR, BR, BL),
|
||||||
|
or None on error.
|
||||||
|
"""
|
||||||
|
log_prefix = f"{self._log_prefix} SAR Corners Geo"
|
||||||
|
logging.debug(f"{log_prefix} Calculating SAR corner geographic coordinates...")
|
||||||
|
|
||||||
|
if not self._geod:
|
||||||
|
logging.error(f"{log_prefix} Geodetic calculator (pyproj.Geod) not initialized.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Extract necessary info (ensure keys exist and values are valid)
|
||||||
|
center_lat_rad = geo_info['lat']
|
||||||
|
center_lon_rad = geo_info['lon']
|
||||||
|
orient_rad = geo_info['orientation']
|
||||||
|
ref_x = geo_info['ref_x']
|
||||||
|
ref_y = geo_info['ref_y']
|
||||||
|
scale_x = geo_info['scale_x'] # meters/pixel
|
||||||
|
scale_y = geo_info['scale_y'] # meters/pixel
|
||||||
|
width = geo_info['width_px']
|
||||||
|
height = geo_info['height_px']
|
||||||
|
|
||||||
|
if not (scale_x > 0 and scale_y > 0 and width > 0 and height > 0):
|
||||||
|
logging.error(f"{log_prefix} Invalid scale or dimensions in geo_info.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 1. Calculate pixel coordinates of corners relative to the reference pixel (ref_x, ref_y)
|
||||||
|
# Origin (0,0) is top-left. Y increases downwards in pixel space.
|
||||||
|
corners_pixel = [
|
||||||
|
(0 - ref_x, ref_y - 0), # Top-Left (dx, dy relative to ref, y inverted)
|
||||||
|
(width - 1 - ref_x, ref_y - 0), # Top-Right
|
||||||
|
(width - 1 - ref_x, ref_y - (height - 1)), # Bottom-Right
|
||||||
|
(0 - ref_x, ref_y - (height - 1)) # Bottom-Left
|
||||||
|
]
|
||||||
|
|
||||||
|
# 2. Convert pixel offsets to meter offsets
|
||||||
|
corners_meters = [
|
||||||
|
(dx * scale_x, dy * scale_y) for dx, dy in corners_pixel
|
||||||
|
] # (delta_meters_east, delta_meters_north)
|
||||||
|
|
||||||
|
# 3. Apply inverse rotation to meter offsets if necessary
|
||||||
|
# The map needs the *geographic* corners, so we need to find where
|
||||||
|
# the image corners land geographically. We start from the geo center
|
||||||
|
# and calculate the destination point by applying the *rotated* meter offsets.
|
||||||
|
corners_meters_rotated = []
|
||||||
|
if abs(orient_rad) > 1e-6: # Apply rotation if significant
|
||||||
|
cos_o = math.cos(orient_rad)
|
||||||
|
sin_o = math.sin(orient_rad)
|
||||||
|
for dx_m, dy_m in corners_meters:
|
||||||
|
# Rotate the offset vector (dx_m, dy_m) by orient_rad
|
||||||
|
rot_dx = dx_m * cos_o - dy_m * sin_o
|
||||||
|
rot_dy = dx_m * sin_o + dy_m * cos_o
|
||||||
|
corners_meters_rotated.append((rot_dx, rot_dy))
|
||||||
|
logging.debug(f"{log_prefix} Applied rotation ({math.degrees(orient_rad):.2f} deg) to meter offsets.")
|
||||||
|
else:
|
||||||
|
corners_meters_rotated = corners_meters # No rotation needed
|
||||||
|
logging.debug(f"{log_prefix} Skipping rotation for meter offsets (angle near zero).")
|
||||||
|
|
||||||
|
|
||||||
|
# 4. Calculate geographic coordinates of corners using pyproj.Geod.fwd
|
||||||
|
# This requires calculating distance and azimuth from the center to each rotated meter offset.
|
||||||
|
sar_corners_geo_deg = []
|
||||||
|
for dx_m_rot, dy_m_rot in corners_meters_rotated:
|
||||||
|
# Calculate distance from center (0,0) in rotated meter space
|
||||||
|
distance_m = math.sqrt(dx_m_rot**2 + dy_m_rot**2)
|
||||||
|
# Calculate azimuth from center (North=0, East=90)
|
||||||
|
# atan2(dx, dy) gives angle relative to North axis
|
||||||
|
azimuth_rad = math.atan2(dx_m_rot, dy_m_rot)
|
||||||
|
azimuth_deg = math.degrees(azimuth_rad)
|
||||||
|
|
||||||
|
# Use geod.fwd from the known center lat/lon (radians needed for input?)
|
||||||
|
# pyproj fwd expects degrees for lon, lat, az
|
||||||
|
center_lon_deg = math.degrees(center_lon_rad)
|
||||||
|
center_lat_deg = math.degrees(center_lat_rad)
|
||||||
|
|
||||||
|
# Calculate the destination point
|
||||||
|
endlon, endlat, _ = self._geod.fwd(center_lon_deg, center_lat_deg, azimuth_deg, distance_m)
|
||||||
|
|
||||||
|
# Append (lon, lat) tuple in degrees
|
||||||
|
sar_corners_geo_deg.append((endlon, endlat))
|
||||||
|
logging.debug(f"{log_prefix} Calculated corner: Dist={distance_m:.1f}m, Az={azimuth_deg:.2f}deg -> Lon={endlon:.6f}, Lat={endlat:.6f}")
|
||||||
|
|
||||||
|
if len(sar_corners_geo_deg) != 4:
|
||||||
|
logging.error(f"{log_prefix} Failed to calculate all 4 corner coordinates.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logging.info(f"{log_prefix} Successfully calculated 4 SAR corner geographic coordinates.")
|
||||||
|
return sar_corners_geo_deg
|
||||||
|
|
||||||
|
except KeyError as ke:
|
||||||
|
logging.error(f"{log_prefix} Missing required key in geo_info: {ke}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"{log_prefix} Error calculating SAR corner coordinates:")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# --- NUOVA FUNZIONE HELPER (SCHELETRO/PLACEHOLDER) ---
|
||||||
|
def _geo_coords_to_map_pixels(
|
||||||
|
self,
|
||||||
|
coords_deg: List[Tuple[float, float]],
|
||||||
|
map_bounds: mercantile.LngLatBbox,
|
||||||
|
map_tile_ranges: Tuple[Tuple[int, int], Tuple[int, int]],
|
||||||
|
zoom: int,
|
||||||
|
stitched_map_shape: Tuple[int, int] # (height, width)
|
||||||
|
) -> Optional[List[Tuple[int, int]]]:
|
||||||
|
"""
|
||||||
|
Converts a list of geographic coordinates (lon, lat degrees) to pixel
|
||||||
|
coordinates (x, y) relative to the top-left corner of the stitched map image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coords_deg (List[Tuple[float, float]]): List of (longitude, latitude) tuples in degrees.
|
||||||
|
map_bounds (mercantile.LngLatBbox): Geographic bounds of the *top-left tile* used for stitching.
|
||||||
|
Used as the reference for pixel conversion.
|
||||||
|
map_tile_ranges (Tuple[Tuple[int, int], Tuple[int, int]]): ((min_x, max_x), (min_y, max_y)) tile indices.
|
||||||
|
zoom (int): The zoom level of the map tiles.
|
||||||
|
stitched_map_shape (Tuple[int, int]): The shape (height, width) of the stitched map image in pixels.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[List[Tuple[int, int]]]: List of (x, y) pixel coordinates corresponding
|
||||||
|
to the input geographic coordinates, relative to the
|
||||||
|
top-left of the stitched map image. Returns None on error.
|
||||||
|
"""
|
||||||
|
log_prefix = f"{self._log_prefix} Geo to Pixel"
|
||||||
|
logging.debug(f"{log_prefix} Converting {len(coords_deg)} geo coordinates to map pixels...")
|
||||||
|
|
||||||
|
if mercantile is None:
|
||||||
|
logging.error(f"{log_prefix} Mercantile library not available.")
|
||||||
|
return None
|
||||||
|
if not stitched_map_shape or stitched_map_shape[0] <= 0 or stitched_map_shape[1] <= 0:
|
||||||
|
logging.error(f"{log_prefix} Invalid stitched map shape: {stitched_map_shape}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
pixel_coords = []
|
||||||
|
map_height_px, map_width_px = stitched_map_shape
|
||||||
|
# Tile size from config or service? Assume 256 for mercantile functions
|
||||||
|
tile_size = self._map_service.tile_size if self._map_service else 256
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get the coordinates of the top-left corner of the entire stitched map in the world pixel space (at the given zoom)
|
||||||
|
# This is the top-left corner of the top-left tile (min_x, min_y)
|
||||||
|
min_tile_x = map_tile_ranges[0][0]
|
||||||
|
min_tile_y = map_tile_ranges[1][0]
|
||||||
|
# mercantile.xy_bounds(tile) gives bounds in projected meters, not pixels
|
||||||
|
# We need the pixel coordinates using mercantile.xy() perhaps?
|
||||||
|
|
||||||
|
# Let's try converting each geographic point to its world pixel coordinate at the given zoom
|
||||||
|
# and then find its position relative to the top-left corner of our stitched map area.
|
||||||
|
|
||||||
|
# Calculate the world pixel coordinate (at zoom level) of the top-left corner of our stitched map area
|
||||||
|
# This corresponds to the top-left of tile (min_tile_x, min_tile_y)
|
||||||
|
tl_tile_bounds = mercantile.xy_bounds(min_tile_x, min_tile_y, zoom)
|
||||||
|
# mercantile.xy() converts lon/lat to projected meters (Web Mercator)
|
||||||
|
# We need a function to convert lon/lat directly to *tile pixel coordinates* or *world pixel coordinates*
|
||||||
|
# mercantile doesn't seem to offer this directly. We might need to implement the math:
|
||||||
|
# https://developers.google.com/maps/documentation/javascript/examples/map-coordinates
|
||||||
|
|
||||||
|
# --- Alternative Approach using mercantile.xy and relating to tile bounds ---
|
||||||
|
# 1. Find the projected meter coordinates (Web Mercator) of the top-left corner of the stitched area.
|
||||||
|
tl_tile_mercator_bounds = mercantile.xy_bounds(min_tile_x, min_tile_y, zoom)
|
||||||
|
map_origin_x_mercator = tl_tile_mercator_bounds.left
|
||||||
|
map_origin_y_mercator = tl_tile_mercator_bounds.top # Top has higher Y in Mercator
|
||||||
|
|
||||||
|
# 2. Calculate the total span of the stitched map in Mercator meters
|
||||||
|
max_tile_x = map_tile_ranges[0][1]
|
||||||
|
max_tile_y = map_tile_ranges[1][1]
|
||||||
|
br_tile_mercator_bounds = mercantile.xy_bounds(max_tile_x, max_tile_y, zoom)
|
||||||
|
map_total_width_mercator = br_tile_mercator_bounds.right - map_origin_x_mercator
|
||||||
|
map_total_height_mercator = map_origin_y_mercator - br_tile_mercator_bounds.bottom # Top Y > Bottom Y
|
||||||
|
|
||||||
|
if map_total_width_mercator <= 0 or map_total_height_mercator <=0:
|
||||||
|
logging.error(f"{log_prefix} Invalid map span in Mercator coordinates calculated.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 3. For each input geographic coordinate:
|
||||||
|
for lon, lat in coords_deg:
|
||||||
|
# a. Convert geo coord to Mercator meters
|
||||||
|
point_x_mercator, point_y_mercator = mercantile.xy(lon, lat)
|
||||||
|
|
||||||
|
# b. Calculate the coordinate relative to the map's top-left origin in Mercator meters
|
||||||
|
relative_x_mercator = point_x_mercator - map_origin_x_mercator
|
||||||
|
relative_y_mercator = map_origin_y_mercator - point_y_mercator # Invert Y difference
|
||||||
|
|
||||||
|
# c. Scale the relative Mercator coordinates to pixel coordinates based on the total map span and pixel dimensions
|
||||||
|
pixel_x = int(round((relative_x_mercator / map_total_width_mercator) * map_width_px))
|
||||||
|
pixel_y = int(round((relative_y_mercator / map_total_height_mercator) * map_height_px))
|
||||||
|
|
||||||
|
# Clamp pixel coordinates to be within the stitched map bounds
|
||||||
|
pixel_x_clamped = max(0, min(pixel_x, map_width_px - 1))
|
||||||
|
pixel_y_clamped = max(0, min(pixel_y, map_height_px - 1))
|
||||||
|
|
||||||
|
if pixel_x != pixel_x_clamped or pixel_y != pixel_y_clamped:
|
||||||
|
logging.warning(f"{log_prefix} Clamped pixel coords for ({lon:.4f},{lat:.4f}): ({pixel_x},{pixel_y}) -> ({pixel_x_clamped},{pixel_y_clamped})")
|
||||||
|
|
||||||
|
pixel_coords.append((pixel_x_clamped, pixel_y_clamped))
|
||||||
|
logging.debug(f"{log_prefix} Converted ({lon:.4f},{lat:.4f}) -> MercatorRel({relative_x_mercator:.1f},{relative_y_mercator:.1f}) -> Pixel({pixel_x_clamped},{pixel_y_clamped})")
|
||||||
|
|
||||||
|
logging.info(f"{log_prefix} Successfully converted {len(pixel_coords)} coordinates to map pixels.")
|
||||||
|
return pixel_coords
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"{log_prefix} Error converting geo coordinates to map pixels:")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# --- END OF FILE map_integration.py ---
|
# --- END OF FILE map_integration.py ---
|
||||||
82
receiver.py
82
receiver.py
@ -644,7 +644,8 @@ class UdpReceiver:
|
|||||||
def reassemble_sar_image(self, image_leader, image_data, log_prefix):
|
def reassemble_sar_image(self, image_leader, image_data, log_prefix):
|
||||||
"""
|
"""
|
||||||
Extracts SAR metadata and pixel data (normalized uint8) from buffer.
|
Extracts SAR metadata and pixel data (normalized uint8) from buffer.
|
||||||
Handles corrected radian interpretation for orientation.
|
Interprets ORIENTATION, LATITUDE, and LONGITUDE as RADIANS directly from the buffer
|
||||||
|
based on TN-IMGSER specification.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
image_leader (ImageLeaderData): Parsed leader structure.
|
image_leader (ImageLeaderData): Parsed leader structure.
|
||||||
@ -660,7 +661,7 @@ class UdpReceiver:
|
|||||||
image_key_log = f"SAR(FCNT={fcounter})" # For specific logs within this func
|
image_key_log = f"SAR(FCNT={fcounter})" # For specific logs within this func
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Extract and validate HeaderData - DEBUG for details
|
# 1. Extract and validate HeaderData
|
||||||
hdr_d = image_leader.HEADER_DATA
|
hdr_d = image_leader.HEADER_DATA
|
||||||
dx, dy, bpp = int(hdr_d.DX), int(hdr_d.DY), int(hdr_d.BPP)
|
dx, dy, bpp = int(hdr_d.DX), int(hdr_d.DY), int(hdr_d.BPP)
|
||||||
stride_pixels, pal_type = int(hdr_d.STRIDE), int(hdr_d.PALTYPE)
|
stride_pixels, pal_type = int(hdr_d.STRIDE), int(hdr_d.PALTYPE)
|
||||||
@ -674,7 +675,6 @@ class UdpReceiver:
|
|||||||
or stride_pixels < dx
|
or stride_pixels < dx
|
||||||
or pal_type != 0
|
or pal_type != 0
|
||||||
):
|
):
|
||||||
# ERROR for invalid metadata
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{log_prefix} {image_key_log}: Invalid SAR metadata. Cannot reassemble."
|
f"{log_prefix} {image_key_log}: Invalid SAR metadata. Cannot reassemble."
|
||||||
)
|
)
|
||||||
@ -683,21 +683,18 @@ class UdpReceiver:
|
|||||||
pixel_dtype = np.uint8 if bpp == 1 else np.uint16
|
pixel_dtype = np.uint8 if bpp == 1 else np.uint16
|
||||||
pixel_bytes = bpp
|
pixel_bytes = bpp
|
||||||
|
|
||||||
# 2. Calculate pixel offset - DEBUG for offset calc
|
# 2. Calculate pixel offset
|
||||||
pixel_data_offset = self._calculate_pixel_data_offset(
|
pixel_data_offset = self._calculate_pixel_data_offset(image_leader)
|
||||||
image_leader
|
|
||||||
) # Logs internally
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"{log_prefix} {image_key_log}: Using pixel data offset: {pixel_data_offset}"
|
f"{log_prefix} {image_key_log}: Using pixel data offset: {pixel_data_offset}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Validate offset and buffer size - DEBUG for validation steps
|
# 3. Validate offset and buffer size
|
||||||
available_data_length = len(image_data)
|
available_data_length = len(image_data)
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"{log_prefix} {image_key_log}: Validating offset ({pixel_data_offset}) vs buffer size ({available_data_length})."
|
f"{log_prefix} {image_key_log}: Validating offset ({pixel_data_offset}) vs buffer size ({available_data_length})."
|
||||||
)
|
)
|
||||||
if pixel_data_offset >= available_data_length:
|
if pixel_data_offset >= available_data_length:
|
||||||
# ERROR if offset invalid
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{log_prefix} {image_key_log}: Pixel offset >= buffer size. Cannot extract pixel data."
|
f"{log_prefix} {image_key_log}: Pixel offset >= buffer size. Cannot extract pixel data."
|
||||||
)
|
)
|
||||||
@ -705,14 +702,13 @@ class UdpReceiver:
|
|||||||
minimum_required_core_bytes = dy * dx * pixel_bytes
|
minimum_required_core_bytes = dy * dx * pixel_bytes
|
||||||
actual_pixel_bytes_available = available_data_length - pixel_data_offset
|
actual_pixel_bytes_available = available_data_length - pixel_data_offset
|
||||||
if actual_pixel_bytes_available < minimum_required_core_bytes:
|
if actual_pixel_bytes_available < minimum_required_core_bytes:
|
||||||
# ERROR if insufficient data
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{log_prefix} {image_key_log}: Insufficient pixel data in buffer (Need min {minimum_required_core_bytes}, Found {actual_pixel_bytes_available})."
|
f"{log_prefix} {image_key_log}: Insufficient pixel data in buffer (Need min {minimum_required_core_bytes}, Found {actual_pixel_bytes_available})."
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
logging.debug(f"{log_prefix} {image_key_log}: Buffer size validated.")
|
logging.debug(f"{log_prefix} {image_key_log}: Buffer size validated.")
|
||||||
|
|
||||||
# 4. Create NumPy view - DEBUG for view creation attempt
|
# 4. Create NumPy view
|
||||||
try:
|
try:
|
||||||
stride_bytes = stride_pixels * pixel_bytes
|
stride_bytes = stride_pixels * pixel_bytes
|
||||||
logging.debug(
|
logging.debug(
|
||||||
@ -729,7 +725,6 @@ class UdpReceiver:
|
|||||||
f"{log_prefix} {image_key_log}: NumPy view created successfully."
|
f"{log_prefix} {image_key_log}: NumPy view created successfully."
|
||||||
)
|
)
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
# ERROR for view creation failure
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{log_prefix} {image_key_log}: Failed to create SAR NumPy view (Shape/stride/offset mismatch?): {ve}"
|
f"{log_prefix} {image_key_log}: Failed to create SAR NumPy view (Shape/stride/offset mismatch?): {ve}"
|
||||||
)
|
)
|
||||||
@ -742,15 +737,14 @@ class UdpReceiver:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 6. Normalize image view to uint8 - DEBUG for normalization step
|
# 6. Normalize image view to uint8
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"{log_prefix} {image_key_log}: Normalizing SAR view to uint8..."
|
f"{log_prefix} {image_key_log}: Normalizing SAR view to uint8..."
|
||||||
)
|
)
|
||||||
normalized_image_uint8 = normalize_image(
|
normalized_image_uint8 = normalize_image(
|
||||||
sar_image_view, target_type=np.uint8
|
sar_image_view, target_type=np.uint8
|
||||||
) # Logs internally
|
)
|
||||||
if normalized_image_uint8 is None:
|
if normalized_image_uint8 is None:
|
||||||
# ERROR for normalization failure
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{log_prefix} {image_key_log}: SAR normalization to uint8 failed."
|
f"{log_prefix} {image_key_log}: SAR normalization to uint8 failed."
|
||||||
)
|
)
|
||||||
@ -759,27 +753,26 @@ class UdpReceiver:
|
|||||||
f"{log_prefix} {image_key_log}: Normalization complete (Shape: {normalized_image_uint8.shape})."
|
f"{log_prefix} {image_key_log}: Normalization complete (Shape: {normalized_image_uint8.shape})."
|
||||||
)
|
)
|
||||||
|
|
||||||
# 7. Extract and Convert Geo Info (RADIANS) - Use specific prefix
|
# --- MODIFICATION START: Correct reading of GeoData fields as Radians ---
|
||||||
|
# 7. Extract and Validate Geo Info (RADIANS)
|
||||||
geo_log_prefix = "[Geo extract]"
|
geo_log_prefix = "[Geo extract]"
|
||||||
geo_info_radians = {"valid": False}
|
geo_info_radians = {"valid": False} # Initialize as invalid
|
||||||
try:
|
try:
|
||||||
geo_d = image_leader.GEO_DATA
|
geo_d = image_leader.GEO_DATA
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"{geo_log_prefix} {image_key_log}: Extracting and interpreting GeoData (Orientation as RADIANS)..."
|
f"{geo_log_prefix} {image_key_log}: Extracting GeoData (interpreting ORIENTATION, LATITUDE, LONGITUDE as RADIANS)..."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Read orientation directly as RADIANS (corrected)
|
# Read ORIENTATION, LATITUDE, LONGITUDE directly as RADIANS
|
||||||
orient_rad_raw = float(geo_d.ORIENTATION)
|
# (Assuming they are stored as float representing radians in the buffer)
|
||||||
# Read lat/lon as DEGREES (from structure assumption) and convert
|
lat_rad = float(geo_d.LATITUDE)
|
||||||
lat_deg_raw = float(geo_d.LATITUDE)
|
lon_rad = float(geo_d.LONGITUDE)
|
||||||
lon_deg_raw = float(geo_d.LONGITUDE)
|
orient_rad = float(geo_d.ORIENTATION)
|
||||||
lat_rad = math.radians(lat_deg_raw)
|
|
||||||
lon_rad = math.radians(lon_deg_raw)
|
|
||||||
|
|
||||||
# Store RADIANS internally
|
# Store RADIANS directly in the dictionary
|
||||||
geo_info_radians["lat"] = lat_rad
|
geo_info_radians["lat"] = lat_rad
|
||||||
geo_info_radians["lon"] = lon_rad
|
geo_info_radians["lon"] = lon_rad
|
||||||
geo_info_radians["orientation"] = orient_rad_raw
|
geo_info_radians["orientation"] = orient_rad
|
||||||
geo_info_radians["ref_x"] = int(geo_d.REF_X)
|
geo_info_radians["ref_x"] = int(geo_d.REF_X)
|
||||||
geo_info_radians["ref_y"] = int(geo_d.REF_Y)
|
geo_info_radians["ref_y"] = int(geo_d.REF_Y)
|
||||||
geo_info_radians["scale_x"] = float(geo_d.SCALE_X)
|
geo_info_radians["scale_x"] = float(geo_d.SCALE_X)
|
||||||
@ -787,45 +780,54 @@ class UdpReceiver:
|
|||||||
geo_info_radians["width_px"] = dx
|
geo_info_radians["width_px"] = dx
|
||||||
geo_info_radians["height_px"] = dy
|
geo_info_radians["height_px"] = dy
|
||||||
|
|
||||||
# Validate scale - DEBUG for validation result
|
# Validate scale and basic lat/lon/orient ranges (radians)
|
||||||
if geo_info_radians["scale_x"] > 0 and geo_info_radians["scale_y"] > 0:
|
# Basic range check: lat [-pi/2, pi/2], lon [-pi, pi]
|
||||||
|
is_scale_valid = (
|
||||||
|
geo_info_radians["scale_x"] > 0 and geo_info_radians["scale_y"] > 0
|
||||||
|
)
|
||||||
|
is_lat_valid = -math.pi / 2 <= lat_rad <= math.pi / 2
|
||||||
|
is_lon_valid = -math.pi <= lon_rad <= math.pi
|
||||||
|
# Orientation check can be less strict, maybe check finite?
|
||||||
|
is_orient_valid = math.isfinite(orient_rad)
|
||||||
|
|
||||||
|
if is_scale_valid and is_lat_valid and is_lon_valid and is_orient_valid:
|
||||||
geo_info_radians["valid"] = True
|
geo_info_radians["valid"] = True
|
||||||
# Log extracted values (DEBUG controlled by DEBUG_RECEIVER_GEO)
|
# Log extracted values (convert to degrees *only for logging* if needed)
|
||||||
orient_deg_for_log = math.degrees(orient_rad_raw)
|
lat_deg_log = math.degrees(lat_rad)
|
||||||
|
lon_deg_log = math.degrees(lon_rad)
|
||||||
|
orient_deg_log = math.degrees(orient_rad)
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"{geo_log_prefix} {image_key_log}: GeoInfo Extracted: Valid={geo_info_radians['valid']}, "
|
f"{geo_log_prefix} {image_key_log}: GeoInfo Extracted: Valid={geo_info_radians['valid']}, "
|
||||||
f"Lat={lat_deg_raw:.4f}deg({lat_rad:.6f}rad), Lon={lon_deg_raw:.4f}deg({lon_rad:.6f}rad), "
|
f"Lat={lat_deg_log:.4f}deg({lat_rad:.6f}rad), Lon={lon_deg_log:.4f}deg({lon_rad:.6f}rad), "
|
||||||
f"Orient={orient_deg_for_log:.2f}deg({orient_rad_raw:.6f}rad), "
|
f"Orient={orient_deg_log:.2f}deg({orient_rad:.6f}rad), "
|
||||||
f"Ref=({geo_info_radians['ref_x']},{geo_info_radians['ref_y']}), "
|
f"Ref=({geo_info_radians['ref_x']},{geo_info_radians['ref_y']}), "
|
||||||
f"Scale=({geo_info_radians['scale_x']:.3f},{geo_info_radians['scale_y']:.3f}), "
|
f"Scale=({geo_info_radians['scale_x']:.3f},{geo_info_radians['scale_y']:.3f}), "
|
||||||
f"Size=({dx},{dy})"
|
f"Size=({dx},{dy})"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# WARNING for invalid scale marking Geo invalid
|
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"{geo_log_prefix} {image_key_log}: Invalid scale values found (ScaleX={geo_info_radians['scale_x']}, ScaleY={geo_info_radians['scale_y']}). GeoInfo marked invalid."
|
f"{geo_log_prefix} {image_key_log}: Invalid geo values found (ScaleValid={is_scale_valid}, LatValid={is_lat_valid}, LonValid={is_lon_valid}, OrientValid={is_orient_valid}). GeoInfo marked invalid."
|
||||||
)
|
)
|
||||||
geo_info_radians["valid"] = False
|
geo_info_radians["valid"] = False # Ensure marked invalid
|
||||||
|
|
||||||
except OverflowError as oe:
|
except OverflowError as oe:
|
||||||
# ERROR for math errors
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{geo_log_prefix} {image_key_log}: Math OverflowError during GeoData conversion: {oe}. GeoInfo marked invalid."
|
f"{geo_log_prefix} {image_key_log}: Math OverflowError during GeoData conversion: {oe}. GeoInfo marked invalid."
|
||||||
)
|
)
|
||||||
geo_info_radians = {"valid": False}
|
geo_info_radians = {"valid": False}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Keep EXCEPTION for other geo errors
|
|
||||||
logging.exception(
|
logging.exception(
|
||||||
f"{geo_log_prefix} {image_key_log}: Failed during GeoData extraction/conversion: {e}"
|
f"{geo_log_prefix} {image_key_log}: Failed during GeoData extraction/conversion: {e}"
|
||||||
)
|
)
|
||||||
geo_info_radians = {"valid": False}
|
geo_info_radians = {"valid": False}
|
||||||
|
# --- MODIFICATION END ---
|
||||||
|
|
||||||
# 8. Return results - DEBUG for successful exit
|
# 8. Return results
|
||||||
logging.debug(f"{log_prefix} Exiting reassemble_sar_image successfully.")
|
logging.debug(f"{log_prefix} Exiting reassemble_sar_image successfully.")
|
||||||
|
# Return a *copy* of the normalized image and the geo info dict
|
||||||
return normalized_image_uint8.copy(), geo_info_radians
|
return normalized_image_uint8.copy(), geo_info_radians
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Keep EXCEPTION for unexpected errors in reassembly
|
|
||||||
logging.exception(
|
logging.exception(
|
||||||
f"{log_prefix} {image_key_log}: Unexpected error during SAR reassembly: {e}"
|
f"{log_prefix} {image_key_log}: Unexpected error during SAR reassembly: {e}"
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user