Files
SOLRICKS-comfyui-solricks/VideoTAADLAA.py
T

226 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Created by MSXYZ (AI-assisted)
# Temporal Anti-Aliasing (TAA) + Lightweight DLAA-style Sharpening
# v0.1.1 - Fix DLAANet weight loading, Improved Stability
import torch
import torch.nn as nn
import torch.nn.functional as F
import comfy.model_management as mm
import os
import logging
logger = logging.getLogger("VideoTAADLAA")
# Deep Learning Anti-Aliasing (DLAA) refinement
class _DLAANet(nn.Module):
def __init__(self):
super().__init__()
# Sobel filters and jitter
self.register_buffer("sobel_x", torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=torch.float32).view(1, 1, 3, 3))
self.register_buffer("sobel_y", torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=torch.float32).view(1, 1, 3, 3))
self.register_buffer("jitter_offsets", torch.tensor([[0.25, 0.25], [-0.25, -0.25], [-0.25, 0.25], [0.25, -0.25]], dtype=torch.float32))
# Feature extractor
self.extract_feature = nn.Conv2d(3, 16, 3, padding=1, bias=False)
# Texture refinement
self.refiner = nn.Conv2d(16, 16, 3, padding=1, bias=False)
# Artifact removal
self.reconstructor = nn.Conv2d(16, 3, 3, padding=1, bias=False)
# Initialize weights (residual + orthogonal init)
self._init_weights()
def _init_weights(self):
nn.init.orthogonal_(self.extract_feature.weight)
nn.init.orthogonal_(self.refiner.weight)
nn.init.zeros_(self.reconstructor.weight)
def forward(self, x):
features = F.leaky_relu(self.extract_feature(x), 0.2)
refined = F.leaky_relu(self.refiner(features), 0.2)
residual = self.reconstructor(refined)
return x + residual
# Temporal Anti-Aliasing (TAA)
class _TAAState:
def __init__(self):
self.history = None
self.frame_id = 0
def reset(self):
self.history = None
self.frame_id = 0
def update(self, frame, alpha, sensitivity):
if self.history is None or self.history.shape != frame.shape:
self.history = frame.detach().clone().to(frame.device)
return frame
# Neighborhood clamping for stability
local_min = -F.max_pool2d(-frame, kernel_size=3, stride=1, padding=1)
local_max = F.max_pool2d(frame, kernel_size=3, stride=1, padding=1)
history_clipped = torch.clamp(self.history, local_min, local_max)
# Motion masking
diff = torch.abs(frame - history_clipped).mean(dim=1, keepdim=True)
motion_mask = torch.sigmoid((diff - sensitivity) * 45.0)
dynamic_alpha = alpha * (1.0 - motion_mask)
out = torch.lerp(frame, history_clipped, dynamic_alpha)
self.history = out.detach()
return out
# Main Node
class VideoTAADLAA:
def __init__(self):
self.net_cache = {}
self.taa = _TAAState()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"taa_strength": ("FLOAT", {"default": 0.30, "min": 0, "max": 1, "step": 0.05}),
"taa_alpha": ("FLOAT", {"default": 0.20, "min": 0, "max": 0.9, "step": 0.01}),
"motion_sensitivity": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 0.4, "step": 0.01}),
"jitter_scale": ("FLOAT", {"default": 0.08, "min": 0, "max": 0.4, "step": 0.01}),
"dlaa_strength": ("FLOAT", {"default": 0.50, "min": 0, "max": 1, "step": 0.05}),
"edge_threshold": ("FLOAT", {"default": 0.25, "min": 0.05, "max": 0.35, "step": 0.01}),
"brightness": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 1.5, "step": 0.05}),
"contrast": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 1.5, "step": 0.05}),
"gamma": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.5, "step": 0.05}),
"blur_radius": ("INT", {"default": 0, "min": 0, "max": 3, "step": 1}),
"reset_history": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "execute"
CATEGORY = "CustomPostProcess"
def _net(self, device):
key = str(device)
if key not in self.net_cache:
net = _DLAANet().to(device)
# Pretrained weight load
base_path = os.path.dirname(os.path.realpath(__file__))
path = os.path.join(base_path, "dlaanet.pth")
if os.path.exists(path):
try:
state_dict = torch.load(path, map_location=device)
net.load_state_dict(state_dict, strict=False)
print(f"\033[92m[DLAA] Model Loaded Successfully: \033[0m{os.path.basename(path)}")
except Exception as e:
print(f"VideoTAADLAA: Model loading error: {e}")
else:
print(f"\033[92m[DLAA] WARNING! {path} Not found. Default settings are being used.")
net.eval()
self.net_cache[key] = net
return self.net_cache[key]
def jitter(self, x, idx, scale, net):
if scale < 1e-5: return x
off = net.jitter_offsets[idx % 4]
B, C, H, W = x.shape
theta = torch.eye(2, 3, device=x.device).unsqueeze(0).repeat(B, 1, 1)
theta[:, 0, 2], theta[:, 1, 2] = off[0] * scale / W, off[1] * scale / H
grid = F.affine_grid(theta, x.shape, align_corners=False)
return F.grid_sample(x, grid, mode="bilinear", padding_mode="reflection", align_corners=False)
def edge_aa(self, x, thr, blur, net):
if blur <= 0: return x
# Grayscale for edge detection
gray = 0.2126 * x[:, 0:1] + 0.7152 * x[:, 1:2] + 0.0722 * x[:, 2:3]
sx, sy = F.conv2d(gray, net.sobel_x, padding=1), F.conv2d(gray, net.sobel_y, padding=1)
edge = torch.sqrt(sx*sx + sy*sy + 1e-6)
mask = torch.sigmoid((edge - thr) * 8.0)
blurred = F.avg_pool2d(F.pad(x, [blur]*4, mode="reflect"), blur*2+1, stride=1)
return x*(1-mask) + blurred*mask
def execute(self, images, taa_strength, taa_alpha, motion_sensitivity,
jitter_scale, dlaa_strength, edge_threshold,
brightness, contrast, gamma,
blur_radius, reset_history=True):
device = mm.get_torch_device()
if reset_history:
self.taa.reset()
torch.cuda.empty_cache()
net = self._net(device)
B, H, W, C = images.shape
out_list = []
# Baseline calibration
actual_brightness = brightness * 1.5
actual_contrast = contrast * 1.0
actual_gamma = gamma * 1.5
with torch.no_grad():
for i in range(B):
logger.debug(f"Frame {i+1}/{B}")
# convert to NCHW for PyTorch
img = images[i:i+1].to(device).permute(0, 3, 1, 2).float()
# split RGB channels
rgb = img[:, :3]
# 1. spatial preprocessing step
fid = self.taa.frame_id
self.taa.frame_id = (fid + 1) % 4
rgb = self.jitter(rgb, fid, jitter_scale, net)
rgb = self.edge_aa(rgb, edge_threshold, blur_radius, net)
# 2. temporal update
taa_out = self.taa.update(rgb, taa_alpha, motion_sensitivity)
rgb = torch.lerp(rgb, taa_out, taa_strength)
# 3. sharpening pass (DLAA)
if dlaa_strength > 0:
refined_output = net(rgb)
# Debug
diff_check = torch.abs(refined_output - rgb).mean().item()
if i == 0: # Sadece ilk karede yazdır
print(f"\033[92m[DLAA] Model Delta (MSE): \033[93m{diff_check:.6f}\033[0m")
if diff_check < 1e-6:
print("\033[92m[DLAA] Model output unchanged. Weights may not be loaded correctly.")
residual = refined_output - rgb
# Sharpening x3
luma_res = 0.2126 * residual[:, 0:1] + 0.7152 * residual[:, 1:2] + 0.0722 * residual[:, 2:3]
rgb = rgb + (luma_res * dlaa_strength * 5.0)
# 4. Color Correction
if actual_brightness != 1.0 or actual_contrast != 1.0 or actual_gamma != 1.0:
luma_orig = 0.2126 * rgb[:, 0:1] + 0.7152 * rgb[:, 1:2] + 0.0722 * rgb[:, 2:3]
mean_luma = torch.mean(luma_orig, dim=(1,2,3), keepdim=True)
rgb = (rgb - mean_luma) * actual_contrast + (mean_luma * actual_brightness)
rgb = torch.pow(torch.clamp(rgb, 0.001, 1.0), actual_gamma)
rgb = torch.clamp(rgb, 0.0, 1.0)
out_list.append(rgb.permute(0,2,3,1).cpu())
if i % 10 == 0:
mm.soft_empty_cache()
return (torch.cat(out_list, dim=0),)
NODE_CLASS_MAPPINGS = {"VideoTAADLAA": VideoTAADLAA}
NODE_DISPLAY_NAME_MAPPINGS = {"VideoTAADLAA": "🎮 Video TAA + DLAA Anti-Aliasing"}