518 lines
23 KiB
Python
518 lines
23 KiB
Python
# display.py
|
|
"""
|
|
THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
|
|
Manages the display of MFD and SAR images using OpenCV windows.
|
|
Handles window creation, positioning, image updates (called from main thread),
|
|
mouse event callbacks for the SAR window, and window destruction during shutdown.
|
|
Uses standardized logging prefixes and levels.
|
|
"""
|
|
|
|
# Standard library imports
|
|
import logging
|
|
import queue
|
|
import time
|
|
from typing import Optional # For type hinting
|
|
|
|
# Third-party imports
|
|
import cv2
|
|
import numpy as np
|
|
|
|
# Local application imports
|
|
import config # For default sizes and other constants
|
|
from utils import put_queue
|
|
|
|
|
|
class DisplayManager:
|
|
"""
|
|
Manages MFD and SAR OpenCV display windows and related interactions.
|
|
Requires image update calls (show_mfd_image, show_sar_image) to be made
|
|
from the main GUI thread. Logs operations with the [DisplayMgr] prefix.
|
|
"""
|
|
|
|
# --- MODIFIED FUNCTION: __init__ ---
|
|
def __init__(
|
|
self,
|
|
app,
|
|
sar_queue,
|
|
mouse_queue,
|
|
sar_x,
|
|
sar_y,
|
|
mfd_x,
|
|
mfd_y,
|
|
initial_sar_width: Optional[int] = None, # <<< ADDED Argument
|
|
initial_sar_height: Optional[int] = None, # <<< ADDED Argument
|
|
):
|
|
"""
|
|
Initializes the DisplayManager.
|
|
|
|
Args:
|
|
app (App): Reference to the main application instance.
|
|
sar_queue (queue.Queue): Queue for receiving processed SAR images.
|
|
mouse_queue (queue.Queue): Queue for sending raw mouse coordinates.
|
|
sar_x (int): Initial X position for the SAR window.
|
|
sar_y (int): Initial Y position for the SAR window.
|
|
mfd_x (int): Initial X position for the MFD window.
|
|
mfd_y (int): Initial Y position for the MFD window.
|
|
initial_sar_width (Optional[int]): Initial width for SAR placeholder/window. Defaults to config.
|
|
initial_sar_height (Optional[int]): Initial height for SAR placeholder/window. Defaults to config.
|
|
"""
|
|
# Define log prefix for this class instance
|
|
self.log_prefix = "[DisplayMgr]"
|
|
logging.debug(f"{self.log_prefix} Initializing DisplayManager...")
|
|
|
|
self.app = app # Reference to main application
|
|
self.sar_queue = sar_queue
|
|
self.mouse_queue = mouse_queue
|
|
self.sar_x = sar_x
|
|
self.sar_y = sar_y
|
|
self.mfd_x = mfd_x
|
|
self.mfd_y = mfd_y
|
|
self.sar_window_name = "SAR"
|
|
self.mfd_window_name = "MFD"
|
|
self.sar_window_initialized = False
|
|
self.mfd_window_initialized = False
|
|
self.sar_mouse_callback_set = False
|
|
|
|
# Store initial dimensions from arguments or config for placeholders
|
|
# Use passed values if provided, otherwise fall back to config defaults
|
|
self.initial_mfd_width = (
|
|
config.INITIAL_MFD_WIDTH
|
|
) # MFD size not changed by map overlay (yet)
|
|
self.initial_mfd_height = config.INITIAL_MFD_HEIGHT
|
|
self.initial_sar_width = (
|
|
initial_sar_width
|
|
if initial_sar_width is not None
|
|
else config.INITIAL_SAR_WIDTH
|
|
)
|
|
self.initial_sar_height = (
|
|
initial_sar_height
|
|
if initial_sar_height is not None
|
|
else config.INITIAL_SAR_HEIGHT
|
|
)
|
|
logging.debug(
|
|
f"{self.log_prefix} Using Initial Sizes - MFD: {self.initial_mfd_width}x{self.initial_mfd_height}, SAR: {self.initial_sar_width}x{self.initial_sar_height}"
|
|
)
|
|
|
|
logging.debug(f"{self.log_prefix} DisplayManager initialization complete.")
|
|
|
|
# --- END MODIFIED FUNCTION: __init__ ---
|
|
|
|
# --- MODIFIED FUNCTION: initialize_display_windows ---
|
|
def initialize_display_windows(self):
|
|
"""
|
|
Creates and displays initial placeholder windows for MFD and SAR.
|
|
Forces OpenCV windows to appear at startup with default background.
|
|
MUST be called from the main GUI thread.
|
|
Uses the initial dimensions stored during __init__.
|
|
"""
|
|
# Use INFO level for significant initialization step
|
|
logging.info(
|
|
f"{self.log_prefix} Initializing MFD and SAR display windows with placeholders..."
|
|
)
|
|
try:
|
|
# MFD Placeholder - Uses self.initial_mfd_... attributes
|
|
logging.debug(
|
|
f"{self.log_prefix} Creating placeholder MFD image ({self.initial_mfd_height}x{self.initial_mfd_width})."
|
|
)
|
|
placeholder_mfd = np.zeros(
|
|
(self.initial_mfd_height, self.initial_mfd_width, 3), dtype=np.uint8
|
|
)
|
|
placeholder_mfd[:, :] = [30, 30, 30] # Dark Gray BGR
|
|
|
|
logging.debug(f"{self.log_prefix} Attempting to show placeholder MFD...")
|
|
# show_mfd_image logs internally
|
|
self.show_mfd_image(placeholder_mfd)
|
|
# Log success after the call if window became initialized
|
|
if self.mfd_window_initialized:
|
|
logging.info(
|
|
f"{self.log_prefix} MFD window '{self.mfd_window_name}' initialized with placeholder."
|
|
)
|
|
else:
|
|
logging.warning(
|
|
f"{self.log_prefix} MFD window initialization attempt finished, but window not marked as initialized."
|
|
)
|
|
|
|
except Exception as e:
|
|
# Use ERROR for failure during initialization
|
|
logging.error(
|
|
f"{self.log_prefix} Failed to initialize MFD window with placeholder: {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
try:
|
|
# SAR Placeholder - Uses self.initial_sar_... attributes
|
|
logging.debug(
|
|
f"{self.log_prefix} Creating placeholder SAR image ({self.initial_sar_height}x{self.initial_sar_width})."
|
|
)
|
|
# Ensure placeholder is BGR (3 channels) as show_sar_image might receive colorized images later
|
|
placeholder_sar = np.zeros(
|
|
(self.initial_sar_height, self.initial_sar_width, 3), dtype=np.uint8
|
|
)
|
|
placeholder_sar[:, :] = [60, 60, 60] # Lighter Gray BGR
|
|
|
|
logging.debug(f"{self.log_prefix} Attempting to show placeholder SAR...")
|
|
# show_sar_image logs internally
|
|
self.show_sar_image(placeholder_sar)
|
|
# Log success after the call
|
|
if self.sar_window_initialized:
|
|
logging.info(
|
|
f"{self.log_prefix} SAR window '{self.sar_window_name}' initialized with placeholder."
|
|
)
|
|
else:
|
|
logging.warning(
|
|
f"{self.log_prefix} SAR window initialization attempt finished, but window not marked as initialized."
|
|
)
|
|
|
|
except Exception as e:
|
|
# Use ERROR for failure during initialization
|
|
logging.error(
|
|
f"{self.log_prefix} Failed to initialize SAR window with placeholder: {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
# --- END MODIFIED FUNCTION: initialize_display_windows ---
|
|
|
|
def show_mfd_image(self, image):
|
|
"""
|
|
Displays the MFD image in its OpenCV window. Creates/moves window on first call.
|
|
MUST be called from the main GUI thread. Includes cv2.waitKey(1).
|
|
|
|
Args:
|
|
image (numpy.ndarray): The MFD image to display (BGR format).
|
|
"""
|
|
# Use DEBUG for start of display function
|
|
logging.debug(
|
|
f"{self.log_prefix} Received request to show MFD image (Shape: {getattr(image, 'shape', 'N/A')})."
|
|
)
|
|
|
|
# Validate input - Use ERROR for invalid input causing failure
|
|
if image is None or not isinstance(image, np.ndarray) or image.size == 0:
|
|
logging.error(
|
|
f"{self.log_prefix} show_mfd_image received invalid image data. Aborting display."
|
|
)
|
|
return
|
|
if image.ndim != 3 or image.shape[2] != 3:
|
|
logging.error(
|
|
f"{self.log_prefix} show_mfd_image received non-BGR image (ndim={image.ndim}, channels={image.shape[2] if image.ndim==3 else 'N/A'}). Aborting display."
|
|
)
|
|
return
|
|
|
|
try:
|
|
window_name = self.mfd_window_name
|
|
# Create and move window only once
|
|
if not self.mfd_window_initialized:
|
|
# DEBUG for first-time setup
|
|
logging.debug(
|
|
f"{self.log_prefix} First call for '{window_name}'. Creating and positioning window."
|
|
)
|
|
cv2.imshow(window_name, image)
|
|
try:
|
|
cv2.moveWindow(window_name, self.mfd_x, self.mfd_y)
|
|
self.mfd_window_initialized = (
|
|
True # Set flag AFTER successful operations
|
|
)
|
|
# INFO log for successful window creation/move
|
|
logging.info(
|
|
f"{self.log_prefix} '{window_name}' window created and moved to ({self.mfd_x}, {self.mfd_y})."
|
|
)
|
|
cv2.waitKey(1) # Allow window to draw/position
|
|
except cv2.error as move_e:
|
|
# WARNING if move fails but window might still exist
|
|
logging.warning(
|
|
f"{self.log_prefix} Could not move '{window_name}' window on initialization: {move_e}. Window might appear elsewhere."
|
|
)
|
|
# Set flag true anyway, assuming imshow worked.
|
|
self.mfd_window_initialized = True
|
|
else:
|
|
# Just update the image - DEBUG for subsequent updates
|
|
logging.debug(
|
|
f"{self.log_prefix} Updating existing MFD window '{window_name}'."
|
|
)
|
|
cv2.imshow(window_name, image)
|
|
|
|
# Essential waitKey - DEBUG for calling waitKey
|
|
# logging.debug(f"{self.log_prefix} Calling waitKey(1) for MFD window.") # Less verbose
|
|
cv2.waitKey(1)
|
|
|
|
except cv2.error as e:
|
|
# Handle cases where window might have been closed manually
|
|
# Use WARNING as it's recoverable
|
|
if "NULL window" in str(e) or "invalid window" in str(e):
|
|
logging.warning(
|
|
f"{self.log_prefix} OpenCV window '{window_name}' seems closed. Will re-initialize on next valid image."
|
|
)
|
|
self.mfd_window_initialized = False # Reset flag
|
|
else:
|
|
# Keep EXCEPTION for other OpenCV errors
|
|
logging.exception(
|
|
f"{self.log_prefix} OpenCV error displaying MFD image: {e}"
|
|
)
|
|
except Exception as e:
|
|
# Keep EXCEPTION for unexpected errors
|
|
logging.exception(
|
|
f"{self.log_prefix} Unexpected error displaying MFD image: {e}"
|
|
)
|
|
|
|
def show_sar_image(self, image):
|
|
"""
|
|
Displays the SAR image in its OpenCV window. Handles first-time setup
|
|
(creation, move, mouse callback) and subsequent updates.
|
|
MUST be called from the main GUI thread. Includes cv2.waitKey(1).
|
|
|
|
Args:
|
|
image (numpy.ndarray): The SAR image (grayscale or BGR colorized).
|
|
"""
|
|
# DEBUG for start of display function
|
|
logging.debug(
|
|
f"{self.log_prefix} Received request to show SAR image (Shape: {getattr(image, 'shape', 'N/A')})."
|
|
)
|
|
|
|
# Validate input - ERROR for invalid input
|
|
if image is None or not isinstance(image, np.ndarray) or image.size == 0:
|
|
logging.error(
|
|
f"{self.log_prefix} show_sar_image received invalid image data. Aborting display."
|
|
)
|
|
return
|
|
if image.shape[0] == 0 or image.shape[1] == 0:
|
|
logging.error(
|
|
f"{self.log_prefix} show_sar_image received image with invalid dimensions: {image.shape}. Aborting."
|
|
)
|
|
return
|
|
# Basic check for supported types (grayscale or BGR)
|
|
if not (image.ndim == 2 or (image.ndim == 3 and image.shape[2] == 3)):
|
|
logging.error(
|
|
f"{self.log_prefix} show_sar_image received unsupported image format (ndim={image.ndim}, channels={image.shape[2] if image.ndim==3 else 'N/A'}). Expected Grayscale or BGR. Aborting."
|
|
)
|
|
return
|
|
|
|
try:
|
|
window_name = self.sar_window_name
|
|
# Create, move window, and set callback only once
|
|
if not self.sar_window_initialized:
|
|
# DEBUG for first-time setup
|
|
logging.debug(
|
|
f"{self.log_prefix} First call for '{window_name}'. Creating, positioning window, and setting callback."
|
|
)
|
|
cv2.imshow(window_name, image)
|
|
try:
|
|
cv2.moveWindow(window_name, self.sar_x, self.sar_y)
|
|
self.sar_window_initialized = True # Mark initialized
|
|
# INFO for successful window creation/move
|
|
logging.info(
|
|
f"{self.log_prefix} '{window_name}' window created and moved to ({self.sar_x}, {self.sar_y})."
|
|
)
|
|
|
|
# Set mouse callback immediately after successful initialization
|
|
if not self.sar_mouse_callback_set:
|
|
cv2.setMouseCallback(window_name, self.sar_mouse_callback)
|
|
self.sar_mouse_callback_set = True
|
|
# INFO for successful callback setup
|
|
logging.info(
|
|
f"{self.log_prefix} Mouse callback set for '{window_name}'."
|
|
)
|
|
|
|
cv2.waitKey(1) # Allow window to draw/position
|
|
except cv2.error as move_e:
|
|
# WARNING if move fails
|
|
logging.warning(
|
|
f"{self.log_prefix} Could not move '{window_name}' window on initialization: {move_e}."
|
|
)
|
|
self.sar_window_initialized = True # Assume imshow worked
|
|
# Still try setting callback even if move failed
|
|
if not self.sar_mouse_callback_set:
|
|
try:
|
|
cv2.setMouseCallback(window_name, self.sar_mouse_callback)
|
|
self.sar_mouse_callback_set = True
|
|
logging.info(
|
|
f"{self.log_prefix} Mouse callback set for '{window_name}' (after move failed)."
|
|
)
|
|
except cv2.error as cb_e:
|
|
# ERROR if callback setting fails
|
|
logging.error(
|
|
f"{self.log_prefix} Failed to set mouse callback for '{window_name}': {cb_e}"
|
|
)
|
|
else:
|
|
# Just update the image - DEBUG for subsequent updates
|
|
logging.debug(
|
|
f"{self.log_prefix} Updating existing SAR window '{window_name}'."
|
|
)
|
|
cv2.imshow(window_name, image)
|
|
|
|
# Essential waitKey - DEBUG for calling waitKey
|
|
# logging.debug(f"{self.log_prefix} Calling waitKey(1) for SAR window.") # Less verbose
|
|
cv2.waitKey(1)
|
|
|
|
except cv2.error as e:
|
|
# WARNING for recoverable closed window error
|
|
if "NULL window" in str(e) or "invalid window" in str(e):
|
|
logging.warning(
|
|
f"{self.log_prefix} OpenCV window '{self.sar_window_name}' seems closed. Will re-initialize on next valid image."
|
|
)
|
|
self.sar_window_initialized = False
|
|
self.sar_mouse_callback_set = False # Reset callback flag too
|
|
else:
|
|
# Keep EXCEPTION for other OpenCV errors
|
|
logging.exception(
|
|
f"{self.log_prefix} OpenCV error displaying SAR image: {e}"
|
|
)
|
|
except Exception as e:
|
|
# Keep EXCEPTION for unexpected errors
|
|
logging.exception(
|
|
f"{self.log_prefix} Unexpected error displaying SAR image: {e}"
|
|
)
|
|
|
|
# Method inside DisplayManager class in display.py
|
|
def sar_mouse_callback(self, event, x, y, flags, param):
|
|
"""
|
|
OpenCV mouse callback for the SAR window. Processes left mouse clicks,
|
|
clamps coordinates, and puts raw (x, y) onto the application's mouse_queue.
|
|
Updates only on click, removing the need for rate limiting.
|
|
"""
|
|
# --- MODIFIED: Process only left button down events ---
|
|
if event == cv2.EVENT_LBUTTONDOWN:
|
|
log_prefix_cb = f"{self.log_prefix} MouseClick"
|
|
logging.debug(
|
|
f"{log_prefix_cb} SAR Window Left Click detected at ({x}, {y})"
|
|
)
|
|
try:
|
|
current_display_width = self.app.state.sar_display_width
|
|
current_display_height = self.app.state.sar_display_height
|
|
if current_display_width <= 0 or current_display_height <= 0:
|
|
return
|
|
|
|
x_clamped = max(0, min(x, current_display_width - 1))
|
|
y_clamped = max(0, min(y, current_display_height - 1))
|
|
|
|
# --- Action 1: Queue raw coords for geo calculation (original queue) ---
|
|
logging.debug(
|
|
f"{log_prefix_cb} Putting clamped coords ({x_clamped}, {y_clamped}) onto mouse_queue for geo calc."
|
|
)
|
|
put_queue(
|
|
self.app.mouse_queue,
|
|
(x_clamped, y_clamped),
|
|
queue_name="mouse",
|
|
app_instance=self.app,
|
|
)
|
|
|
|
# --- >>> START OF NEW ACTION <<< ---
|
|
# --- Action 2: Queue pixel coords to update marker state (tkinter queue) ---
|
|
click_command = "SAR_CLICK_UPDATE"
|
|
click_payload = (x_clamped, y_clamped)
|
|
logging.debug(
|
|
f"{log_prefix_cb} Putting command '{click_command}' payload {click_payload} onto tkinter_queue for marker state."
|
|
)
|
|
put_queue(
|
|
queue_obj=self.app.tkinter_queue,
|
|
item=(click_command, click_payload),
|
|
queue_name="tkinter",
|
|
app_instance=self.app,
|
|
)
|
|
# --- >>> END OF NEW ACTION <<< ---
|
|
|
|
except Exception as e:
|
|
logging.exception(f"{log_prefix_cb} Error in SAR mouse callback: {e}")
|
|
|
|
def update_sar_position(self, x, y):
|
|
"""Updates the stored position and attempts to move the SAR window."""
|
|
# DEBUG for position update request
|
|
logging.debug(f"{self.log_prefix} Updating SAR window position to ({x}, {y}).")
|
|
self.sar_x = x
|
|
self.sar_y = y
|
|
if self.sar_window_initialized:
|
|
try:
|
|
cv2.moveWindow(self.sar_window_name, self.sar_x, self.sar_y)
|
|
# INFO for successful move
|
|
logging.info(
|
|
f"{self.log_prefix} Moved SAR window '{self.sar_window_name}' to ({x}, {y})"
|
|
)
|
|
except cv2.error as e:
|
|
# WARNING if move fails
|
|
logging.warning(
|
|
f"{self.log_prefix} Could not move SAR window '{self.sar_window_name}': {e}"
|
|
)
|
|
else:
|
|
logging.debug(
|
|
f"{self.log_prefix} SAR window not initialized, cannot move yet."
|
|
)
|
|
|
|
def update_mfd_position(self, x, y):
|
|
"""Updates the stored position and attempts to move the MFD window."""
|
|
# DEBUG for position update request
|
|
logging.debug(f"{self.log_prefix} Updating MFD window position to ({x}, {y}).")
|
|
self.mfd_x = x
|
|
self.mfd_y = y
|
|
if self.mfd_window_initialized:
|
|
try:
|
|
cv2.moveWindow(self.mfd_window_name, self.mfd_x, self.mfd_y)
|
|
# INFO for successful move
|
|
logging.info(
|
|
f"{self.log_prefix} Moved MFD window '{self.mfd_window_name}' to ({x}, {y})"
|
|
)
|
|
except cv2.error as e:
|
|
# WARNING if move fails
|
|
logging.warning(
|
|
f"{self.log_prefix} Could not move MFD window '{self.mfd_window_name}': {e}"
|
|
)
|
|
else:
|
|
logging.debug(
|
|
f"{self.log_prefix} MFD window not initialized, cannot move yet."
|
|
)
|
|
|
|
def destroy_windows(self):
|
|
"""Explicitly destroys the managed OpenCV windows during application shutdown."""
|
|
# INFO for start of significant cleanup action
|
|
logging.info(
|
|
f"{self.log_prefix} Attempting to destroy managed OpenCV windows (MFD, SAR)..."
|
|
)
|
|
destroyed_count = 0
|
|
|
|
# Attempt to destroy SAR window
|
|
# Check name existence as fallback if init flag is somehow false
|
|
if self.sar_window_initialized or hasattr(self, "sar_window_name"):
|
|
try:
|
|
# DEBUG for specific window destruction
|
|
logging.debug(
|
|
f"{self.log_prefix} Destroying OpenCV window: '{self.sar_window_name}'"
|
|
)
|
|
cv2.destroyWindow(self.sar_window_name)
|
|
destroyed_count += 1
|
|
self.sar_window_initialized = False # Reset flags
|
|
self.sar_mouse_callback_set = False
|
|
except cv2.error as e:
|
|
# WARNING for known error if window already closed
|
|
logging.warning(
|
|
f"{self.log_prefix} Ignoring OpenCV error destroying window '{self.sar_window_name}' (may already be closed): {e}"
|
|
)
|
|
except Exception as e:
|
|
# Keep EXCEPTION for unexpected errors
|
|
logging.exception(
|
|
f"{self.log_prefix} Unexpected error destroying window '{self.sar_window_name}': {e}"
|
|
)
|
|
|
|
# Attempt to destroy MFD window
|
|
if self.mfd_window_initialized or hasattr(self, "mfd_window_name"):
|
|
try:
|
|
# DEBUG for specific window destruction
|
|
logging.debug(
|
|
f"{self.log_prefix} Destroying OpenCV window: '{self.mfd_window_name}'"
|
|
)
|
|
cv2.destroyWindow(self.mfd_window_name)
|
|
destroyed_count += 1
|
|
self.mfd_window_initialized = False # Reset flag
|
|
except cv2.error as e:
|
|
# WARNING for known error
|
|
logging.warning(
|
|
f"{self.log_prefix} Ignoring OpenCV error destroying window '{self.mfd_window_name}' (may already be closed): {e}"
|
|
)
|
|
except Exception as e:
|
|
# Keep EXCEPTION for unexpected errors
|
|
logging.exception(
|
|
f"{self.log_prefix} Unexpected error destroying window '{self.mfd_window_name}': {e}"
|
|
)
|
|
|
|
# INFO for completion of cleanup action
|
|
logging.info(
|
|
f"{self.log_prefix} Finished destroying managed OpenCV windows (attempted {destroyed_count})."
|
|
)
|