blur fix + bg export

This commit is contained in:
JJ
2026-03-23 19:48:02 +00:00
parent a924164948
commit e63688d2ee
+141 -41
View File
@@ -3,6 +3,7 @@ import torch.nn.functional as F # Added F
import folder_paths
from PIL import Image
import numpy as np
import cv2
import os
from concurrent.futures import ThreadPoolExecutor
@@ -570,7 +571,7 @@ class ImageBlur_GPU:
"display": "number"
}),
"mask_grow": ("FLOAT", {
"default": 10.0,
"default": 0.0,
"min": 0.0,
"max": 30.0,
"step": 0.5,
@@ -586,56 +587,78 @@ class ImageBlur_GPU:
"step": 0.1,
"display": "slider"
}),
"mask_in_blur": ("FLOAT", {
"char_str_mult": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 3.0,
"step": 0.01,
"display": "slider"
}),
"char_blur": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"display": "slider"
}),
"mask_out_blur": ("FLOAT", {
"bg_blur": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"display": "slider"
}),
"mask_duplicate": ("INT", {
"duplicate_char": ("INT", {
"default": 32,
"min": 1,
"max": 32,
"step": 1,
"display": "number"
}),
"fill_cutout": ("BOOLEAN", {
"default": True
}),
}
}
# 3-layer compositing:
# 1. Base layer (original image)
# 2. Mask out blur: blur whole image, mask OUT white areas (background blur)
# 3. Mask in blur: blur masked pixels, extends beyond mask, composite on top (foreground blur)
# - mask_duplicate: controls how many times to composite blurred layer (builds intensity)
# 2. bg_blur: blur whole image, mask OUT white areas (background blur)
# 3. char_blur: blur masked pixels, extends beyond mask, composite on top (foreground blur)
# - char_str_mult: multiplier for blur strength on character layer (0.0-3.0, 1.0 = same)
# - duplicate_char: controls how many times to composite blurred layer (builds intensity)
# - fill_cutout: when true, fills white mask areas with border pixels before background blur
RETURN_TYPES = ("IMAGE",)
RETURN_TYPES = ("IMAGE", "IMAGE",)
RETURN_NAMES = ("image", "bg_image",)
FUNCTION = "apply_radial_zoom_blur_gpu"
CATEGORY = "WanVideoWrapper_QQ/image" # Categorize the node
def apply_radial_zoom_blur_gpu(self, image, mode="radial", strength=50.0, directional_angle=90.0, center_x=0.5, center_y=0.5, num_samples=30, mask=None, mask_blur=0.0, mask_in_blur=1.0, mask_out_blur=0.0, mask_duplicate=1, mask_grow=0.0):
def apply_radial_zoom_blur_gpu(self, image, mode="radial", strength=50.0, directional_angle=90.0, center_x=0.5, center_y=0.5, num_samples=30, mask=None, mask_blur=0.0, char_str_mult=1.0, char_blur=1.0, bg_blur=0.0, duplicate_char=1, mask_grow=0.0, fill_cutout=True):
# Process each image in the batch
processed_images = []
bg_images = []
for i, single_image in enumerate(image):
single_mask = mask[i:i+1] if mask is not None else None
processed_images.append(self._apply_single_image(single_image.unsqueeze(0), mode, strength, directional_angle, center_x, center_y, num_samples, single_mask, mask_blur, mask_in_blur, mask_out_blur, mask_duplicate, mask_grow))
return (torch.cat(processed_images, dim=0),)
result, bg = self._apply_single_image(single_image.unsqueeze(0), mode, strength, directional_angle, center_x, center_y, num_samples, single_mask, mask_blur, char_str_mult, char_blur, bg_blur, duplicate_char, mask_grow, fill_cutout)
processed_images.append(result)
bg_images.append(bg)
return (torch.cat(processed_images, dim=0), torch.cat(bg_images, dim=0))
def _apply_single_image(self, image_bhwc, mode="radial", strength=50.0, directional_angle=90.0, center_x=0.5, center_y=0.5, num_samples=30, mask=None, mask_blur=0.0, mask_in_blur=1.0, mask_out_blur=0.0, mask_duplicate=1, mask_grow=0.0):
def _apply_single_image(self, image_bhwc, mode="radial", strength=50.0, directional_angle=90.0, center_x=0.5, center_y=0.5, num_samples=30, mask=None, mask_blur=0.0, char_str_mult=1.0, char_blur=1.0, bg_blur=0.0, duplicate_char=1, mask_grow=0.0, fill_cutout=True):
"""
Applies the radial zoom blur with 3-layer compositing:
1. Base layer (original image)
2. Mask out blur: blur whole image, mask OUT white areas (background blur)
3. Mask in blur: blur masked pixels with extension, composite on top (foreground blur)
- mask_duplicate: controls how many times to composite blurred layer (builds intensity)
2. bg_blur: blur whole image, mask OUT white areas (background blur)
3. char_blur: blur masked pixels, extends beyond mask, composite on top (foreground blur)
- char_str_mult: multiplier for blur strength on character layer (0.0-3.0, 1.0 = same)
- duplicate_char: controls how many times to composite blurred layer (builds intensity)
- mask_grow: expands mask outward before blur (morphological dilation)
- fill_cutout: fills white mask areas with border pixels before background blur
Returns:
result: final composited image with all blur layers
bg_image: background image without character (filled and blurred)
"""
# ComfyUI image tensor is [B, H, W, C], float [0, 1]
# Move to CUDA device if available
@@ -681,30 +704,50 @@ class ImageBlur_GPU:
# Start with base layer
result = base_layer
# --- Layer 2: Mask Out Blur (background blur) ---
# Initialize bg_image (will be set in bg_blur section or defaults to base_layer)
bg_image = base_layer.clone()
# --- Layer 2: Background Blur (bg_blur) ---
# Blur whole image, then mask OUT white areas (show blur in black areas)
if has_mask and mask_out_blur > 0.0:
# Blur the entire base layer with border padding (no extension)
blurred_out = self._apply_blur_to_image(base_layer, mode, strength, directional_angle, center_x, center_y, num_samples, padding_mode='border')
if has_mask and bg_blur > 0.0:
# If fill_cutout is enabled, fill white mask areas with border pixels first
if fill_cutout:
# Create filled version of base layer (white areas filled with background)
base_layer_filled = self._inpaint_masked_areas(base_layer, mask_tensor)
else:
base_layer_filled = base_layer
# Blur the entire (possibly filled) base layer with border padding (no extension)
blurred_out = self._apply_blur_to_image(base_layer_filled, mode, strength, directional_angle, center_x, center_y, num_samples, padding_mode='border')
# Store the background image (filled and blurred, without character)
bg_image = blurred_out.clone()
# Composite using inverted mask (white areas keep original, black areas show blur)
# alpha_out = (1 - mask) * mask_out_blur
alpha_out = (1.0 - mask_tensor) * mask_out_blur # [1, H, W]
# alpha_out = (1 - mask) * bg_blur
alpha_out = (1.0 - mask_tensor) * bg_blur # [1, H, W]
alpha_out = alpha_out.unsqueeze(-1) # [1, H, W, 1]
# Blend: result = base * (1 - alpha_out) + blurred * alpha_out
result = base_layer * (1.0 - alpha_out) + blurred_out * alpha_out
result = torch.clamp(result, 0.0, 1.0)
elif has_mask and fill_cutout:
# Even if bg_blur is 0, if fill_cutout is enabled, provide filled background
base_layer_filled = self._inpaint_masked_areas(base_layer, mask_tensor)
bg_image = base_layer_filled.clone()
# --- Layer 3: Mask In Blur (foreground blur) ---
# --- Layer 3: Character Blur (char_blur) ---
# Blur masked region with proper alpha handling to avoid black outlines
if has_mask and mask_in_blur > 0.0:
if has_mask and char_blur > 0.0:
# Apply strength multiplier for character layer
char_strength = strength * char_str_mult
# For directional blur, extend image borders where mask touches to prevent gaps
if mode == "directional":
# Calculate padding amount based on blur strength and direction
# More strength = more extension needed
max_dist = torch.sqrt(torch.tensor((w - 1.0)**2 + (h - 1.0)**2, device=device, dtype=base_layer.dtype))
blur_extent = max_dist * (strength / 100.0) / self.RADIAL_DIRECTIONAL_DAMPENING
blur_extent = max_dist * (char_strength / 100.0) / self.RADIAL_DIRECTIONAL_DAMPENING
pad_size = int(blur_extent.item()) + 10 # Add buffer
# Extend image and mask where mask touches borders
@@ -714,13 +757,13 @@ class ImageBlur_GPU:
# Blur the extended image with border padding
blurred_full_extended = self._apply_blur_to_image(
base_layer_extended, mode, strength, directional_angle,
base_layer_extended, mode, char_strength, directional_angle,
center_x, center_y, num_samples, padding_mode='border'
)
# Blur the extended alpha channel
blurred_alpha_extended = self._apply_blur_to_alpha_with_padding(
mask_tensor_extended, mode, strength, directional_angle,
mask_tensor_extended, mode, char_strength, directional_angle,
center_x, center_y, num_samples, padding_mode='border'
)
@@ -729,14 +772,14 @@ class ImageBlur_GPU:
blurred_alpha = blurred_alpha_extended[pad_size:pad_size+h, pad_size:pad_size+w]
else:
# For radial and gaussian blur, use standard approach
blurred_full = self._apply_blur_to_image(base_layer, mode, strength, directional_angle, center_x, center_y, num_samples, padding_mode='border')
blurred_full = self._apply_blur_to_image(base_layer, mode, char_strength, directional_angle, center_x, center_y, num_samples, padding_mode='border')
# Blur the alpha channel with border padding for smooth edges
blurred_alpha = self._apply_blur_to_alpha_with_padding(mask_tensor, mode, strength, directional_angle, center_x, center_y, num_samples, padding_mode='border')
blurred_alpha = self._apply_blur_to_alpha_with_padding(mask_tensor, mode, char_strength, directional_angle, center_x, center_y, num_samples, padding_mode='border')
# Composite using blurred alpha for smooth blending
# The blurred alpha allows the blur to extend beyond the original mask boundaries
alpha_in = blurred_alpha * mask_in_blur # [H, W] or [1, H, W]
alpha_in = blurred_alpha * char_blur # [H, W] or [1, H, W]
# Ensure proper shape for broadcasting
if alpha_in.dim() == 2: # [H, W]
@@ -744,20 +787,22 @@ class ImageBlur_GPU:
elif alpha_in.dim() == 3: # [1, H, W]
alpha_in = alpha_in.unsqueeze(-1) # [1, H, W, 1]
# Proper alpha compositing: blend result with blurred image using blurred alpha
# Use the original mask to ensure we only blend where there's content
mask_broadcast = mask_tensor.unsqueeze(-1) if mask_tensor.dim() == 3 else mask_tensor.unsqueeze(0).unsqueeze(-1)
# Apply the blur multiple times if mask_duplicate > 1
# Apply the blur multiple times if duplicate_char > 1
# This builds up intensity when blurred pixels have low opacity
for dup_iteration in range(mask_duplicate):
# Blend: result = current * (1 - alpha_in) + blurred_full * alpha_in
# Only apply where original mask exists to avoid bleeding into empty areas
blend_alpha = alpha_in * mask_broadcast
result = result * (1.0 - blend_alpha) + blurred_full * blend_alpha
# Use blurred alpha directly to allow blur to extend beyond mask boundaries
if duplicate_char > 1:
for dup_iteration in range(duplicate_char):
# In-place operations to save memory
# result = result * (1 - alpha) + blurred * alpha
inv_blend = 1.0 - alpha_in
result.mul_(inv_blend).add_(blurred_full * alpha_in)
result.clamp_(0.0, 1.0)
else:
# Single iteration - normal operations
result = result * (1.0 - alpha_in) + blurred_full * alpha_in
result = torch.clamp(result, 0.0, 1.0)
return result.cpu()
return result.cpu(), bg_image.cpu()
def _extend_borders_for_mask(self, image_bhwc, mask_tensor, pad_size, mode, directional_angle):
"""
@@ -1270,7 +1315,7 @@ class ImageBlur_GPU:
"""
Apply morphological dilation to grow/expand the mask outward.
mask_tensor: [1, H, W] or [H, W]
grow_amount: float, controls how many pixels to grow (0-20)
grow_amount: float, controls how many pixels to grow (0-30)
"""
device = mask_tensor.device
@@ -1321,6 +1366,61 @@ class ImageBlur_GPU:
else:
return grown_mask # [1, 1, H, W]
def _inpaint_masked_areas(self, image_bhwc, mask_tensor):
"""
Fill masked (white) areas of the image using inpainting from border pixels.
Similar to VideoInpaint approach - uses OpenCV inpainting to fill white areas.
Args:
image_bhwc: [1, H, W, C] image tensor
mask_tensor: [1, H, W] mask tensor (white = areas to fill)
Returns:
filled_image: [1, H, W, C] image tensor with white areas filled
"""
try:
# Convert to numpy for OpenCV
image_np = image_bhwc[0].mul(255).byte().cpu().numpy() # [H, W, C] uint8
mask_np = mask_tensor[0].cpu().numpy() # [H, W]
# Ensure RGB format
if image_np.shape[-1] != 3:
# If not RGB, use first 3 channels
if image_np.shape[-1] >= 3:
image_np = image_np[..., :3]
else:
# Grayscale, convert to RGB
image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
# Prepare mask (white areas = areas to inpaint)
# Mask threshold: consider pixels > 0.01 as "to be filled"
mask_binary = (mask_np > 0.01).astype(np.uint8) * 255
# Check if mask has any active areas
if not np.any(mask_binary > 0):
return image_bhwc
# Convert to BGR for OpenCV
image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
# Apply inpainting using Telea algorithm
# inpaintRadius = 3 (same as VideoInpaint)
inpainted_bgr = cv2.inpaint(image_bgr, mask_binary, 3, cv2.INPAINT_TELEA)
# Convert back to RGB
inpainted_rgb = cv2.cvtColor(inpainted_bgr, cv2.COLOR_BGR2RGB)
# Convert back to tensor
inpainted_tensor = torch.from_numpy(inpainted_rgb).float().div(255.0).to(image_bhwc.device)
inpainted_tensor = inpainted_tensor.unsqueeze(0) # [1, H, W, C]
return inpainted_tensor
except Exception as e:
print(f"[ImageBlur_GPU WARNING] inpaint_masked_areas failed: {e}")
# Fallback: return original image
return image_bhwc
def _blur_mask(self, mask_tensor, blur_strength):
"""
Apply Gaussian blur to mask with replicate padding to avoid edge bleeding.