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
825 lines
37 KiB
Python
825 lines
37 KiB
Python
"""
|
|
Advanced Sharpening Processor
|
|
Implementation of cutting-edge sharpening techniques from 2024-2025 research:
|
|
- Smart Sharpening with overshoot detection
|
|
- High Radius Low Amount (HiRaLoAm) technique
|
|
- Edge-directional kernels with orientation-specific enhancement
|
|
- Multi-scale Laplacian pyramids
|
|
- Guided filtering with edge-preserving smoothing
|
|
|
|
FIXED: NumPy 2.0 / OpenCV 4.11 compatibility - ensures all arrays are contiguous
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
from typing import Tuple, Dict, Any, Optional, List
|
|
from scipy import ndimage
|
|
from scipy.ndimage import gaussian_filter
|
|
from skimage import filters, feature, color
|
|
from skimage.restoration import denoise_bilateral
|
|
|
|
try:
|
|
from skimage.metrics import peak_signal_noise_ratio, structural_similarity
|
|
SKIMAGE_AVAILABLE = True
|
|
except ImportError:
|
|
SKIMAGE_AVAILABLE = False
|
|
|
|
class AdvancedSharpeningProcessor:
|
|
"""Advanced sharpening processor with multiple sophisticated techniques"""
|
|
|
|
def __init__(self):
|
|
self.name = "Advanced Sharpening Processor"
|
|
self.version = "1.0.1" # Updated version for NumPy 2.0 compatibility
|
|
|
|
def smart_sharpening(self, image: np.ndarray, strength: float = 1.0,
|
|
radius: float = 1.0, threshold: float = 0.1,
|
|
overshoot_protection: bool = True) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
"""
|
|
Smart sharpening with overshoot detection and adaptive radius control
|
|
FIXED: Scale-aware processing for real-world megapixel images
|
|
|
|
Args:
|
|
image: Input image (H, W) or (H, W, C) in range [0, 1]
|
|
strength: Sharpening strength (0.1 to 3.0)
|
|
radius: Base sharpening radius (0.5 to 5.0)
|
|
threshold: Edge threshold for selective sharpening (0.01 to 0.5)
|
|
overshoot_protection: Enable overshoot detection and control
|
|
|
|
Returns:
|
|
Tuple of (sharpened_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)) # Base size 512x512
|
|
scale_factor = np.clip(scale_factor, 0.5, 4.0)
|
|
|
|
# FIXED: Scale-aware parameter adjustment
|
|
adjusted_radius = radius * scale_factor
|
|
adjusted_threshold = threshold / scale_factor # Lower threshold for larger images
|
|
# MODIFIED: Use full strength instead of reducing it (was * 0.7)
|
|
# This gives more visible results as requested by users
|
|
adjusted_strength = strength
|
|
|
|
# Convert to working format
|
|
if len(image.shape) == 3:
|
|
is_color = True
|
|
# Work in luminance for edge detection, preserve color
|
|
lab_image = color.rgb2lab(image)
|
|
# CRITICAL: Ensure lab_image is contiguous immediately after conversion
|
|
lab_image = np.ascontiguousarray(lab_image)
|
|
# CRITICAL: Ensure lab_image is contiguous immediately after conversion
|
|
lab_image = np.ascontiguousarray(lab_image)
|
|
luminance = lab_image[:, :, 0] / 100.0 # Normalize L channel
|
|
# FIXED: Ensure contiguous array for OpenCV operations
|
|
work_image = np.ascontiguousarray(luminance)
|
|
else:
|
|
is_color = False
|
|
work_image = np.ascontiguousarray(image.copy())
|
|
|
|
# Adaptive radius control based on local image content
|
|
edge_map = feature.canny(work_image, sigma=adjusted_radius * 0.5,
|
|
low_threshold=adjusted_threshold * 0.5,
|
|
high_threshold=adjusted_threshold * 2.0)
|
|
|
|
# Calculate local edge density for adaptive radius
|
|
kernel_size = max(3, int(adjusted_radius * 4) // 2 * 2 + 1)
|
|
edge_density = ndimage.uniform_filter(edge_map.astype(np.float32), size=kernel_size)
|
|
|
|
# Adaptive radius: smaller radius for high edge density areas
|
|
adaptive_radius = adjusted_radius * (1.0 - 0.5 * edge_density)
|
|
adaptive_radius = np.clip(adaptive_radius, adjusted_radius * 0.3, adjusted_radius * 1.5)
|
|
# FIXED: Ensure contiguous array after arithmetic operations
|
|
adaptive_radius = np.ascontiguousarray(adaptive_radius)
|
|
|
|
# Create adaptive blur using variable radius
|
|
blurred = self._adaptive_gaussian_blur(work_image, adaptive_radius)
|
|
|
|
# Calculate unsharp mask
|
|
detail = work_image - blurred
|
|
# FIXED: Ensure contiguous array after arithmetic operation
|
|
detail = np.ascontiguousarray(detail)
|
|
|
|
# Threshold-based selective sharpening with soft falloff
|
|
# MODIFIED: Instead of hard cutoff, use soft weighting for more visible results
|
|
significant_edges = np.abs(detail) > adjusted_threshold
|
|
edge_weight = np.clip(np.abs(detail) / (adjusted_threshold + 1e-6), 0, 1)
|
|
# Blend between full detail (edges) and partial detail (smooth areas)
|
|
# This gives more visible sharpening across the entire image
|
|
selective_detail = detail * (0.3 + 0.7 * edge_weight) # Min 30% effect everywhere
|
|
# FIXED: Ensure contiguous array after arithmetic operation
|
|
selective_detail = np.ascontiguousarray(selective_detail)
|
|
|
|
# Overshoot detection and control
|
|
if overshoot_protection:
|
|
selective_detail = self._control_overshoot(work_image, selective_detail, adjusted_strength)
|
|
# FIXED: Ensure contiguous array after overshoot control
|
|
selective_detail = np.ascontiguousarray(selective_detail)
|
|
|
|
# Apply sharpening
|
|
sharpened_luminance = work_image + adjusted_strength * selective_detail
|
|
sharpened_luminance = np.clip(sharpened_luminance, 0, 1)
|
|
|
|
# Reconstruct color image if needed
|
|
if is_color:
|
|
lab_result = lab_image.copy()
|
|
lab_result[:, :, 0] = sharpened_luminance * 100.0
|
|
# FIXED: Ensure contiguous array before color conversion
|
|
lab_result = np.ascontiguousarray(lab_result)
|
|
result = color.lab2rgb(lab_result)
|
|
# CRITICAL FIX: Ensure result from color.lab2rgb is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
else:
|
|
result = sharpened_luminance
|
|
# Ensure result is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
|
|
# Calculate processing info
|
|
info = {
|
|
'method': 'Smart Sharpening',
|
|
'strength': strength,
|
|
'adjusted_strength': adjusted_strength,
|
|
'base_radius': radius,
|
|
'adjusted_radius': adjusted_radius,
|
|
'threshold': threshold,
|
|
'adjusted_threshold': adjusted_threshold,
|
|
'scale_factor': scale_factor,
|
|
'image_size': f"{image.shape[0]}x{image.shape[1]}",
|
|
'overshoot_protection': overshoot_protection,
|
|
'edge_pixels_detected': int(np.sum(edge_map)),
|
|
'significant_edges': int(np.sum(significant_edges)),
|
|
'adaptive_radius_range': [float(adaptive_radius.min()), float(adaptive_radius.max())],
|
|
'luminance_only_processing': is_color
|
|
}
|
|
|
|
# Calculate quality metrics if possible
|
|
if SKIMAGE_AVAILABLE:
|
|
try:
|
|
if is_color:
|
|
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)
|
|
else:
|
|
psnr = peak_signal_noise_ratio(image, result, data_range=1.0)
|
|
ssim = structural_similarity(image, result, data_range=1.0)
|
|
info['psnr'] = psnr
|
|
info['ssim'] = ssim
|
|
except:
|
|
pass
|
|
|
|
return result, info
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"\nSmart sharpening error: {str(e)}")
|
|
traceback.print_exc()
|
|
return None, {'error': f"Smart sharpening failed: {str(e)}"}
|
|
|
|
def hiraloam_sharpening(self, image: np.ndarray, radius_ratio: float = 4.0,
|
|
amount_ratio: float = 0.25, blur_type: str = 'gaussian',
|
|
frequency_bands: int = 3) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
"""
|
|
High Radius Low Amount (HiRaLoAm) technique with multiple blur kernels
|
|
|
|
Args:
|
|
image: Input image (H, W) or (H, W, C) in range [0, 1]
|
|
radius_ratio: High radius multiplier (2.0 to 6.0)
|
|
amount_ratio: Low amount multiplier (0.1 to 0.5)
|
|
blur_type: Type of blur ('gaussian', 'bilateral', 'mixed')
|
|
frequency_bands: Number of frequency bands (2 to 5)
|
|
|
|
Returns:
|
|
Tuple of (sharpened_image, processing_info)
|
|
"""
|
|
try:
|
|
# Work in luminance for color images
|
|
if len(image.shape) == 3:
|
|
is_color = True
|
|
lab_image = color.rgb2lab(image)
|
|
# CRITICAL: Ensure lab_image is contiguous immediately after conversion
|
|
lab_image = np.ascontiguousarray(lab_image)
|
|
luminance = lab_image[:, :, 0] / 100.0
|
|
# FIXED: Ensure contiguous array for OpenCV operations
|
|
work_image = np.ascontiguousarray(luminance)
|
|
else:
|
|
is_color = False
|
|
work_image = np.ascontiguousarray(image.copy())
|
|
|
|
result = work_image.copy()
|
|
processing_details = []
|
|
|
|
# Multi-frequency band processing
|
|
for band in range(frequency_bands):
|
|
# Calculate radius for this frequency band
|
|
base_radius = 1.0 + (band * radius_ratio / frequency_bands)
|
|
|
|
# Create appropriate blur for this band
|
|
if blur_type == 'gaussian':
|
|
blurred = gaussian_filter(work_image, sigma=base_radius)
|
|
elif blur_type == 'bilateral':
|
|
# Convert to 8-bit for bilateral filter
|
|
img_8bit = (work_image * 255).astype(np.uint8)
|
|
# FIXED: Ensure contiguous array for OpenCV
|
|
img_8bit = np.ascontiguousarray(img_8bit)
|
|
try:
|
|
blurred_8bit = cv2.bilateralFilter(img_8bit,
|
|
d=int(base_radius * 2) + 1,
|
|
sigmaColor=50, sigmaSpace=50)
|
|
blurred = blurred_8bit.astype(np.float32) / 255.0
|
|
except:
|
|
# Fallback to Gaussian if bilateral filter fails
|
|
blurred = gaussian_filter(work_image, sigma=base_radius)
|
|
elif blur_type == 'mixed':
|
|
# Alternate between gaussian and bilateral
|
|
if band % 2 == 0:
|
|
blurred = gaussian_filter(work_image, sigma=base_radius)
|
|
else:
|
|
img_8bit = (work_image * 255).astype(np.uint8)
|
|
# FIXED: Ensure contiguous array for OpenCV
|
|
img_8bit = np.ascontiguousarray(img_8bit)
|
|
try:
|
|
blurred_8bit = cv2.bilateralFilter(img_8bit,
|
|
d=int(base_radius * 2) + 1,
|
|
sigmaColor=50, sigmaSpace=50)
|
|
blurred = blurred_8bit.astype(np.float32) / 255.0
|
|
except:
|
|
# Fallback to Gaussian if bilateral filter fails
|
|
blurred = gaussian_filter(work_image, sigma=base_radius)
|
|
|
|
# FIXED: Ensure blurred is contiguous before arithmetic
|
|
blurred = np.ascontiguousarray(blurred)
|
|
|
|
# Calculate detail for this frequency band
|
|
detail = work_image - blurred
|
|
# FIXED: Ensure contiguous array after arithmetic operation
|
|
detail = np.ascontiguousarray(detail)
|
|
|
|
# Apply low amount enhancement
|
|
band_amount = amount_ratio / (band + 1) # Reduce amount for higher frequencies
|
|
result = result + band_amount * detail
|
|
# FIXED: Ensure contiguous after arithmetic
|
|
result = np.ascontiguousarray(result)
|
|
|
|
processing_details.append({
|
|
'band': band,
|
|
'radius': base_radius,
|
|
'amount': band_amount,
|
|
'blur_type': blur_type if blur_type != 'mixed' else ('gaussian' if band % 2 == 0 else 'bilateral')
|
|
})
|
|
|
|
result = np.clip(result, 0, 1)
|
|
|
|
# Reconstruct color image if needed
|
|
if is_color:
|
|
lab_result = lab_image.copy()
|
|
lab_result[:, :, 0] = result * 100.0
|
|
# FIXED: Ensure contiguous array before color conversion
|
|
lab_result = np.ascontiguousarray(lab_result)
|
|
result = color.lab2rgb(lab_result)
|
|
# CRITICAL FIX: Ensure result from color.lab2rgb is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
else:
|
|
# Ensure result is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
|
|
info = {
|
|
'method': 'HiRaLoAm Sharpening',
|
|
'radius_ratio': radius_ratio,
|
|
'amount_ratio': amount_ratio,
|
|
'blur_type': blur_type,
|
|
'frequency_bands': frequency_bands,
|
|
'processing_details': processing_details,
|
|
'luminance_only_processing': is_color
|
|
}
|
|
|
|
return result, info
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"\nHiRaLoAm sharpening error: {str(e)}")
|
|
traceback.print_exc()
|
|
return None, {'error': f"HiRaLoAm sharpening failed: {str(e)}"}
|
|
|
|
def edge_directional_sharpening(self, image: np.ndarray, strength: float = 1.0,
|
|
num_directions: int = 8,
|
|
luminance_only: bool = True) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
"""
|
|
Edge-directional sharpening with orientation-specific enhancement
|
|
|
|
Args:
|
|
image: Input image (H, W) or (H, W, C) in range [0, 1]
|
|
strength: Sharpening strength (0.1 to 2.0)
|
|
num_directions: Number of directional filters (4, 8, or 16)
|
|
luminance_only: Process only luminance to avoid color artifacts
|
|
|
|
Returns:
|
|
Tuple of (sharpened_image, processing_info)
|
|
"""
|
|
try:
|
|
# Work in luminance for color images if specified
|
|
if len(image.shape) == 3 and luminance_only:
|
|
is_color = True
|
|
lab_image = color.rgb2lab(image)
|
|
# CRITICAL: Ensure lab_image is contiguous immediately after conversion
|
|
lab_image = np.ascontiguousarray(lab_image)
|
|
luminance = lab_image[:, :, 0] / 100.0
|
|
# FIXED: Ensure contiguous array for OpenCV operations
|
|
work_image = np.ascontiguousarray(luminance)
|
|
else:
|
|
is_color = False
|
|
work_image = np.ascontiguousarray(image.copy())
|
|
|
|
# Create directional filters
|
|
directional_responses = []
|
|
angles = np.linspace(0, np.pi, num_directions, endpoint=False)
|
|
|
|
for i, angle in enumerate(angles):
|
|
# Create oriented edge detection kernel
|
|
kernel = self._create_directional_kernel(angle, size=5)
|
|
|
|
# Apply directional filter
|
|
response = ndimage.convolve(work_image, kernel, mode='reflect')
|
|
directional_responses.append(response)
|
|
|
|
# Combine directional responses with maximum response selection
|
|
max_response = np.zeros_like(work_image)
|
|
direction_map = np.zeros_like(work_image, dtype=np.int32)
|
|
|
|
for i, response in enumerate(directional_responses):
|
|
mask = np.abs(response) > np.abs(max_response)
|
|
max_response[mask] = response[mask]
|
|
direction_map[mask] = i
|
|
|
|
# Apply orientation-specific enhancement
|
|
enhanced = work_image + strength * max_response
|
|
enhanced = np.clip(enhanced, 0, 1)
|
|
|
|
# Reconstruct color image if needed
|
|
if is_color:
|
|
lab_result = lab_image.copy()
|
|
lab_result[:, :, 0] = enhanced * 100.0
|
|
# FIXED: Ensure contiguous array before color conversion
|
|
lab_result = np.ascontiguousarray(lab_result)
|
|
result = color.lab2rgb(lab_result)
|
|
# CRITICAL FIX: Ensure result from color.lab2rgb is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
else:
|
|
result = enhanced
|
|
# Ensure result is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
|
|
info = {
|
|
'method': 'Edge-Directional Sharpening',
|
|
'strength': strength,
|
|
'num_directions': num_directions,
|
|
'luminance_only': luminance_only,
|
|
'direction_angles': angles.tolist(),
|
|
'max_response_range': [float(max_response.min()), float(max_response.max())],
|
|
'dominant_directions': [int(d) for d in np.unique(direction_map)]
|
|
}
|
|
|
|
return result, info
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"\nEdge-directional sharpening error: {str(e)}")
|
|
traceback.print_exc()
|
|
return None, {'error': f"Edge-directional sharpening failed: {str(e)}"}
|
|
|
|
def multiscale_laplacian_sharpening(self, image: np.ndarray, strength: float = 1.0,
|
|
scales: List[float] = [0.5, 1.0, 2.0, 4.0],
|
|
kernel_sizes: List[int] = [3, 5, 7, 9],
|
|
scaling_factors: List[float] = [0.2, 0.4, 0.6, 0.8]) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
"""
|
|
Multi-scale Laplacian pyramid sharpening with adaptive scaling
|
|
|
|
Args:
|
|
image: Input image (H, W) or (H, W, C) in range [0, 1]
|
|
strength: Overall sharpening strength (0.1 to 2.0)
|
|
scales: Gaussian pyramid scales
|
|
kernel_sizes: Laplacian kernel sizes (must match scales length)
|
|
scaling_factors: Adaptive scaling factors (0.2-0.8 range)
|
|
|
|
Returns:
|
|
Tuple of (sharpened_image, processing_info)
|
|
"""
|
|
try:
|
|
# Ensure parameter lists have same length
|
|
num_scales = len(scales)
|
|
if len(kernel_sizes) != num_scales:
|
|
kernel_sizes = kernel_sizes[:num_scales] + [kernel_sizes[-1]] * (num_scales - len(kernel_sizes))
|
|
if len(scaling_factors) != num_scales:
|
|
scaling_factors = scaling_factors[:num_scales] + [scaling_factors[-1]] * (num_scales - len(scaling_factors))
|
|
|
|
# Work in luminance for color images
|
|
if len(image.shape) == 3:
|
|
is_color = True
|
|
lab_image = color.rgb2lab(image)
|
|
# CRITICAL: Ensure lab_image is contiguous immediately after conversion
|
|
lab_image = np.ascontiguousarray(lab_image)
|
|
luminance = lab_image[:, :, 0] / 100.0
|
|
# FIXED: Ensure contiguous array for OpenCV operations
|
|
work_image = np.ascontiguousarray(luminance)
|
|
else:
|
|
is_color = False
|
|
work_image = np.ascontiguousarray(image.copy())
|
|
|
|
# Build Gaussian pyramid
|
|
pyramid = [work_image]
|
|
for scale in scales[1:]:
|
|
sigma = scale
|
|
blurred = gaussian_filter(work_image, sigma=sigma)
|
|
pyramid.append(blurred)
|
|
|
|
# Calculate Laplacian pyramid
|
|
laplacian_pyramid = []
|
|
scale_details = []
|
|
|
|
for i in range(len(pyramid) - 1):
|
|
# Calculate Laplacian
|
|
laplacian = pyramid[i] - pyramid[i + 1]
|
|
|
|
# Apply kernel-specific enhancement
|
|
kernel_size = kernel_sizes[i]
|
|
if kernel_size > 3:
|
|
# Create custom Laplacian kernel
|
|
kernel = self._create_laplacian_kernel(kernel_size)
|
|
enhanced_laplacian = ndimage.convolve(laplacian, kernel, mode='reflect')
|
|
else:
|
|
enhanced_laplacian = laplacian
|
|
|
|
# Apply adaptive scaling
|
|
scaling_factor = scaling_factors[i]
|
|
scaled_laplacian = enhanced_laplacian * scaling_factor
|
|
laplacian_pyramid.append(scaled_laplacian)
|
|
|
|
scale_details.append({
|
|
'scale': scales[i],
|
|
'kernel_size': kernel_size,
|
|
'scaling_factor': scaling_factor,
|
|
'laplacian_range': [float(scaled_laplacian.min()), float(scaled_laplacian.max())]
|
|
})
|
|
|
|
# Reconstruct enhanced image
|
|
result = pyramid[-1] # Start with finest blur
|
|
for laplacian in reversed(laplacian_pyramid):
|
|
result = result + strength * laplacian
|
|
# FIXED: Ensure contiguous after each addition
|
|
result = np.ascontiguousarray(result)
|
|
|
|
result = np.clip(result, 0, 1)
|
|
|
|
# Reconstruct color image if needed
|
|
if is_color:
|
|
lab_result = lab_image.copy()
|
|
lab_result[:, :, 0] = result * 100.0
|
|
# FIXED: Ensure contiguous array before color conversion
|
|
lab_result = np.ascontiguousarray(lab_result)
|
|
result = color.lab2rgb(lab_result)
|
|
# CRITICAL FIX: Ensure result from color.lab2rgb is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
else:
|
|
# Ensure result is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
|
|
info = {
|
|
'method': 'Multi-scale Laplacian Sharpening',
|
|
'strength': strength,
|
|
'num_scales': num_scales,
|
|
'scales': scales,
|
|
'kernel_sizes': kernel_sizes,
|
|
'scaling_factors': scaling_factors,
|
|
'scale_details': scale_details,
|
|
'luminance_only_processing': is_color
|
|
}
|
|
|
|
return result, info
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"\nMulti-scale Laplacian sharpening error: {str(e)}")
|
|
traceback.print_exc()
|
|
return None, {'error': f"Multi-scale Laplacian sharpening failed: {str(e)}"}
|
|
|
|
def guided_filter_sharpening(self, image: np.ndarray, strength: float = 1.0,
|
|
radius: int = 8, epsilon: float = 0.1) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
"""
|
|
Guided filtering with edge-preserving smoothing and sharpening feedback
|
|
|
|
Args:
|
|
image: Input image (H, W) or (H, W, C) in range [0, 1]
|
|
strength: Sharpening strength (0.1 to 2.0)
|
|
radius: Guided filter radius (4 to 16)
|
|
epsilon: Regularization parameter (0.01 to 1.0)
|
|
|
|
Returns:
|
|
Tuple of (sharpened_image, processing_info)
|
|
"""
|
|
try:
|
|
# Work in luminance for color images
|
|
if len(image.shape) == 3:
|
|
is_color = True
|
|
lab_image = color.rgb2lab(image)
|
|
# CRITICAL: Ensure lab_image is contiguous immediately after conversion
|
|
lab_image = np.ascontiguousarray(lab_image)
|
|
luminance = lab_image[:, :, 0] / 100.0
|
|
# FIXED: Ensure contiguous array for OpenCV operations
|
|
work_image = np.ascontiguousarray(luminance)
|
|
else:
|
|
is_color = False
|
|
work_image = np.ascontiguousarray(image.copy())
|
|
|
|
# Apply guided filter (edge-preserving smoothing)
|
|
guided_filtered = self._guided_filter(work_image, work_image, radius, epsilon)
|
|
|
|
# Calculate detail layer
|
|
detail = work_image - guided_filtered
|
|
# FIXED: Ensure contiguous array after arithmetic operation
|
|
detail = np.ascontiguousarray(detail)
|
|
|
|
# Apply edge-preserving enhancement with feedback
|
|
# Use bilateral filter to preserve edge structure in detail layer
|
|
detail_preserved = denoise_bilateral(detail, sigma_color=0.1, sigma_spatial=radius//2)
|
|
# Ensure contiguous after denoise_bilateral
|
|
detail_preserved = np.ascontiguousarray(detail_preserved)
|
|
|
|
# Combine with sharpening feedback
|
|
enhanced = work_image + strength * detail_preserved
|
|
enhanced = np.clip(enhanced, 0, 1)
|
|
|
|
# Reconstruct color image if needed
|
|
if is_color:
|
|
lab_result = lab_image.copy()
|
|
lab_result[:, :, 0] = enhanced * 100.0
|
|
# FIXED: Ensure contiguous array before color conversion
|
|
lab_result = np.ascontiguousarray(lab_result)
|
|
result = color.lab2rgb(lab_result)
|
|
# CRITICAL FIX: Ensure result from color.lab2rgb is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
else:
|
|
result = enhanced
|
|
# Ensure result is contiguous
|
|
result = np.ascontiguousarray(result)
|
|
|
|
info = {
|
|
'method': 'Guided Filter Sharpening',
|
|
'strength': strength,
|
|
'radius': radius,
|
|
'epsilon': epsilon,
|
|
'detail_range': [float(detail.min()), float(detail.max())],
|
|
'preserved_detail_range': [float(detail_preserved.min()), float(detail_preserved.max())],
|
|
'luminance_only_processing': is_color
|
|
}
|
|
|
|
return result, info
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"\nGuided filter sharpening error: {str(e)}")
|
|
traceback.print_exc()
|
|
return None, {'error': f"Guided filter sharpening failed: {str(e)}"}
|
|
|
|
def process_image(self, image: np.ndarray, method: str = 'smart', **kwargs) -> Tuple[Optional[np.ndarray], Dict[str, Any]]:
|
|
"""
|
|
Main processing method with automatic method selection
|
|
|
|
Args:
|
|
image: Input image (H, W) or (H, W, C) in range [0, 1]
|
|
method: Sharpening method ('smart', 'hiraloam', 'directional', 'multiscale', 'guided', 'auto')
|
|
**kwargs: Method-specific parameters
|
|
|
|
Returns:
|
|
Tuple of (processed_image, processing_info)
|
|
"""
|
|
try:
|
|
if method == 'auto':
|
|
# Automatic method selection based on image characteristics
|
|
method = self._select_optimal_method(image)
|
|
kwargs['auto_selected'] = True
|
|
|
|
if method == 'smart':
|
|
return self.smart_sharpening(image, **kwargs)
|
|
elif method == 'hiraloam':
|
|
return self.hiraloam_sharpening(image, **kwargs)
|
|
elif method == 'directional':
|
|
return self.edge_directional_sharpening(image, **kwargs)
|
|
elif method == 'multiscale':
|
|
return self.multiscale_laplacian_sharpening(image, **kwargs)
|
|
elif method == 'guided':
|
|
return self.guided_filter_sharpening(image, **kwargs)
|
|
else:
|
|
return None, {'error': f"Unknown sharpening method: {method}"}
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"\nAdvanced sharpening error: {str(e)}")
|
|
traceback.print_exc()
|
|
return None, {'error': f"Advanced sharpening processing failed: {str(e)}"}
|
|
|
|
# Helper methods
|
|
def _adaptive_gaussian_blur(self, image: np.ndarray, radius_map: np.ndarray) -> np.ndarray:
|
|
"""Apply Gaussian blur with spatially varying radius"""
|
|
# Approximate adaptive blur with multiple fixed-radius blurs
|
|
# This is a simplified implementation - full adaptive blur would require more complex algorithms
|
|
|
|
# Sample a few different radii
|
|
radii = [radius_map.min(), np.median(radius_map), radius_map.max()]
|
|
|
|
# Create blurred versions
|
|
blurred_versions = []
|
|
for r in radii:
|
|
blurred = gaussian_filter(image, sigma=r)
|
|
blurred_versions.append(blurred)
|
|
|
|
# Interpolate based on local radius
|
|
result = np.zeros_like(image)
|
|
for i in range(image.shape[0]):
|
|
for j in range(image.shape[1]):
|
|
local_radius = radius_map[i, j]
|
|
|
|
# Find interpolation weights
|
|
if local_radius <= radii[0]:
|
|
result[i, j] = blurred_versions[0][i, j]
|
|
elif local_radius >= radii[-1]:
|
|
result[i, j] = blurred_versions[-1][i, j]
|
|
else:
|
|
# Linear interpolation between nearest radii
|
|
for k in range(len(radii) - 1):
|
|
if radii[k] <= local_radius <= radii[k + 1]:
|
|
t = (local_radius - radii[k]) / (radii[k + 1] - radii[k])
|
|
result[i, j] = (1 - t) * blurred_versions[k][i, j] + t * blurred_versions[k + 1][i, j]
|
|
break
|
|
|
|
# FIXED: Ensure contiguous array after pixel-wise construction
|
|
return np.ascontiguousarray(result)
|
|
|
|
def _control_overshoot(self, original: np.ndarray, detail: np.ndarray, strength: float) -> np.ndarray:
|
|
"""Control overshoot artifacts in sharpening"""
|
|
# Calculate potential result
|
|
potential_result = original + strength * detail
|
|
|
|
# Detect overshoot (values outside [0, 1] range)
|
|
overshoot_mask = (potential_result < 0) | (potential_result > 1)
|
|
|
|
# Reduce detail strength in overshoot areas
|
|
controlled_detail = detail.copy()
|
|
|
|
# For negative overshoot
|
|
negative_mask = potential_result < 0
|
|
if np.any(negative_mask):
|
|
# Reduce detail to prevent going below 0
|
|
max_negative_detail = -original[negative_mask]
|
|
controlled_detail[negative_mask] = np.maximum(detail[negative_mask], max_negative_detail / strength)
|
|
|
|
# For positive overshoot
|
|
positive_mask = potential_result > 1
|
|
if np.any(positive_mask):
|
|
# Reduce detail to prevent going above 1
|
|
max_positive_detail = (1 - original[positive_mask])
|
|
controlled_detail[positive_mask] = np.minimum(detail[positive_mask], max_positive_detail / strength)
|
|
|
|
return controlled_detail
|
|
|
|
def _create_directional_kernel(self, angle: float, size: int = 5) -> np.ndarray:
|
|
"""Create edge detection kernel for specific direction"""
|
|
kernel = np.zeros((size, size))
|
|
center = size // 2
|
|
|
|
# Create oriented edge kernel using Sobel-like approach
|
|
cos_angle = np.cos(angle)
|
|
sin_angle = np.sin(angle)
|
|
|
|
for i in range(size):
|
|
for j in range(size):
|
|
x = j - center
|
|
y = i - center
|
|
|
|
# Project coordinates onto direction
|
|
parallel = x * cos_angle + y * sin_angle
|
|
perpendicular = -x * sin_angle + y * cos_angle
|
|
|
|
# Create edge response (derivative in perpendicular direction)
|
|
if abs(perpendicular) <= 1:
|
|
kernel[i, j] = np.sign(perpendicular) * np.exp(-parallel**2 / 2)
|
|
|
|
# Normalize kernel
|
|
kernel = kernel / (np.sum(np.abs(kernel)) + 1e-10)
|
|
|
|
return kernel
|
|
|
|
def _create_laplacian_kernel(self, size: int) -> np.ndarray:
|
|
"""Create Laplacian kernel of specified size"""
|
|
if size == 3:
|
|
return np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]])
|
|
elif size == 5:
|
|
return np.array([
|
|
[0, 0, -1, 0, 0],
|
|
[0, -1, -2, -1, 0],
|
|
[-1, -2, 16, -2, -1],
|
|
[0, -1, -2, -1, 0],
|
|
[0, 0, -1, 0, 0]
|
|
]) / 8
|
|
else:
|
|
# Generate larger Laplacian kernel
|
|
kernel = np.zeros((size, size))
|
|
center = size // 2
|
|
|
|
# Simple approach: negative weight at distance 1, positive at center
|
|
for i in range(size):
|
|
for j in range(size):
|
|
if i == center and j == center:
|
|
kernel[i, j] = (size - 1) * 4 # Positive center
|
|
elif abs(i - center) + abs(j - center) == 1:
|
|
kernel[i, j] = -1 # Negative neighbors
|
|
|
|
return kernel
|
|
|
|
def _guided_filter(self, guide: np.ndarray, input_img: np.ndarray,
|
|
radius: int, epsilon: float) -> np.ndarray:
|
|
"""Guided filter implementation"""
|
|
# Box filter
|
|
def box_filter(img, r):
|
|
return ndimage.uniform_filter(img.astype(np.float64), size=2*r+1, mode='reflect')
|
|
|
|
# Mean and variance of guide
|
|
mean_guide = box_filter(guide, radius)
|
|
mean_input = box_filter(input_img, radius)
|
|
mean_guide_input = box_filter(guide * input_img, radius)
|
|
|
|
# Covariance and variance
|
|
cov_guide_input = mean_guide_input - mean_guide * mean_input
|
|
var_guide = box_filter(guide * guide, radius) - mean_guide * mean_guide
|
|
|
|
# Coefficients
|
|
a = cov_guide_input / (var_guide + epsilon)
|
|
b = mean_input - a * mean_guide
|
|
|
|
# Filter coefficients
|
|
mean_a = box_filter(a, radius)
|
|
mean_b = box_filter(b, radius)
|
|
|
|
# Output
|
|
output = mean_a * guide + mean_b
|
|
# FIXED: Ensure contiguous after arithmetic
|
|
output = np.ascontiguousarray(output)
|
|
|
|
return output
|
|
|
|
def _select_optimal_method(self, image: np.ndarray) -> str:
|
|
"""Automatically select optimal sharpening method based on image characteristics"""
|
|
# Analyze image characteristics
|
|
if len(image.shape) == 3:
|
|
try:
|
|
gray = color.rgb2gray(image)
|
|
except:
|
|
# Fallback if color conversion fails
|
|
gray = np.mean(image, axis=2)
|
|
else:
|
|
gray = image
|
|
|
|
# Calculate image metrics with error handling
|
|
try:
|
|
edges = feature.canny(gray, sigma=1.0)
|
|
edge_density = np.mean(edges)
|
|
except:
|
|
edge_density = 0.0
|
|
|
|
# Calculate local variance (texture measure)
|
|
try:
|
|
local_variance = ndimage.generic_filter(gray, np.var, size=5)
|
|
texture_level = np.mean(local_variance)
|
|
except:
|
|
texture_level = 0.0
|
|
|
|
# Calculate gradient magnitude
|
|
try:
|
|
grad_x = ndimage.sobel(gray, axis=0)
|
|
grad_y = ndimage.sobel(gray, axis=1)
|
|
gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2)
|
|
gradient_level = np.mean(gradient_magnitude)
|
|
except:
|
|
gradient_level = 0.0
|
|
|
|
# Decision logic with fallbacks
|
|
if edge_density > 0.1 and gradient_level > 0.2:
|
|
return 'directional' # High edge content - use directional
|
|
elif texture_level > 0.01:
|
|
return 'multiscale' # High texture - use multi-scale
|
|
elif edge_density > 0.05:
|
|
return 'guided' # Moderate edges - use guided filter
|
|
elif gradient_level < 0.1:
|
|
return 'hiraloam' # Low contrast - use HiRaLoAm
|
|
else:
|
|
return 'smart' # Default to smart sharpening
|
|
|
|
def get_method_info(self) -> Dict[str, str]:
|
|
"""Get information about available sharpening methods"""
|
|
return {
|
|
'smart': 'Smart Sharpening with overshoot detection and adaptive radius control',
|
|
'hiraloam': 'High Radius Low Amount technique with multiple blur kernels',
|
|
'directional': 'Edge-directional sharpening with orientation-specific enhancement',
|
|
'multiscale': 'Multi-scale Laplacian pyramid sharpening with adaptive scaling',
|
|
'guided': 'Guided filtering with edge-preserving smoothing and feedback',
|
|
'auto': 'Automatic method selection based on image characteristics'
|
|
}
|