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
723 lines
33 KiB
Python
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'
|
|
}
|