531 lines
21 KiB
Python
531 lines
21 KiB
Python
import torch
|
|
import numpy as np
|
|
import os
|
|
import json
|
|
from PIL import Image, ImageOps, ImageSequence
|
|
from PIL.PngImagePlugin import PngInfo
|
|
import folder_paths
|
|
import node_helpers
|
|
|
|
class LoadImagesFromPathRGBA:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"directory": ("STRING", {"default": ""}),
|
|
},
|
|
"optional": {
|
|
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
|
|
"start_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
|
|
"load_always": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK", "STRING")
|
|
RETURN_NAMES = ("IMAGE", "MASK", "FILE PATH")
|
|
OUTPUT_IS_LIST = (True, True, True)
|
|
|
|
FUNCTION = "load_images"
|
|
|
|
CATEGORY = "Swan"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
if 'load_always' in kwargs and kwargs['load_always']:
|
|
return float("NaN")
|
|
else:
|
|
return hash(frozenset(kwargs))
|
|
|
|
def load_images(self, directory: str, image_load_cap: int = 0, start_index: int = 0, load_always=False):
|
|
if not os.path.isdir(directory):
|
|
raise FileNotFoundError(f"Directory '{directory}' cannot be found.")
|
|
dir_files = os.listdir(directory)
|
|
if len(dir_files) == 0:
|
|
raise FileNotFoundError(f"No files in directory '{directory}'.")
|
|
|
|
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp', '.bmp']
|
|
dir_files = [f for f in dir_files if any(f.lower().endswith(ext) for ext in valid_extensions)]
|
|
|
|
dir_files = sorted(dir_files)
|
|
dir_files = [os.path.join(directory, x) for x in dir_files]
|
|
|
|
dir_files = dir_files[start_index:]
|
|
|
|
images = []
|
|
masks = []
|
|
file_paths = []
|
|
|
|
limit_images = False
|
|
if image_load_cap > 0:
|
|
limit_images = True
|
|
image_count = 0
|
|
|
|
for image_path in dir_files:
|
|
if os.path.isdir(image_path):
|
|
continue
|
|
if limit_images and image_count >= image_load_cap:
|
|
break
|
|
|
|
try:
|
|
i = Image.open(image_path)
|
|
i = ImageOps.exif_transpose(i)
|
|
|
|
# 转换为RGBA模式
|
|
if i.mode != 'RGBA':
|
|
image = i.convert("RGBA")
|
|
else:
|
|
image = i
|
|
|
|
# 转换为numpy数组
|
|
image_np = np.array(image).astype(np.float32) / 255.0
|
|
image_tensor = torch.from_numpy(image_np)[None,]
|
|
|
|
# 提取Alpha通道作为mask
|
|
mask_np = image_np[:, :, 3]
|
|
mask_tensor = torch.from_numpy(mask_np)
|
|
|
|
images.append(image_tensor)
|
|
masks.append(mask_tensor)
|
|
file_paths.append(str(image_path))
|
|
image_count += 1
|
|
|
|
except Exception as e:
|
|
print(f"Failed to load image {image_path}: {str(e)}")
|
|
continue
|
|
|
|
if len(images) == 0:
|
|
raise Exception(f"No valid images found in directory '{directory}' after filtering and starting from index {start_index}")
|
|
|
|
return (images, masks, file_paths)
|
|
|
|
class SaveImageRGBA:
|
|
def __init__(self):
|
|
self.output_dir = folder_paths.get_output_directory()
|
|
self.type = "output"
|
|
self.prefix_append = ""
|
|
self.compress_level = 4
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE", {"tooltip": "The RGBA images to save."}),
|
|
"filename_prefix": ("STRING", {"default": "ComfyUI_RGBA", "tooltip": "The prefix for the file to save."})
|
|
},
|
|
"hidden": {
|
|
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_images_rgba"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "Swan"
|
|
DESCRIPTION = "Saves the input RGBA images as PNG with transparency to your ComfyUI output directory."
|
|
|
|
def save_images_rgba(self, images, filename_prefix="ComfyUI_RGBA", prompt=None, extra_pnginfo=None):
|
|
filename_prefix += self.prefix_append
|
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
|
results = list()
|
|
|
|
for (batch_number, image) in enumerate(images):
|
|
# 确保图像有4个通道 (RGBA)
|
|
if image.shape[2] < 4:
|
|
# 如果图像没有Alpha通道,添加一个不透明的Alpha通道
|
|
height, width = image.shape[0], image.shape[1]
|
|
alpha = torch.ones((height, width, 1), dtype=image.dtype)
|
|
image_rgba = torch.cat([image, alpha], dim=2)
|
|
else:
|
|
image_rgba = image
|
|
|
|
# 转换为numpy数组并缩放到0-255范围
|
|
i = 255. * image_rgba.cpu().numpy()
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8), 'RGBA')
|
|
|
|
metadata = None
|
|
if not hasattr(self, 'disable_metadata') or not self.disable_metadata:
|
|
metadata = PngInfo()
|
|
if prompt is not None:
|
|
metadata.add_text("prompt", json.dumps(prompt))
|
|
if extra_pnginfo is not None:
|
|
for x in extra_pnginfo:
|
|
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
|
|
|
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
|
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
|
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
|
|
results.append({
|
|
"filename": file,
|
|
"subfolder": subfolder,
|
|
"type": self.type
|
|
})
|
|
counter += 1
|
|
|
|
return { "ui": { "images": results } }
|
|
|
|
|
|
class LoadImageWithAlpha:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
input_dir = folder_paths.get_input_directory()
|
|
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
|
|
files = folder_paths.filter_files_content_types(files, ["image"])
|
|
return {"required":
|
|
{"image": (sorted(files), {"image_upload": True})},
|
|
}
|
|
|
|
CATEGORY = "Swan"
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT")
|
|
RETURN_NAMES = ("image", "mask", "width", "height")
|
|
FUNCTION = "load_image"
|
|
|
|
def load_image(self, image):
|
|
image_path = folder_paths.get_annotated_filepath(image)
|
|
|
|
img = node_helpers.pillow(Image.open, image_path)
|
|
|
|
output_images = []
|
|
output_masks = []
|
|
w, h = None, None
|
|
|
|
excluded_formats = ['MPO']
|
|
|
|
for i in ImageSequence.Iterator(img):
|
|
i = node_helpers.pillow(ImageOps.exif_transpose, i)
|
|
|
|
if i.mode == 'I':
|
|
i = i.point(lambda i: i * (1 / 255))
|
|
|
|
# 始终转换为RGBA模式
|
|
if i.mode != 'RGBA':
|
|
image = i.convert("RGBA")
|
|
else:
|
|
image = i
|
|
|
|
if len(output_images) == 0:
|
|
w = image.size[0]
|
|
h = image.size[1]
|
|
|
|
if image.size[0] != w or image.size[1] != h:
|
|
continue
|
|
|
|
# 将RGBA图像转换为numpy数组
|
|
image_np = np.array(image).astype(np.float32) / 255.0
|
|
image_tensor = torch.from_numpy(image_np)[None,]
|
|
|
|
# 提取Alpha通道作为mask
|
|
mask_np = image_np[:, :, 3]
|
|
mask_tensor = torch.from_numpy(mask_np)
|
|
|
|
output_images.append(image_tensor)
|
|
output_masks.append(mask_tensor.unsqueeze(0))
|
|
|
|
if len(output_images) > 1 and img.format not in excluded_formats:
|
|
output_image = torch.cat(output_images, dim=0)
|
|
output_mask = torch.cat(output_masks, dim=0)
|
|
else:
|
|
output_image = output_images[0]
|
|
output_mask = output_masks[0]
|
|
|
|
return (output_image, output_mask, w, h)
|
|
|
|
|
|
class ImageAlphaCrop:
|
|
"""
|
|
自动裁剪图像Alpha通道的透明边框
|
|
可以处理四周透明和中间镂空的情况
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE",),
|
|
"padding": ("INT", {
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 100,
|
|
"step": 1,
|
|
"display": "number"
|
|
}),
|
|
"alpha_threshold": ("FLOAT", {
|
|
"default": 0.01,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.01,
|
|
"display": "number"
|
|
}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("cropped_images",)
|
|
FUNCTION = "crop_alpha"
|
|
CATEGORY = "Swan"
|
|
DESCRIPTION = "根据Alpha通道自动裁剪图像的透明边框,输出带Alpha通道的图像"
|
|
|
|
def crop_alpha(self, images, padding=0, alpha_threshold=0.01):
|
|
"""
|
|
根据Alpha通道裁剪图像的透明边框
|
|
|
|
Args:
|
|
images: 输入图像张量 [B, H, W, C]
|
|
padding: 裁剪后添加的额外边距
|
|
alpha_threshold: Alpha阈值,低于此值视为透明
|
|
|
|
Returns:
|
|
cropped_images: 裁剪后的带Alpha通道的图像
|
|
"""
|
|
|
|
batch_size, height, width, channels = images.shape
|
|
cropped_images = []
|
|
|
|
for i in range(batch_size):
|
|
image_tensor = images[i]
|
|
|
|
# 确保图像有Alpha通道
|
|
if channels < 4:
|
|
# 如果没有Alpha通道,创建一个完全不透明的Alpha
|
|
alpha = torch.ones((height, width, 1), dtype=image_tensor.dtype)
|
|
image_with_alpha = torch.cat([image_tensor, alpha], dim=2)
|
|
else:
|
|
image_with_alpha = image_tensor
|
|
|
|
# 转换为numpy数组进行边界检测
|
|
image_np = (image_with_alpha.cpu().numpy() * 255).astype(np.uint8)
|
|
alpha_channel = image_np[:, :, 3]
|
|
|
|
# 找到非透明区域的边界
|
|
non_transparent = alpha_channel > (alpha_threshold * 255)
|
|
|
|
if not np.any(non_transparent):
|
|
# 如果整个图像都是透明的,返回原图
|
|
cropped_images.append(image_with_alpha.unsqueeze(0))
|
|
continue
|
|
|
|
# 找到边界坐标
|
|
rows = np.any(non_transparent, axis=1)
|
|
cols = np.any(non_transparent, axis=0)
|
|
|
|
y_min, y_max = np.where(rows)[0][[0, -1]]
|
|
x_min, x_max = np.where(cols)[0][[0, -1]]
|
|
|
|
# 添加边距
|
|
y_min = max(0, y_min - padding)
|
|
y_max = min(height - 1, y_max + padding)
|
|
x_min = max(0, x_min - padding)
|
|
x_max = min(width - 1, x_max + padding)
|
|
|
|
# 裁剪图像
|
|
cropped_image_np = image_np[y_min:y_max+1, x_min:x_max+1]
|
|
|
|
# 转换回tensor (保持RGBA格式)
|
|
cropped_image_tensor = torch.from_numpy(cropped_image_np.astype(np.float32) / 255.0)
|
|
|
|
cropped_images.append(cropped_image_tensor.unsqueeze(0))
|
|
|
|
# 合并批次
|
|
cropped_images_batch = torch.cat(cropped_images, dim=0)
|
|
|
|
return (cropped_images_batch,)
|
|
|
|
|
|
class ImageAlphaCropAdvanced:
|
|
"""
|
|
高级Alpha通道裁剪,支持保持宽高比和指定输出尺寸
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"transparent_images": ("IMAGE",), # 修改显示名称为"透明图像"
|
|
"padding": ("INT", {
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 100,
|
|
"step": 1
|
|
}),
|
|
"alpha_threshold": ("FLOAT", {
|
|
"default": 0.01,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.01
|
|
}),
|
|
"target_width": ("INT", {
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 4096,
|
|
"step": 8
|
|
}),
|
|
"target_height": ("INT", {
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 4096,
|
|
"step": 8
|
|
}),
|
|
"keep_aspect_ratio": (["true", "false"], {
|
|
"default": "true"
|
|
}),
|
|
"background_color": (["transparent", "black", "white"], {
|
|
"default": "transparent"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"original_images": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("cropped_images",)
|
|
FUNCTION = "crop_alpha_advanced"
|
|
CATEGORY = "Swan"
|
|
DESCRIPTION = "高级Alpha通道裁剪,支持保持宽高比和调整尺寸"
|
|
|
|
def crop_alpha_advanced(self, transparent_images, padding=0, alpha_threshold=0.01,
|
|
target_width=0, target_height=0, keep_aspect_ratio="true",
|
|
background_color="transparent", original_images=None):
|
|
"""
|
|
高级Alpha通道裁剪功能
|
|
|
|
Args:
|
|
transparent_images: 透明图像,用于确定裁剪区域
|
|
original_images: 原始图像,可选,如果提供则裁剪此图像
|
|
其他参数保持不变
|
|
"""
|
|
|
|
batch_size, height, width, channels = transparent_images.shape
|
|
cropped_images = []
|
|
|
|
# 如果没有提供原始图像,则使用透明图像
|
|
if original_images is None:
|
|
source_images = transparent_images
|
|
else:
|
|
source_images = original_images
|
|
|
|
source_batch_size = source_images.shape[0]
|
|
|
|
for i in range(batch_size):
|
|
source_idx = i if i < source_batch_size else source_batch_size - 1
|
|
|
|
transparent_image = transparent_images[i]
|
|
source_image = source_images[source_idx]
|
|
|
|
# 确保源图像有Alpha通道(如果是RGB,添加不透明Alpha通道)
|
|
source_channels = source_image.shape[2]
|
|
if source_channels < 4:
|
|
source_height, source_width = source_image.shape[0], source_image.shape[1]
|
|
alpha = torch.ones((source_height, source_width, 1), dtype=source_image.dtype)
|
|
source_with_alpha = torch.cat([source_image, alpha], dim=2)
|
|
else:
|
|
source_with_alpha = source_image
|
|
|
|
# 确保透明图像有Alpha通道
|
|
transparent_channels = transparent_image.shape[2]
|
|
if transparent_channels < 4:
|
|
transparent_height, transparent_width = transparent_image.shape[0], transparent_image.shape[1]
|
|
alpha = torch.ones((transparent_height, transparent_width, 1), dtype=transparent_image.dtype)
|
|
transparent_with_alpha = torch.cat([transparent_image, alpha], dim=2)
|
|
else:
|
|
transparent_with_alpha = transparent_image
|
|
|
|
# 调整源图像尺寸以匹配透明图像
|
|
if source_with_alpha.shape[0] != transparent_with_alpha.shape[0] or source_with_alpha.shape[1] != transparent_with_alpha.shape[1]:
|
|
# 使用PIL进行高质量缩放
|
|
source_np = (source_with_alpha.cpu().numpy() * 255).astype(np.uint8)
|
|
transparent_np = (transparent_with_alpha.cpu().numpy() * 255).astype(np.uint8)
|
|
|
|
source_pil = Image.fromarray(source_np, mode='RGBA')
|
|
target_size = (transparent_np.shape[1], transparent_np.shape[0])
|
|
resized_source_pil = source_pil.resize(target_size, Image.LANCZOS)
|
|
resized_source_np = np.array(resized_source_pil).astype(np.float32) / 255.0
|
|
source_with_alpha = torch.from_numpy(resized_source_np)
|
|
|
|
# 使用透明图像的Alpha通道确定裁剪区域
|
|
transparent_np = (transparent_with_alpha.cpu().numpy() * 255).astype(np.uint8)
|
|
alpha_channel = transparent_np[:, :, 3]
|
|
|
|
# 找到非透明区域的边界
|
|
non_transparent = alpha_channel > (alpha_threshold * 255)
|
|
|
|
if not np.any(non_transparent):
|
|
# 如果整个图像都是透明的,返回原图
|
|
cropped_images.append(source_with_alpha.unsqueeze(0))
|
|
continue
|
|
|
|
# 找到边界坐标
|
|
rows = np.any(non_transparent, axis=1)
|
|
cols = np.any(non_transparent, axis=0)
|
|
|
|
y_min, y_max = np.where(rows)[0][[0, -1]]
|
|
x_min, x_max = np.where(cols)[0][[0, -1]]
|
|
|
|
# 添加边距
|
|
y_min = max(0, y_min - padding)
|
|
y_max = min(transparent_np.shape[0] - 1, y_max + padding)
|
|
x_min = max(0, x_min - padding)
|
|
x_max = min(transparent_np.shape[1] - 1, x_max + padding)
|
|
|
|
# 裁剪源图像
|
|
source_np = (source_with_alpha.cpu().numpy() * 255).astype(np.uint8)
|
|
cropped_image_np = source_np[y_min:y_max+1, x_min:x_max+1]
|
|
crop_height, crop_width = cropped_image_np.shape[:2]
|
|
|
|
# 调整尺寸(如果需要)
|
|
if target_width > 0 and target_height > 0:
|
|
if keep_aspect_ratio == "true":
|
|
# 保持宽高比调整尺寸
|
|
scale = min(target_width / crop_width, target_height / crop_height)
|
|
new_width = int(crop_width * scale)
|
|
new_height = int(crop_height * scale)
|
|
|
|
pil_image = Image.fromarray(cropped_image_np, mode='RGBA')
|
|
resized_pil = pil_image.resize((new_width, new_height), Image.LANCZOS)
|
|
|
|
# 如果需要,创建带背景的图像
|
|
if background_color != "transparent" and new_width != target_width or new_height != target_height:
|
|
background_img = Image.new('RGBA', (target_width, target_height),
|
|
(0, 0, 0, 0) if background_color == "transparent" else
|
|
(255, 255, 255, 255) if background_color == "white" else
|
|
(0, 0, 0, 255))
|
|
x_offset = (target_width - new_width) // 2
|
|
y_offset = (target_height - new_height) // 2
|
|
background_img.paste(resized_pil, (x_offset, y_offset), resized_pil)
|
|
resized_pil = background_img
|
|
|
|
resized_np = np.array(resized_pil).astype(np.float32) / 255.0
|
|
cropped_image_tensor = torch.from_numpy(resized_np)
|
|
else:
|
|
pil_image = Image.fromarray(cropped_image_np, mode='RGBA')
|
|
resized_pil = pil_image.resize((target_width, target_height), Image.LANCZOS)
|
|
resized_np = np.array(resized_pil).astype(np.float32) / 255.0
|
|
cropped_image_tensor = torch.from_numpy(resized_np)
|
|
else:
|
|
cropped_image_tensor = torch.from_numpy(cropped_image_np.astype(np.float32) / 255.0)
|
|
|
|
cropped_images.append(cropped_image_tensor.unsqueeze(0))
|
|
|
|
cropped_images_batch = torch.cat(cropped_images, dim=0)
|
|
|
|
return (cropped_images_batch,)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"LoadImageWithAlpha": LoadImageWithAlpha,
|
|
"ImageAlphaCrop": ImageAlphaCrop,
|
|
"ImageAlphaCropAdvanced": ImageAlphaCropAdvanced,
|
|
"SaveImageRGBA": SaveImageRGBA,
|
|
"LoadImagesFromPathRGBA": LoadImagesFromPathRGBA,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"LoadImageWithAlpha": "Load Image (With Alpha)",
|
|
"ImageAlphaCrop": "Image Alpha Crop",
|
|
"ImageAlphaCropAdvanced": "Image Alpha Crop (Advanced)",
|
|
"SaveImageRGBA": "Save Image (RGBA)",
|
|
"LoadImagesFromPathRGBA": "Load Images From Path (RGBA)",
|
|
}
|