Files
2025-11-26 18:16:38 +01:00

488 lines
18 KiB
Python
Raw Permalink 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.
import torch
import torch.nn.functional as F
import numpy as np
from scipy import ndimage
# --- 原有的文本节点 ---
class MultipleTextInputs:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"inputcount": ("INT", {"default": 3, "min": 2, "max": 1000, "step": 1}),
"string_1": ("STRING", {"default": '', "forceInput": True}),
"delimiter": ("STRING", {"default": '', "multiline": False}),
},
"optional": {
"string_2": ("STRING", {"default": '', "forceInput": True}),
"string_3": ("STRING", {"default": '', "forceInput": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("string",)
FUNCTION = "combine"
CATEGORY = "MyTemplate"
def combine(self, inputcount, delimiter, **kwargs):
result_list = []
if "string_1" in kwargs:
result_list.append(kwargs["string_1"])
for c in range(1, inputcount):
key = f"string_{c + 1}"
new_string = kwargs.get(key, "")
if new_string:
result_list.append(new_string)
final_string = delimiter.join(result_list)
return (final_string,)
# --- 原有的 RGB 遮罩节点 (保留) ---
class MultipleMasksToImage:
def __init__(self):
self.colors = self._generate_color_palette()
def _generate_color_palette(self):
levels = [0, 255, 128, 192, 64]
palette = []
for r in levels:
for g in levels:
for b in levels:
if r == 0 and g == 0 and b == 0:
continue
palette.append((r, g, b))
def sort_key(color):
r, g, b = color
non_zero = sum(1 for c in [r, g, b] if c > 0)
is_primary = 1 if non_zero == 1 and 255 in [r, g, b] else 0
score = 0
if is_primary: score += 1000
elif non_zero == 1: score += 500
elif non_zero == 2 and 255 in [r, g, b]: score += 300
brightness = r + g + b
return (-score, -brightness)
palette.sort(key=sort_key)
return palette
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"inputcount": ("INT", {
"default": 3,
"min": 2,
"max": 100,
"step": 1,
"tooltip": "Number of mask inputs. Click 'Update inputs' to refresh ports."
}),
"mask_1": ("MASK", {
"tooltip": "The base mask. All other masks will be resized to match this mask's dimensions."
}),
"binarize": ("BOOLEAN", {
"default": False,
"label_on": "Enabled",
"label_off": "Disabled",
"tooltip": "If enabled, converts soft mask edges (gray) to hard edges (black/white)."
}),
"binarize_threshold": ("FLOAT", {
"default": 0.2,
"min": 0.01,
"max": 0.99,
"step": 0.01,
"tooltip": "The pixel value threshold. Pixels brighter than this become 1, darker become 0."
}),
"remove_noise": ("BOOLEAN", {
"default": False,
"label_on": "Enabled",
"label_off": "Disabled",
"tooltip": "If enabled, removes small isolated pixel islands (dust/noise) from the mask."
}),
},
"optional": {
"mask_2": ("MASK",),
"mask_3": ("MASK",),
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "color_info")
FUNCTION = "combine_masks"
CATEGORY = "MyTemplate"
def combine_masks(self, inputcount, mask_1, binarize, binarize_threshold, remove_noise, **kwargs):
# 1. 收集输入 (Collection)
raw_inputs = [mask_1]
for c in range(1, inputcount):
key = f"mask_{c + 1}"
m = kwargs.get(key, None)
if m is not None:
raw_inputs.append(m)
# 2. 深度展平 (Deep Flattening & Batch Unpacking)
# 将 List 和 Batch Tensor 全部拆解为单独的 [1, H, W] Tensor
masks_to_process = []
for item in raw_inputs:
# Handle List input
if isinstance(item, list):
sub_items = item
else:
sub_items = [item]
for m in sub_items:
# 统一维度为 [B, H, W]
if m.dim() == 2:
m = m.unsqueeze(0)
# Handle Batch > 1
batch_count = m.shape[0]
if batch_count == 1:
masks_to_process.append(m)
else:
# 拆分 Batch 为多个独立 Mask
for i in range(batch_count):
masks_to_process.append(m[i:i+1])
if not masks_to_process:
# 防御性编程,理论上必有 mask_1
raise ValueError("No masks provided.")
# 检查颜色池是否足够
num_masks = len(masks_to_process)
if num_masks > len(self.colors):
raise ValueError(f"Too many masks! Maximum supported is {len(self.colors)}, but got {num_masks} (including batch items).")
# 3. 确定基准尺寸 (使用展平后的第一个 Mask)
ref_mask = masks_to_process[0]
# ref_mask 已经是 [1, H, W]
target_h, target_w = ref_mask.shape[1], ref_mask.shape[2]
# 创建纯黑底图 [1, H, W, 3] (RGB)
output_image = torch.zeros((1, target_h, target_w, 3), dtype=torch.float32, device=ref_mask.device)
used_colors_rgb = []
used_colors_hex = []
# 4. 遍历处理并叠加
for i, mask in enumerate(masks_to_process):
# 获取颜色
r_int, g_int, b_int = self.colors[i]
color_float = (r_int / 255.0, g_int / 255.0, b_int / 255.0)
color_tensor = torch.tensor(color_float, device=mask.device, dtype=torch.float32)
used_colors_rgb.append(f"({r_int}, {g_int}, {b_int})")
used_colors_hex.append(f"0x{r_int:02x}{g_int:02x}{b_int:02x}")
# A. 缩放
# mask 是 [1, H, W],interpolate 需要 [1, 1, H, W]
mask_in = mask.unsqueeze(1)
if mask.shape[1] != target_h or mask.shape[2] != target_w:
mask_in = F.interpolate(mask_in, size=(target_h, target_w), mode="nearest")
# 取回 [H, W]
current_mask = mask_in[0, 0, :, :]
# B. 二值化
if binarize:
current_mask = (current_mask > binarize_threshold).float()
# C. 去噪
if remove_noise:
np_mask = current_mask.cpu().numpy()
binary_mask = np_mask > 0.1
labeled, num_features = ndimage.label(binary_mask)
component_sizes = ndimage.sum(binary_mask, labeled, range(num_features + 1))
too_small = component_sizes < 25
remove_pixel = too_small[labeled]
np_mask[remove_pixel] = 0
current_mask = torch.from_numpy(np_mask).to(mask.device)
# D. 叠加颜色
# output_image 是 [1, H, W, 3]
# current_mask 是 [H, W]
mask_3d = current_mask.unsqueeze(-1) # [H, W, 1]
mask_indices = mask_3d > 0.01
# 扩展维度以匹配 output_image [1, H, W, 3]
# mask_indices: [H, W, 1] -> [1, H, W, 1] -> broadcast to [1, H, W, 3]
mask_indices_expanded = mask_indices.unsqueeze(0).expand(-1, -1, -1, 3)
# color_tensor: [3] -> [1, 1, 1, 3]
color_target = color_tensor.view(1, 1, 1, 3).expand(1, target_h, target_w, 3)
# 使用 torch.where 进行叠加
output_image = torch.where(mask_indices_expanded, color_target, output_image)
rgb_str = ", ".join(used_colors_rgb)
hex_str = " , ".join(used_colors_hex)
info_string = f"including colors :\nRGB : {rgb_str}\nHex color : {hex_str}"
return (output_image, info_string)
class MultipleImageStitch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"inputcount": ("INT", {"default": 3, "min": 2, "max": 50, "step": 1, "tooltip": "Number of inputs."}),
"image_1": ("IMAGE", {"tooltip": "Base image for dimensions."}),
"direction": (["right", "down"], {"default": "right", "tooltip": "Stitch direction."}),
"size": ("INT", {"default": -1, "min": -1, "max": 8192, "step": 1, "tooltip": "Resize result. -1 to keep original."}),
},
"optional": {
"image_2": ("IMAGE",),
"image_3": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("stitched_image", "image_list")
OUTPUT_IS_LIST = (False, True)
FUNCTION = "process_images"
CATEGORY = "MyTemplate"
def process_images(self, inputcount, image_1, direction, size, **kwargs):
# 1. 初始收集
raw_inputs = [image_1]
for c in range(1, inputcount):
key = f"image_{c + 1}"
val = kwargs.get(key, None)
if val is not None:
raw_inputs.append(val)
# 2. 深度展平 (Handle List & Handle Batch)
flat_images = []
for item in raw_inputs:
# 2.1 先解开 Python List
# (有些节点 output_is_list=True 会传过来 list)
if isinstance(item, list):
sub_items = item
else:
sub_items = [item]
# 2.2 再解开 Tensor Batch
for img in sub_items:
# ComfyUI 图片通常是 [B, H, W, C]
# 容错:如果是 [H, W, C] 先升维
if img.dim() == 3:
img = img.unsqueeze(0)
# 核心逻辑:检测 Batch 维度
batch_count = img.shape[0]
if batch_count == 1:
flat_images.append(img)
else:
# 如果 Batch > 1,拆分为多个 [1, H, W, C]
for i in range(batch_count):
# 使用切片 [i:i+1] 保持维度为 4D [1, H, W, C]
flat_images.append(img[i:i+1])
if not flat_images:
raise ValueError("No images provided.")
# ============================================================
# [Alpha 通道兼容处理]
# ============================================================
max_channels = 3
for img in flat_images:
if img.shape[-1] > max_channels:
max_channels = img.shape[-1]
normalized_images = []
for img in flat_images:
current_c = img.shape[-1]
if current_c < max_channels:
if current_c == 3 and max_channels == 4:
alpha = torch.ones((img.shape[0], img.shape[1], img.shape[2], 1),
device=img.device, dtype=img.dtype)
img = torch.cat((img, alpha), dim=-1)
elif current_c == 1:
img = img.repeat(1, 1, 1, 3)
if max_channels == 4:
alpha = torch.ones((img.shape[0], img.shape[1], img.shape[2], 1),
device=img.device, dtype=img.dtype)
img = torch.cat((img, alpha), dim=-1)
normalized_images.append(img)
flat_images = normalized_images
# ============================================================
# 3. 确定基准尺寸
ref_img = flat_images[0]
ref_h, ref_w = ref_img.shape[1], ref_img.shape[2]
processed_images = []
# 4. 遍历处理 (缩放)
for img in flat_images:
img_permuted = img.permute(0, 3, 1, 2)
if direction == "right":
if img.shape[1] != ref_h:
ratio = ref_h / img.shape[1]
new_w = int(img.shape[2] * ratio)
img_stitch = F.interpolate(img_permuted, size=(ref_h, new_w), mode="bilinear", align_corners=False)
else:
img_stitch = img_permuted
else:
if img.shape[2] != ref_w:
ratio = ref_w / img.shape[2]
new_h = int(img.shape[1] * ratio)
img_stitch = F.interpolate(img_permuted, size=(new_h, ref_w), mode="bilinear", align_corners=False)
else:
img_stitch = img_permuted
processed_images.append(img_stitch.permute(0, 2, 3, 1))
# 5. 拼接
cat_dim = 2 if direction == "right" else 1
# 因为我们之前已经把 Batch 拆散了,这里 processed_images 全都是 Batch=1
# 所以直接 cat 肯定不会报错 (只要长宽处理对了)
stitched = torch.cat(processed_images, dim=cat_dim)
# 6. Global Resize
if size > 0:
final_h, final_w = stitched.shape[1], stitched.shape[2]
stitched_permuted = stitched.permute(0, 3, 1, 2)
if direction == "right":
scale_factor = size / final_h
new_w = int(final_w * scale_factor)
stitched_permuted = F.interpolate(stitched_permuted, size=(size, new_w), mode="bilinear", align_corners=False)
else:
scale_factor = size / final_w
new_h = int(final_h * scale_factor)
stitched_permuted = F.interpolate(stitched_permuted, size=(new_h, size), mode="bilinear", align_corners=False)
stitched = stitched_permuted.permute(0, 2, 3, 1)
# 返回: (Stitched Image, List of [1, H, W, C] tensors)
return (stitched, flat_images)
class MultipleMaskStitch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"inputcount": ("INT", {"default": 3, "min": 2, "max": 50, "step": 1, "tooltip": "Number of inputs."}),
"mask_1": ("MASK", {"tooltip": "Base mask for dimensions."}),
"direction": (["right", "down"], {"default": "right"}),
"size": ("INT", {"default": -1, "min": -1, "max": 8192, "step": 1}),
},
"optional": {
"mask_2": ("MASK",),
"mask_3": ("MASK",),
}
}
RETURN_TYPES = ("MASK", "MASK")
RETURN_NAMES = ("stitched_mask", "mask_list")
OUTPUT_IS_LIST = (False, True)
FUNCTION = "process_masks"
CATEGORY = "MyTemplate"
def process_masks(self, inputcount, mask_1, direction, size, **kwargs):
# 1. 初始收集
raw_inputs = [mask_1]
for c in range(1, inputcount):
key = f"mask_{c + 1}"
val = kwargs.get(key, None)
if val is not None:
raw_inputs.append(val)
# 2. 深度展平 (Handle List & Handle Batch)
flat_masks = []
for item in raw_inputs:
if isinstance(item, list):
sub_items = item
else:
sub_items = [item]
for m in sub_items:
# Mask 可能是 [H, W] 或 [B, H, W]
if m.dim() == 2:
m = m.unsqueeze(0) # 统一变成 [1, H, W]
batch_count = m.shape[0]
if batch_count == 1:
flat_masks.append(m)
else:
# 拆分 Batch -> 多个 [1, H, W]
for i in range(batch_count):
flat_masks.append(m[i:i+1])
if not flat_masks:
raise ValueError("No masks provided.")
# 3. 确定基准
ref_mask = flat_masks[0]
ref_h, ref_w = ref_mask.shape[1], ref_mask.shape[2]
processed_masks = []
# 4. 遍历处理
for m in flat_masks:
# m 已经是 [1, H, W]
m_in = m.unsqueeze(1) # [1, 1, H, W] for interpolate
if direction == "right":
if m.shape[1] != ref_h:
ratio = ref_h / m.shape[1]
new_w = int(m.shape[2] * ratio)
m_stitch = F.interpolate(m_in, size=(ref_h, new_w), mode="nearest")
else:
m_stitch = m_in
else:
if m.shape[2] != ref_w:
ratio = ref_w / m.shape[2]
new_h = int(m.shape[1] * ratio)
m_stitch = F.interpolate(m_in, size=(new_h, ref_w), mode="nearest")
else:
m_stitch = m_in
processed_masks.append(m_stitch.squeeze(1)) # Back to [1, H, W]
# 5. 拼接
cat_dim = 2 if direction == "right" else 1
stitched = torch.cat(processed_masks, dim=cat_dim)
# 6. Global Resize
if size > 0:
stitched_in = stitched.unsqueeze(1)
final_h, final_w = stitched.shape[1], stitched.shape[2]
if direction == "right":
scale_factor = size / final_h
new_w = int(final_w * scale_factor)
stitched_in = F.interpolate(stitched_in, size=(size, new_w), mode="nearest")
else:
scale_factor = size / final_w
new_h = int(final_h * scale_factor)
stitched_in = F.interpolate(stitched_in, size=(new_h, size), mode="nearest")
stitched = stitched_in.squeeze(1)
return (stitched, flat_masks)
NODE_CLASS_MAPPINGS = {
"MultipleTextInputs": MultipleTextInputs,
"MultipleMasksToImage": MultipleMasksToImage,
"MultipleImageStitch": MultipleImageStitch,
"MultipleMaskStitch": MultipleMaskStitch
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MultipleTextInputs": "Multiple Text Inputs",
"MultipleMasksToImage": "Multiple Masks to RGB Image",
"MultipleImageStitch": "Multiple Image Stitch",
"MultipleMaskStitch": "Multiple Mask Stitch"
}