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

842 lines
36 KiB
Python

"""
Auto-Denoise Integration Script
Self-supervised and unsupervised CNN denoising methods implementation
Integrates state-of-the-art methods:
- Noise2Void: Single image self-supervised denoising
- Noise2Noise: Paired image denoising
- Deep Image Prior: Unsupervised restoration
"""
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from typing import Optional, Tuple, Dict, Any, Union
import warnings
from pathlib import Path
try:
import cv2
CV2_AVAILABLE = True
except ImportError:
CV2_AVAILABLE = False
warnings.warn("OpenCV not available, some features may be limited")
try:
from skimage.restoration import estimate_sigma
from skimage.metrics import peak_signal_noise_ratio, structural_similarity
SKIMAGE_AVAILABLE = True
except ImportError:
SKIMAGE_AVAILABLE = False
warnings.warn("scikit-image not available, some metrics unavailable")
class Noise2VoidProcessor:
"""
Noise2Void: Self-supervised denoising from single noisy images
Revolutionary approach that doesn't require clean reference images.
Works by training on blind spots in the same image.
Key advantages:
- Single image processing (no pairs needed)
- Works with any noise type
- Self-supervised learning
- No clean data required
"""
def __init__(self, device: str = "auto", model_size: str = "small"):
"""
Initialize Noise2Void processor
Args:
device: Processing device ('auto', 'cpu', 'cuda')
model_size: Model complexity ('small', 'medium', 'large')
"""
self.device = self._get_device(device)
self.model_size = model_size
self.model = None
self.trained = False
# Model architectures
self.architectures = {
'small': {'channels': [32, 64, 128], 'depth': 3},
'medium': {'channels': [64, 128, 256], 'depth': 4},
'large': {'channels': [64, 128, 256, 512], 'depth': 5}
}
def _get_device(self, device: str) -> torch.device:
"""Determine optimal processing device"""
if device == "auto":
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
return torch.device(device)
def _create_model(self, in_channels: int = 3) -> nn.Module:
"""Create Noise2Void U-Net architecture"""
arch = self.architectures[self.model_size]
class Noise2VoidUNet(nn.Module):
def __init__(self, in_channels, channels, depth):
super().__init__()
self.depth = depth
self.encoders = nn.ModuleList()
self.decoders = nn.ModuleList()
self.pools = nn.ModuleList()
# Encoder path
prev_ch = in_channels
for i, ch in enumerate(channels):
self.encoders.append(self._conv_block(prev_ch, ch))
if i < len(channels) - 1:
self.pools.append(nn.MaxPool2d(2))
prev_ch = ch
# Decoder path
for i in range(len(channels) - 2, -1, -1):
self.decoders.append(
nn.ConvTranspose2d(channels[i + 1], channels[i], 2, stride=2)
)
self.decoders.append(self._conv_block(channels[i] * 2, channels[i]))
# Output layer
self.final = nn.Conv2d(channels[0], in_channels, 1)
def _conv_block(self, in_ch, out_ch):
return nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
nn.Conv2d(out_ch, out_ch, 3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True)
)
def forward(self, x):
# Encoder
enc_features = []
for i, encoder in enumerate(self.encoders):
x = encoder(x)
enc_features.append(x)
if i < len(self.pools):
x = self.pools[i](x)
# Decoder
dec_idx = 0
for i in range(len(enc_features) - 2, -1, -1):
x = self.decoders[dec_idx](x) # Upsample
# FIXED: Handle dimension mismatch from odd-sized inputs
# Crop or pad x to match enc_features[i] dimensions
if x.shape[2:] != enc_features[i].shape[2:]:
# Calculate size difference
diff_h = enc_features[i].shape[2] - x.shape[2]
diff_w = enc_features[i].shape[3] - x.shape[3]
# Pad if upsampled tensor is smaller
if diff_h > 0 or diff_w > 0:
pad_h = max(0, diff_h)
pad_w = max(0, diff_w)
x = torch.nn.functional.pad(x, (0, pad_w, 0, pad_h))
# Crop if upsampled tensor is larger
if diff_h < 0 or diff_w < 0:
x = x[:, :, :enc_features[i].shape[2], :enc_features[i].shape[3]]
x = torch.cat([x, enc_features[i]], dim=1) # Skip connection
x = self.decoders[dec_idx + 1](x) # Conv block
dec_idx += 2
return self.final(x)
return Noise2VoidUNet(in_channels, arch['channels'], arch['depth'])
def _create_blind_spot_mask(self, shape: Tuple[int, ...], mask_size: int = 5) -> torch.Tensor:
"""Create blind spot mask for Noise2Void training"""
batch, channels, height, width = shape
mask = torch.ones(shape, dtype=torch.bool)
# Create random blind spots
num_blind_spots = max(1, (height * width) // (mask_size * mask_size * 10))
for _ in range(num_blind_spots):
if height <= mask_size or width <= mask_size:
break # Degenerate case handled by sparse fallback
y = torch.randint(0, height - mask_size + 1, (1,))
x = torch.randint(0, width - mask_size + 1, (1,))
mask[:, :, y:y+mask_size, x:x+mask_size] = False
return mask
def _create_sparse_mask(self, shape: Tuple[int, ...], drop_fraction: float = 0.001) -> torch.Tensor:
"""Create sparse dropout mask ensuring at least one masked element"""
mask = torch.ones(shape, dtype=torch.bool)
flat = mask.view(-1)
total = flat.numel()
drop_count = max(1, int(total * drop_fraction))
idx = torch.randperm(total)[:drop_count]
flat[idx] = False
return mask.view(shape)
def train_model(self, image: np.ndarray, epochs: int = 100, learning_rate: float = 1e-3) -> Dict[str, Any]:
"""
Train Noise2Void model on single image
Args:
image: Input noisy image (H, W, C) in range [0, 1]
epochs: Number of training epochs
learning_rate: Learning rate for optimization
Returns:
Training statistics dictionary
"""
print(f"Training Noise2Void model on {image.shape} image...")
# Prepare data
if len(image.shape) == 2:
image = np.expand_dims(image, axis=2)
# FIXED: Ensure contiguous array before transpose
image = np.ascontiguousarray(image)
# Convert to tensor
img_tensor = torch.from_numpy(image.transpose(2, 0, 1)).float().unsqueeze(0)
img_tensor = img_tensor.to(self.device)
img_tensor.requires_grad_(False) # Target tensor should not require gradients
# Create model
channels = image.shape[2]
self.model = self._create_model(channels).to(self.device)
# FIXED: Ensure all model parameters require gradients
self.model.train() # Set to training mode first
for param in self.model.parameters():
param.requires_grad_(True) # Explicitly enable gradients
# Optimizer and loss
optimizer = optim.Adam(self.model.parameters(), lr=learning_rate)
criterion = nn.MSELoss()
# Training loop
losses = []
print(f"Training on {self.device} for {epochs} epochs...")
with torch.enable_grad():
gradless_retries = 0
for epoch in range(epochs):
try:
optimizer.zero_grad()
# Create blind spot mask
mask = self._create_blind_spot_mask(img_tensor.shape).to(self.device)
# Ensure we actually masked something; fall back to sparse random mask if needed
if torch.count_nonzero(~mask).item() == 0:
mask = self._create_sparse_mask(img_tensor.shape, drop_fraction=0.005).to(self.device)
# Forward pass
output = self.model(img_tensor)
# Loss only on blind spots (where mask is False)
# Use masked_select to avoid gradient issues
masked_output = torch.masked_select(output, ~mask)
masked_target = torch.masked_select(img_tensor, ~mask)
# Fallback: if mask produced no samples, randomly sample a subset to avoid zero gradient
if masked_output.numel() == 0:
flat_output = output.view(-1)
flat_target = img_tensor.view(-1)
sample_count = max(1, flat_output.numel() // 1024)
sample_idx = torch.randperm(flat_output.numel(), device=self.device)[:sample_count]
masked_output = flat_output[sample_idx]
masked_target = flat_target[sample_idx]
loss = criterion(masked_output, masked_target)
if not loss.requires_grad:
gradless_retries += 1
if gradless_retries <= 3:
print(
f"⚠️ Noise2Void loss had no gradients at epoch {epoch + 1}; "
"resampling blind spots."
)
continue
raise RuntimeError("Noise2Void loss does not require gradients after multiple attempts")
gradless_retries = 0
# Backward pass
loss.backward()
optimizer.step()
losses.append(loss.item())
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch + 1}/{epochs}, Loss: {loss.item():.6f}")
except RuntimeError as e:
if "out of memory" in str(e).lower():
print(f"GPU memory error at epoch {epoch+1}, stopping early")
break
else:
raise e
self.trained = True
# Ensure we have at least one loss value
if not losses:
losses = [float('inf')] # Fallback value
return {
'final_loss': losses[-1],
'training_losses': losses,
'epochs': epochs,
'model_size': self.model_size
}
def process_image(self, image: np.ndarray, train_epochs: int = 100) -> Optional[np.ndarray]:
"""
Process image with Noise2Void
Args:
image: Input noisy image (H, W, C) in range [0, 1]
train_epochs: Number of training epochs
Returns:
Denoised image or None if processing fails
"""
try:
# FIXED: Ensure contiguous array
image = np.ascontiguousarray(image)
# Train model on the image if epochs > 0
if train_epochs > 0:
self.train_model(image, epochs=train_epochs)
elif not self.trained:
# If no training requested but model not trained, do minimal training
self.train_model(image, epochs=10)
# Prepare input
if len(image.shape) == 2:
image = np.expand_dims(image, axis=2)
# Ensure contiguous after expand_dims
image = np.ascontiguousarray(image)
img_tensor = torch.from_numpy(image.transpose(2, 0, 1)).float().unsqueeze(0)
img_tensor = img_tensor.to(self.device)
img_tensor.requires_grad_(False) # No gradients needed for inference
# Inference
self.model.eval()
with torch.no_grad():
denoised = self.model(img_tensor)
denoised = torch.clamp(denoised, 0, 1)
# Convert back to numpy
result = denoised.cpu().squeeze(0).numpy().transpose(1, 2, 0)
if result.shape[2] == 1:
result = result.squeeze(2)
# Clean up GPU memory
if self.device.type == 'cuda':
torch.cuda.empty_cache()
print(f"✅ Noise2Void processing completed successfully")
return result
except Exception as e:
print(f"❌ Noise2Void processing failed: {e}")
# Clean up GPU memory on error
if self.device.type == 'cuda':
torch.cuda.empty_cache()
return None
class DeepImagePriorProcessor:
"""
Deep Image Prior: Unsupervised restoration using network architecture as prior
No training data required - uses the implicit bias of CNN architectures
for image restoration tasks including denoising, super-resolution, and inpainting.
IMPORTANT NOTES:
- Each image requires its OWN training session (model cannot be reused)
- The trained model is overfitted to ONE specific noisy image
- Uses GPU if available for much faster training (~10-50x speedup)
- Training time: ~1-5 minutes on GPU, ~30-120 minutes on CPU
"""
def __init__(self, device: str = "auto"):
"""Initialize Deep Image Prior processor"""
self.device = self._get_device(device)
self.model = None
self.trained = False
self.last_image_shape = None
# Performance tracking
if self.device.type == "cuda":
gpu_name = torch.cuda.get_device_name(0)
print(f"🚀 Deep Image Prior using GPU: {gpu_name}")
else:
print(f"⚠️ Deep Image Prior using CPU (will be slow)")
print(f" Tip: For ~30x speedup, use a CUDA-capable GPU")
def _get_device(self, device: str) -> torch.device:
"""Determine optimal processing device"""
if device == "auto":
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
return torch.device(device)
def _create_model(self, in_channels: int, out_channels: int) -> nn.Module:
"""Create Deep Image Prior network architecture"""
class DIPNetwork(nn.Module):
def __init__(self, in_channels, out_channels):
super().__init__()
# Encoder
self.enc1 = self._conv_block(in_channels, 128)
self.enc2 = self._conv_block(128, 128)
self.enc3 = self._conv_block(128, 128)
self.enc4 = self._conv_block(128, 128)
self.enc5 = self._conv_block(128, 128)
# Decoder
self.dec5 = self._conv_block(128, 128)
self.dec4 = self._conv_block(256, 128)
self.dec3 = self._conv_block(256, 128)
self.dec2 = self._conv_block(256, 128)
self.dec1 = self._conv_block(256, 128)
# Output
self.output = nn.Conv2d(128, out_channels, 1)
# Initialize weights
self._initialize_weights()
def _conv_block(self, in_channels, out_channels):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(out_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels),
nn.LeakyReLU(0.2, inplace=True)
)
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='leaky_relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def forward(self, x):
# Encoder with skip connections
e1 = self.enc1(x)
e2 = self.enc2(nn.functional.max_pool2d(e1, 2))
e3 = self.enc3(nn.functional.max_pool2d(e2, 2))
e4 = self.enc4(nn.functional.max_pool2d(e3, 2))
e5 = self.enc5(nn.functional.max_pool2d(e4, 2))
# Decoder with skip connections - match dimensions exactly
d5 = self.dec5(e5)
d4_up = nn.functional.interpolate(d5, size=e4.shape[2:], mode='bilinear', align_corners=False)
d4 = self.dec4(torch.cat([d4_up, e4], 1))
d3_up = nn.functional.interpolate(d4, size=e3.shape[2:], mode='bilinear', align_corners=False)
d3 = self.dec3(torch.cat([d3_up, e3], 1))
d2_up = nn.functional.interpolate(d3, size=e2.shape[2:], mode='bilinear', align_corners=False)
d2 = self.dec2(torch.cat([d2_up, e2], 1))
d1_up = nn.functional.interpolate(d2, size=e1.shape[2:], mode='bilinear', align_corners=False)
d1 = self.dec1(torch.cat([d1_up, e1], 1))
return torch.sigmoid(self.output(d1))
return DIPNetwork(in_channels, out_channels)
def process_image(self, image: np.ndarray, iterations: int = 3000,
learning_rate: float = 1e-2) -> Optional[np.ndarray]:
"""
Process image with Deep Image Prior
IMPORTANT: The trained model CANNOT be reused on other images!
Why? Deep Image Prior works by OVERFITTING a network to one specific noisy image.
The network learns to reconstruct THAT PARTICULAR IMAGE from random noise.
The "magic" is that natural image structure is learned faster than noise patterns.
This means:
- Each image needs its own training session (1-5 min on GPU, 30-120 min on CPU)
- The model is useless after training - it only knows how to make this one image
- Cannot batch process multiple images with same model
- Cannot save and reuse the trained weights
Performance tips:
- Use GPU for 30-50x speedup (auto-detected)
- Reduce iterations to 1000-2000 for faster results
- Early stopping typically finishes before max iterations
Args:
image: Input noisy image (H, W, C) in range [0, 1]
iterations: Number of optimization iterations (default: 3000)
- 500-1000: Fast but lower quality
- 2000-3000: Good balance (recommended)
- 5000+: Better quality but slower
learning_rate: Learning rate for optimization (default: 0.01)
Returns:
Restored image or None if processing fails
"""
grad_enabled_state = torch.is_grad_enabled()
torch.set_grad_enabled(True)
try:
print(f"Processing with Deep Image Prior ({iterations} iterations)...")
# Handle zero iterations case
if iterations <= 0:
print(" ⚠️ Zero iterations specified - returning original image")
return image.copy()
# Prepare data
if len(image.shape) == 2:
image = np.expand_dims(image, axis=2)
# FIXED: Ensure contiguous array before transpose
image = np.ascontiguousarray(image)
channels = image.shape[2]
best_loss = float('inf')
best_output = None
patience = 200 # Stop if no improvement for 200 iterations
no_improve_count = 0
import time
start_time = time.time()
with torch.inference_mode(False):
img_tensor = torch.from_numpy(image.transpose(2, 0, 1)).float().unsqueeze(0)
img_tensor = img_tensor.to(self.device)
# Target tensor should not require gradients
img_tensor.requires_grad_(False)
# Create model and ensure parameters require gradients
self.model = self._create_model(channels, channels).to(self.device)
self.model.train() # Set to training mode
# Ensure all model parameters require gradients
for param in self.model.parameters():
param.requires_grad_(True)
# FIXED: Fixed random input (this is the key insight of DIP)
# The noise input should NOT require gradients - it's fixed!
# We optimize the MODEL parameters, not the input
noise_input = torch.randn_like(img_tensor) * 0.1
noise_input = noise_input.to(self.device)
noise_input.requires_grad_(False) # Input is FIXED - we optimize model params
# Optimizer - optimize the model parameters
# PERFORMANCE: Use AdamW which is slightly faster and more memory efficient
optimizer = optim.AdamW(self.model.parameters(), lr=learning_rate, weight_decay=1e-4)
criterion = nn.MSELoss()
# FIXED: Disable mixed precision - causes gradient flow issues with BatchNorm
# Mixed precision autocast can prevent gradients from flowing through BatchNorm layers
# This is a known issue with autocast + BatchNorm in PyTorch 2.x
# Since DIP is already fast (single image), the 2x speedup isn't worth broken training
use_amp = False # Disabled - use FP32 for reliable gradient flow
if use_amp:
try:
scaler = torch.cuda.amp.GradScaler()
print(f" ⚡ Using mixed precision (FP16) for 2-3x speedup")
except:
use_amp = False
scaler = None
else:
scaler = None
with torch.enable_grad():
gradless_iterations = 0
for i in range(iterations):
try:
optimizer.zero_grad()
# PERFORMANCE: Mixed precision forward pass
if use_amp:
with torch.cuda.amp.autocast():
output = self.model(noise_input)
loss = criterion(output, img_tensor)
else:
output = self.model(noise_input)
loss = criterion(output, img_tensor)
# FIXED: Check output has gradients (should be True since model has gradients)
if not output.requires_grad:
gradless_iterations += 1
if gradless_iterations <= 10 and i < 25:
if i < 5:
print(f"⚠️ Warning: Output doesn't require gradients at iteration {i+1}")
print(f" This means the model may not be training properly.")
continue
else:
print(f"❌ Critical: No gradients after {i+1} iterations, stopping.")
break
gradless_iterations = 0
if not loss.requires_grad:
if i < 5:
print(f"⚠️ Warning: Loss doesn't require gradients at iteration {i+1}")
continue
# PERFORMANCE: Mixed precision backward pass
if use_amp and scaler is not None:
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
else:
loss.backward()
optimizer.step()
grad_norm = 0
for param in self.model.parameters():
if param.grad is not None:
grad_norm += param.grad.data.norm(2).item() ** 2
grad_norm = grad_norm ** 0.5
if grad_norm == 0 and (i + 1) % 100 == 0:
print(f"⚠️ Warning: Zero gradients at iteration {i+1}")
current_loss = loss.item()
if current_loss < best_loss:
best_loss = current_loss
best_output = output.detach().clone()
no_improve_count = 0
else:
no_improve_count += 1
if no_improve_count >= patience:
elapsed = time.time() - start_time
print(f" ⏱️ Early stopping at iteration {i + 1} (no improvement for {patience} iterations)")
print(f" Time elapsed: {elapsed:.1f}s, Best loss: {best_loss:.6f}")
break
if (i + 1) % 500 == 0:
elapsed = time.time() - start_time
iter_per_sec = (i + 1) / elapsed
eta = (iterations - i - 1) / iter_per_sec if iter_per_sec > 0 else 0
print(f"Iteration {i + 1}/{iterations}, Loss: {current_loss:.6f}, "
f"Best: {best_loss:.6f}, Speed: {iter_per_sec:.1f} it/s, ETA: {eta:.0f}s")
except RuntimeError as e:
if "out of memory" in str(e).lower():
print(f"GPU memory error at iteration {i+1}, stopping early")
break
elif "grad" in str(e).lower():
print(f"Gradient error at iteration {i+1}: {e}")
continue
else:
raise e
# Convert result
if best_output is not None:
total_time = time.time() - start_time
actual_iterations = i + 1
result = best_output.cpu().squeeze(0).numpy().transpose(1, 2, 0)
if result.shape[2] == 1:
result = result.squeeze(2)
# Clean up GPU memory
if self.device.type == 'cuda':
torch.cuda.empty_cache()
print(f"✅ Deep Image Prior processing completed")
print(f" 📊 Stats: {actual_iterations} iterations in {total_time:.1f}s "
f"({actual_iterations/total_time:.1f} it/s)")
print(f" 🎯 Final loss: {best_loss:.6f}")
print(f" 💾 Device: {self.device.type.upper()}")
if use_amp:
print(f" ⚡ Mixed precision: Enabled")
return result
else:
# Clean up GPU memory
if self.device.type == 'cuda':
torch.cuda.empty_cache()
return None
except Exception as e:
print(f"❌ Deep Image Prior processing failed: {e}")
import traceback
print("Full traceback:")
traceback.print_exc()
print(f"Image shape: {image.shape}, dtype: {image.dtype}")
print(f"Image range: [{image.min():.4f}, {image.max():.4f}]")
# Clean up GPU memory on error
if self.device.type == 'cuda':
torch.cuda.empty_cache()
return None
finally:
torch.set_grad_enabled(grad_enabled_state)
class AutoDenoiseProcessor:
"""
Unified Auto-Denoise processor combining multiple state-of-the-art methods
Automatically selects and applies the best denoising method based on image characteristics
"""
def __init__(self, device: str = "auto"):
"""Initialize Auto-Denoise processor"""
self.device = device
self.n2v_processor = Noise2VoidProcessor(device)
self.dip_processor = DeepImagePriorProcessor(device)
def analyze_image(self, image: np.ndarray) -> Dict[str, Any]:
"""Analyze image to determine optimal processing method"""
analysis = {}
try:
# Basic image statistics
analysis['mean'] = np.mean(image)
analysis['std'] = np.std(image)
analysis['min'] = np.min(image)
analysis['max'] = np.max(image)
# Noise estimation
if SKIMAGE_AVAILABLE:
if len(image.shape) == 3:
# Multi-channel noise estimation
noise_levels = []
for c in range(image.shape[2]):
noise_levels.append(estimate_sigma(image[:, :, c], channel_axis=None))
analysis['noise_level'] = np.mean(noise_levels)
else:
analysis['noise_level'] = estimate_sigma(image, channel_axis=None)
else:
# Fallback noise estimation
analysis['noise_level'] = np.std(image) * 0.5
# Image complexity (gradient magnitude)
if len(image.shape) == 3:
gray = np.mean(image, axis=2)
else:
gray = image
grad_x = np.diff(gray, axis=1)
grad_y = np.diff(gray, axis=0)
gradient_magnitude = np.sqrt(grad_x[:-1, :]**2 + grad_y[:, :-1]**2)
analysis['complexity'] = np.mean(gradient_magnitude)
# Recommend method based on analysis
if analysis['noise_level'] > 0.1:
analysis['recommended_method'] = 'noise2void'
analysis['reason'] = 'High noise level detected - Noise2Void recommended'
elif analysis['complexity'] < 0.05:
analysis['recommended_method'] = 'deep_image_prior'
analysis['reason'] = 'Low complexity image - Deep Image Prior recommended'
else:
analysis['recommended_method'] = 'noise2void'
analysis['reason'] = 'General case - Noise2Void recommended'
except Exception as e:
print(f"Analysis error: {e}")
analysis['recommended_method'] = 'noise2void'
analysis['reason'] = 'Analysis failed - using default method'
return analysis
def process_image(self, image: np.ndarray, method: str = "auto",
**kwargs) -> Optional[np.ndarray]:
"""
Process image with Auto-Denoise
Args:
image: Input image (H, W, C) in range [0, 1]
method: Processing method ('auto', 'noise2void', 'deep_image_prior')
**kwargs: Additional parameters for specific methods
Returns:
Processed image or None if processing fails
"""
try:
# FIXED: Ensure contiguous array
image = np.ascontiguousarray(image)
# Auto-select method if needed
if method == "auto":
analysis = self.analyze_image(image)
method = analysis['recommended_method']
print(f"Auto-selected method: {method}")
print(f"Reason: {analysis['reason']}")
# Process with selected method
if method == "noise2void":
epochs = kwargs.get('train_epochs', 100)
result = self.n2v_processor.process_image(image, train_epochs=epochs)
if result is None:
print(f"❌ Noise2Void returned None - processing failed")
return result
elif method == "deep_image_prior":
iterations = kwargs.get('iterations', 3000)
lr = kwargs.get('learning_rate', 1e-2)
print(f"🔄 Starting Deep Image Prior with {iterations} iterations...")
result = self.dip_processor.process_image(image, iterations=iterations, learning_rate=lr)
if result is None:
print(f"❌ Deep Image Prior returned None - processing failed")
return result
else:
print(f"❌ Unknown method: {method}")
return None
except Exception as e:
import traceback
print(f"❌ Auto-denoise processing error: {e}")
print(f"Traceback: {traceback.format_exc()}")
return None
def compare_methods(self, image: np.ndarray) -> Dict[str, Optional[np.ndarray]]:
"""
Compare multiple denoising methods on the same image
Args:
image: Input image to process
Returns:
Dictionary with results from different methods
"""
results = {}
print("Comparing Auto-Denoise methods...")
# Noise2Void
print("Processing with Noise2Void...")
results['noise2void'] = self.n2v_processor.process_image(image, train_epochs=50)
# Deep Image Prior (reduced iterations for comparison)
print("Processing with Deep Image Prior...")
results['deep_image_prior'] = self.dip_processor.process_image(image, iterations=1000)
# Calculate metrics if possible
if SKIMAGE_AVAILABLE:
for method, result in results.items():
if result is not None:
try:
psnr = peak_signal_noise_ratio(image, result, data_range=1.0)
ssim = structural_similarity(image, result, multichannel=True, channel_axis=-1, data_range=1.0)
print(f"{method.upper()}: PSNR={psnr:.2f}, SSIM={ssim:.4f}")
except:
print(f"{method.upper()}: Metrics calculation failed")
return results