Files
ryanontheinside-ComfyUI_Rea…/image_enhance.py
2025-03-26 15:39:49 +00:00

497 lines
18 KiB
Python

import torch
import torch.nn.functional as F
class HistogramStretch:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"clip_percent": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 0.5, "step": 0.05}),
"amount": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, clip_percent, amount):
# Automatically stretch histogram for better contrast
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Calculate histogram bounds
low = torch.quantile(x, clip_percent, dim=2, keepdim=True)
low = torch.quantile(low, clip_percent, dim=3, keepdim=True)
high = torch.quantile(x, 1.0 - clip_percent, dim=2, keepdim=True)
high = torch.quantile(high, 1.0 - clip_percent, dim=3, keepdim=True)
# Stretch contrast with amount control
enhanced = x + amount * ((x - low) / (high - low) - x)
enhanced = torch.clamp(enhanced, 0, 1)
return (enhanced.permute(0, 2, 3, 1),)
class AdaptiveSharpen:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"max_amount": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
"edge_threshold": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 10.0, "step": 0.5}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, max_amount, edge_threshold):
# Content-aware sharpening using Laplacian
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Laplacian kernel for edge detection
laplacian = torch.tensor([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=torch.float32, device=x.device).view(1, 1, 3, 3)
# Detect edges
edges = F.conv2d(x, laplacian.expand(C, 1, 3, 3), padding=1, groups=C)
edges = torch.abs(edges)
# Adaptive amount based on edge strength
amount = torch.sigmoid(edges * edge_threshold) * max_amount
# Apply sharpening
enhanced = x + amount * edges
enhanced = torch.clamp(enhanced, 0, 1)
return (enhanced.permute(0, 2, 3, 1),)
class ChannelBalance:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"amount": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, amount):
# Automatic RGB channel balancing
B, H, W, C = image.shape
# Calculate mean for each channel
means = image.mean(dim=(0, 1, 2), keepdim=True)
target_mean = means.mean()
# Adjust each channel to match target mean with amount control
balance = (target_mean / means)
enhanced = image + amount * (image * balance - image)
enhanced = torch.clamp(enhanced, 0, 1)
return (enhanced,)
class MultiScaleDetail:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"small_amount": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.005}),
"medium_amount": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.005}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, small_amount, medium_amount):
# Multi-scale detail enhancement with zero-sum kernels
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Small details (3x3) - zero-sum kernel
small_kernel = torch.tensor([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], dtype=torch.float32, device=x.device).view(1, 1, 3, 3)
small_details = F.conv2d(x, small_kernel.expand(C, 1, 3, 3), padding=1, groups=C)
# Medium details (5x5) - zero-sum kernel
medium_kernel = torch.tensor([[-1, -1, -1, -1, -1], [-1, 2, 2, 2, -1], [-1, 2, 8, 2, -1], [-1, 2, 2, 2, -1], [-1, -1, -1, -1, -1]], dtype=torch.float32, device=x.device).view(1, 1, 5, 5)
medium_kernel = medium_kernel - medium_kernel.mean() # Ensure zero-sum
medium_details = F.conv2d(x, medium_kernel.expand(C, 1, 5, 5), padding=2, groups=C)
# Combine details with adjustable weighting
enhanced = x + small_amount * small_details + medium_amount * medium_details
enhanced = torch.clamp(enhanced, 0, 1)
return (enhanced.permute(0, 2, 3, 1),)
class UnsharpMask:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"blur_radius": ("FLOAT", {"default": 1.2, "min": 0.1, "max": 10.0, "step": 0.1}),
"amount": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 3.0, "step": 0.05}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "sharpen"
CATEGORY = "image/enhance"
def sharpen(self, image, blur_radius, amount):
# Input: (B, H, W, C)
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Gaussian kernel (separable)
def gaussian_kernel1d(radius, sigma):
size = int(2 * radius + 1)
coords = torch.arange(size) - radius
kernel = torch.exp(-coords**2 / (2 * sigma**2))
kernel /= kernel.sum()
return kernel.to(x.device)
radius = int(blur_radius)
kernel = gaussian_kernel1d(radius, sigma=blur_radius)
# Create vertical and horizontal kernels
kernel_v = kernel.view(1, 1, -1, 1)
kernel_h = kernel.view(1, 1, 1, -1)
# Blur H and W
blurred = F.conv2d(x, kernel_v.expand(C, 1, -1, 1), padding=(0, radius), groups=C)
blurred = F.conv2d(blurred, kernel_h.expand(C, 1, 1, -1), padding=(radius, 0), groups=C)
# Unsharp mask
sharpened = x + amount * (x - blurred)
sharpened = torch.clamp(sharpened, 0, 1)
return (sharpened.permute(0, 2, 3, 1),)
class ICAS:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"amount": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 2.0}),
"radius": ("INT", {"default": 2, "min": 1, "max": 5}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, amount, radius):
# Image Contrast Adaptive Sharpen
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Efficient Gaussian blur using separable kernels
def gaussian_kernel1d(radius, sigma):
size = int(2 * radius + 1)
coords = torch.arange(size) - radius
kernel = torch.exp(-coords**2 / (2 * sigma**2))
kernel /= kernel.sum()
return kernel.to(x.device)
# Create and apply Gaussian blur
kernel = gaussian_kernel1d(radius, sigma=radius)
kernel_v = kernel.view(1, 1, -1, 1)
kernel_h = kernel.view(1, 1, 1, -1)
# Apply separable blur
blurred = F.conv2d(x, kernel_v.expand(C, 1, -1, 1), padding=(0, radius), groups=C)
blurred = F.conv2d(blurred, kernel_h.expand(C, 1, 1, -1), padding=(radius, 0), groups=C)
# Calculate local contrast
local_contrast = torch.abs(x - blurred)
local_contrast = F.avg_pool2d(local_contrast, kernel_size=3, stride=1, padding=1)
# Adaptive sharpening mask
mask = torch.sigmoid(local_contrast * 10.0) # Adjust sensitivity
mask = mask * amount
# Apply sharpening
sharpened = x + mask * (x - blurred)
sharpened = torch.clamp(sharpened, 0, 1)
return (sharpened.permute(0, 2, 3, 1),)
class FastBilateralDetail:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"amount": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 2.0}),
"radius": ("INT", {"default": 3, "min": 1, "max": 7}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, amount, radius):
# Fast bilateral filter-based detail enhancement
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Efficient bilateral filter approximation using separable Gaussian
def gaussian_kernel1d(radius, sigma):
size = int(2 * radius + 1)
coords = torch.arange(size) - radius
kernel = torch.exp(-coords**2 / (2 * sigma**2))
kernel /= kernel.sum()
return kernel.to(x.device)
# Create kernels
kernel = gaussian_kernel1d(radius, sigma=radius)
kernel_v = kernel.view(1, 1, -1, 1)
kernel_h = kernel.view(1, 1, 1, -1)
# Apply separable Gaussian blur
blurred = F.conv2d(x, kernel_v.expand(C, 1, -1, 1), padding=(0, radius), groups=C)
blurred = F.conv2d(blurred, kernel_h.expand(C, 1, 1, -1), padding=(radius, 0), groups=C)
# Calculate edge strength using Sobel
sobel_x = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=torch.float32, device=x.device).view(1, 1, 3, 3)
sobel_y = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=torch.float32, device=x.device).view(1, 1, 3, 3)
grad_x = F.conv2d(x, sobel_x.expand(C, 1, 3, 3), padding=1, groups=C)
grad_y = F.conv2d(x, sobel_y.expand(C, 1, 3, 3), padding=1, groups=C)
edge_strength = torch.sqrt(grad_x**2 + grad_y**2)
edge_strength = F.avg_pool2d(edge_strength, kernel_size=3, stride=1, padding=1)
# Create adaptive mask based on edge strength
mask = torch.sigmoid(edge_strength * 5.0) # Adjust sensitivity
mask = mask * amount
# Apply detail enhancement
enhanced = x + mask * (x - blurred)
enhanced = torch.clamp(enhanced, 0, 1)
return (enhanced.permute(0, 2, 3, 1),)
class WaveletDetail:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"amount": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 2.0}),
"scale": ("INT", {"default": 2, "min": 1, "max": 4}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, amount, scale):
# Wavelet-based detail enhancement
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Haar wavelet decomposition
def haar_decompose(x):
# Downsample
x_low = F.avg_pool2d(x, kernel_size=2, stride=2)
# Upsample and subtract for detail
x_high = x - F.interpolate(x_low, size=x.shape[2:], mode='bilinear', align_corners=False)
return x_low, x_high
# Multi-scale decomposition
details = []
current = x
for _ in range(scale):
current, detail = haar_decompose(current)
# Upsample detail to match original size
detail = F.interpolate(detail, size=(H, W), mode='bilinear', align_corners=False)
details.append(detail)
# Enhance details
enhanced = x
for detail in details:
enhanced = enhanced + amount * detail
enhanced = torch.clamp(enhanced, 0, 1)
return (enhanced.permute(0, 2, 3, 1),)
class FrequencySelective:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"amount": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 2.0}),
"radius": ("INT", {"default": 3, "min": 1, "max": 7}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, amount, radius):
# Frequency-selective detail enhancement
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Efficient Gaussian blur using separable kernels
def gaussian_kernel1d(radius, sigma):
size = int(2 * radius + 1)
coords = torch.arange(size) - radius
kernel = torch.exp(-coords**2 / (2 * sigma**2))
kernel /= kernel.sum()
return kernel.to(x.device)
# Create kernels
kernel = gaussian_kernel1d(radius, sigma=radius)
kernel_v = kernel.view(1, 1, -1, 1)
kernel_h = kernel.view(1, 1, 1, -1)
# Apply separable Gaussian blur
blurred = F.conv2d(x, kernel_v.expand(C, 1, -1, 1), padding=(0, radius), groups=C)
blurred = F.conv2d(blurred, kernel_h.expand(C, 1, 1, -1), padding=(radius, 0), groups=C)
# Frequency separation
high_freq = x - blurred
low_freq = blurred
# Enhance high frequencies
enhanced = low_freq + amount * high_freq
enhanced = torch.clamp(enhanced, 0, 1)
return (enhanced.permute(0, 2, 3, 1),)
class GuidedDetail:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"amount": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 2.0}),
"radius": ("INT", {"default": 3, "min": 1, "max": 7}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "enhance"
CATEGORY = "image/enhance"
def enhance(self, image, amount, radius):
# Guided filter-based detail enhancement
B, H, W, C = image.shape
x = image.permute(0, 3, 1, 2) # to BCHW
# Efficient Gaussian blur using separable kernels
def gaussian_kernel1d(radius, sigma):
size = int(2 * radius + 1)
coords = torch.arange(size) - radius
kernel = torch.exp(-coords**2 / (2 * sigma**2))
kernel /= kernel.sum()
return kernel.to(x.device)
# Create kernels
kernel = gaussian_kernel1d(radius, sigma=radius)
kernel_v = kernel.view(1, 1, -1, 1)
kernel_h = kernel.view(1, 1, 1, -1)
# Apply separable Gaussian blur
blurred = F.conv2d(x, kernel_v.expand(C, 1, -1, 1), padding=(0, radius), groups=C)
blurred = F.conv2d(blurred, kernel_h.expand(C, 1, 1, -1), padding=(radius, 0), groups=C)
# Calculate local variance
local_mean = F.avg_pool2d(x, kernel_size=2*radius+1, stride=1, padding=radius)
local_var = F.avg_pool2d(x**2, kernel_size=2*radius+1, stride=1, padding=radius) - local_mean**2
# Create adaptive mask based on local variance
mask = torch.sigmoid(local_var * 10.0) # Adjust sensitivity
mask = mask * amount
# Apply detail enhancement
enhanced = x + mask * (x - blurred)
enhanced = torch.clamp(enhanced, 0, 1)
return (enhanced.permute(0, 2, 3, 1),)
class ImageConcat:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image1": ("IMAGE",),
"image2": ("IMAGE",),
"direction": (["horizontal", "vertical"],),
"spacing": ("INT", {"default": 0, "min": 0, "max": 100}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "concat"
CATEGORY = "image/enhance"
def concat(self, image1, image2, direction, spacing):
# Ensure both images have the same number of channels
if image1.shape[-1] != image2.shape[-1]:
raise ValueError("Images must have the same number of channels")
# Add spacing
if spacing > 0:
if direction == "horizontal":
spacer = torch.zeros((image1.shape[0], image1.shape[1], spacing, image1.shape[3]), device=image1.device)
return (torch.cat([image1, spacer, image2], dim=2),)
else:
spacer = torch.zeros((image1.shape[0], spacing, image1.shape[2], image1.shape[3]), device=image1.device)
return (torch.cat([image1, spacer, image2], dim=1),)
# Concatenate without spacing
if direction == "horizontal":
return (torch.cat([image1, image2], dim=2),)
else:
return (torch.cat([image1, image2], dim=1),)
NODE_CLASS_MAPPINGS = {
"HistogramStretch": HistogramStretch,
"AdaptiveSharpen": AdaptiveSharpen,
"ChannelBalance": ChannelBalance,
"MultiScaleDetail": MultiScaleDetail,
"UnsharpMask": UnsharpMask,
"ICAS": ICAS,
"FastBilateralDetail": FastBilateralDetail,
"WaveletDetail": WaveletDetail,
"FrequencySelective": FrequencySelective,
"GuidedDetail": GuidedDetail,
"ImageConcat": ImageConcat,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"HistogramStretch": "Histogram Stretch",
"AdaptiveSharpen": "Adaptive Sharpen",
"ChannelBalance": "Channel Balance",
"MultiScaleDetail": "Multi-Scale Detail",
"UnsharpMask": "Unsharp Mask",
"ICAS": "Image Contrast Adaptive Sharpen",
"FastBilateralDetail": "Fast Bilateral Detail",
"WaveletDetail": "Wavelet Detail",
"FrequencySelective": "Frequency Selective",
"GuidedDetail": "Guided Detail",
"ImageConcat": "Image Concat",
}