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
305 lines
10 KiB
Python
305 lines
10 KiB
Python
"""
|
|
Non-Local Means denoising implementation
|
|
Excellent for preserving textures and repetitive patterns in photographs
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
from typing import Optional, Tuple
|
|
from skimage.restoration import denoise_nl_means, estimate_sigma
|
|
from skimage import img_as_float, img_as_ubyte
|
|
|
|
|
|
def nonlocal_means_denoise(
|
|
image: np.ndarray,
|
|
h: Optional[float] = None,
|
|
patch_size: int = 7,
|
|
patch_distance: int = 11,
|
|
multichannel: bool = True,
|
|
method: str = 'opencv',
|
|
fast_mode: bool = True,
|
|
sigma: Optional[float] = None
|
|
) -> np.ndarray:
|
|
"""
|
|
Non-Local Means denoising with automatic parameter estimation
|
|
|
|
This algorithm excels at:
|
|
- Preserving textures and repetitive patterns
|
|
- Natural photographs with organic noise
|
|
- Images where traditional filters blur important details
|
|
|
|
Args:
|
|
image: Input image as numpy array [H, W, C] with values 0-255
|
|
h: Filtering strength (auto-estimated if None)
|
|
- Low values (3-10): Preserve more detail, less denoising
|
|
- Medium values (10-15): Balanced denoising (recommended)
|
|
- High values (15-25): Strong denoising, may blur details
|
|
patch_size: Size of patches for comparison (odd numbers)
|
|
- 5: Fast, good for small textures
|
|
- 7: Standard choice, good balance (recommended)
|
|
- 9-11: Better for large textures, slower
|
|
patch_distance: Search window size
|
|
- 11: Fast, sufficient for most cases (recommended)
|
|
- 15-21: Better quality, much slower
|
|
- Must be larger than patch_size
|
|
multichannel: Process color images as multichannel
|
|
method: Implementation to use ('opencv', 'skimage')
|
|
- 'opencv': Faster, good quality
|
|
- 'skimage': Slower, potentially better quality
|
|
fast_mode: Use fast mode (opencv only)
|
|
- True: ~3x faster with slight quality loss
|
|
- False: Higher quality, slower
|
|
sigma: Noise level estimate (auto-estimated if None)
|
|
|
|
Returns:
|
|
Denoised image as numpy array [H, W, C] with values 0-255
|
|
"""
|
|
|
|
# Validate inputs
|
|
if image.ndim not in [2, 3]:
|
|
raise ValueError("Image must be 2D (grayscale) or 3D (color)")
|
|
|
|
if patch_distance <= patch_size:
|
|
raise ValueError("patch_distance must be larger than patch_size")
|
|
|
|
# Estimate noise level if not provided
|
|
if sigma is None:
|
|
if image.ndim == 3:
|
|
# Estimate on luminance channel
|
|
image = np.ascontiguousarray(image)
|
|
|
|
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
|
# FIXED: Ensure contiguous array after cv2.cvtColor
|
|
gray = np.ascontiguousarray(gray)
|
|
else:
|
|
gray = image
|
|
|
|
sigma = estimate_sigma(gray.astype(np.float32) / 255.0, channel_axis=None) * 255
|
|
|
|
# Auto-estimate h parameter if not provided
|
|
if h is None:
|
|
# Empirical relationship between sigma and h
|
|
h = max(5.0, min(25.0, sigma * 0.6))
|
|
|
|
print(f"Using h={h:.1f}, estimated sigma={sigma:.1f}")
|
|
|
|
# Choose implementation
|
|
if method == 'opencv':
|
|
return _opencv_nlm_denoise(image, h, patch_size, patch_distance, multichannel, fast_mode)
|
|
elif method == 'skimage':
|
|
return _skimage_nlm_denoise(image, h, patch_size, patch_distance, multichannel, sigma)
|
|
else:
|
|
raise ValueError(f"Unknown method: {method}")
|
|
|
|
|
|
def _opencv_nlm_denoise(
|
|
image: np.ndarray,
|
|
h: float,
|
|
patch_size: int,
|
|
patch_distance: int,
|
|
multichannel: bool,
|
|
fast_mode: bool
|
|
) -> np.ndarray:
|
|
"""OpenCV implementation of Non-Local Means"""
|
|
|
|
if image.ndim == 2:
|
|
# Grayscale image
|
|
if fast_mode:
|
|
denoised = cv2.fastNlMeansDenoising(
|
|
image, None, h, patch_size, patch_distance
|
|
)
|
|
else:
|
|
denoised = cv2.fastNlMeansDenoising(
|
|
image, None, h, patch_size, patch_distance
|
|
)
|
|
else:
|
|
# Color image
|
|
if multichannel:
|
|
if fast_mode:
|
|
denoised = cv2.fastNlMeansDenoisingColored(
|
|
image, None, h, h, patch_size, patch_distance
|
|
)
|
|
else:
|
|
denoised = cv2.fastNlMeansDenoisingColored(
|
|
image, None, h, h, patch_size, patch_distance
|
|
)
|
|
else:
|
|
# Process as grayscale
|
|
image = np.ascontiguousarray(image)
|
|
|
|
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
|
# FIXED: Ensure contiguous array after cv2.cvtColor
|
|
gray = np.ascontiguousarray(gray)
|
|
if fast_mode:
|
|
denoised_gray = cv2.fastNlMeansDenoising(
|
|
gray, None, h, patch_size, patch_distance
|
|
)
|
|
else:
|
|
denoised_gray = cv2.fastNlMeansDenoising(
|
|
gray, None, h, patch_size, patch_distance
|
|
)
|
|
# Convert back to color
|
|
denoised_gray = np.ascontiguousarray(denoised_gray)
|
|
|
|
denoised = cv2.cvtColor(denoised_gray, cv2.COLOR_GRAY2RGB)
|
|
|
|
return denoised
|
|
|
|
|
|
def _skimage_nlm_denoise(
|
|
image: np.ndarray,
|
|
h: float,
|
|
patch_size: int,
|
|
patch_distance: int,
|
|
multichannel: bool,
|
|
sigma: float
|
|
) -> np.ndarray:
|
|
"""Scikit-image implementation of Non-Local Means"""
|
|
|
|
# Convert to float for processing
|
|
img_float = img_as_float(image)
|
|
|
|
# Adjust h for skimage (uses different scaling)
|
|
h_adjusted = h / 255.0
|
|
sigma_adjusted = sigma / 255.0
|
|
|
|
# Process image
|
|
if img_float.ndim == 2:
|
|
channel_axis = None
|
|
else:
|
|
channel_axis = -1 if multichannel else None
|
|
|
|
try:
|
|
# Try new scikit-image API (channel_axis parameter)
|
|
denoised_float = denoise_nl_means(
|
|
img_float,
|
|
h=h_adjusted,
|
|
patch_size=patch_size,
|
|
patch_distance=patch_distance,
|
|
channel_axis=channel_axis,
|
|
preserve_range=False,
|
|
sigma=sigma_adjusted
|
|
)
|
|
except TypeError:
|
|
# Fallback to old API (multichannel parameter)
|
|
denoised_float = denoise_nl_means(
|
|
img_float,
|
|
h=h_adjusted,
|
|
patch_size=patch_size,
|
|
patch_distance=patch_distance,
|
|
multichannel=multichannel,
|
|
preserve_range=False,
|
|
sigma=sigma_adjusted
|
|
)
|
|
|
|
# Convert back to uint8
|
|
denoised = img_as_ubyte(denoised_float)
|
|
|
|
return denoised
|
|
|
|
|
|
def adaptive_nonlocal_means(
|
|
image: np.ndarray,
|
|
noise_level: str = 'auto',
|
|
quality: str = 'balanced',
|
|
preserve_textures: bool = True
|
|
) -> np.ndarray:
|
|
"""
|
|
Simplified interface with automatic parameter selection
|
|
|
|
Args:
|
|
image: Input image [H, W, C] with values 0-255
|
|
noise_level: 'low', 'medium', 'high', or 'auto'
|
|
quality: 'fast', 'balanced', or 'high'
|
|
preserve_textures: Adjust parameters to better preserve textures
|
|
|
|
Returns:
|
|
Denoised image
|
|
"""
|
|
|
|
# Auto-detect noise level
|
|
if noise_level == 'auto':
|
|
if image.ndim == 3:
|
|
image = np.ascontiguousarray(image)
|
|
|
|
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
|
# FIXED: Ensure contiguous array after cv2.cvtColor
|
|
gray = np.ascontiguousarray(gray)
|
|
else:
|
|
gray = image
|
|
|
|
sigma = estimate_sigma(gray.astype(np.float32) / 255.0, channel_axis=None) * 255
|
|
|
|
if sigma < 10:
|
|
noise_level = 'low'
|
|
elif sigma < 20:
|
|
noise_level = 'medium'
|
|
else:
|
|
noise_level = 'high'
|
|
|
|
print(f"Auto-detected noise level: {noise_level} (sigma={sigma:.1f})")
|
|
|
|
# Parameter sets based on quality and noise level
|
|
params = {
|
|
'fast': {
|
|
'low': {'h': 8, 'patch_size': 5, 'patch_distance': 11},
|
|
'medium': {'h': 12, 'patch_size': 5, 'patch_distance': 11},
|
|
'high': {'h': 18, 'patch_size': 7, 'patch_distance': 11}
|
|
},
|
|
'balanced': {
|
|
'low': {'h': 10, 'patch_size': 7, 'patch_distance': 11},
|
|
'medium': {'h': 15, 'patch_size': 7, 'patch_distance': 15},
|
|
'high': {'h': 22, 'patch_size': 7, 'patch_distance': 15}
|
|
},
|
|
'high': {
|
|
'low': {'h': 12, 'patch_size': 9, 'patch_distance': 21},
|
|
'medium': {'h': 18, 'patch_size': 9, 'patch_distance': 21},
|
|
'high': {'h': 25, 'patch_size': 9, 'patch_distance': 21}
|
|
}
|
|
}
|
|
|
|
# Adjust for texture preservation
|
|
if preserve_textures:
|
|
params[quality][noise_level]['h'] *= 0.8
|
|
if quality != 'fast':
|
|
params[quality][noise_level]['patch_size'] = max(7, params[quality][noise_level]['patch_size'])
|
|
|
|
# Get parameters
|
|
p = params[quality][noise_level]
|
|
|
|
# Use fast mode for 'fast' quality
|
|
fast_mode = (quality == 'fast')
|
|
|
|
return nonlocal_means_denoise(
|
|
image,
|
|
h=p['h'],
|
|
patch_size=p['patch_size'],
|
|
patch_distance=p['patch_distance'],
|
|
multichannel=True,
|
|
method='opencv',
|
|
fast_mode=fast_mode
|
|
)
|
|
|
|
|
|
def get_recommended_parameters(image_type: str, noise_level: str) -> dict:
|
|
"""Get recommended parameters for different image types and noise levels"""
|
|
|
|
recommendations = {
|
|
'photograph': {
|
|
'low': {'h': 10, 'patch_size': 7, 'patch_distance': 11, 'fast_mode': True},
|
|
'medium': {'h': 15, 'patch_size': 7, 'patch_distance': 15, 'fast_mode': False},
|
|
'high': {'h': 22, 'patch_size': 9, 'patch_distance': 21, 'fast_mode': False}
|
|
},
|
|
'scanned_photo': {
|
|
'low': {'h': 12, 'patch_size': 7, 'patch_distance': 15, 'fast_mode': False},
|
|
'medium': {'h': 18, 'patch_size': 9, 'patch_distance': 21, 'fast_mode': False},
|
|
'high': {'h': 25, 'patch_size': 9, 'patch_distance': 21, 'fast_mode': False}
|
|
},
|
|
'digital_art': {
|
|
'low': {'h': 8, 'patch_size': 5, 'patch_distance': 11, 'fast_mode': True},
|
|
'medium': {'h': 12, 'patch_size': 7, 'patch_distance': 11, 'fast_mode': True},
|
|
'high': {'h': 18, 'patch_size': 7, 'patch_distance': 15, 'fast_mode': False}
|
|
}
|
|
}
|
|
|
|
return recommendations.get(image_type, recommendations['photograph']).get(noise_level, recommendations['photograph']['medium']) |