SXXXXXXX_ControlPanel/image_processing.py

425 lines
17 KiB
Python

# 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