diff --git a/app.py b/app.py index 7f83d9e..400f875 100644 --- a/app.py +++ b/app.py @@ -20,6 +20,7 @@ import sys import socket # Required for network setup from typing import Optional, Tuple, Any, Dict, TYPE_CHECKING import datetime +import cv2 # --- Third-party imports --- import tkinter as tk @@ -60,6 +61,7 @@ from utils import ( decimal_to_dms, generate_sar_kml, launch_google_earth, + cleanup_old_kml_files ) from network import create_udp_socket, close_udp_socket from receiver import UdpReceiver @@ -1390,9 +1392,17 @@ class App: ) # Passa geo_info if kml_success: - logging.info( + logging.debug( f"{kml_log_prefix} KML file generated successfully: {kml_output_path}" ) + + logging.debug(f"{kml_log_prefix} Calling KML cleanup utility...") + try: + cleanup_old_kml_files(config.KML_OUTPUT_DIRECTORY, config.MAX_KML_FILES) + except Exception as cleanup_e: + # Log error during cleanup but don't stop subsequent actions (like GE launch) + logging.exception(f"{kml_log_prefix} Error during KML cleanup call:") + # Lancia Google Earth se richiesto if config.AUTO_LAUNCH_GOOGLE_EARTH: logging.debug( diff --git a/config.py b/config.py index dbdb5ab..89281b6 100644 --- a/config.py +++ b/config.py @@ -180,5 +180,11 @@ AUTO_LAUNCH_GOOGLE_EARTH = False # Imposta a True per tentare di aprire automat # Opzionale: potresti aggiungere un percorso esplicito all'eseguibile di Google Earth se non รจ nel PATH # GOOGLE_EARTH_EXECUTABLE_PATH = "C:/Program Files/Google/Google Earth Pro/client/googleearth.exe" # Esempio Windows +# Maximum number of KML files to keep in the output directory. +# Older files will be deleted when a new one is created if the count exceeds this limit. +# Set to 0 or less to disable cleanup. +MAX_KML_FILES = 50 + + # --- END OF FILE config.py --- diff --git a/image_pipeline.py b/image_pipeline.py index 77b9bcc..1f68dd4 100644 --- a/image_pipeline.py +++ b/image_pipeline.py @@ -194,40 +194,68 @@ class ImagePipeline: if not self._app_state.shutting_down: logging.exception(f"{log_prefix} Error during SAR processing pipeline:") + # Only the modified function _rotate_image is sent. + # The process_sar_for_display function does not need changes for this specific request, + # as it will receive the potentially larger image from the modified _rotate_image + # and pass it correctly to _resize_sar_image. + + # Function to be modified: _rotate_image + def _rotate_image(self, img: np.ndarray, angle_rad: float) -> Optional[np.ndarray]: """ - Helper method to rotate an image using OpenCV. + Helper method to rotate an image using OpenCV, ensuring the entire + rotated image is contained within the output canvas by resizing the canvas + and adding borders as necessary. Args: - img (np.ndarray): Input image. + img (np.ndarray): Input image (can be grayscale or BGR). angle_rad (float): Rotation angle in radians. Returns: - Optional[np.ndarray]: The rotated image, or None on critical error. + Optional[np.ndarray]: The rotated image within an expanded canvas, + or None on critical error. """ log_prefix = f"{self._log_prefix} SAR Rotate Helper" try: - deg = math.degrees(angle_rad) + # 1. Get image dimensions and center h, w = img.shape[:2] - center = (w // 2, h // 2) - # Get rotation matrix - M = cv2.getRotationMatrix2D(center, deg, 1.0) - # Determine border color (black for BGR, 0 for grayscale) + center_x, center_y = w // 2, h // 2 + + # 2. Calculate rotation matrix for the original center + deg = math.degrees(angle_rad) + M = cv2.getRotationMatrix2D((center_x, center_y), deg, 1.0) + + # 3. Calculate the new bounding box dimensions + cos = np.abs(M[0, 0]) + sin = np.abs(M[0, 1]) + new_w = int((h * sin) + (w * cos)) + new_h = int((h * cos) + (w * sin)) + logging.debug(f"{log_prefix} Original dims ({w}x{h}), Rotated BBox dims ({new_w}x{new_h}) for angle {deg:.2f} deg.") + + # 4. Adjust the rotation matrix to account for translation + # The matrix needs to shift the image center to the center of the new, larger canvas. + M[0, 2] += (new_w / 2) - center_x + M[1, 2] += (new_h / 2) - center_y + logging.debug(f"{log_prefix} Rotation matrix adjusted for translation.") + + # 5. Determine border color (black) border_color = [0, 0, 0] if img.ndim == 3 else 0 - # Perform affine warp + + # 6. Perform the affine transformation on the larger canvas rotated_img = cv2.warpAffine( img, M, - (w, h), # Output size same as input - flags=cv2.INTER_LINEAR, # Linear interpolation + (new_w, new_h), # Use the new bounding box dimensions + flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, - borderValue=border_color, # Fill borders with black + borderValue=border_color # Fill borders with black ) - logging.debug(f"{log_prefix} Rotation successful.") + logging.debug(f"{log_prefix} Rotation and warpAffine successful. Output shape: {rotated_img.shape}") return rotated_img + except Exception as e: # Log error and return None to indicate failure - logging.exception(f"{log_prefix} Rotation warpAffine error:") + logging.exception(f"{log_prefix} Rotation/warpAffine error:") return None def _resize_sar_image( diff --git a/utils.py b/utils.py index 102d1be..405efc4 100644 --- a/utils.py +++ b/utils.py @@ -13,11 +13,12 @@ Uses standardized logging prefixes. Drop counts are now managed within AppState. import queue import logging import math -import os # Aggiunto -import datetime # Aggiunto per timestamp -import sys # Aggiunto per platform check -import subprocess # Aggiunto per lanciare processi -import shutil # Aggiunto per trovare eseguibili (opzionale) +import os +import datetime +import sys +import subprocess +import shutil +from pathlib import Path # Importa le librerie KML e GEO, gestendo l'ImportError try: @@ -377,7 +378,7 @@ def generate_sar_kml(geo_info_radians, output_path) -> bool: # Salva il file KML logging.debug(f"{log_prefix} Saving KML to: {output_path}") kml.save(output_path) - logging.info(f"{log_prefix} KML file saved successfully: {output_path}") + logging.debug(f"{log_prefix} KML file saved successfully: {output_path}") return True except Exception as e: @@ -435,6 +436,77 @@ def launch_google_earth(kml_path): logging.error(f"{log_prefix} Error launching KML handler: {e}") except Exception as e: logging.exception(f"{log_prefix} Unexpected error launching Google Earth:") + +def cleanup_old_kml_files(kml_directory: str, max_files_to_keep: int): + """ + Removes the oldest KML files from the specified directory if the total number + of KML files exceeds max_files_to_keep. + + Args: + kml_directory (str): The path to the directory containing KML files. + max_files_to_keep (int): The maximum number of KML files to retain. + If 0 or less, cleanup is disabled. + """ + log_prefix = "[Utils KML Cleanup]" + if max_files_to_keep <= 0: + logging.debug(f"{log_prefix} KML cleanup disabled (max_files_to_keep={max_files_to_keep}).") + return + + logging.debug(f"{log_prefix} Checking directory '{kml_directory}' for KML files older than the newest {max_files_to_keep}.") + + try: + kml_dir_path = Path(kml_directory) + if not kml_dir_path.is_dir(): + logging.warning(f"{log_prefix} Directory '{kml_directory}' not found. Cannot perform cleanup.") + return + + # 1. List all .kml files with their modification times + kml_files = [] + for item in kml_dir_path.glob('*.kml'): + if item.is_file(): + try: + # Get modification time + mtime = item.stat().st_mtime + kml_files.append((item, mtime)) + except OSError as stat_err: + logging.warning(f"{log_prefix} Could not get modification time for '{item.name}': {stat_err}") + except Exception as stat_e: + logging.exception(f"{log_prefix} Unexpected error getting stat for '{item.name}':") + + + # 2. Check if cleanup is needed + current_file_count = len(kml_files) + logging.debug(f"{log_prefix} Found {current_file_count} KML files. Max allowed: {max_files_to_keep}.") + if current_file_count <= max_files_to_keep: + logging.debug(f"{log_prefix} File count is within limit. No cleanup needed.") + return + + # 3. Sort files by modification time (oldest first) + kml_files.sort(key=lambda x: x[1]) # Sort by the second element (mtime) + + # 4. Determine files to delete + num_to_delete = current_file_count - max_files_to_keep + files_to_delete = kml_files[:num_to_delete] # Get the oldest ones + + logging.debug(f"{log_prefix} Need to delete {num_to_delete} oldest KML files.") + + # 5. Delete the oldest files + deleted_count = 0 + for file_path, _ in files_to_delete: + try: + file_path.unlink() # Use unlink() for Path objects (equivalent to os.remove) + logging.debug(f"{log_prefix} Deleted old KML file: {file_path.name}") + deleted_count += 1 + except OSError as delete_err: + logging.error(f"{log_prefix} Failed to delete file '{file_path.name}': {delete_err}") + except Exception as delete_e: + logging.exception(f"{log_prefix} Unexpected error deleting file '{file_path.name}':") + + + logging.debug(f"{log_prefix} Cleanup finished. Deleted {deleted_count}/{num_to_delete} files.") + + except Exception as e: + logging.exception(f"{log_prefix} Error during KML cleanup process:") # --- END OF FILE utils.py ---