Files
EricRollei-Eric_Image_Proce…/scripts/adaptive_frequency_decomposition.py
Eric Rollei d6dd6470cf Add complete Eric Image Processing Nodes implementation
Features:
- 40+ image processing algorithms
- Traditional denoising: Wavelet, NLM, BM3D (GPU-accelerated)
- AI models: DnCNN, SwinIR, SCUNet, NAFNet, Noise-DA
- DeepInv diffusion denoising integration
- Advanced film grain detection and processing
- Frequency domain processing
- Richardson-Lucy deconvolution
- Wiener filter
- External DeepInv service with FastAPI
- Comprehensive documentation and guides
2025-11-06 23:33:19 -08:00

723 lines
33 KiB
Python

"""
Adaptive Frequency Decomposition (AFD) Processor
Implementation of cutting-edge frequency domain enhancement from 2024-2025 research:
- Multi-scale frequency band decomposition with adaptive thresholds
- Color-aware frequency processing in perceptual spaces
- Selective frequency enhancement based on image content analysis
- Advanced wavelet and Fourier hybrid decomposition
- Perceptual frequency weighting and reconstruction
"""
import numpy as np
import cv2
from typing import Tuple, Dict, Any, Optional, List, Union
from scipy import ndimage
from scipy.fft import fft2, ifft2, fftshift, ifftshift
from skimage import color, restoration, feature
from skimage.util import img_as_float
import pywt
try:
from scipy.signal import butter, filtfilt
SCIPY_SIGNAL_AVAILABLE = True
except ImportError:
SCIPY_SIGNAL_AVAILABLE = False
class AdaptiveFrequencyDecompositionProcessor:
"""Adaptive Frequency Decomposition processor with color-aware multi-scale analysis"""
def __init__(self):
self.name = "Adaptive Frequency Decomposition Processor"
self.version = "1.0"
self.wavelet_cache = {}
def adaptive_frequency_decomposition(self, image: np.ndarray,
color_space: str = 'lab',
decomposition_method: str = 'hybrid',
frequency_bands: int = 6,
adaptive_thresholds: bool = True,
color_aware_processing: bool = True,
enhancement_strength: float = 1.0,
selective_enhancement: bool = True) -> Tuple[np.ndarray, Dict[str, Any]]:
"""
Adaptive Frequency Decomposition with color-aware multi-scale processing
FIXED: Scale-aware processing for real-world megapixel images
Args:
image: Input image (H, W) or (H, W, C) in range [0, 1]
color_space: Color space ('lab', 'oklab', 'jzazbz', 'hsv', 'rgb')
decomposition_method: Method ('wavelet', 'fourier', 'hybrid', 'adaptive')
frequency_bands: Number of frequency bands (3 to 8)
adaptive_thresholds: Use adaptive thresholds for each band
color_aware_processing: Process frequency bands differently per color channel
enhancement_strength: Overall enhancement strength (0.1 to 3.0)
selective_enhancement: Apply selective enhancement based on content analysis
Returns:
Tuple of (enhanced_image, processing_info)
"""
try:
# FIXED: Calculate scale factor based on image size
image_area = image.shape[0] * image.shape[1]
scale_factor = np.sqrt(image_area / (512 * 512)) # Normalize to 512x512 base
scale_factor = np.clip(scale_factor, 0.5, 4.0) # Reasonable bounds
# Convert to working color space
work_image, color_info = self._convert_to_colorspace(image, color_space)
# Analyze image characteristics for adaptive processing
image_analysis = self._analyze_image_characteristics(work_image, color_space)
# FIXED: Add scale information to analysis
image_analysis['scale_factor'] = scale_factor
image_analysis['image_size'] = f"{image.shape[0]}x{image.shape[1]}"
# Select optimal decomposition method if adaptive
if decomposition_method == 'adaptive':
decomposition_method = self._select_optimal_decomposition(image_analysis)
# Perform frequency decomposition with scale-aware parameters
frequency_bands_data = self._decompose_frequencies(
work_image, decomposition_method, frequency_bands, color_space, scale_factor
)
# Calculate adaptive thresholds if enabled
if adaptive_thresholds:
thresholds = self._calculate_adaptive_thresholds(
frequency_bands_data, image_analysis
)
else:
# FIXED: Scale-aware default thresholds
base_threshold = 0.05 / scale_factor # Smaller threshold for larger images
thresholds = [base_threshold * (i + 1) for i in range(frequency_bands)]
# Apply color-aware processing if enabled
if color_aware_processing and len(work_image.shape) == 3:
enhanced_bands = self._color_aware_frequency_processing(
frequency_bands_data, thresholds, enhancement_strength, color_space, scale_factor
)
else:
enhanced_bands = self._standard_frequency_processing(
frequency_bands_data, thresholds, enhancement_strength, scale_factor, color_space
)
# Apply selective enhancement if enabled
if selective_enhancement:
enhanced_bands = self._selective_frequency_enhancement(
enhanced_bands, image_analysis, work_image
)
# Reconstruct enhanced image
result = self._reconstruct_from_frequency_bands(
enhanced_bands, decomposition_method, work_image.shape
)
# Convert back to original color space
final_result = self._convert_from_colorspace(result, color_space, color_info)
# Calculate processing metrics
processing_info = self._calculate_processing_metrics(
image, final_result, frequency_bands_data, enhanced_bands, image_analysis
)
info = {
'method': 'Adaptive Frequency Decomposition',
'color_space': color_space,
'decomposition_method': decomposition_method,
'frequency_bands': frequency_bands,
'adaptive_thresholds': adaptive_thresholds,
'color_aware_processing': color_aware_processing,
'enhancement_strength': enhancement_strength,
'selective_enhancement': selective_enhancement,
'image_analysis': image_analysis,
'thresholds': thresholds,
'processing_metrics': processing_info
}
return final_result, info
except Exception as e:
return None, {'error': f"Adaptive frequency decomposition failed: {str(e)}"}
def _convert_to_colorspace(self, image: np.ndarray, color_space: str) -> Tuple[np.ndarray, Dict[str, Any]]:
"""Convert image to specified color space"""
if len(image.shape) == 2:
return image, {'grayscale': True}
color_info = {'grayscale': False, 'original_space': 'rgb'}
if color_space == 'lab':
work_image = color.rgb2lab(image)
work_image[:, :, 0] = work_image[:, :, 0] / 100.0 # Normalize L channel
color_info['lab_conversion'] = True
elif color_space == 'oklab':
# Approximate with LAB for now
work_image = color.rgb2lab(image)
work_image[:, :, 0] = work_image[:, :, 0] / 100.0
color_info['oklab_approximation'] = True
elif color_space == 'jzazbz':
# Approximate with LAB for now
work_image = color.rgb2lab(image)
work_image[:, :, 0] = work_image[:, :, 0] / 100.0
color_info['jzazbz_approximation'] = True
elif color_space == 'hsv':
work_image = color.rgb2hsv(image)
color_info['hsv_conversion'] = True
else: # RGB
work_image = image.copy()
color_info['rgb_passthrough'] = True
return work_image, color_info
def _convert_from_colorspace(self, image: np.ndarray, color_space: str,
color_info: Dict[str, Any]) -> np.ndarray:
"""Convert image back from specified color space"""
if color_info.get('grayscale', False):
return image
if color_space == 'lab' or color_info.get('oklab_approximation') or color_info.get('jzazbz_approximation'):
lab_image = image.copy()
lab_image[:, :, 0] = lab_image[:, :, 0] * 100.0 # Denormalize L channel
# FIXED: Ensure contiguous array after slice assignment before color conversion
lab_image = np.ascontiguousarray(lab_image)
result = color.lab2rgb(lab_image)
elif color_space == 'hsv':
result = color.hsv2rgb(image)
else: # RGB
result = image.copy()
return np.clip(result, 0, 1)
def _analyze_image_characteristics(self, image: np.ndarray, color_space: str) -> Dict[str, Any]:
"""Analyze image characteristics for adaptive processing"""
# Use luminance for analysis
if len(image.shape) == 3:
if color_space in ['lab', 'oklab', 'jzazbz']:
luminance = image[:, :, 0]
# FIXED: Ensure contiguous array after channel extraction
luminance = np.ascontiguousarray(luminance)
else:
luminance = color.rgb2gray(image)
else:
luminance = image
analysis = {}
try:
# Basic statistics
analysis['mean_luminance'] = float(np.mean(luminance))
analysis['std_luminance'] = float(np.std(luminance))
analysis['dynamic_range'] = float(np.max(luminance) - np.min(luminance))
# Frequency content analysis
fft_lum = np.abs(fft2(luminance))
fft_centered = fftshift(fft_lum)
# Calculate frequency energy distribution
h, w = fft_centered.shape
center_y, center_x = h // 2, w // 2
# Define frequency regions
low_freq_mask = self._create_frequency_mask(fft_centered.shape, center_y, center_x, 0.1)
mid_freq_mask = self._create_frequency_mask(fft_centered.shape, center_y, center_x, 0.4) & ~low_freq_mask
high_freq_mask = ~(low_freq_mask | mid_freq_mask)
total_energy = np.sum(fft_centered**2)
analysis['low_freq_energy'] = float(np.sum(fft_centered[low_freq_mask]**2) / total_energy)
analysis['mid_freq_energy'] = float(np.sum(fft_centered[mid_freq_mask]**2) / total_energy)
analysis['high_freq_energy'] = float(np.sum(fft_centered[high_freq_mask]**2) / total_energy)
# Texture analysis
try:
edges = feature.canny(luminance, sigma=1.0)
analysis['edge_density'] = float(np.mean(edges))
# Local variance (texture measure)
local_variance = ndimage.generic_filter(luminance, np.var, size=5)
analysis['texture_level'] = float(np.mean(local_variance))
analysis['texture_variation'] = float(np.std(local_variance))
except:
analysis['edge_density'] = 0.0
analysis['texture_level'] = 0.0
analysis['texture_variation'] = 0.0
# Color distribution analysis (if color image)
if len(image.shape) == 3:
try:
if color_space in ['lab', 'oklab', 'jzazbz']:
a_channel = image[:, :, 1]
b_channel = image[:, :, 2]
analysis['color_variance_a'] = float(np.var(a_channel))
analysis['color_variance_b'] = float(np.var(b_channel))
analysis['color_saturation'] = float(np.sqrt(a_channel**2 + b_channel**2).mean())
else:
# For RGB/HSV, calculate general color variance
analysis['color_variance_1'] = float(np.var(image[:, :, 1]))
analysis['color_variance_2'] = float(np.var(image[:, :, 2]))
analysis['color_range'] = float(np.std(image, axis=2).mean())
except:
analysis['color_variance_a'] = 0.0
analysis['color_variance_b'] = 0.0
analysis['color_saturation'] = 0.0
except Exception as e:
analysis['analysis_error'] = str(e)
# Fallback values
analysis.update({
'mean_luminance': 0.5,
'std_luminance': 0.2,
'dynamic_range': 1.0,
'low_freq_energy': 0.5,
'mid_freq_energy': 0.3,
'high_freq_energy': 0.2,
'edge_density': 0.1,
'texture_level': 0.1
})
return analysis
def _create_frequency_mask(self, shape: Tuple[int, int], center_y: int, center_x: int,
radius_fraction: float) -> np.ndarray:
"""Create circular frequency mask"""
h, w = shape
y, x = np.ogrid[:h, :w]
radius = min(h, w) * radius_fraction / 2
mask = (x - center_x)**2 + (y - center_y)**2 <= radius**2
return mask
def _select_optimal_decomposition(self, analysis: Dict[str, Any]) -> str:
"""Select optimal decomposition method based on image characteristics"""
high_freq_energy = analysis.get('high_freq_energy', 0.2)
texture_level = analysis.get('texture_level', 0.1)
edge_density = analysis.get('edge_density', 0.1)
if high_freq_energy > 0.3 and texture_level > 0.15:
return 'wavelet' # Good for textured images
elif edge_density > 0.15:
return 'hybrid' # Good for images with strong edges
else:
return 'fourier' # Good for smooth images
def _decompose_frequencies(self, image: np.ndarray, method: str,
num_bands: int, color_space: str, scale_factor: float = 1.0) -> List[np.ndarray]:
"""Decompose image into frequency bands with scale-aware parameters"""
if method == 'wavelet':
return self._wavelet_decomposition(image, num_bands, scale_factor)
elif method == 'fourier':
return self._fourier_decomposition(image, num_bands, scale_factor)
elif method == 'hybrid':
return self._hybrid_decomposition(image, num_bands, scale_factor)
else:
# Default to wavelet
return self._wavelet_decomposition(image, num_bands, scale_factor)
def _wavelet_decomposition(self, image: np.ndarray, num_bands: int, scale_factor: float = 1.0) -> List[np.ndarray]:
"""Wavelet-based frequency decomposition"""
bands = []
# Process each channel separately if color image
if len(image.shape) == 3:
for c in range(image.shape[2]):
channel_bands = self._wavelet_decompose_channel(image[:, :, c], num_bands)
if c == 0:
bands = [np.zeros((*band.shape, image.shape[2])) for band in channel_bands]
for i, band in enumerate(channel_bands):
bands[i][:, :, c] = band
else:
bands = self._wavelet_decompose_channel(image, num_bands)
return bands
def _wavelet_decompose_channel(self, channel: np.ndarray, num_bands: int) -> List[np.ndarray]:
"""Decompose single channel using wavelets"""
# Use Daubechies wavelet
wavelet = 'db8'
# Perform multi-level decomposition
coeffs = pywt.wavedec2(channel, wavelet, level=min(num_bands-1, 6))
# Convert coefficients to frequency bands
bands = []
# Approximation coefficients (lowest frequency)
approx = coeffs[0]
# Resize to original size
bands.append(cv2.resize(approx, (channel.shape[1], channel.shape[0]), interpolation=cv2.INTER_CUBIC))
# Detail coefficients (higher frequencies)
for i, (cH, cV, cD) in enumerate(coeffs[1:]):
# Combine horizontal, vertical, and diagonal details
detail_combined = np.sqrt(cH**2 + cV**2 + cD**2)
# Resize to original size
detail_resized = cv2.resize(detail_combined, (channel.shape[1], channel.shape[0]),
interpolation=cv2.INTER_CUBIC)
bands.append(detail_resized)
# If we need more bands, create additional bands through Gaussian pyramid
while len(bands) < num_bands:
# Create additional band by further decomposing the highest frequency band
last_band = bands[-1]
gaussian_blur = ndimage.gaussian_filter(last_band, sigma=0.5)
high_freq = last_band - gaussian_blur
bands.append(high_freq)
return bands[:num_bands]
def _fourier_decomposition(self, image: np.ndarray, num_bands: int, scale_factor: float = 1.0) -> List[np.ndarray]:
"""Fourier-based frequency decomposition"""
bands = []
# Process each channel separately if color image
if len(image.shape) == 3:
for c in range(image.shape[2]):
channel_bands = self._fourier_decompose_channel(image[:, :, c], num_bands)
if c == 0:
bands = [np.zeros((*band.shape, image.shape[2])) for band in channel_bands]
for i, band in enumerate(channel_bands):
bands[i][:, :, c] = band
else:
bands = self._fourier_decompose_channel(image, num_bands)
return bands
def _fourier_decompose_channel(self, channel: np.ndarray, num_bands: int) -> List[np.ndarray]:
"""Decompose single channel using Fourier transform"""
# Take FFT
fft_channel = fft2(channel)
fft_shifted = fftshift(fft_channel)
h, w = channel.shape
center_y, center_x = h // 2, w // 2
bands = []
# Create frequency bands with increasing radii
for i in range(num_bands):
if i == 0:
# Lowest frequencies
radius_fraction = 0.1
mask = self._create_frequency_mask(channel.shape, center_y, center_x, radius_fraction)
elif i == num_bands - 1:
# Highest frequencies (everything else)
if num_bands > 2:
prev_radius = 0.1 + (i-1) * (0.5 - 0.1) / (num_bands - 2)
else:
prev_radius = 0.1 # For num_bands = 2, just use the base radius
mask = ~self._create_frequency_mask(channel.shape, center_y, center_x, prev_radius)
else:
# Intermediate frequencies - avoid division by zero
if num_bands > 1:
radius_fraction = 0.1 + i * (0.5 - 0.1) / (num_bands - 1)
prev_radius = 0.1 + (i-1) * (0.5 - 0.1) / (num_bands - 1)
else:
radius_fraction = 0.3 # Default for single band
prev_radius = 0.1
outer_mask = self._create_frequency_mask(channel.shape, center_y, center_x, radius_fraction)
inner_mask = self._create_frequency_mask(channel.shape, center_y, center_x, prev_radius)
mask = outer_mask & ~inner_mask
# Apply mask and inverse FFT
filtered_fft = fft_shifted * mask
filtered_channel = np.real(ifft2(ifftshift(filtered_fft)))
bands.append(filtered_channel)
return bands
def _hybrid_decomposition(self, image: np.ndarray, num_bands: int, scale_factor: float = 1.0) -> List[np.ndarray]:
"""Hybrid wavelet-Fourier decomposition"""
# Use wavelet for first few bands (good for texture)
wavelet_bands = min(3, num_bands // 2)
fourier_bands = num_bands - wavelet_bands
# Get wavelet bands
wav_bands = self._wavelet_decomposition(image, wavelet_bands, scale_factor)
# Get Fourier bands from residual
if fourier_bands > 0:
# Use the highest frequency wavelet band as input for Fourier decomposition
if len(image.shape) == 3:
residual = wav_bands[-1]
else:
residual = wav_bands[-1]
four_bands = self._fourier_decomposition(residual, fourier_bands, scale_factor)
# Combine bands (replace last wavelet band with Fourier bands)
combined_bands = wav_bands[:-1] + four_bands
else:
combined_bands = wav_bands
return combined_bands[:num_bands]
def _calculate_adaptive_thresholds(self, frequency_bands: List[np.ndarray],
analysis: Dict[str, Any]) -> List[float]:
"""Calculate adaptive thresholds for each frequency band"""
thresholds = []
# Base threshold depends on image characteristics
base_threshold = 0.1
# Adjust based on noise level (estimated from high frequency content)
high_freq_energy = analysis.get('high_freq_energy', 0.2)
if high_freq_energy > 0.3:
base_threshold *= 1.5 # More aggressive for noisy images
elif high_freq_energy < 0.1:
base_threshold *= 0.7 # Gentler for clean images
for i, band in enumerate(frequency_bands):
# Calculate band-specific statistics
band_std = np.std(band)
band_mean = np.abs(np.mean(band))
# Adaptive threshold based on band characteristics
if i == 0: # Lowest frequency band
threshold = base_threshold * 0.5 # More conservative
elif i == len(frequency_bands) - 1: # Highest frequency band
threshold = base_threshold * 2.0 # More aggressive
else: # Intermediate bands
threshold = base_threshold * (1.0 + 0.3 * i / len(frequency_bands))
# Adjust based on band statistics
if band_std > 0.2: # High variation band
threshold *= 1.2
elif band_std < 0.05: # Low variation band
threshold *= 0.8
thresholds.append(float(np.clip(threshold, 0.01, 0.5)))
return thresholds
def _color_aware_frequency_processing(self, frequency_bands: List[np.ndarray],
thresholds: List[float], strength: float,
color_space: str, scale_factor: float = 1.0) -> List[np.ndarray]:
"""Apply color-aware frequency processing with scale awareness"""
enhanced_bands = []
# FIXED: Scale-aware strength adjustment
base_strength = strength * scale_factor * 0.5 # More conservative for larger images
for i, band in enumerate(frequency_bands):
if len(band.shape) == 3: # Color image
enhanced_band = np.zeros_like(band)
# Different processing for different channels based on color space
for c in range(band.shape[2]):
if color_space in ['lab', 'oklab', 'jzazbz']:
if c == 0: # Luminance channel
channel_strength = base_strength # Full strength for luminance
else: # Color channels - DON'T PROCESS AT ALL
channel_strength = 0.0 # No processing for color channels
elif color_space == 'hsv':
if c == 2: # Value channel
channel_strength = base_strength
else: # Hue and saturation
channel_strength = base_strength * 0.2 # More conservative
else: # RGB
channel_strength = base_strength # Equal for all channels
enhanced_band[:, :, c] = self._enhance_frequency_band(
band[:, :, c], thresholds[i], channel_strength
)
enhanced_bands.append(enhanced_band)
else: # Grayscale
enhanced_band = self._enhance_frequency_band(band, thresholds[i], strength)
enhanced_bands.append(enhanced_band)
return enhanced_bands
def _standard_frequency_processing(self, frequency_bands: List[np.ndarray],
thresholds: List[float], strength: float,
scale_factor: float = 1.0, color_space: str = 'rgb') -> List[np.ndarray]:
"""Apply standard frequency processing with scale-aware enhancement"""
enhanced_bands = []
# FIXED: Scale-aware strength adjustment
adjusted_strength = strength * scale_factor * 0.5 # More conservative for larger images
for i, band in enumerate(frequency_bands):
if len(band.shape) == 3: # Color image
enhanced_band = np.zeros_like(band)
for c in range(band.shape[2]):
# Apply different strength based on color space even in standard processing
if color_space in ['lab', 'oklab', 'jzazbz']:
if c == 0: # Luminance channel
channel_strength = adjusted_strength
else: # Color channels - DON'T PROCESS AT ALL
channel_strength = 0.0 # No processing for color channels
elif color_space == 'hsv':
if c == 2: # Value channel
channel_strength = adjusted_strength
else: # Hue and saturation
channel_strength = adjusted_strength * 0.2
else: # RGB
channel_strength = adjusted_strength
enhanced_band[:, :, c] = self._enhance_frequency_band(
band[:, :, c], thresholds[i], channel_strength
)
enhanced_bands.append(enhanced_band)
else: # Grayscale
enhanced_band = self._enhance_frequency_band(band, thresholds[i], adjusted_strength)
enhanced_bands.append(enhanced_band)
return enhanced_bands
def _enhance_frequency_band(self, band: np.ndarray, threshold: float, strength: float) -> np.ndarray:
"""Enhance individual frequency band"""
# If strength is 0, return original band unchanged
if strength == 0.0:
return band.copy()
# Apply threshold
mask = np.abs(band) > threshold
# Selective enhancement
enhanced = band.copy()
enhanced[mask] = enhanced[mask] * (1.0 + strength)
# Soft clipping to prevent artifacts
enhanced = np.tanh(enhanced / 0.5) * 0.5
return enhanced
def _selective_frequency_enhancement(self, enhanced_bands: List[np.ndarray],
analysis: Dict[str, Any],
original_image: np.ndarray) -> List[np.ndarray]:
"""Apply selective enhancement based on image content"""
# Create enhancement map based on local image characteristics
if len(original_image.shape) == 3:
luminance = original_image[:, :, 0] # Assume first channel is luminance
else:
luminance = original_image
# Calculate local enhancement factors
local_contrast = ndimage.generic_filter(luminance, np.std, size=5)
local_edges = ndimage.generic_filter(luminance, lambda x: np.sum(np.abs(np.diff(x))), size=3)
# Normalize factors
contrast_norm = (local_contrast - local_contrast.min()) / (local_contrast.max() - local_contrast.min() + 1e-10)
edges_norm = (local_edges - local_edges.min()) / (local_edges.max() - local_edges.min() + 1e-10)
# Combine into enhancement map
enhancement_map = 0.6 * contrast_norm + 0.4 * edges_norm
# Apply selective enhancement
selective_bands = []
for i, band in enumerate(enhanced_bands):
# Different weights for different frequency bands
if i == 0: # Low frequency
weight = 0.3
elif i == len(enhanced_bands) - 1: # High frequency
weight = 1.0
else: # Mid frequency
weight = 0.5 + 0.5 * i / len(enhanced_bands)
if len(band.shape) == 3:
selective_band = np.zeros_like(band)
for c in range(band.shape[2]):
# Blend original and enhanced based on enhancement map
original_band = enhanced_bands[i][:, :, c]
enhanced_part = band[:, :, c]
blend_factor = enhancement_map * weight
selective_band[:, :, c] = original_band + blend_factor * (enhanced_part - original_band)
else:
# Blend original and enhanced based on enhancement map
original_band = enhanced_bands[i]
enhanced_part = band
blend_factor = enhancement_map * weight
selective_band = original_band + blend_factor * (enhanced_part - original_band)
selective_bands.append(selective_band)
return selective_bands
def _reconstruct_from_frequency_bands(self, enhanced_bands: List[np.ndarray],
method: str, original_shape: Tuple) -> np.ndarray:
"""Reconstruct image from enhanced frequency bands"""
# Simple reconstruction: sum all bands
if enhanced_bands:
result = enhanced_bands[0].copy()
for band in enhanced_bands[1:]:
result = result + band
# FIXED: Proper clamping instead of destructive normalization
# Only clamp values outside [0, 1] range, preserving brightness relationships
result = np.clip(result, 0, 1)
return result
else:
# Fallback: return zeros
return np.zeros(original_shape)
def _calculate_processing_metrics(self, original: np.ndarray, enhanced: np.ndarray,
original_bands: List[np.ndarray], enhanced_bands: List[np.ndarray],
analysis: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate processing metrics"""
try:
# Convert to grayscale for metrics if needed
if len(original.shape) == 3:
orig_gray = color.rgb2gray(original)
enh_gray = color.rgb2gray(enhanced)
else:
orig_gray = original
enh_gray = enhanced
# Frequency content changes
orig_fft = np.abs(fft2(orig_gray))
enh_fft = np.abs(fft2(enh_gray))
freq_energy_change = np.sum(enh_fft**2) / (np.sum(orig_fft**2) + 1e-10)
# Contrast improvement
orig_contrast = np.std(orig_gray)
enh_contrast = np.std(enh_gray)
contrast_improvement = enh_contrast / (orig_contrast + 1e-10)
# Detail enhancement
orig_laplacian = np.abs(ndimage.laplace(orig_gray))
enh_laplacian = np.abs(ndimage.laplace(enh_gray))
detail_enhancement = np.mean(enh_laplacian) / (np.mean(orig_laplacian) + 1e-10)
return {
'frequency_energy_change': float(freq_energy_change),
'contrast_improvement': float(contrast_improvement),
'detail_enhancement': float(detail_enhancement),
'num_bands_processed': len(enhanced_bands),
'original_contrast': float(orig_contrast),
'enhanced_contrast': float(enh_contrast)
}
except Exception as e:
return {'metrics_error': str(e)}
def process_image(self, image: np.ndarray, method: str = 'adaptive', **kwargs) -> Tuple[Optional[np.ndarray], Dict[str, Any]]:
"""
Main processing method
Args:
image: Input image (H, W) or (H, W, C) in range [0, 1]
method: Processing method (only 'adaptive' for now)
**kwargs: Method-specific parameters
Returns:
Tuple of (processed_image, processing_info)
"""
try:
return self.adaptive_frequency_decomposition(image, **kwargs)
except Exception as e:
return None, {'error': f"Adaptive frequency decomposition processing failed: {str(e)}"}
def get_method_info(self) -> Dict[str, str]:
"""Get information about available methods"""
return {
'adaptive_frequency_decomposition': 'Adaptive Frequency Decomposition with color-aware multi-scale processing'
}