added image overlay node

This commit is contained in:
Fill
2025-10-23 15:25:46 -07:00
parent 4174000323
commit 540095cb8a
3 changed files with 272 additions and 1 deletions
+3
View File
@@ -103,6 +103,7 @@ from .nodes.image.FL_ImageBatch import FL_ImageBatch
from .nodes.image.FL_ImageBatchListConverter import FL_ImageListToImageBatch, FL_ImageBatchToImageList
from .nodes.image.FL_ImageBatchToGrid import FL_ImageBatchToGrid
from .nodes.image.FL_ImageNotes import FL_ImageNotes
from .nodes.image.FL_ImageOverlay import FL_ImageOverlay
from .nodes.image.FL_ImageSelector import FL_ImageSelector
from .nodes.image.FL_ImageSlicer import FL_ImageSlicer
from .nodes.image.FL_Image_AddToBatch import FL_ImageAddToBatch
@@ -307,6 +308,7 @@ NODE_CLASS_MAPPINGS = {
"FL_Math": FL_Math,
"FL_ImageSlicer": FL_ImageSlicer,
"FL_ImageSelector": FL_ImageSelector,
"FL_ImageOverlay": FL_ImageOverlay,
"FL_ImageAspectCropper": FL_ImageAspectCropper,
"FL_HF_UploaderAbsolute": FL_HF_UploaderAbsolute,
"FL_ImageListToImageBatch": FL_ImageListToImageBatch,
@@ -486,6 +488,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FL_Math": "FL Math",
"FL_ImageSlicer": "FL Image Slicer",
"FL_ImageSelector": "FL Image Selector",
"FL_ImageOverlay": "FL Image Overlay",
"FL_ImageAspectCropper": "FL Image Aspect Cropper",
"FL_HF_UploaderAbsolute": "FL HF Uploader Absolute",
"FL_ImageListToImageBatch": "FL Image List To Image Batch",
+268
View File
@@ -0,0 +1,268 @@
import torch
import numpy as np
from PIL import Image, ImageFilter
from ..utils import tensor_to_pil, pil_to_tensor
class FL_ImageOverlay:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base_image": ("IMAGE",),
"overlay_image": ("IMAGE",),
"mask": ("MASK",),
"x_offset": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
"y_offset": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
"alignment": ([
"custom",
"center",
"top-left",
"top-center",
"top-right",
"center-left",
"center-right",
"bottom-left",
"bottom-center",
"bottom-right"
], {"default": "custom"}),
"resize_overlay": ([
"none",
"fit_to_base",
"scale_50%",
"scale_75%",
"scale_125%",
"scale_150%",
"scale_200%"
], {"default": "none"}),
"blend_mode": ([
"normal",
"multiply",
"screen",
"overlay",
"add"
], {"default": "normal"}),
"opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"invert_mask": ("BOOLEAN", {"default": False}),
"mask_feather": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
"boundary_behavior": (["clip", "extend_canvas"], {"default": "clip"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "overlay_images"
CATEGORY = "🏵️Fill Nodes/Image"
def overlay_images(self, base_image, overlay_image, mask, x_offset, y_offset,
alignment, resize_overlay, blend_mode, opacity, invert_mask,
mask_feather, boundary_behavior):
# Process batch - use first image from each batch
base_pil = tensor_to_pil(base_image, batch_index=0)
overlay_pil = tensor_to_pil(overlay_image, batch_index=0)
# Convert mask tensor to PIL (masks are typically [B, H, W])
if len(mask.shape) == 3:
mask_np = mask[0].cpu().numpy()
elif len(mask.shape) == 2:
mask_np = mask.cpu().numpy()
else:
raise ValueError(f"Unexpected mask shape: {mask.shape}")
# Convert to 0-255 range and create PIL image
mask_pil = Image.fromarray((mask_np * 255).astype(np.uint8), mode='L')
# Resize mask to match overlay dimensions
if mask_pil.size != overlay_pil.size:
mask_pil = mask_pil.resize(overlay_pil.size, Image.Resampling.LANCZOS)
# Invert mask if requested
if invert_mask:
mask_pil = Image.eval(mask_pil, lambda x: 255 - x)
# Apply feathering to mask
if mask_feather > 0:
mask_pil = mask_pil.filter(ImageFilter.GaussianBlur(radius=mask_feather))
# Resize overlay if requested
overlay_pil = self.resize_overlay_image(overlay_pil, base_pil, resize_overlay, mask_pil)
# Recalculate mask size after resize
if mask_pil.size != overlay_pil.size:
mask_pil = mask_pil.resize(overlay_pil.size, Image.Resampling.LANCZOS)
# Calculate position based on alignment
x_pos, y_pos = self.calculate_position(
base_pil.size, overlay_pil.size, x_offset, y_offset, alignment
)
# Perform the compositing
result_pil = self.composite_images(
base_pil, overlay_pil, mask_pil, x_pos, y_pos,
blend_mode, opacity, boundary_behavior
)
# Convert back to tensor
result_tensor = pil_to_tensor(result_pil)
return (result_tensor,)
def resize_overlay_image(self, overlay, base, resize_mode, mask):
"""Resize the overlay image based on the selected mode"""
if resize_mode == "none":
return overlay
base_w, base_h = base.size
overlay_w, overlay_h = overlay.size
if resize_mode == "fit_to_base":
# Resize overlay to match base dimensions
new_size = (base_w, base_h)
else:
# Extract scale percentage
scale_map = {
"scale_50%": 0.5,
"scale_75%": 0.75,
"scale_125%": 1.25,
"scale_150%": 1.5,
"scale_200%": 2.0
}
scale = scale_map.get(resize_mode, 1.0)
new_size = (int(overlay_w * scale), int(overlay_h * scale))
resized_overlay = overlay.resize(new_size, Image.Resampling.LANCZOS)
return resized_overlay
def calculate_position(self, base_size, overlay_size, x_offset, y_offset, alignment):
"""Calculate the position to place the overlay based on alignment"""
base_w, base_h = base_size
overlay_w, overlay_h = overlay_size
# Alignment presets
if alignment == "center":
x = (base_w - overlay_w) // 2
y = (base_h - overlay_h) // 2
elif alignment == "top-left":
x, y = 0, 0
elif alignment == "top-center":
x = (base_w - overlay_w) // 2
y = 0
elif alignment == "top-right":
x = base_w - overlay_w
y = 0
elif alignment == "center-left":
x = 0
y = (base_h - overlay_h) // 2
elif alignment == "center-right":
x = base_w - overlay_w
y = (base_h - overlay_h) // 2
elif alignment == "bottom-left":
x = 0
y = base_h - overlay_h
elif alignment == "bottom-center":
x = (base_w - overlay_w) // 2
y = base_h - overlay_h
elif alignment == "bottom-right":
x = base_w - overlay_w
y = base_h - overlay_h
else: # custom
x, y = 0, 0
# Apply offsets
x += x_offset
y += y_offset
return x, y
def composite_images(self, base, overlay, mask, x_pos, y_pos, blend_mode, opacity, boundary_behavior):
"""Composite the overlay onto the base image"""
base_w, base_h = base.size
overlay_w, overlay_h = overlay.size
# Handle boundary behavior
if boundary_behavior == "extend_canvas":
# Calculate required canvas size
canvas_w = max(base_w, x_pos + overlay_w, abs(min(0, x_pos)) + base_w)
canvas_h = max(base_h, y_pos + overlay_h, abs(min(0, y_pos)) + base_h)
# Create extended canvas
canvas = Image.new('RGB', (canvas_w, canvas_h), (0, 0, 0))
# Paste base image at appropriate position
base_x = abs(min(0, x_pos))
base_y = abs(min(0, y_pos))
canvas.paste(base, (base_x, base_y))
# Adjust overlay position for extended canvas
overlay_x = x_pos if x_pos >= 0 else 0
overlay_y = y_pos if y_pos >= 0 else 0
result = canvas.copy()
else: # clip
result = base.copy()
overlay_x = x_pos
overlay_y = y_pos
# Calculate visible region of overlay
src_x = max(0, -x_pos)
src_y = max(0, -y_pos)
dst_x = max(0, x_pos)
dst_y = max(0, y_pos)
# Calculate dimensions of visible region
visible_w = min(overlay_w - src_x, base_w - dst_x)
visible_h = min(overlay_h - src_y, base_h - dst_y)
# If overlay is completely outside bounds, return base image
if visible_w <= 0 or visible_h <= 0:
return base
# Crop overlay and mask to visible region
overlay = overlay.crop((src_x, src_y, src_x + visible_w, src_y + visible_h))
mask = mask.crop((src_x, src_y, src_x + visible_w, src_y + visible_h))
overlay_x = dst_x
overlay_y = dst_y
# Apply blend mode
if blend_mode != "normal":
overlay = self.apply_blend_mode(result, overlay, blend_mode, overlay_x, overlay_y)
# Apply global opacity to mask
if opacity < 1.0:
mask_np = np.array(mask).astype(np.float32)
mask_np = (mask_np * opacity).astype(np.uint8)
mask = Image.fromarray(mask_np, mode='L')
# Composite using the mask
result.paste(overlay, (overlay_x, overlay_y), mask)
return result
def apply_blend_mode(self, base, overlay, mode, x_pos, y_pos):
"""Apply blend mode to overlay based on the underlying base image region"""
# Extract the region from base that overlay will cover
overlay_w, overlay_h = overlay.size
base_region = base.crop((x_pos, y_pos, x_pos + overlay_w, y_pos + overlay_h))
# Convert to numpy arrays for blending
base_np = np.array(base_region).astype(np.float32) / 255.0
overlay_np = np.array(overlay).astype(np.float32) / 255.0
# Apply blend mode
if mode == "multiply":
result_np = base_np * overlay_np
elif mode == "screen":
result_np = 1 - (1 - base_np) * (1 - overlay_np)
elif mode == "overlay":
# Overlay blend mode
mask = base_np < 0.5
result_np = np.where(mask,
2 * base_np * overlay_np,
1 - 2 * (1 - base_np) * (1 - overlay_np))
elif mode == "add":
result_np = np.clip(base_np + overlay_np, 0, 1)
else: # normal
result_np = overlay_np
# Convert back to PIL
result_np = (result_np * 255).astype(np.uint8)
return Image.fromarray(result_np, mode='RGB')
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_fill-nodes"
description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection."
version = "2.0.2"
version = "2.0.3"
license = "LICENSE"
dependencies = ["diffusers", "librosa", "sounddevice", "glitch_this", "PyOpenGL", "glfw", "scipy>=1.13.1", "requests", "aiohttp", "moviepy", "matplotlib", "reportlab", "openai", "PyPDF2", "pdf2image", "PyMuPDF", "reportlab", "PyPDF2", "ollama", "kornia", "opencv-python", "gdown", "open_clip_torch", "google-genai"]