# --- START OF FILE image_processing.py --- # image_processing.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. Provides utility functions for loading, processing, and manipulating images using OpenCV and NumPy. Includes normalization, resizing, brightness/contrast adjustment, and color palette application. Uses standardized logging prefixes. """ # Standard library imports import os import logging # Third-party imports import cv2 import numpy as np # Local application imports import config # Import config for constants if needed (e.g., placeholder sizes) def load_image(path, expected_dtype): """ Loads an image from the specified path with error handling and type conversion. Uses placeholders if loading fails. Args: path (str): The file path to the image. expected_dtype (numpy.dtype): The desired NumPy data type for the output image. Returns: numpy.ndarray: The loaded (and potentially converted) image, or a placeholder on error. """ log_prefix = "[ImageProcessing Load]" # Specific prefix for this function logging.debug(f"{log_prefix} Attempting to load image from: {path}") # Placeholder creation function (internal helper) def _create_placeholder(dtype): if dtype == np.uint8: # Assuming MFD placeholder dimensions if uint8 needed return np.zeros((config.MFD_HEIGHT, config.MFD_WIDTH), dtype=np.uint8) else: # Assuming SAR placeholder dimensions otherwise return np.zeros( (config.SAR_HEIGHT, config.SAR_WIDTH), dtype=config.SAR_DATA_TYPE ) # Check if file exists if not os.path.exists(path): # Keep ERROR level as file not found is a significant issue if expected logging.error( f"{log_prefix} Image file not found at {path}. Using placeholder." ) return _create_placeholder(expected_dtype) try: # Load image using OpenCV, preserving original depth img = cv2.imread(path, cv2.IMREAD_ANYDEPTH) if img is None: # Keep ERROR level for load failure logging.error( f"{log_prefix} OpenCV could not load image at {path}. Using placeholder." ) return _create_placeholder(expected_dtype) # Log success and original type at DEBUG level logging.debug( f"{log_prefix} Image loaded successfully from {path}. Original dtype: {img.dtype}" ) # Check and convert dtype if necessary if img.dtype != expected_dtype: # Use WARNING level for implicit type conversion logging.warning( f"{log_prefix} Converting image from {img.dtype} to {expected_dtype}." ) try: img = img.astype(expected_dtype) logging.debug(f"{log_prefix} Image dtype conversion successful.") except Exception as e: # Keep ERROR if conversion fails logging.error( f"{log_prefix} Failed to convert image dtype: {e}. Returning original or placeholder." ) # Return original image if conversion fails but load succeeded, # otherwise fallback to placeholder if original img was somehow bad return img if img is not None else _create_placeholder(expected_dtype) return img except Exception as e: # Keep EXCEPTION for unexpected errors during loading/processing logging.exception( f"{log_prefix} Unexpected error loading image from {path}: {e}" ) return _create_placeholder(expected_dtype) def create_brightness_contrast_lut(brightness=0, contrast=1.0): """ Creates a Look-Up Table (LUT) for adjusting brightness and contrast. Args: brightness (int): Amount to add to each pixel (-255 to 255). contrast (float): Factor to multiply each pixel by (> 0.0). Returns: numpy.ndarray: A 256-element uint8 LUT. """ log_prefix = "[ImageProcessing LUT]" # Specific prefix # DEBUG level for LUT creation details logging.debug( f"{log_prefix} Creating B/C LUT with Brightness={brightness}, Contrast={contrast:.2f}" ) # Ensure contrast is slightly positive to avoid issues contrast = max(0.01, contrast) try: # Use vectorized operation for potentially better performance lut_values = np.arange(256) # 0 to 255 adjusted_values = (lut_values * contrast) + brightness # Clip values to 0-255 range and convert to uint8 lut = np.clip(np.round(adjusted_values), 0, 255).astype(np.uint8) logging.debug(f"{log_prefix} B/C LUT created successfully.") return lut except Exception as e: # Keep EXCEPTION for errors during LUT calculation logging.exception(f"{log_prefix} Error creating B/C LUT:") # Keep ERROR for fallback action logging.error(f"{log_prefix} Returning identity LUT as fallback.") return np.arange(256, dtype=np.uint8) # Return identity LUT on error def apply_brightness_contrast(image, brightness=0, contrast=1.0): """ Applies brightness and contrast adjustment to a uint8 image using a LUT. Args: image (numpy.ndarray): The input uint8 image. brightness (int): Brightness adjustment value. contrast (float): Contrast adjustment value. Returns: numpy.ndarray: The adjusted uint8 image, or original on error. """ log_prefix = "[ImageProcessing B/C Apply]" # Specific prefix # DEBUG for function entry logging.debug( f"{log_prefix} Applying B/C adjustment (B={brightness}, C={contrast:.2f})" ) # Validate input type - ERROR if invalid type provided if image.dtype != np.uint8: logging.error( f"{log_prefix} apply_brightness_contrast requires uint8 input, got {image.dtype}. Returning original." ) return image try: # Create the LUT - uses its own logging brightness_contrast_lut = create_brightness_contrast_lut(brightness, contrast) # Apply the LUT using OpenCV logging.debug(f"{log_prefix} Applying precomputed LUT to image...") adjusted_image = cv2.LUT(image, brightness_contrast_lut) logging.debug(f"{log_prefix} B/C adjustment applied successfully.") return adjusted_image except cv2.error as e: # Keep EXCEPTION for OpenCV errors logging.exception(f"{log_prefix} OpenCV error applying B/C LUT: {e}") return image # Return original on error except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception(f"{log_prefix} Unexpected error applying B/C adjustment: {e}") return image # Return original on error def apply_color_palette(image, palette): """ Applies a named OpenCV colormap to a grayscale image. Args: image (numpy.ndarray): The input grayscale image (uint8). If BGR, attempts conversion. palette (str): The name of the OpenCV colormap (e.g., "JET", "HOT"). Case-insensitive. Returns: numpy.ndarray: The colorized BGR image, or a BGR version of the input on error. """ log_prefix = "[ImageProcessing Palette]" # Specific prefix # DEBUG for function entry logging.debug(f"{log_prefix} Applying color palette '{palette}'") # Validate input type - Allow uint8 only, attempt conversion if needed if image.dtype != np.uint8: # ERROR for unsupported input type logging.error( f"{log_prefix} apply_color_palette requires uint8 input, got {image.dtype}. Cannot apply palette." ) # Try converting to BGR if grayscale-like, otherwise return original if image.ndim == 2: try: return cv2.cvtColor( image.astype(np.uint8), cv2.COLOR_GRAY2BGR ) # Attempt recovery except: return image else: return image # Ensure input is grayscale for colormap application processed_image = image if image.ndim > 2: # WARNING if input is already color, attempting grayscale conversion logging.warning( f"{log_prefix} Input image has multiple channels (shape={image.shape}). Converting to grayscale before applying palette." ) try: processed_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) logging.debug(f"{log_prefix} Converted input image to grayscale.") except cv2.error as e: # ERROR if conversion fails logging.error( f"{log_prefix} Failed to convert input image to grayscale: {e}. Cannot apply palette." ) return ( cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) if image.ndim == 2 else image ) # Return BGR/Original # Apply the colormap try: colormap_name = f"COLORMAP_{palette.upper()}" colormap = getattr(cv2, colormap_name) # DEBUG for applying map logging.debug(f"{log_prefix} Applying OpenCV colormap: {colormap_name}") # Use optimized/vectorized approach unless disabled for debugging if config.DISABLE_COLORMAP_OPTIMIZATION: # WARNING if using non-optimized path logging.warning( f"{log_prefix} Using non-vectorized colormap application (debug option enabled)." ) colorized_image = np.zeros( (processed_image.shape[0], processed_image.shape[1], 3), dtype=np.uint8 ) # This loop is slow, avoid in production for i in range(processed_image.shape[0]): for j in range(processed_image.shape[1]): pixel_value = np.array([[processed_image[i, j]]], dtype=np.uint8) colored_pixel = cv2.applyColorMap(pixel_value, colormap) colorized_image[i, j, :] = colored_pixel[0, 0, :] else: # Standard vectorized approach colorized_image = cv2.applyColorMap(processed_image, colormap) logging.debug(f"{log_prefix} Colormap '{palette}' applied successfully.") return colorized_image except AttributeError: # ERROR if the specified palette doesn't exist in OpenCV logging.error( f"{log_prefix} OpenCV Colormap '{palette}' not found. Returning BGR grayscale." ) return cv2.cvtColor( processed_image, cv2.COLOR_GRAY2BGR ) # Return BGR grayscale fallback except cv2.error as e: # Keep EXCEPTION for OpenCV errors during applyColorMap logging.exception(f"{log_prefix} OpenCV error applying colormap: {e}") return cv2.cvtColor(processed_image, cv2.COLOR_GRAY2BGR) # Fallback except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception(f"{log_prefix} Unexpected error applying colormap: {e}") return cv2.cvtColor(processed_image, cv2.COLOR_GRAY2BGR) # Fallback def normalize_image(image, target_min=0, target_max=255, target_type=np.uint8): """ Normalizes the image pixel values to a specified range and data type using OpenCV. Args: image (numpy.ndarray): The input image. Can be any NumPy type OpenCV supports. target_min (int): The minimum value of the target range (default: 0). target_max (int): The maximum value of the target range (default: 255). target_type (numpy.dtype): The target NumPy data type (e.g., np.uint8). Returns: numpy.ndarray: The normalized image, or original image on error. """ log_prefix = "[ImageProcessing Normalize]" # Specific prefix # DEBUG for function entry and parameters logging.debug( f"{log_prefix} Normalizing image (shape={getattr(image, 'shape', 'N/A')}, dtype={getattr(image, 'dtype', 'N/A')}) " f"to range [{target_min}, {target_max}], target type: {target_type.__name__}" ) if image is None or image.size == 0: logging.error( f"{log_prefix} Cannot normalize None or empty image. Returning None." ) return None try: # Determine the OpenCV dtype constant based on the target NumPy type if target_type == np.uint8: cv_dtype = cv2.CV_8U elif target_type == np.uint16: cv_dtype = cv2.CV_16U elif target_type == np.int16: cv_dtype = cv2.CV_16S elif target_type == np.int32: cv_dtype = cv2.CV_32S elif target_type == np.float32: cv_dtype = cv2.CV_32F elif target_type == np.float64: cv_dtype = cv2.CV_64F else: # Use -1 to keep the original depth if target type is not explicitly handled # WARNING if using fallback depth logging.warning( f"{log_prefix} Unsupported target_type {target_type} for explicit OpenCV dtype. " f"Using source depth (-1) during normalization." ) cv_dtype = -1 # Keep source depth # Perform normalization using OpenCV logging.debug( f"{log_prefix} Calling cv2.normalize with alpha={target_min}, beta={target_max}, dtype={cv_dtype}..." ) normalized_img = cv2.normalize( src=image, dst=None, # Create new destination array alpha=target_min, beta=target_max, norm_type=cv2.NORM_MINMAX, dtype=cv_dtype, # Target OpenCV depth, or -1 for same as source ) logging.debug( f"{log_prefix} cv2.normalize completed. Output dtype: {normalized_img.dtype}" ) # Ensure the final NumPy dtype matches the requested target_type exactly if normalized_img.dtype != target_type: # DEBUG if casting is needed after normalization logging.debug( f"{log_prefix} Casting normalized image from {normalized_img.dtype} to required {target_type.__name__}..." ) try: normalized_img = normalized_img.astype(target_type) logging.debug(f"{log_prefix} Casting successful.") except Exception as astype_e: # ERROR if final casting fails logging.error( f"{log_prefix} Failed to cast normalized image to target type {target_type}: {astype_e}. Returning original." ) return image # Return original on casting error logging.debug( f"{log_prefix} Normalization successful. Final shape={normalized_img.shape}, dtype={normalized_img.dtype}" ) return normalized_img except cv2.error as e: # Keep EXCEPTION for OpenCV errors logging.exception(f"{log_prefix} OpenCV error during normalization: {e}") return image # Return original on OpenCV error except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception( f"{log_prefix} Unexpected error during image normalization: {e}" ) return image # Return original on other errors def resize_image(image, width, height, interpolation=cv2.INTER_LINEAR): """ Resizes the image to the specified width and height using OpenCV. Args: image (numpy.ndarray): The input image. width (int): The target width. height (int): The target height. interpolation (int): OpenCV interpolation flag (default: cv2.INTER_LINEAR). Returns: numpy.ndarray: The resized image, or original image on error. """ log_prefix = "[ImageProcessing Resize]" # Specific prefix # DEBUG for function entry and parameters logging.debug( f"{log_prefix} Resizing image (shape={getattr(image, 'shape', 'N/A')}) to {width}x{height} using interpolation {interpolation}" ) # Validate target size - ERROR for invalid size if width <= 0 or height <= 0: logging.error( f"{log_prefix} Invalid target size: {width}x{height}. Returning original." ) return image # Validate input image - ERROR for invalid input if image is None or image.size == 0: logging.error( f"{log_prefix} Cannot resize None or empty image. Returning original." ) return image # Check if resize is necessary - DEBUG for skipping unnecessary resize if image.shape[1] == width and image.shape[0] == height: logging.debug(f"{log_prefix} Image is already target size. Skipping resize.") return image try: # Perform resize using OpenCV logging.debug(f"{log_prefix} Calling cv2.resize...") resized_img = cv2.resize(image, (width, height), interpolation=interpolation) logging.debug(f"{log_prefix} Resize successful. New shape: {resized_img.shape}") return resized_img except cv2.error as e: # Keep EXCEPTION for OpenCV errors logging.exception(f"{log_prefix} OpenCV error during resize: {e}") return image # Return original on error except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception(f"{log_prefix} Unexpected error during image resize: {e}") return image # Return original on error # --- END OF FILE image_processing.py ---