Files
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

523 lines
18 KiB
Python

"""
SFHformer Implementation
Based on 2024-2025 research findings for dual-domain image processing
SFHformer (Spatial-Frequency Hybrid Transformer) combines spatial and frequency
domain processing through dual-domain hybrid structure, achieving superior
results for restoration tasks.
Features:
- Dual-domain hybrid architecture
- FFT mechanisms integrated into Transformer
- Spatial and frequency domain feature extraction
- Cross-domain attention mechanisms
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from typing import Dict, Any, Optional, Tuple
import warnings
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SFHformerProcessor:
"""SFHformer processor for dual-domain image enhancement"""
def __init__(self, device: str = "auto"):
"""
Initialize SFHformer processor
Args:
device: Processing device (auto, cpu, cuda)
"""
self.device = self._get_device(device)
self.model = None
self.is_model_loaded = False
def _get_device(self, device: str) -> torch.device:
"""Get appropriate device for processing"""
if device == "auto":
if torch.cuda.is_available():
return torch.device("cuda")
else:
return torch.device("cpu")
return torch.device(device)
def _load_model(self, **kwargs):
"""Load or create SFHformer model"""
try:
# Try to load pre-trained model if available
# For now, we'll create a simplified implementation
config = {
'embed_dim': kwargs.get('embed_dim', 64),
'num_heads': kwargs.get('num_heads', 8),
'num_layers': kwargs.get('num_layers', 4),
'mlp_ratio': kwargs.get('mlp_ratio', 4),
'window_size': kwargs.get('window_size', 8)
}
self.model = SFHformerModel(**config).to(self.device)
self.model.eval()
self.is_model_loaded = True
logger.info(f"Created SFHformer model with config: {config}")
return True
except Exception as e:
logger.error(f"Failed to load SFHformer model: {e}")
return False
def process_image(self, image: np.ndarray, **kwargs) -> np.ndarray:
"""
Process image with SFHformer
Args:
image: Input image (H, W, C) in RGB format
**kwargs: Additional processing parameters
Returns:
Enhanced image
"""
if not self.is_model_loaded:
if not self._load_model(**kwargs):
return self._fallback_dual_domain_processing(image, **kwargs)
try:
# Prepare image tensor
if image.dtype != np.float32:
image = image.astype(np.float32)
if image.max() > 1.0:
image = image / 255.0
# Convert to tensor
img_tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0).to(self.device)
# Process with SFHformer
with torch.no_grad():
enhanced_tensor = self.model(img_tensor)
# Convert back to numpy
enhanced = enhanced_tensor.squeeze(0).permute(1, 2, 0).cpu().numpy()
enhanced = np.clip(enhanced, 0, 1)
return enhanced
except Exception as e:
logger.error(f"SFHformer processing failed: {e}")
return self._fallback_dual_domain_processing(image, **kwargs)
def _fallback_dual_domain_processing(self, image: np.ndarray, **kwargs) -> np.ndarray:
"""Fallback dual-domain processing when model is not available"""
try:
# Spatial domain processing
spatial_enhanced = self._spatial_domain_enhancement(image, **kwargs)
# Frequency domain processing
freq_enhanced = self._frequency_domain_enhancement(image, **kwargs)
# Combine spatial and frequency enhancements
alpha = kwargs.get('domain_blend', 0.5)
combined = alpha * spatial_enhanced + (1 - alpha) * freq_enhanced
return np.clip(combined, 0, 1)
except Exception as e:
logger.error(f"Fallback processing failed: {e}")
return image
def _spatial_domain_enhancement(self, image: np.ndarray, **kwargs) -> np.ndarray:
"""Spatial domain enhancement using traditional methods"""
import cv2
# Convert to uint8 for OpenCV operations
img_uint8 = (image * 255).astype(np.uint8)
# Apply spatial enhancements
enhanced = img_uint8.copy()
# Bilateral filtering for noise reduction
bilateral_d = kwargs.get('bilateral_d', 9)
bilateral_sigma = kwargs.get('bilateral_sigma', 75)
enhanced = cv2.bilateralFilter(enhanced, bilateral_d, bilateral_sigma, bilateral_sigma)
# Adaptive histogram equalization
if len(enhanced.shape) == 3:
lab = cv2.cvtColor(enhanced, cv2.COLOR_RGB2LAB)
l_channel = lab[:, :, 0]
# FIXED: Ensure contiguous array before CLAHE operation
l_channel = np.ascontiguousarray(l_channel)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
l_channel = clahe.apply(l_channel)
lab[:, :, 0] = l_channel
# FIXED: Ensure contiguous array after slice assignment before color conversion
lab = np.ascontiguousarray(lab)
enhanced = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)
else:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(enhanced)
# Unsharp masking
gaussian = cv2.GaussianBlur(enhanced, (0, 0), 1.0)
sharpening_strength = kwargs.get('sharpening_strength', 0.5)
enhanced = cv2.addWeighted(enhanced, 1 + sharpening_strength, gaussian, -sharpening_strength, 0)
return enhanced.astype(np.float32) / 255.0
def _frequency_domain_enhancement(self, image: np.ndarray, **kwargs) -> np.ndarray:
"""Frequency domain enhancement using FFT"""
enhanced_channels = []
# Process each channel separately
for c in range(image.shape[2]):
channel = image[:, :, c]
# FFT
fft = np.fft.fft2(channel)
fft_shifted = np.fft.fftshift(fft)
# Create frequency domain filter
rows, cols = channel.shape
crow, ccol = rows // 2, cols // 2
# High-pass filter for sharpening
high_pass_radius = kwargs.get('high_pass_radius', 30)
mask = np.ones((rows, cols), dtype=np.float32)
y, x = np.ogrid[:rows, :cols]
mask_area = (x - ccol)**2 + (y - crow)**2 <= high_pass_radius**2
mask[mask_area] = 0.2 # Reduce low frequencies
# Low-pass filter for noise reduction
low_pass_radius = kwargs.get('low_pass_radius', 100)
gaussian_mask = np.exp(-((x - ccol)**2 + (y - crow)**2) / (2 * low_pass_radius**2))
# Combine filters
filter_strength = kwargs.get('filter_strength', 0.5)
combined_mask = filter_strength * mask + (1 - filter_strength) * gaussian_mask
# Apply filter
fft_filtered = fft_shifted * combined_mask
# Inverse FFT
fft_ishifted = np.fft.ifftshift(fft_filtered)
enhanced_channel = np.fft.ifft2(fft_ishifted)
enhanced_channel = np.real(enhanced_channel)
enhanced_channels.append(enhanced_channel)
# Stack channels
enhanced = np.stack(enhanced_channels, axis=2)
return np.clip(enhanced, 0, 1)
def get_model_info(self) -> Dict[str, Any]:
"""Get model information"""
return {
"model_name": "SFHformer",
"architecture": "Spatial-Frequency Hybrid Transformer",
"device": str(self.device),
"model_loaded": self.is_model_loaded,
"description": "Dual-domain image processing with FFT-integrated Transformer",
"features": [
"Spatial domain feature extraction",
"Frequency domain processing",
"Cross-domain attention mechanisms",
"Hybrid architecture for superior restoration"
],
"use_cases": [
"Image restoration",
"Noise reduction",
"Detail enhancement",
"Artifact removal"
]
}
class SFHformerModel(nn.Module):
"""Simplified SFHformer model implementation"""
def __init__(self, embed_dim=64, num_heads=8, num_layers=4, mlp_ratio=4, window_size=8):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.num_layers = num_layers
self.window_size = window_size
# Input projection
self.input_proj = nn.Conv2d(3, embed_dim, kernel_size=3, padding=1)
# Spatial-Frequency Hybrid Transformer blocks
self.sf_blocks = nn.ModuleList([
SFHBlock(embed_dim, num_heads, mlp_ratio, window_size)
for _ in range(num_layers)
])
# Output projection
self.output_proj = nn.Conv2d(embed_dim, 3, kernel_size=3, padding=1)
# Skip connection
self.skip_conv = nn.Conv2d(3, 3, kernel_size=1)
def forward(self, x):
# Store input for skip connection
input_x = x
# Input projection
x = self.input_proj(x)
# Apply SF blocks
for block in self.sf_blocks:
x = block(x)
# Output projection
x = self.output_proj(x)
# Skip connection
x = x + self.skip_conv(input_x)
return x
class SFHBlock(nn.Module):
"""Spatial-Frequency Hybrid block"""
def __init__(self, embed_dim, num_heads, mlp_ratio, window_size):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.window_size = window_size
# Spatial branch
self.spatial_norm1 = nn.LayerNorm(embed_dim)
self.spatial_attn = WindowAttention(embed_dim, num_heads, window_size)
self.spatial_norm2 = nn.LayerNorm(embed_dim)
self.spatial_mlp = MLP(embed_dim, int(embed_dim * mlp_ratio))
# Frequency branch
self.freq_norm1 = nn.LayerNorm(embed_dim)
self.freq_attn = FrequencyAttention(embed_dim, num_heads)
self.freq_norm2 = nn.LayerNorm(embed_dim)
self.freq_mlp = MLP(embed_dim, int(embed_dim * mlp_ratio))
# Cross-domain fusion
self.fusion = nn.Conv2d(embed_dim * 2, embed_dim, kernel_size=1)
def forward(self, x):
B, C, H, W = x.shape
# Spatial branch
x_spatial = x.permute(0, 2, 3, 1).contiguous().view(B, H * W, C)
x_spatial = x_spatial + self.spatial_attn(self.spatial_norm1(x_spatial), H, W)
x_spatial = x_spatial + self.spatial_mlp(self.spatial_norm2(x_spatial))
x_spatial = x_spatial.view(B, H, W, C).permute(0, 3, 1, 2)
# Frequency branch
x_freq = x.permute(0, 2, 3, 1).contiguous().view(B, H * W, C)
x_freq = x_freq + self.freq_attn(self.freq_norm1(x_freq), H, W)
x_freq = x_freq + self.freq_mlp(self.freq_norm2(x_freq))
x_freq = x_freq.view(B, H, W, C).permute(0, 3, 1, 2)
# Fuse spatial and frequency features
x_fused = torch.cat([x_spatial, x_freq], dim=1)
x_fused = self.fusion(x_fused)
return x_fused
class WindowAttention(nn.Module):
"""Window-based spatial attention"""
def __init__(self, embed_dim, num_heads, window_size):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.window_size = window_size
self.head_dim = embed_dim // num_heads
self.qkv = nn.Linear(embed_dim, embed_dim * 3)
self.proj = nn.Linear(embed_dim, embed_dim)
def forward(self, x, H, W):
B, L, C = x.shape
# Generate QKV
qkv = self.qkv(x).reshape(B, L, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2]
# Compute attention
attn = (q @ k.transpose(-2, -1)) * (self.head_dim ** -0.5)
attn = F.softmax(attn, dim=-1)
# Apply attention
x = (attn @ v).transpose(1, 2).reshape(B, L, C)
x = self.proj(x)
return x
class FrequencyAttention(nn.Module):
"""Frequency domain attention using FFT"""
def __init__(self, embed_dim, num_heads):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.qkv = nn.Linear(embed_dim, embed_dim * 3)
self.proj = nn.Linear(embed_dim, embed_dim)
def forward(self, x, H, W):
B, L, C = x.shape
# Reshape to spatial
x_spatial = x.view(B, H, W, C).permute(0, 3, 1, 2)
# Apply FFT
x_fft = torch.fft.fft2(x_spatial, dim=(-2, -1))
x_fft_real = torch.real(x_fft)
x_fft_imag = torch.imag(x_fft)
# Combine real and imaginary parts
x_freq = torch.cat([x_fft_real, x_fft_imag], dim=1)
x_freq = x_freq.permute(0, 2, 3, 1).contiguous().view(B, H * W, -1)
# Apply attention in frequency domain
qkv = self.qkv(x_freq).reshape(B, L, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2]
attn = (q @ k.transpose(-2, -1)) * (q.shape[-1] ** -0.5)
attn = F.softmax(attn, dim=-1)
x_freq = (attn @ v).transpose(1, 2).reshape(B, L, -1)
x_freq = self.proj(x_freq)
return x_freq
class MLP(nn.Module):
"""Multi-layer perceptron"""
def __init__(self, embed_dim, hidden_dim):
super().__init__()
self.fc1 = nn.Linear(embed_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, embed_dim)
self.act = nn.GELU()
def forward(self, x):
x = self.fc1(x)
x = self.act(x)
x = self.fc2(x)
return x
def get_sfhformer_presets() -> Dict[str, Dict[str, Any]]:
"""Get available SFHformer presets"""
return {
"restoration": {
"embed_dim": 64,
"num_heads": 8,
"num_layers": 4,
"bilateral_d": 9,
"bilateral_sigma": 75,
"sharpening_strength": 0.3,
"domain_blend": 0.5,
"description": "Balanced restoration for general images"
},
"denoising": {
"embed_dim": 64,
"num_heads": 8,
"num_layers": 6,
"bilateral_d": 15,
"bilateral_sigma": 100,
"sharpening_strength": 0.1,
"domain_blend": 0.3,
"description": "Strong denoising with detail preservation"
},
"sharpening": {
"embed_dim": 64,
"num_heads": 8,
"num_layers": 3,
"bilateral_d": 5,
"bilateral_sigma": 50,
"sharpening_strength": 0.7,
"domain_blend": 0.7,
"description": "Detail enhancement and sharpening"
},
"high_quality": {
"embed_dim": 96,
"num_heads": 12,
"num_layers": 8,
"bilateral_d": 9,
"bilateral_sigma": 75,
"sharpening_strength": 0.4,
"domain_blend": 0.5,
"description": "High-quality processing (requires more VRAM)"
}
}
def process_with_sfhformer(image: np.ndarray,
preset: str = "restoration",
device: str = "auto",
**kwargs) -> Tuple[np.ndarray, Dict[str, Any]]:
"""
Process image with SFHformer
Args:
image: Input image (H, W, C) in RGB format
preset: Processing preset
device: Processing device
**kwargs: Additional parameters
Returns:
(enhanced_image, processing_info)
"""
# Get preset configuration
presets = get_sfhformer_presets()
if preset in presets:
config = presets[preset]
kwargs.update(config)
# Create processor
processor = SFHformerProcessor(device)
# Process image
enhanced = processor.process_image(image, **kwargs)
# Get model info
info = processor.get_model_info()
info.update({
'preset': preset,
'processing_parameters': kwargs,
'input_shape': image.shape,
'output_shape': enhanced.shape
})
return enhanced, info
# Example usage and testing
if __name__ == "__main__":
# Test with synthetic image
test_image = np.random.rand(256, 256, 3).astype(np.float32)
print("Testing SFHformer processing...")
for preset in ["restoration", "denoising", "sharpening"]:
try:
enhanced, info = process_with_sfhformer(test_image, preset=preset)
print(f"{preset}: {enhanced.shape}, model_loaded: {info['model_loaded']}")
except Exception as e:
print(f"Error with {preset}: {e}")