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

206 lines
7.3 KiB
Python

"""Torch-accelerated Richardson-Lucy deconvolution."""
from __future__ import annotations
import math
from typing import Optional, Tuple
import numpy as np
try: # Optional dependency
import torch
import torch.nn.functional as F
except ImportError as exc: # pragma: no cover - runtime guard
torch = None # type: ignore
_TORCH_IMPORT_ERROR = exc
else:
_TORCH_IMPORT_ERROR = None
def _select_device(device_preference: str) -> "torch.device":
if torch is None: # pragma: no cover - guard for static analyzers
raise RuntimeError("PyTorch is required for GPU Richardson-Lucy") from _TORCH_IMPORT_ERROR
pref = (device_preference or "auto").lower()
if pref == "auto":
if torch.cuda.is_available():
return torch.device("cuda")
mps_backend = getattr(torch.backends, "mps", None)
if mps_backend and mps_backend.is_available():
return torch.device("mps")
return torch.device("cpu")
try:
device = torch.device(pref)
except (TypeError, ValueError):
print(f"Unknown device '{device_preference}', falling back to CPU")
return torch.device("cpu")
if device.type == "cuda" and not torch.cuda.is_available():
print("CUDA requested but not available, falling back to CPU")
return torch.device("cpu")
if device.type == "mps":
mps_backend = getattr(torch.backends, "mps", None)
if not (mps_backend and mps_backend.is_available()):
print("MPS requested but not available, falling back to CPU")
return torch.device("cpu")
return device
def _generate_psf_tensor(
blur_type: str,
blur_size: float,
motion_angle: float,
motion_length: float,
device: "torch.device",
dtype: "torch.dtype",
) -> "torch.Tensor":
blur_type = blur_type.lower()
if blur_type == "gaussian":
sigma = max(blur_size, 1e-3)
size = int(2 * math.ceil(3 * sigma) + 1)
size = max(size, 3)
coords = torch.arange(size, device=device, dtype=dtype)
coords = coords - coords.mean()
yy, xx = torch.meshgrid(coords, coords, indexing="ij")
psf = torch.exp(-((xx ** 2 + yy ** 2) / (2 * sigma ** 2)))
elif blur_type == "motion":
length = max(1, int(round(motion_length)))
size = max(length, 3)
if size % 2 == 0:
size += 1
psf = torch.zeros((size, size), device=device, dtype=dtype)
center = size // 2
angle_rad = math.radians(motion_angle)
dx = math.cos(angle_rad) * (length - 1) / 2.0
dy = math.sin(angle_rad) * (length - 1) / 2.0
x0, y0 = center - dx, center - dy
x1, y1 = center + dx, center + dy
for step in range(length):
t = step / max(length - 1, 1)
x = round(x0 + (x1 - x0) * t)
y = round(y0 + (y1 - y0) * t)
if 0 <= x < size and 0 <= y < size:
psf[y, x] = 1.0
else:
raise ValueError(f"Unsupported blur type '{blur_type}' for GPU Richardson-Lucy")
psf_sum = psf.sum()
if psf_sum <= 0:
psf.fill_(0)
psf[psf.shape[0] // 2, psf.shape[1] // 2] = 1.0
psf_sum = psf.sum()
return psf / psf_sum
def _depthwise_conv(image: "torch.Tensor", kernel: "torch.Tensor") -> "torch.Tensor":
pad_h = kernel.shape[-2] // 2
pad_w = kernel.shape[-1] // 2
padded = F.pad(image, (pad_w, pad_w, pad_h, pad_h), mode="reflect")
return F.conv2d(padded, kernel, groups=image.shape[1])
def _apply_laplacian_regularization(
image: "torch.Tensor", strength: float, epsilon: float
) -> "torch.Tensor":
if strength <= 0:
return image
laplacian_kernel = torch.tensor(
[[0.0, 1.0, 0.0], [1.0, -4.0, 1.0], [0.0, 1.0, 0.0]],
device=image.device,
dtype=image.dtype,
).view(1, 1, 3, 3)
laplacian_kernel = laplacian_kernel.repeat(image.shape[1], 1, 1, 1)
laplacian = _depthwise_conv(image, laplacian_kernel)
return torch.clamp(image - strength * laplacian, min=epsilon)
def richardson_lucy_deconvolution_gpu(
image: np.ndarray,
psf: Optional[np.ndarray] = None,
iterations: int = 10,
clip: bool = True,
filter_epsilon: Optional[float] = None,
blur_type: str = "gaussian",
blur_size: float = 2.0,
motion_angle: float = 0.0,
motion_length: float = 10.0,
regularization: float = 0.0,
device: str = "auto",
precision: str = "fp32",
) -> np.ndarray:
"""Accelerated Richardson-Lucy deconvolution using PyTorch."""
if torch is None:
raise RuntimeError("PyTorch is required for GPU Richardson-Lucy") from _TORCH_IMPORT_ERROR
if image.ndim not in (2, 3):
raise ValueError("Image must be 2D or 3D array")
if iterations < 1:
raise ValueError("Iterations must be >= 1")
image = np.ascontiguousarray(image)
alpha: Optional[np.ndarray] = None
if image.ndim == 3 and image.shape[2] == 4:
alpha = image[..., 3:]
image = image[..., :3]
input_float = image.astype(np.float32) / 255.0
if input_float.ndim == 2:
input_tensor = torch.from_numpy(input_float).unsqueeze(0).unsqueeze(0)
else:
input_tensor = torch.from_numpy(input_float).permute(2, 0, 1).unsqueeze(0)
target_device = _select_device(device)
compute_dtype = torch.float16 if precision == "fp16" and target_device.type == "cuda" else torch.float32
if compute_dtype == torch.float16 and target_device.type != "cuda":
compute_dtype = torch.float32
observed = input_tensor.to(device=target_device, dtype=compute_dtype)
estimate = observed.clone()
eps = filter_epsilon if filter_epsilon is not None else torch.finfo(observed.dtype).eps
if psf is not None:
if psf.ndim != 2:
raise ValueError("Custom PSF must be 2D")
psf_tensor = torch.from_numpy(np.ascontiguousarray(psf.astype(np.float32)))
psf_tensor = psf_tensor.to(device=target_device, dtype=compute_dtype)
psf_tensor = psf_tensor / psf_tensor.sum().clamp(min=eps)
else:
psf_tensor = _generate_psf_tensor(
blur_type, blur_size, motion_angle, motion_length, target_device, compute_dtype
)
kernel = psf_tensor.view(1, 1, psf_tensor.shape[0], psf_tensor.shape[1])
kernel = kernel.repeat(observed.shape[1], 1, 1, 1)
kernel_flipped = torch.flip(kernel, dims=(-2, -1))
for _ in range(iterations):
convolved = _depthwise_conv(estimate, kernel)
convolved = torch.clamp(convolved, min=eps)
ratio = observed / convolved
correlation = _depthwise_conv(ratio, kernel_flipped)
estimate = estimate * correlation
estimate = _apply_laplacian_regularization(estimate, regularization, eps)
estimate = torch.clamp(estimate, min=eps)
if clip:
estimate = torch.clamp(estimate, 0.0, 1.0)
estimate = estimate.squeeze(0)
if estimate.ndim == 3:
result = estimate.permute(1, 2, 0).to(torch.float32).cpu().numpy()
result = np.clip(result * 255.0 + 0.5, 0, 255).astype(np.uint8)
if alpha is not None:
result = np.concatenate([result, alpha], axis=2)
else:
result = estimate.to(torch.float32).cpu().numpy()
result = np.clip(result * 255.0 + 0.5, 0, 255).astype(np.uint8)
return np.ascontiguousarray(result)
__all__ = ["richardson_lucy_deconvolution_gpu"]