171 lines
4.5 KiB
Python
171 lines
4.5 KiB
Python
# Video Adaptive Anti-aliasing
|
|
# v0.1.0
|
|
# SOLRICKS
|
|
# AI-assisted
|
|
|
|
|
|
import torch
|
|
import torch.nn.functional as F
|
|
|
|
|
|
try:
|
|
from comfy.utils import ProgressBar
|
|
except ImportError:
|
|
ProgressBar = None
|
|
|
|
try:
|
|
import comfy.model_management as mm
|
|
except ImportError:
|
|
mm = None
|
|
|
|
|
|
EDGE_EPSILON = 1e-6
|
|
EDGE_FADE_WIDTH = 0.10
|
|
LUMA_WEIGHTS = (0.2126, 0.7152, 0.0722)
|
|
|
|
SOBEL_X = (
|
|
(-1.0, 0.0, 1.0),
|
|
(-2.0, 0.0, 2.0),
|
|
(-1.0, 0.0, 1.0),
|
|
)
|
|
|
|
SOBEL_Y = (
|
|
(-1.0, -2.0, -1.0),
|
|
(0.0, 0.0, 0.0),
|
|
(1.0, 2.0, 1.0),
|
|
)
|
|
|
|
|
|
class VideoAdaptiveAA:
|
|
def __init__(self):
|
|
self._sobel_cache = {}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE",),
|
|
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
|
"edge_threshold": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"blur_radius": ("INT", {"default": 1, "min": 0, "max": 3, "step": 1}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "apply_aa"
|
|
CATEGORY = "CustomPostProcess"
|
|
|
|
def _get_sobel_kernels(self, dtype, device):
|
|
cache_key = (dtype, str(device))
|
|
|
|
if cache_key not in self._sobel_cache:
|
|
sobel_x = torch.tensor(SOBEL_X, dtype=dtype, device=device).view(1, 1, 3, 3)
|
|
sobel_y = torch.tensor(SOBEL_Y, dtype=dtype, device=device).view(1, 1, 3, 3)
|
|
|
|
self._sobel_cache[cache_key] = (sobel_x, sobel_y)
|
|
|
|
return self._sobel_cache[cache_key]
|
|
|
|
def _apply_aa_frame(self, frame, strength, edge_threshold, blur_radius):
|
|
img = frame.permute(0, 3, 1, 2)
|
|
|
|
if img.shape[1] not in (3, 4):
|
|
raise ValueError("VideoAdaptiveAA expects RGB or RGBA images.")
|
|
|
|
has_alpha = img.shape[1] == 4
|
|
|
|
if has_alpha:
|
|
alpha = img[:, 3:4, :, :]
|
|
img_rgb = img[:, :3, :, :]
|
|
else:
|
|
img_rgb = img
|
|
|
|
img_gray = (
|
|
LUMA_WEIGHTS[0] * img_rgb[:, 0:1, :, :]
|
|
+ LUMA_WEIGHTS[1] * img_rgb[:, 1:2, :, :]
|
|
+ LUMA_WEIGHTS[2] * img_rgb[:, 2:3, :, :]
|
|
)
|
|
|
|
sobel_x, sobel_y = self._get_sobel_kernels(img.dtype, img.device)
|
|
|
|
edges_x = F.conv2d(img_gray, sobel_x, padding=1)
|
|
edges_y = F.conv2d(img_gray, sobel_y, padding=1)
|
|
|
|
edges = torch.sqrt(edges_x.square() + edges_y.square())
|
|
|
|
edge_max = edges.amax(dim=(1, 2, 3), keepdim=True).clamp_min(EDGE_EPSILON)
|
|
edges = edges / edge_max
|
|
|
|
edge_mask = torch.clamp(
|
|
(edges - edge_threshold) / EDGE_FADE_WIDTH,
|
|
0.0,
|
|
1.0,
|
|
)
|
|
|
|
edge_mask = torch.clamp(edge_mask * strength, 0.0, 1.0)
|
|
|
|
kernel_size = blur_radius * 2 + 1
|
|
|
|
img_padded = F.pad(
|
|
img_rgb,
|
|
(blur_radius, blur_radius, blur_radius, blur_radius),
|
|
mode="replicate",
|
|
)
|
|
|
|
blurred = F.avg_pool2d(
|
|
img_padded,
|
|
kernel_size=kernel_size,
|
|
stride=1,
|
|
padding=0,
|
|
)
|
|
|
|
result_rgb = img_rgb * (1.0 - edge_mask) + blurred * edge_mask
|
|
result_rgb = torch.clamp(result_rgb, 0.0, 1.0)
|
|
|
|
if has_alpha:
|
|
result = torch.cat([result_rgb, alpha], dim=1)
|
|
else:
|
|
result = result_rgb
|
|
|
|
return result.permute(0, 2, 3, 1).contiguous()
|
|
|
|
@torch.inference_mode()
|
|
def apply_aa(self, images, strength, edge_threshold, blur_radius):
|
|
if strength <= 0.0 or blur_radius <= 0:
|
|
return (images,)
|
|
|
|
frame_count = images.shape[0]
|
|
pbar = ProgressBar(frame_count) if ProgressBar is not None else None
|
|
|
|
out_tensor = torch.empty_like(images)
|
|
|
|
for i in range(frame_count):
|
|
if mm is not None and hasattr(mm, "throw_exception_if_processing_interrupted"):
|
|
mm.throw_exception_if_processing_interrupted()
|
|
|
|
frame = images[i:i + 1]
|
|
frame_out = self._apply_aa_frame(
|
|
frame,
|
|
strength,
|
|
edge_threshold,
|
|
blur_radius,
|
|
)
|
|
|
|
if frame_out.dtype != out_tensor.dtype or frame_out.device != out_tensor.device:
|
|
frame_out = frame_out.to(device=out_tensor.device, dtype=out_tensor.dtype)
|
|
|
|
out_tensor[i:i + 1].copy_(frame_out)
|
|
|
|
if pbar is not None:
|
|
pbar.update(1)
|
|
|
|
return (out_tensor,)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"VideoAdaptiveAA": VideoAdaptiveAA
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"VideoAdaptiveAA": "✨ Video Adaptive Anti-Aliasing"
|
|
} |