Files
2025-05-12 09:15:17 +08:00

222 lines
8.0 KiB
Python

import torch
import numpy as np
from PIL import Image
import json
import os
class ImageOverlayNode:
def __init__(self):
self.output_dir = "output"
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base_image": ("IMAGE",),
"overlay_image": ("IMAGE",),
"coordinates": ("STRING", {
"default": '[{"x": 0, "y": 0}]',
"multiline": False
}),
"scale": ("FLOAT", {
"default": 1.0,
"min": 0.1,
"max": 10.0,
"step": 0.1
}),
"rotation": ("FLOAT", {
"default": 0.0,
"min": -360.0,
"max": 360.0,
"step": 1.0
}),
"flip_horizontal": ("BOOLEAN", {
"default": False,
"label_on": "是",
"label_off": "否"
}),
"flip_vertical": ("BOOLEAN", {
"default": False,
"label_on": "是",
"label_off": "否"
}),
"blend_mode": (["normal", "multiply", "screen", "overlay", "soft_light", "hard_light", "color_dodge", "color_burn", "darken", "lighten"],),
"opacity": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
}),
"output_format": (["png", "jpg", "webp"],),
},
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image", "mask")
FUNCTION = "overlay_images"
CATEGORY = "image"
def apply_blend_mode(self, base, overlay, mode):
if mode == "normal":
return overlay
elif mode == "multiply":
return base * overlay
elif mode == "screen":
return 1 - (1 - base) * (1 - overlay)
elif mode == "overlay":
mask = base > 0.5
result = np.zeros_like(base)
result[mask] = 1 - 2 * (1 - base[mask]) * (1 - overlay[mask])
result[~mask] = 2 * base[~mask] * overlay[~mask]
return result
elif mode == "soft_light":
mask = overlay > 0.5
result = np.zeros_like(base)
result[mask] = base[mask] * (1 - (1 - base[mask]) * (1 - (overlay[mask] - 0.5) * 2))
result[~mask] = base[~mask] * (1 + (2 * overlay[~mask] - 1) * base[~mask])
return result
elif mode == "hard_light":
mask = overlay > 0.5
result = np.zeros_like(base)
result[mask] = 1 - 2 * (1 - base[mask]) * (1 - overlay[mask])
result[~mask] = 2 * base[~mask] * overlay[~mask]
return result
elif mode == "color_dodge":
return np.minimum(1, base / (1 - overlay + 1e-6))
elif mode == "color_burn":
return 1 - np.minimum(1, (1 - base) / (overlay + 1e-6))
elif mode == "darken":
return np.minimum(base, overlay)
elif mode == "lighten":
return np.maximum(base, overlay)
return overlay
def overlay_images(self, base_image, overlay_image, coordinates, scale, rotation, flip_horizontal, flip_vertical, blend_mode, opacity, output_format):
# 解析坐标JSON
try:
coords = json.loads(coordinates)
if isinstance(coords, list) and len(coords) > 0:
position_x = coords[0].get("x", 0)
position_y = coords[0].get("y", 0)
else:
position_x = 0
position_y = 0
except:
position_x = 0
position_y = 0
# 确保输入是numpy数组
if isinstance(base_image, torch.Tensor):
base_image = base_image.cpu().numpy()
if isinstance(overlay_image, torch.Tensor):
overlay_image = overlay_image.cpu().numpy()
# 转换为PIL图像
base_pil = Image.fromarray((base_image[0] * 255).astype(np.uint8))
overlay_pil = Image.fromarray((overlay_image[0] * 255).astype(np.uint8))
# 调整修改图大小
new_size = (int(overlay_pil.width * scale), int(overlay_pil.height * scale))
overlay_pil = overlay_pil.resize(new_size, Image.Resampling.LANCZOS)
# 应用翻转
if flip_horizontal:
overlay_pil = overlay_pil.transpose(Image.FLIP_LEFT_RIGHT)
if flip_vertical:
overlay_pil = overlay_pil.transpose(Image.FLIP_TOP_BOTTOM)
# 旋转修改图
if rotation != 0:
overlay_pil = overlay_pil.rotate(rotation, expand=True, resample=Image.Resampling.BICUBIC)
rotated_width, rotated_height = overlay_pil.size
if rotation % 180 != 0:
position_x = position_x - (rotated_width - new_size[0]) // 2
position_y = position_y - (rotated_height - new_size[1]) // 2
# 将底图转换为RGBA
if base_pil.mode != 'RGBA':
base_pil = base_pil.convert('RGBA')
# 将修改图转换为RGBA
if overlay_pil.mode != 'RGBA':
overlay_pil = overlay_pil.convert('RGBA')
# 应用不透明度
if opacity < 1.0:
overlay_pil.putalpha(int(255 * opacity))
# 计算位置
x = position_x
y = position_y
# 创建新的透明图层
result = base_pil.copy() # 使用底图的副本作为基础
# 粘贴修改图
result.paste(overlay_pil, (x, y), overlay_pil)
# 应用混合模式
if blend_mode != "normal":
base_np = np.array(base_pil).astype(np.float32) / 255.0
overlay_np = np.array(overlay_pil).astype(np.float32) / 255.0
overlay_resized = np.zeros_like(base_np)
h, w = overlay_np.shape[:2]
bh, bw = base_np.shape[:2]
x = max(0, min(x, bw - 1))
y = max(0, min(y, bh - 1))
end_x = min(x + w, bw)
end_y = min(y + h, bh)
usable_w = end_x - x
usable_h = end_y - y
overlay_resized[y:end_y, x:end_x] = overlay_np[:usable_h, :usable_w]
blended = self.apply_blend_mode(base_np, overlay_resized, blend_mode)
result = Image.fromarray((blended * 255).astype(np.uint8))
# 创建遮罩蒙版
mask = Image.new('L', base_pil.size, 0)
if overlay_pil.mode == 'RGBA':
# 使用alpha通道作为蒙版
alpha = overlay_pil.split()[3]
mask.paste(alpha, (x, y))
else:
# 如果没有alpha通道,使用图像亮度作为蒙版
gray = overlay_pil.convert('L')
mask.paste(gray, (x, y))
# 根据输出格式保存
if output_format == "jpg":
result = result.convert('RGB')
elif output_format == "webp":
result = result.convert('RGBA')
# 转换回numpy数组
result_np = np.array(result)
if output_format == "jpg":
result_np = result_np.astype(np.float32) / 255.0
else:
result_np = result_np.astype(np.float32) / 255.0
# 添加批次维度
result_np = np.expand_dims(result_np, 0)
# 转换蒙版为numpy数组
mask_np = np.array(mask).astype(np.float32) / 255.0
mask_np = np.expand_dims(mask_np, 0)
return (torch.from_numpy(result_np), torch.from_numpy(mask_np))
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True