visualizzazione mappa corretta, controllare misure e rotazioni

This commit is contained in:
VALLONGOL 2025-04-08 13:27:49 +02:00
parent 1ee934505a
commit 03ff5ec672
4 changed files with 365 additions and 110 deletions

BIN
GitUlitity_icona.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

@ -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
) )

View File

@ -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
@ -145,75 +147,101 @@ 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" log_prefix = f"{self._log_prefix} InitialMap"
# Check dependencies initialized in __init__
# Check if default lat/lon are set to 0.0 to prevent initial display
if config.SAR_CENTER_LAT == 0.0 and config.SAR_CENTER_LON == 0.0:
# ... (codice per saltare e aggiornare lo stato, come prima) ...
# ... (assicurati che questa parte sia corretta come nella risposta precedente) ...
logging.info(f"{log_prefix} Initial map display skipped based on config defaults (0,0). Waiting for valid GeoInfo.")
if not self._app_state.shutting_down:
status_msg = "Status Unavailable" # Default
try:
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
# Se le coordinate di default *non* sono (0,0), procedi
logging.info(f"{log_prefix} Calculating initial map area based on non-zero config defaults...")
# Check dependencies
if not (self._map_tile_manager and self._map_display_window): 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.") 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) put_queue(self._tkinter_queue, ('SHOW_MAP', None), "tkinter", self._app)
return return
# Check shutdown flag early
if self._app_state.shutting_down: if self._app_state.shutting_down:
logging.info(f"{log_prefix} Shutdown detected. Aborting.") logging.info(f"{log_prefix} Shutdown detected. Aborting.")
return return
logging.info(f"{log_prefix} Calculating initial map area...")
map_image_pil: Optional[Image.Image] = None map_image_pil: Optional[Image.Image] = None
try: try:
# Use default center/size from config for the initial view # --- MODIFICA QUI: Definisci 'zoom' PRIMA di usarlo ---
zoom = config.DEFAULT_MAP_ZOOM_LEVEL
logging.debug(f"{log_prefix} Using default zoom level: {zoom}")
# --- FINE MODIFICA ---
# Usa default center/size da config
bbox = get_bounding_box_from_center_size( bbox = get_bounding_box_from_center_size(
config.SAR_CENTER_LAT, config.SAR_CENTER_LON, config.SAR_IMAGE_SIZE_KM config.SAR_CENTER_LAT, config.SAR_CENTER_LON, config.SAR_IMAGE_SIZE_KM
) )
if bbox is None: if bbox is None:
raise MapCalculationError("Failed to calculate initial bounding box.") raise MapCalculationError("Failed to calculate initial bounding box.")
zoom = config.DEFAULT_MAP_ZOOM_LEVEL # Calcola i tile ranges USANDO la variabile zoom definita sopra
tile_ranges = get_tile_ranges_for_bbox(bbox, zoom) tile_ranges = get_tile_ranges_for_bbox(bbox, zoom)
if tile_ranges is None: if tile_ranges is None:
raise MapCalculationError("Failed to calculate initial tile ranges.") raise MapCalculationError("Failed to calculate initial tile ranges.")
# --- Check shutdown again before potentially long tile stitching ---
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 before stitching.")
return return
# Ora puoi usare 'zoom' nel messaggio di log
logging.info(f"{log_prefix} Stitching initial map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})...") logging.info(f"{log_prefix} Stitching initial map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})...")
map_image_pil = self._map_tile_manager.stitch_map_image( map_image_pil = self._map_tile_manager.stitch_map_image(
zoom, tile_ranges[0], tile_ranges[1] zoom, tile_ranges[0], tile_ranges[1]
) # stitch_map_image uses placeholders internally if needed )
# --- Check shutdown again after stitching ---
if self._app_state.shutting_down: if self._app_state.shutting_down:
logging.info(f"{log_prefix} Shutdown detected after stitching.") logging.info(f"{log_prefix} Shutdown detected after stitching.")
# Don't queue result if shutting down
return return
if map_image_pil: if map_image_pil:
logging.info(f"{log_prefix} Initial map area stitched successfully.") logging.info(f"{log_prefix} Initial map area stitched successfully.")
else: else:
# This case should be less likely if stitch_map_image uses placeholders logging.error(f"{log_prefix} Failed to stitch initial map area.")
logging.error(f"{log_prefix} Failed to stitch initial map area (returned None even with placeholders).")
except ImportError as e: except (ImportError, MapCalculationError) as e:
# Should be caught by __init__, but handle defensively
logging.critical(f"{log_prefix} Missing library during map calculation: {e}")
map_image_pil = None # Ensure None is queued on error
except MapCalculationError as e:
logging.error(f"{log_prefix} Calculation error: {e}") logging.error(f"{log_prefix} Calculation error: {e}")
map_image_pil = None # Ensure None is queued on error map_image_pil = None
except Exception as e: except Exception as e:
logging.exception(f"{log_prefix} Unexpected error calculating initial map:") logging.exception(f"{log_prefix} Unexpected error calculating initial map:")
map_image_pil = None # Ensure None is queued on error map_image_pil = None
finally: finally:
# Always queue the result (PIL image or None) for the main thread to handle display
# Check shutdown one last time before queueing
if not self._app_state.shutting_down: if not self._app_state.shutting_down:
logging.debug(f"{log_prefix} Queueing SHOW_MAP command for main thread.") logging.debug(f"{log_prefix} Queueing SHOW_MAP command for initial map.")
# The payload is the PIL image (or None)
put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_pil), "tkinter", self._app) put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_pil), "tkinter", self._app)
logging.debug(f"{log_prefix} Initial map display thread finished.") logging.debug(f"{log_prefix} Initial map display thread finished.")
@ -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 ---

View File

@ -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}"
) )