482 lines
19 KiB
Python
482 lines
19 KiB
Python
# Copyright (c) 2024 Sousakujikken HIRO
|
|
# This software is released under the MIT License.
|
|
# https://opensource.org/licenses/MIT
|
|
|
|
import torch
|
|
import torch.nn.functional as F
|
|
|
|
class CRTLikeEffectNode:
|
|
kernel_cache = {}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"gaussian_width_x": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 3.0, "step": 0.1}),
|
|
"gaussian_width_y": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 3.0, "step": 0.1}),
|
|
"intensity_scale": ("FLOAT", {"default": 5.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
|
"gamma": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 5.0, "step": 0.1}),
|
|
"gaussian_kernel_size": ([5, 7, 9, 11, 13, 15], {"default": 11}),
|
|
"enable_resize": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
|
|
"resize_pixels": ([128, 192, 256, 320, 384, 448, 512], {"default": 256}),
|
|
"max_batch_size": ([1, 2, 4, 8, 16, 24, 32, 48, 64], {"default": 4}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "apply_crt_effect"
|
|
CATEGORY = "image/Pixydust Quantizer🧚✨"
|
|
|
|
def get_gaussian_kernel(self, kernel_size, width_x, width_y, gamma, device, dtype):
|
|
key = (kernel_size, width_x, width_y, gamma)
|
|
if key not in self.kernel_cache:
|
|
x = torch.arange(kernel_size, device=device, dtype=dtype).float()
|
|
y = torch.arange(kernel_size, device=device, dtype=dtype).float()
|
|
Y, X = torch.meshgrid(y, x, indexing='ij')
|
|
|
|
distance_squared = (((X - (kernel_size / 2 - 0.5)) / width_x) ** 2 +
|
|
((Y - (kernel_size / 2 - 0.5)) / width_y) ** 2)
|
|
gaussian = torch.exp(-distance_squared / 2)
|
|
gaussian = torch.pow(gaussian, 1 / gamma)
|
|
gaussian = gaussian / gaussian.sum()
|
|
gaussian = gaussian.unsqueeze(0).unsqueeze(0)
|
|
|
|
self.kernel_cache[key] = gaussian.to(device=device, dtype=dtype)
|
|
return self.kernel_cache[key]
|
|
|
|
def process_batch(self, batch, gaussian_kernel, device, intensity_scale, gamma):
|
|
"""
|
|
単一のミニバッチを処理。データ型の一貫性を保証。
|
|
"""
|
|
# バッチとカーネルを同じデータ型(half)に変換
|
|
batch = batch.half()
|
|
gaussian_kernel = gaussian_kernel.half() # カーネルもhalf精度に変換
|
|
|
|
B, C, H, W = batch.shape
|
|
|
|
# 4倍にアップスケール
|
|
upscaled = F.interpolate(batch, scale_factor=4, mode='nearest')
|
|
_, _, H4, W4 = upscaled.shape
|
|
|
|
# インデックス生成(bool型のままで問題ない)
|
|
rows = torch.arange(H4, device=device).view(1, 1, H4, 1) % 4
|
|
cols = torch.arange(W4, device=device).view(1, 1, 1, W4) % 4
|
|
rows = rows.expand(B, C, H4, W4)
|
|
cols = cols.expand(B, C, H4, W4)
|
|
|
|
# マスク生成(half精度に変換)
|
|
R_mask = ((cols == 0) & (rows < 3)).half()
|
|
G_mask = ((cols == 1) & (rows < 3)).half()
|
|
B_mask = ((cols == 2) & (rows < 3)).half()
|
|
|
|
# シフト処理
|
|
shifted = torch.zeros_like(upscaled)
|
|
shifted[:, 0, :, :] = upscaled[:, 0, :, :] * R_mask[:, 0, :, :]
|
|
shifted[:, 1, :, :] = upscaled[:, 1, :, :] * G_mask[:, 1, :, :]
|
|
shifted[:, 2, :, :] = upscaled[:, 2, :, :] * B_mask[:, 2, :, :]
|
|
|
|
# 畳み込み適用
|
|
convolved = F.conv2d(shifted, gaussian_kernel, padding=0, groups=C)
|
|
|
|
# 後処理
|
|
convolved = convolved * intensity_scale
|
|
convolved = torch.pow(convolved, 1 / gamma)
|
|
convolved = torch.clamp(convolved, 0.0, 1.0)
|
|
|
|
# メモリ解放
|
|
del shifted, upscaled, R_mask, G_mask, B_mask
|
|
torch.cuda.empty_cache()
|
|
|
|
return convolved.permute(0, 2, 3, 1).float()
|
|
|
|
def apply_crt_effect_gpu_optimized(self, image, gaussian_width_x, gaussian_width_y, intensity_scale, gaussian_kernel_size, gamma, max_batch_size):
|
|
"""
|
|
バッチサイズを制限してGPUメモリを効率的に使用
|
|
データ型の一貫性を保証
|
|
"""
|
|
device = image.device
|
|
B, C, H, W = image.shape
|
|
print(f"\nStarting batch processing: total batch size = {B}")
|
|
|
|
# ガウシアンカーネルを準備(まだfloat32のまま)
|
|
gaussian = self.get_gaussian_kernel(gaussian_kernel_size, gaussian_width_x, gaussian_width_y, gamma, device, torch.float32)
|
|
gaussian_kernel = gaussian.repeat(C, 1, 1, 1)
|
|
|
|
# バッチを分割して処理
|
|
outputs = []
|
|
for i in range(0, B, max_batch_size):
|
|
batch = image[i:i + max_batch_size]
|
|
current_batch_size = batch.shape[0]
|
|
print(f"\nProcessing mini-batch {i//max_batch_size + 1}: size = {current_batch_size}")
|
|
|
|
try:
|
|
processed_batch = self.process_batch(
|
|
batch, gaussian_kernel, device,
|
|
intensity_scale, gamma
|
|
)
|
|
print(f"Mini-batch {i//max_batch_size + 1} processed: shape = {processed_batch.shape}")
|
|
|
|
outputs.append(processed_batch.cpu())
|
|
print(f"Mini-batch {i//max_batch_size + 1} transferred to CPU")
|
|
|
|
del processed_batch
|
|
torch.cuda.empty_cache()
|
|
|
|
except RuntimeError as e:
|
|
if "out of memory" in str(e):
|
|
print(f"OOM detected, processing images individually in mini-batch {i//max_batch_size + 1}")
|
|
for j, single_image in enumerate(batch):
|
|
processed_single = self.process_batch(
|
|
single_image.unsqueeze(0),
|
|
gaussian_kernel, device,
|
|
intensity_scale, gamma
|
|
)
|
|
print(f"Processed individual image {j+1}/{current_batch_size} in mini-batch {i//max_batch_size + 1}")
|
|
outputs.append(processed_single.cpu())
|
|
del processed_single
|
|
torch.cuda.empty_cache()
|
|
else:
|
|
raise e
|
|
|
|
# 全バッチの結果を結合
|
|
final_output = torch.cat(outputs, dim=0)
|
|
print(f"\nAll mini-batches combined: final shape = {final_output.shape}")
|
|
return (final_output,)
|
|
|
|
def apply_crt_effect(self, image, gaussian_width_x, gaussian_width_y, intensity_scale, gamma,
|
|
gaussian_kernel_size, enable_resize, resize_pixels, max_batch_size):
|
|
"""
|
|
メインの処理関数
|
|
"""
|
|
print(f"Input image shape: {image.shape}, dtype: {image.dtype}")
|
|
|
|
if image.max() > 1.0:
|
|
image = image / 255.0
|
|
|
|
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
image = image.to(device, dtype=torch.float32)
|
|
|
|
B, H, W, C = image.shape
|
|
if C != 3:
|
|
raise ValueError(f"Expected image with 3 channels (RGB), but got {C} channels.")
|
|
|
|
# [B, C, H, W]形式に変換
|
|
image = image.permute(0, 3, 1, 2)
|
|
|
|
# リサイズ処理
|
|
if enable_resize:
|
|
if H > W:
|
|
new_h, new_w = resize_pixels, int(resize_pixels * W / H)
|
|
else:
|
|
new_h, new_w = int(resize_pixels * H / W), resize_pixels
|
|
|
|
try:
|
|
image = F.interpolate(image, size=(new_h, new_w), mode='nearest')
|
|
print(f"After resize: {image.shape}")
|
|
except RuntimeError as e:
|
|
if "out of memory" in str(e):
|
|
# リサイズでメモリ不足の場合、バッチごとに処理
|
|
resized_batches = []
|
|
for i in range(0, B, max_batch_size):
|
|
batch = image[i:i + max_batch_size]
|
|
resized_batch = F.interpolate(batch, size=(new_h, new_w), mode='nearest')
|
|
resized_batches.append(resized_batch)
|
|
del batch
|
|
torch.cuda.empty_cache()
|
|
image = torch.cat(resized_batches, dim=0)
|
|
else:
|
|
raise e
|
|
else:
|
|
print(f"No resizing applied. Shape: {image.shape}")
|
|
|
|
# CRT効果の適用
|
|
return self.apply_crt_effect_gpu_optimized(
|
|
image, gaussian_width_x, gaussian_width_y,
|
|
intensity_scale, gaussian_kernel_size, gamma,
|
|
max_batch_size
|
|
)
|
|
|
|
class XYBlurNode:
|
|
kernel_cache = {} # キャッシュを追加してカーネル再計算を防ぐ
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"sigma_x": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
|
"sigma_y": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
|
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
|
"max_batch_size": ([1, 2, 4, 8, 16, 24, 32], {"default": 4}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "apply_blur"
|
|
CATEGORY = "image/Pixydust Quantizer🧚✨"
|
|
|
|
def create_gaussian_kernel(self, kernel_size, sigma_x, sigma_y, device, dtype):
|
|
"""
|
|
Generates an anisotropic Gaussian kernel with independent vertical and horizontal components.
|
|
Implements caching for performance optimization.
|
|
"""
|
|
cache_key = (kernel_size, sigma_x, sigma_y, dtype)
|
|
if cache_key not in self.kernel_cache:
|
|
# Generate grid coordinates
|
|
x = torch.arange(-(kernel_size // 2), kernel_size // 2 + 1, dtype=torch.float32, device=device)
|
|
y = torch.arange(-(kernel_size // 2), kernel_size // 2 + 1, dtype=torch.float32, device=device)
|
|
y, x = torch.meshgrid(y, x, indexing='ij')
|
|
|
|
# Calculate Gaussian function
|
|
gaussian = torch.exp(-(x**2 / (2 * sigma_x**2) + y**2 / (2 * sigma_y**2)))
|
|
|
|
# Normalize
|
|
gaussian = gaussian / gaussian.sum()
|
|
|
|
# Cache the kernel
|
|
self.kernel_cache[cache_key] = gaussian.unsqueeze(0).unsqueeze(0).to(dtype=dtype)
|
|
|
|
return self.kernel_cache[cache_key]
|
|
|
|
def calculate_kernel_size(self, sigma_x, sigma_y, max_kernel_size=31):
|
|
"""
|
|
Calculates appropriate kernel size based on sigma values.
|
|
"""
|
|
max_sigma = max(sigma_x, sigma_y)
|
|
kernel_size = int(6 * max_sigma + 1)
|
|
kernel_size = max(3, min(kernel_size + (kernel_size % 2 == 0), max_kernel_size))
|
|
return kernel_size
|
|
|
|
def process_batch(self, batch, kernel, pad_size, intensity, device):
|
|
"""
|
|
Process a single batch of images with the blur effect.
|
|
"""
|
|
# Ensure correct data type (half precision for GPU efficiency)
|
|
batch = batch.half()
|
|
kernel = kernel.half()
|
|
|
|
# Apply convolution
|
|
blurred = F.conv2d(
|
|
batch,
|
|
kernel,
|
|
padding=pad_size,
|
|
groups=batch.shape[1] # Process each channel independently
|
|
)
|
|
|
|
# Apply intensity blending
|
|
blurred = batch * (1 - intensity) + blurred * intensity
|
|
|
|
# Clamp values
|
|
blurred = torch.clamp(blurred, 0, 1)
|
|
|
|
# Convert back to float32 for output
|
|
return blurred.float()
|
|
|
|
def apply_blur_optimized(self, image, sigma_x, sigma_y, intensity, max_batch_size):
|
|
"""
|
|
Applies blur effect with optimized batch processing and GPU memory management.
|
|
"""
|
|
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
B, C, H, W = image.shape
|
|
print(f"\nProcessing total batch size: {B}")
|
|
|
|
# Calculate kernel size and create kernel
|
|
kernel_size = self.calculate_kernel_size(sigma_x, sigma_y)
|
|
kernel = self.create_gaussian_kernel(kernel_size, sigma_x, sigma_y, device, torch.float32)
|
|
kernel = kernel.repeat(C, 1, 1, 1)
|
|
pad_size = kernel_size // 2
|
|
|
|
# Process batches
|
|
outputs = []
|
|
for i in range(0, B, max_batch_size):
|
|
batch = image[i:i + max_batch_size]
|
|
current_batch_size = batch.shape[0]
|
|
print(f"\nProcessing mini-batch {i//max_batch_size + 1}: size = {current_batch_size}")
|
|
|
|
try:
|
|
processed_batch = self.process_batch(
|
|
batch, kernel, pad_size, intensity, device
|
|
)
|
|
print(f"Mini-batch {i//max_batch_size + 1} processed: shape = {processed_batch.shape}")
|
|
|
|
# Move processed batch to CPU to free GPU memory
|
|
outputs.append(processed_batch.cpu())
|
|
print(f"Mini-batch {i//max_batch_size + 1} transferred to CPU")
|
|
|
|
del processed_batch
|
|
torch.cuda.empty_cache()
|
|
|
|
except RuntimeError as e:
|
|
if "out of memory" in str(e):
|
|
print(f"OOM detected, processing images individually in mini-batch {i//max_batch_size + 1}")
|
|
# Process images one by one if batch processing fails
|
|
for j, single_image in enumerate(batch):
|
|
processed_single = self.process_batch(
|
|
single_image.unsqueeze(0),
|
|
kernel, pad_size, intensity, device
|
|
)
|
|
print(f"Processed individual image {j+1}/{current_batch_size} in mini-batch {i//max_batch_size + 1}")
|
|
outputs.append(processed_single.cpu())
|
|
del processed_single
|
|
torch.cuda.empty_cache()
|
|
else:
|
|
raise e
|
|
|
|
# Combine all processed batches
|
|
final_output = torch.cat(outputs, dim=0)
|
|
print(f"\nAll mini-batches combined: final shape = {final_output.shape}")
|
|
return final_output
|
|
|
|
def apply_blur(self, image: torch.Tensor, sigma_x: float, sigma_y: float,
|
|
intensity: float, max_batch_size: int):
|
|
"""
|
|
Main entry point for the blur effect.
|
|
"""
|
|
print(f"Input image shape: {image.shape}, dtype: {image.dtype}")
|
|
|
|
# Normalize input if necessary
|
|
if image.max() > 1.0:
|
|
image = image / 255.0
|
|
|
|
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
image = image.to(device)
|
|
|
|
# Convert from [B, H, W, C] to [B, C, H, W]
|
|
image = image.permute(0, 3, 1, 2)
|
|
|
|
# Apply optimized blur processing
|
|
blurred = self.apply_blur_optimized(
|
|
image, sigma_x, sigma_y, intensity, max_batch_size
|
|
)
|
|
|
|
# Convert back to [B, H, W, C] format
|
|
blurred = blurred.permute(0, 2, 3, 1)
|
|
|
|
# Print debug information
|
|
print(f"After blur: mean={blurred.mean().item():.4f}, "
|
|
f"std={blurred.std().item():.4f}, "
|
|
f"min={blurred.min().item():.4f}, "
|
|
f"max={blurred.max().item():.4f}")
|
|
|
|
return (blurred,)
|
|
|
|
# class XYBlurNode:
|
|
# @classmethod
|
|
# def INPUT_TYPES(cls):
|
|
# return {
|
|
# "required": {
|
|
# "image": ("IMAGE",),
|
|
# "sigma_x": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
|
# "sigma_y": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
|
# "intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
|
# }
|
|
# }
|
|
|
|
# RETURN_TYPES = ("IMAGE",)
|
|
# FUNCTION = "apply_blur"
|
|
# CATEGORY = "image/Pixydust Quantizer🧚✨"
|
|
|
|
# def create_gaussian_kernel(self, kernel_size, sigma_x, sigma_y, device, dtype):
|
|
# """
|
|
# Generates an anisotropic Gaussian kernel with independent vertical and horizontal components.
|
|
# """
|
|
# # Generate grid coordinates
|
|
# x = torch.arange(-(kernel_size // 2), kernel_size // 2 + 1, dtype=torch.float32, device=device)
|
|
# y = torch.arange(-(kernel_size // 2), kernel_size // 2 + 1, dtype=torch.float32, device=device)
|
|
# y, x = torch.meshgrid(y, x, indexing='ij')
|
|
|
|
# # Calculate Gaussian function (independent for vertical and horizontal)
|
|
# gaussian = torch.exp(-(x**2 / (2 * sigma_x**2) + y**2 / (2 * sigma_y**2)))
|
|
|
|
# # Normalize so that the sum is 1
|
|
# gaussian = gaussian / gaussian.sum()
|
|
|
|
# # Adjust the shape of the kernel to [1, 1, kernel_size, kernel_size]
|
|
# return gaussian.unsqueeze(0).unsqueeze(0).to(dtype=dtype)
|
|
|
|
# def calculate_kernel_size(self, sigma_x, sigma_y, max_kernel_size=31):
|
|
# """
|
|
# Calculates an appropriate kernel size based on sigma_x and sigma_y.
|
|
# Typically, the kernel size is set to 6 * sigma and rounded to an odd number.
|
|
# """
|
|
# # Calculate kernel size based on the largest sigma
|
|
# max_sigma = max(sigma_x, sigma_y)
|
|
# kernel_size = int(6 * max_sigma + 1)
|
|
|
|
# # Adjust kernel size to be odd
|
|
# if kernel_size % 2 == 0:
|
|
# kernel_size += 1
|
|
|
|
# # Set minimum kernel size to 3
|
|
# kernel_size = max(kernel_size, 3)
|
|
|
|
# # Set maximum kernel size
|
|
# kernel_size = min(kernel_size, max_kernel_size)
|
|
|
|
# return kernel_size
|
|
|
|
# def apply_blur(self, image: torch.Tensor, sigma_x: float, sigma_y: float, intensity: float):
|
|
# """
|
|
# Applies an anisotropic Gaussian blur to the image.
|
|
# The kernel size is automatically calculated based on sigma_x and sigma_y.
|
|
# """
|
|
# device = image.device
|
|
# dtype = image.dtype
|
|
# B, H, W, C = image.shape
|
|
|
|
# # Calculate kernel size
|
|
# kernel_size = self.calculate_kernel_size(sigma_x, sigma_y)
|
|
# print(f"Calculated kernel_size: {kernel_size} based on sigma_x: {sigma_x}, sigma_y: {sigma_y}")
|
|
|
|
# # Convert to [B, C, H, W] format
|
|
# x = image.permute(0, 3, 1, 2)
|
|
|
|
# # Generate Gaussian kernel
|
|
# kernel = self.create_gaussian_kernel(kernel_size, sigma_x, sigma_y, device, dtype)
|
|
|
|
# # Repeat the same kernel for each channel
|
|
# kernel = kernel.repeat(C, 1, 1, 1)
|
|
|
|
# # Calculate padding size
|
|
# pad_size = kernel_size // 2
|
|
|
|
# # Apply convolution operation (independently for each channel)
|
|
# blurred = F.conv2d(
|
|
# x,
|
|
# kernel,
|
|
# padding=pad_size,
|
|
# groups=C # Process each channel independently
|
|
# )
|
|
|
|
# # Apply the intensity parameter
|
|
# # Allows intensity to be in the range [0, 2]
|
|
# # intensity = 1.0 for normal blur
|
|
# # intensity < 1.0 to decrease the blur intensity
|
|
# # intensity > 1.0 to increase the blur intensity
|
|
# blurred = x * (1 - intensity) + blurred * intensity
|
|
|
|
# # Convert back to [B, H, W, C] format
|
|
# blurred = blurred.permute(0, 2, 3, 1)
|
|
|
|
# # Clamp the values to [0, 1]
|
|
# blurred = torch.clamp(blurred, 0, 1)
|
|
|
|
# # Output debug information
|
|
# print(f"After blur: mean={blurred.mean().item():.4f}, std={blurred.std().item():.4f}, min={blurred.min().item():.4f}, max={blurred.max().item():.4f}")
|
|
|
|
# # Move the tensor to CPU before returning
|
|
# blurred = blurred.cpu()
|
|
|
|
# return (blurred,)
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"CRTLikeEffectNode": CRTLikeEffectNode,
|
|
"XYBlurNode": XYBlurNode
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"CRTLikeEffectNode": "CRTLike Effect",
|
|
"XYBlurNode": "XY Blur"
|
|
}
|