425 lines
17 KiB
Python
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
|