diff --git a/GitUlitity_icona.ico b/GitUlitity_icona.ico new file mode 100644 index 0000000..d585caf Binary files /dev/null and b/GitUlitity_icona.ico differ diff --git a/config.py b/config.py index 19a9714..116191c 100644 --- a/config.py +++ b/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_CACHE_DIRECTORY = "map_cache" # Root directory for cached tiles 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) OFFLINE_MAP_PLACEHOLDER_COLOR = (200, 200, 200) # Light grey 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_CENTER_LAT = 40.7128 # Example: New York City Latitude (Degrees) -SAR_CENTER_LON = -74.0060 # Example: New York City Longitude (Degrees) +# SAR Georeferencing Defaults +# 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 = ( 50.0 # Example: Width/Height of the area to show on the map in Kilometers ) diff --git a/map_integration.py b/map_integration.py index 092bf13..1be7530 100644 --- a/map_integration.py +++ b/map_integration.py @@ -14,7 +14,7 @@ import logging import threading import queue # For type hinting import math -from typing import Optional, Dict, Any, Tuple +from typing import Optional, Dict, Any, Tuple, List # Third-party imports import numpy as np @@ -30,6 +30,8 @@ try: import pyproj except ImportError: pyproj = None + +import cv2 # Local application imports import config @@ -143,79 +145,105 @@ class MapIntegrationManager: def _display_initial_map_area_thread(self): - """ - (Runs in background thread) Calculates the initial map area based on default - config settings and queues the result for display on the main thread. - Moved from App._display_initial_map_area. - """ - log_prefix = f"{self._log_prefix} InitialMap" - # Check dependencies initialized in __init__ - 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 + """ + (Runs in background thread) Calculates the initial map area based on default + config settings and queues the result for display on the main thread, + *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" - logging.info(f"{log_prefix} Calculating initial map area...") - map_image_pil: Optional[Image.Image] = None - try: - # Use default center/size from config for the initial view - bbox = get_bounding_box_from_center_size( - config.SAR_CENTER_LAT, config.SAR_CENTER_LON, config.SAR_IMAGE_SIZE_KM - ) - if bbox is None: - raise MapCalculationError("Failed to calculate initial bounding box.") + # 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 - zoom = config.DEFAULT_MAP_ZOOM_LEVEL - tile_ranges = get_tile_ranges_for_bbox(bbox, zoom) - if tile_ranges is None: - raise MapCalculationError("Failed to calculate initial tile ranges.") + # 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 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: - logging.info(f"{log_prefix} Shutdown detected before stitching.") - return + logging.info(f"{log_prefix} Shutdown detected. Aborting.") + return - 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( - zoom, tile_ranges[0], tile_ranges[1] - ) # stitch_map_image uses placeholders internally if needed + map_image_pil: Optional[Image.Image] = None + try: + # --- 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 --- - # --- Check shutdown again after stitching --- - if self._app_state.shutting_down: - logging.info(f"{log_prefix} Shutdown detected after stitching.") - # Don't queue result if shutting down - return + # Usa default center/size da config + bbox = get_bounding_box_from_center_size( + config.SAR_CENTER_LAT, config.SAR_CENTER_LON, config.SAR_IMAGE_SIZE_KM + ) + if bbox is None: + raise MapCalculationError("Failed to calculate initial bounding box.") - if map_image_pil: - logging.info(f"{log_prefix} Initial map area stitched successfully.") - else: - # This case should be less likely if stitch_map_image uses placeholders - logging.error(f"{log_prefix} Failed to stitch initial map area (returned None even with placeholders).") + # Calcola i tile ranges USANDO la variabile zoom definita sopra + tile_ranges = get_tile_ranges_for_bbox(bbox, zoom) + if tile_ranges is None: + raise MapCalculationError("Failed to calculate initial tile ranges.") - except ImportError 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}") - map_image_pil = None # Ensure None is queued on error - except Exception as e: - logging.exception(f"{log_prefix} Unexpected error calculating initial map:") - map_image_pil = None # Ensure None is queued on error - 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: - logging.debug(f"{log_prefix} Queueing SHOW_MAP command for main thread.") - # The payload is the PIL image (or None) - put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_pil), "tkinter", self._app) - logging.debug(f"{log_prefix} Initial map display thread finished.") + if self._app_state.shutting_down: + logging.info(f"{log_prefix} Shutdown detected before stitching.") + 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]})...") + map_image_pil = self._map_tile_manager.stitch_map_image( + zoom, tile_ranges[0], tile_ranges[1] + ) + + if self._app_state.shutting_down: + logging.info(f"{log_prefix} Shutdown detected after stitching.") + return + + if map_image_pil: + logging.info(f"{log_prefix} Initial map area stitched successfully.") + else: + 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]): @@ -276,8 +304,12 @@ class MapIntegrationManager: # Calculate size in KM, using default from config as fallback if scale_x > 0 and width_px > 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: - 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 # Get zoom level from config zoom = config.DEFAULT_MAP_ZOOM_LEVEL @@ -432,6 +464,223 @@ class MapIntegrationManager: # Clear tile manager cache? Optional, maybe not needed on normal shutdown. 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 --- \ No newline at end of file diff --git a/receiver.py b/receiver.py index fc7e22f..4ec7fdb 100644 --- a/receiver.py +++ b/receiver.py @@ -644,7 +644,8 @@ class UdpReceiver: def reassemble_sar_image(self, image_leader, image_data, log_prefix): """ 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: 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 try: - # 1. Extract and validate HeaderData - DEBUG for details + # 1. Extract and validate HeaderData hdr_d = image_leader.HEADER_DATA 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) @@ -674,7 +675,6 @@ class UdpReceiver: or stride_pixels < dx or pal_type != 0 ): - # ERROR for invalid metadata logging.error( 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_bytes = bpp - # 2. Calculate pixel offset - DEBUG for offset calc - pixel_data_offset = self._calculate_pixel_data_offset( - image_leader - ) # Logs internally + # 2. Calculate pixel offset + pixel_data_offset = self._calculate_pixel_data_offset(image_leader) logging.debug( 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) logging.debug( f"{log_prefix} {image_key_log}: Validating offset ({pixel_data_offset}) vs buffer size ({available_data_length})." ) if pixel_data_offset >= available_data_length: - # ERROR if offset invalid logging.error( 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 actual_pixel_bytes_available = available_data_length - pixel_data_offset if actual_pixel_bytes_available < minimum_required_core_bytes: - # ERROR if insufficient data logging.error( f"{log_prefix} {image_key_log}: Insufficient pixel data in buffer (Need min {minimum_required_core_bytes}, Found {actual_pixel_bytes_available})." ) return None 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: stride_bytes = stride_pixels * pixel_bytes logging.debug( @@ -729,7 +725,6 @@ class UdpReceiver: f"{log_prefix} {image_key_log}: NumPy view created successfully." ) except ValueError as ve: - # ERROR for view creation failure logging.error( 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 - # 6. Normalize image view to uint8 - DEBUG for normalization step + # 6. Normalize image view to uint8 logging.debug( f"{log_prefix} {image_key_log}: Normalizing SAR view to uint8..." ) normalized_image_uint8 = normalize_image( sar_image_view, target_type=np.uint8 - ) # Logs internally + ) if normalized_image_uint8 is None: - # ERROR for normalization failure logging.error( 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})." ) - # 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_info_radians = {"valid": False} + geo_info_radians = {"valid": False} # Initialize as invalid try: geo_d = image_leader.GEO_DATA 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) - orient_rad_raw = float(geo_d.ORIENTATION) - # Read lat/lon as DEGREES (from structure assumption) and convert - lat_deg_raw = float(geo_d.LATITUDE) - lon_deg_raw = float(geo_d.LONGITUDE) - lat_rad = math.radians(lat_deg_raw) - lon_rad = math.radians(lon_deg_raw) + # Read ORIENTATION, LATITUDE, LONGITUDE directly as RADIANS + # (Assuming they are stored as float representing radians in the buffer) + lat_rad = float(geo_d.LATITUDE) + lon_rad = float(geo_d.LONGITUDE) + orient_rad = float(geo_d.ORIENTATION) - # Store RADIANS internally + # Store RADIANS directly in the dictionary geo_info_radians["lat"] = lat_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_y"] = int(geo_d.REF_Y) 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["height_px"] = dy - # Validate scale - DEBUG for validation result - if geo_info_radians["scale_x"] > 0 and geo_info_radians["scale_y"] > 0: + # Validate scale and basic lat/lon/orient ranges (radians) + # 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 - # Log extracted values (DEBUG controlled by DEBUG_RECEIVER_GEO) - orient_deg_for_log = math.degrees(orient_rad_raw) + # Log extracted values (convert to degrees *only for logging* if needed) + lat_deg_log = math.degrees(lat_rad) + lon_deg_log = math.degrees(lon_rad) + orient_deg_log = math.degrees(orient_rad) logging.debug( 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"Orient={orient_deg_for_log:.2f}deg({orient_rad_raw:.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_log:.2f}deg({orient_rad:.6f}rad), " 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"Size=({dx},{dy})" ) else: - # WARNING for invalid scale marking Geo invalid 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: - # ERROR for math errors logging.error( f"{geo_log_prefix} {image_key_log}: Math OverflowError during GeoData conversion: {oe}. GeoInfo marked invalid." ) geo_info_radians = {"valid": False} except Exception as e: - # Keep EXCEPTION for other geo errors logging.exception( f"{geo_log_prefix} {image_key_log}: Failed during GeoData extraction/conversion: {e}" ) 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.") + # Return a *copy* of the normalized image and the geo info dict return normalized_image_uint8.copy(), geo_info_radians except Exception as e: - # Keep EXCEPTION for unexpected errors in reassembly logging.exception( f"{log_prefix} {image_key_log}: Unexpected error during SAR reassembly: {e}" )