no message

This commit is contained in:
陆赛
2025-01-05 23:51:09 +08:00
parent 7dc2b51685
commit ea5a653d26
10 changed files with 1691 additions and 3 deletions
+7 -3
View File
@@ -1,10 +1,14 @@
from .ad_door import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
from .rp import NODE_CLASS_MAPPINGS as RHAPI_NODE_CLASS_MAPPINGS
from .rp import NODE_DISPLAY_NAME_MAPPINGS as RHAPI_NODE_DISPLAY_NAME_MAPPINGS
from .nodes import NODE_CLASS_MAPPINGS as NODES_CLASS_MAPPINGS
from .nodes import NODE_DISPLAY_NAME_MAPPINGS as NODES_DISPLAY_NAME_MAPPINGS
# Update mappings to include RHAPI mappings
# Update mappings to include RHAPI mappings and nodes mappings
NODE_CLASS_MAPPINGS.update(RHAPI_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(NODES_CLASS_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(RHAPI_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(NODES_DISPLAY_NAME_MAPPINGS)
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
# WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
+153
View File
@@ -0,0 +1,153 @@
import torch
import torch.nn.functional as F
import comfy.utils
MAX_RESOLUTION = 8192
class AD_ImageResize:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"width": ("INT", {
"default": 512,
"min": 0,
"max": MAX_RESOLUTION,
"step": 1,
}),
"height": ("INT", {
"default": 512,
"min": 0,
"max": MAX_RESOLUTION,
"step": 1,
}),
"interpolation": (["nearest", "bilinear", "bicubic", "area", "nearest-exact", "lanczos"],),
"method": (["stretch", "keep proportion", "fill / crop", "pad"],),
"condition": (["always", "downscale if bigger", "upscale if smaller", "if bigger area", "if smaller area"],),
"multiple_of": ("INT", {
"default": 0,
"min": 0,
"max": 512,
"step": 1,
}),
},
"optional": {
"reference_image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE", "INT", "INT",)
RETURN_NAMES = ("IMAGE", "width", "height",)
FUNCTION = "execute"
CATEGORY = "🌻 Addoor/image"
def execute(self, image, width, height, method="stretch", interpolation="nearest", condition="always", multiple_of=0, reference_image=None):
# 如果有参考图,使用其尺寸
if reference_image is not None:
_, ref_h, ref_w, _ = reference_image.shape
width = ref_w
height = ref_h
print(f"Using reference image size: {ref_w}x{ref_h}")
_, oh, ow, _ = image.shape
x = y = x2 = y2 = 0
pad_left = pad_right = pad_top = pad_bottom = 0
if multiple_of > 1:
width = width - (width % multiple_of)
height = height - (height % multiple_of)
if method == 'keep proportion' or method == 'pad':
if width == 0 and oh < height:
width = MAX_RESOLUTION
elif width == 0 and oh >= height:
width = ow
if height == 0 and ow < width:
height = MAX_RESOLUTION
elif height == 0 and ow >= width:
height = oh
ratio = min(width / ow, height / oh)
new_width = round(ow*ratio)
new_height = round(oh*ratio)
if method == 'pad':
pad_left = (width - new_width) // 2
pad_right = width - new_width - pad_left
pad_top = (height - new_height) // 2
pad_bottom = height - new_height - pad_top
width = new_width
height = new_height
elif method.startswith('fill'):
width = width if width > 0 else ow
height = height if height > 0 else oh
ratio = max(width / ow, height / oh)
new_width = round(ow*ratio)
new_height = round(oh*ratio)
x = (new_width - width) // 2
y = (new_height - height) // 2
x2 = x + width
y2 = y + height
if x2 > new_width:
x -= (x2 - new_width)
if x < 0:
x = 0
if y2 > new_height:
y -= (y2 - new_height)
if y < 0:
y = 0
width = new_width
height = new_height
else:
width = width if width > 0 else ow
height = height if height > 0 else oh
if "always" in condition \
or ("downscale if bigger" == condition and (oh > height or ow > width)) \
or ("upscale if smaller" == condition and (oh < height or ow < width)) \
or ("bigger area" in condition and (oh * ow > height * width)) \
or ("smaller area" in condition and (oh * ow < height * width)):
outputs = image.permute(0,3,1,2)
if interpolation == "lanczos":
outputs = comfy.utils.lanczos(outputs, width, height)
else:
outputs = F.interpolate(outputs, size=(height, width), mode=interpolation)
if method == 'pad':
if pad_left > 0 or pad_right > 0 or pad_top > 0 or pad_bottom > 0:
outputs = F.pad(outputs, (pad_left, pad_right, pad_top, pad_bottom), value=0)
outputs = outputs.permute(0,2,3,1)
if method.startswith('fill'):
if x > 0 or y > 0 or x2 > 0 or y2 > 0:
outputs = outputs[:, y:y2, x:x2, :]
else:
outputs = image
if multiple_of > 1 and (outputs.shape[2] % multiple_of != 0 or outputs.shape[1] % multiple_of != 0):
width = outputs.shape[2]
height = outputs.shape[1]
x = (width % multiple_of) // 2
y = (height % multiple_of) // 2
x2 = width - ((width % multiple_of) - x)
y2 = height - ((height % multiple_of) - y)
outputs = outputs[:, y:y2, x:x2, :]
outputs = torch.clamp(outputs, 0, 1)
return (outputs, outputs.shape[2], outputs.shape[1],)
NODE_CLASS_MAPPINGS = {
"AD_image-resize": AD_ImageResize,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AD_image-resize": "AD Image Resize",
}
+317
View File
@@ -0,0 +1,317 @@
import PIL.Image as Image
import PIL.ImageDraw as ImageDraw
import PIL.ImageFilter as ImageFilter
import numpy as np
import torch
import torchvision.transforms as t
import math
class AD_MockupMaker:
"""Create mockup with scaled image overlay and blurred background."""
def __init__(self):
pass
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "create_mockup"
CATEGORY = "🌻 Addoor/image"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"overlay_image": ("IMAGE",),
"background_image": ("IMAGE",),
"target_width": ("INT", {
"default": 512,
"min": 64,
"max": 4096,
"step": 8,
"description": "Target width for the overlay"
}),
"target_height": ("INT", {
"default": 512,
"min": 64,
"max": 4096,
"step": 8,
"description": "Target height for the overlay"
}),
"corner_radius": ("INT", {
"default": 0,
"min": 0,
"max": 500,
"step": 1,
"description": "Radius for rounded corners"
}),
"offset_x": ("INT", {
"default": 0,
"min": -1000,
"max": 1000,
"step": 1,
"description": "Horizontal offset in pixels"
}),
"offset_y": ("INT", {
"default": 0,
"min": -1000,
"max": 1000,
"step": 1,
"description": "Vertical offset in pixels"
}),
"blur_radius": ("FLOAT", {
"default": 10.0,
"min": 0.0,
"max": 50.0,
"step": 0.5,
"description": "Gaussian blur radius for background"
}),
"SSAA": ("INT", {
"default": 2,
"min": 1,
"max": 4,
"step": 1,
"description": "Super Sampling Anti-Aliasing factor"
}),
"method": (["lanczos", "bicubic", "bilinear"], {
"default": "lanczos",
"description": "Resampling method"
}),
},
"optional": {
"watermark": ("IMAGE",),
"mask": ("MASK",),
}
}
def add_corners(self, image, radius, ssaa=1):
"""Add rounded corners to an image."""
if radius <= 0:
return image
# 调整圆角半径以适应SSAA
working_radius = radius * ssaa
# 创建圆角蒙版
mask = Image.new('L', image.size, 0)
draw = ImageDraw.Draw(mask)
draw.rounded_rectangle(
[(0, 0), (image.width, image.height)],
radius=working_radius,
fill=255
)
# 确保图像为RGBA模式
output = image.convert('RGBA')
output.putalpha(mask)
return output
def fit_and_crop(self, image, target_width, target_height, method, ssaa=1):
"""Fit image to target size maintaining aspect ratio and crop if necessary."""
# 计算SSAA尺寸
ssaa_width = target_width * ssaa
ssaa_height = target_height * ssaa
# 计算目标尺寸与原始尺寸的比例
width_ratio = ssaa_width / image.width
height_ratio = ssaa_height / image.height
# 使用较大的比例来确保填充目标区域
scale = max(width_ratio, height_ratio)
# 缩放图像
new_width = int(image.width * scale)
new_height = int(image.height * scale)
resized = image.resize((new_width, new_height), method)
# 计算裁切区域
left = (new_width - ssaa_width) // 2
top = (new_height - ssaa_height) // 2
right = left + ssaa_width
bottom = top + ssaa_height
# 裁切到目标尺寸
cropped = resized.crop((left, top, right, bottom))
return cropped
def create_mockup(
self,
overlay_image,
background_image,
target_width: int,
target_height: int,
corner_radius: int,
offset_x: int,
offset_y: int,
blur_radius: float,
SSAA: int,
method: str,
watermark = None,
mask = None,
):
try:
print("Starting mockup creation...")
# 1. 初始化图像
overlay = tensor_to_image(overlay_image[0])
background = tensor_to_image(background_image[0])
print(f"Original sizes - Overlay: {overlay.size}, Background: {background.size}")
# 2. 调整背景图尺寸并模糊
background = background.resize(
(overlay.width, overlay.height),
get_sampler_by_name(method)
)
if blur_radius > 0:
background = background.filter(ImageFilter.GaussianBlur(radius=blur_radius))
# 3. 缩放并裁切主图
scaled_overlay = self.fit_and_crop(
overlay,
target_width,
target_height,
get_sampler_by_name(method),
SSAA
)
print(f"After scaling - Overlay: {scaled_overlay.size}")
# 4. 转换为RGBA模式
scaled_overlay = scaled_overlay.convert('RGBA')
# 5. 添加圆角
if corner_radius > 0:
scaled_overlay = self.add_corners(scaled_overlay, corner_radius, SSAA)
# 6. 如果使用了SSAA,缩小到目标尺寸
if SSAA > 1:
scaled_overlay = scaled_overlay.resize(
(target_width, target_height),
get_sampler_by_name(method)
)
# 7. 应用mask遮罩(如果有)
if mask is not None:
try:
# 处理mask维度
if len(mask.shape) == 3:
mask = mask.squeeze(0)
if len(mask.shape) == 3:
mask = mask.squeeze(-1)
# 转换mask为PIL Image
mask_array = mask.cpu().numpy()
mask_array = (mask_array * 255).astype(np.uint8)
mask_img = Image.fromarray(mask_array, mode='L')
# 首先将mask调整到原图尺寸
mask_img = mask_img.resize(
(background.width, background.height),
get_sampler_by_name(method)
)
# 然后裁切出需要的部分(与scaled_overlay相同大小的区域)
crop_x = (background.width - scaled_overlay.width) // 2 + offset_x
crop_y = (background.height - scaled_overlay.height) // 2 + offset_y
mask_img = mask_img.crop((
crop_x,
crop_y,
crop_x + scaled_overlay.width,
crop_y + scaled_overlay.height
))
print(f"Mask size: {mask_img.size}, Overlay size: {scaled_overlay.size}")
# 获取当前alpha通道
r, g, b, a = scaled_overlay.split()
# 合并mask和现有alpha通道
if corner_radius > 0:
# 如果有圆角,将mask与现有alpha通道相乘
combined_alpha = Image.fromarray(
(np.array(mask_img) * np.array(a) / 255).astype(np.uint8)
)
else:
# 如果没有圆角,直接使用mask
combined_alpha = mask_img
# 更新alpha通道
scaled_overlay.putalpha(combined_alpha)
print("Mask applied successfully")
except Exception as e:
print(f"Error processing mask: {str(e)}")
import traceback
traceback.print_exc()
print(f"Final overlay size: {scaled_overlay.size}")
# 8. 准备最终合成
result = background.copy()
result = result.convert('RGBA')
# 9. 计算粘贴位置并合成主图
paste_x = (background.width - scaled_overlay.width) // 2 + offset_x
paste_y = (background.height - scaled_overlay.height) // 2 + offset_y
temp = Image.new('RGBA', result.size, (0, 0, 0, 0))
temp.paste(scaled_overlay, (paste_x, paste_y), scaled_overlay)
result = Image.alpha_composite(result, temp)
# 10. 添加水印(如果有)
if watermark is not None:
try:
watermark_img = tensor_to_image(watermark[0])
watermark_img = watermark_img.convert('RGBA')
watermark_img = watermark_img.resize(
(background.width, background.height),
get_sampler_by_name(method)
)
result = Image.alpha_composite(result, watermark_img)
except Exception as e:
print(f"Error processing watermark: {str(e)}")
# 11. 最终转换
result = result.convert('RGB')
tensor = image_to_tensor(result)
tensor = tensor.unsqueeze(0)
tensor = tensor.permute(0, 2, 3, 1)
print("Mockup creation completed successfully")
return (tensor,)
except Exception as e:
print(f"Error in create_mockup: {str(e)}")
import traceback
traceback.print_exc()
return (overlay_image,)
def get_sampler_by_name(method: str) -> int:
"""Get PIL resampling method by name."""
samplers = {
"lanczos": Image.LANCZOS,
"bicubic": Image.BICUBIC,
"bilinear": Image.BILINEAR,
}
return samplers.get(method, Image.LANCZOS)
def tensor_to_image(tensor):
"""Convert tensor to PIL Image."""
if len(tensor.shape) == 4:
tensor = tensor.squeeze(0)
return t.ToPILImage()(tensor.permute(2, 0, 1))
def image_to_tensor(image):
"""Convert PIL Image to tensor."""
tensor = t.ToTensor()(image)
return tensor
NODE_CLASS_MAPPINGS = {
"AD_mockup-maker": AD_MockupMaker,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AD_mockup-maker": "AD Mockup Maker",
}
+201
View File
@@ -0,0 +1,201 @@
import PIL.Image as Image
import PIL.ImageDraw as ImageDraw
import numpy as np
import torch
import torchvision.transforms as t
class AD_PosterMaker:
"""Advanced poster maker with scaling, border, background and composition."""
COLOR_PRESETS = {
"white": "#FFFFFF",
"black": "#000000",
"gray": "#808080",
"custom": "custom"
}
def __init__(self):
pass
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "create_poster"
CATEGORY = "🌻 Addoor/image"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"scale": ("FLOAT", {
"default": 0.8,
"min": 0.1,
"max": 1.0,
"step": 0.05,
"description": "Scale factor for the inner image"
}),
"border_size": ("INT", {
"default": 32,
"min": 0,
"max": 256,
"step": 4,
"description": "Border size in pixels"
}),
"border_color": (list(cls.COLOR_PRESETS.keys()), {
"default": "white"
}),
"border_hex": ("STRING", {
"default": "#FFFFFF",
"multiline": False
}),
"background_color": (list(cls.COLOR_PRESETS.keys()), {
"default": "white"
}),
"background_hex": ("STRING", {
"default": "#FFFFFF",
"multiline": False
}),
"position": (["left", "right", "top", "bottom"], {
"default": "right",
"description": "Position of the original image"
}),
"method": (["lanczos", "bicubic", "bilinear"], {
"default": "lanczos",
"description": "Resampling method"
}),
},
"optional": {
"watermark": ("IMAGE",),
"original_image": ("IMAGE",),
}
}
def hex_to_rgb(self, hex_color: str) -> tuple:
"""Convert hex color to RGB tuple."""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def create_poster(
self,
image,
scale: float,
border_size: int,
border_color: str,
border_hex: str,
background_color: str,
background_hex: str,
position: str,
method: str,
watermark = None,
original_image = None,
):
try:
# 转换输入图像为PIL
original = tensor_to_image(image[0])
# 创建带边框的图
bg_rgb = self.hex_to_rgb(self.COLOR_PRESETS[background_color] if background_color != "custom" else background_hex)
background = Image.new(
"RGB",
(original.width + border_size * 2, original.height + border_size * 2),
bg_rgb
)
background.paste(original, (border_size, border_size))
# 处理水印
if watermark is not None:
watermark_img = tensor_to_image(watermark[0])
if watermark_img.mode != 'RGBA':
watermark_img = watermark_img.convert('RGBA')
watermark_img = watermark_img.resize(background.size, get_sampler_by_name(method))
background = background.convert('RGBA')
background = Image.alpha_composite(background, watermark_img)
background = background.convert('RGB')
# 获取目标尺寸(根据original_image或原始image)
if original_image is not None:
_, target_h, target_w, _ = original_image.shape
target_img = tensor_to_image(original_image[0])
else:
target_w, target_h = original.size
target_img = original
# 根据拼接方向调整边框图尺寸
is_horizontal = position in ["left", "right"]
if is_horizontal:
# 横排,高度需要匹配
ratio = target_h / background.height
new_width = int(background.width * ratio)
new_height = target_h
else:
# 竖排,宽度需要匹配
ratio = target_w / background.width
new_width = target_w
new_height = int(background.height * ratio)
# 调整边框图尺寸
background = background.resize((new_width, new_height), get_sampler_by_name(method))
print(f"Adjusted background size: {new_width}x{new_height}")
# 创建最终图像并拼接
if is_horizontal:
final_width = new_width + target_w
final_height = max(new_height, target_h)
else:
final_width = max(new_width, target_w)
final_height = new_height + target_h
final_image = Image.new("RGB", (final_width, final_height))
# 根据位置拼接
if position == "left":
final_image.paste(target_img, (0, 0))
final_image.paste(background, (target_w, 0))
elif position == "right":
final_image.paste(background, (0, 0))
final_image.paste(target_img, (new_width, 0))
elif position == "top":
final_image.paste(target_img, (0, 0))
final_image.paste(background, (0, target_h))
else: # bottom
final_image.paste(background, (0, 0))
final_image.paste(target_img, (0, new_height))
# 转换回tensor
tensor = image_to_tensor(final_image)
tensor = tensor.unsqueeze(0)
tensor = tensor.permute(0, 2, 3, 1)
return (tensor,)
except Exception as e:
print(f"Error creating poster: {str(e)}")
return (image,)
def get_sampler_by_name(method: str) -> int:
"""Get PIL resampling method by name."""
samplers = {
"lanczos": Image.LANCZOS,
"bicubic": Image.BICUBIC,
"bilinear": Image.BILINEAR,
}
return samplers.get(method, Image.LANCZOS)
def tensor_to_image(tensor):
"""Convert tensor to PIL Image."""
if len(tensor.shape) == 4:
tensor = tensor.squeeze(0)
return t.ToPILImage()(tensor.permute(2, 0, 1))
def image_to_tensor(image):
"""Convert PIL Image to tensor."""
tensor = t.ToTensor()(image)
return tensor
NODE_CLASS_MAPPINGS = {
"AD_poster-maker": AD_PosterMaker,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AD_poster-maker": "AD Poster Maker",
}
+119
View File
@@ -0,0 +1,119 @@
"""
@author: ComfyUI Addoor
@title: ComfyUI-PromptSaver
@description: Save prompts to CSV file with customizable naming pattern
@version: 1.0.0
"""
import os
import csv
import folder_paths
from typing import Dict, Any
class AD_PromptSaver:
"""Save prompts to CSV file with customizable naming pattern"""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"prompt": ("STRING", {"multiline": True}),
"csv_filename": ("STRING", {"default": "prompts.csv"}),
"folder": ("STRING", {"default": ""}),
"filename_prefix": ("STRING", {"default": "Image"}),
"filename_delimiter": ("STRING", {"default": "_"}),
"filename_number_padding": ("INT", {"default": 4, "min": 0, "max": 9, "step": 1}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("status",)
FUNCTION = "save_prompt"
CATEGORY = "🌻 Addoor/prompt"
def generate_entry_name(self, filename_prefix: str, filename_delimiter: str, folder: str, filename_number_padding: int) -> str:
"""Generate the entry name using the specified pattern"""
if folder:
# 如果提供了文件夹路径,从 ComfyUI 根目录开始
comfy_path = os.path.dirname(self.output_dir)
full_output_folder = os.path.join(comfy_path, folder)
else:
# 如果没有提供,使用默认输出目录
full_output_folder = self.output_dir
# 确保目录存在
os.makedirs(full_output_folder, exist_ok=True)
# 获取目录中现有的文件数量
counter = 1
pattern = f"{filename_prefix}{filename_delimiter}"
existing_files = [f for f in os.listdir(full_output_folder) if f.startswith(pattern)]
if existing_files:
numbers = []
for f in existing_files:
try:
num = int(f[len(pattern):].split('.')[0])
numbers.append(num)
except ValueError:
continue
if numbers:
counter = max(numbers) + 1
# 使用指定的填充长度
if filename_number_padding > 0:
return f"{filename_prefix}{filename_delimiter}{counter:0{filename_number_padding}d}"
else:
return f"{filename_prefix}"
def save_prompt(self, prompt: str, csv_filename: str, folder: str,
filename_prefix: str, filename_delimiter: str, filename_number_padding: int) -> tuple:
"""Save prompt to CSV file"""
try:
# 处理保存路径
if folder:
# 如果提供了文件夹路径,从 ComfyUI 根目录开始
comfy_path = os.path.dirname(self.output_dir)
full_output_folder = os.path.join(comfy_path, folder)
else:
# 如果没有提供,使用默认输出目录
full_output_folder = self.output_dir
os.makedirs(full_output_folder, exist_ok=True)
# CSV文件完整路径
csv_path = os.path.join(full_output_folder, csv_filename)
# 生成条目名称
entry_name = self.generate_entry_name(filename_prefix, filename_delimiter, folder, filename_number_padding)
# 准备要写入的行
new_row = [entry_name, prompt]
# 检查文件是否存在并写入
file_exists = os.path.exists(csv_path)
mode = 'a' if file_exists else 'w'
with open(csv_path, mode, newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# 如果是新文件,写入标题行
if not file_exists:
writer.writerow(['Name', 'Prompt'])
writer.writerow(new_row)
return (f"Successfully saved prompt for {entry_name}",)
except Exception as e:
return (f"Error saving prompt: {str(e)}",)
# 节点注册
NODE_CLASS_MAPPINGS = {
"AD_prompt-saver": AD_PromptSaver
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AD_prompt-saver": "AD Prompt Saver"
}
+435
View File
@@ -0,0 +1,435 @@
"""
@author: ComfyNodePRs
@title: ComfyUI Advanced Padding
@description: Advanced padding node with scaling capabilities
@version: 1.0.0
@project: https://github.com/ComfyNodePRs/advanced-padding
@author: https://github.com/ComfyNodePRs
"""
import PIL.Image as Image
import PIL.ImageDraw as ImageDraw
import numpy as np
import torch
import torchvision.transforms as t
class AD_PaddingAdvanced:
def __init__(self):
pass
FUNCTION = "process_image"
CATEGORY = "🌻 Addoor/image"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"scale_by": ("FLOAT", {
"default": 1.0,
"min": 0.1,
"max": 8.0,
"step": 0.05,
"display": "number"
}),
"upscale_method": (["nearest-exact", "bilinear", "bicubic", "lanczos"], {"default": "lanczos"}),
"left": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
"top": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
"right": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
"bottom": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
"color": ("STRING", {"default": "#ffffff"}),
"transparent": ("BOOLEAN", {"default": False}),
},
"optional": {
"background": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image", "mask")
def add_padding(self, image, left, top, right, bottom, color="#ffffff", transparent=False):
padded_images = []
image = [self.tensor2pil(img) for img in image]
for img in image:
padded_image = Image.new("RGBA" if transparent else "RGB",
(img.width + left + right, img.height + top + bottom),
(0, 0, 0, 0) if transparent else self.hex_to_tuple(color))
padded_image.paste(img, (left, top))
padded_images.append(self.pil2tensor(padded_image))
return torch.cat(padded_images, dim=0)
def create_mask(self, image, left, top, right, bottom):
masks = []
image = [self.tensor2pil(img) for img in image]
for img in image:
shape = (left, top, img.width + left, img.height + top)
mask_image = Image.new("L", (img.width + left + right, img.height + top + bottom), 255)
draw = ImageDraw.Draw(mask_image)
draw.rectangle(shape, fill=0)
masks.append(self.pil2tensor(mask_image))
return torch.cat(masks, dim=0)
def scale_image(self, image, scale_by, method):
scaled_images = []
image = [self.tensor2pil(img) for img in image]
resampling_methods = {
"nearest-exact": Image.Resampling.NEAREST,
"bilinear": Image.Resampling.BILINEAR,
"bicubic": Image.Resampling.BICUBIC,
"lanczos": Image.Resampling.LANCZOS,
}
for img in image:
# 计算新尺寸
new_width = int(img.width * scale_by)
new_height = int(img.height * scale_by)
# 使用选定的方法进行缩放
scaled_img = img.resize(
(new_width, new_height),
resampling_methods.get(method, Image.Resampling.LANCZOS)
)
scaled_images.append(self.pil2tensor(scaled_img))
return torch.cat(scaled_images, dim=0)
def hex_to_tuple(self, color):
if not isinstance(color, str):
raise ValueError("Color must be a hex string")
color = color.strip("#")
return tuple([int(color[i:i + 2], 16) for i in range(0, len(color), 2)])
def tensor2pil(self, image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def pil2tensor(self, image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def composite_with_background(self, image, background):
"""将图片居中合成到背景上"""
image_pil = self.tensor2pil(image[0]) # 获取第一帧
bg_pil = self.tensor2pil(background[0])
# 创建新的景图像
result = bg_pil.copy()
# 计算居中位置
x = (bg_pil.width - image_pil.width) // 2
y = (bg_pil.height - image_pil.height) // 2
# 如果前景图比背景大,需要裁剪
if image_pil.width > bg_pil.width or image_pil.height > bg_pil.height:
# 计算裁剪区域
crop_left = max(0, (image_pil.width - bg_pil.width) // 2)
crop_top = max(0, (image_pil.height - bg_pil.height) // 2)
crop_right = min(image_pil.width, crop_left + bg_pil.width)
crop_bottom = min(image_pil.height, crop_top + bg_pil.height)
# 裁剪图片
image_pil = image_pil.crop((crop_left, crop_top, crop_right, crop_bottom))
# 更新粘贴位置
x = max(0, (bg_pil.width - image_pil.width) // 2)
y = max(0, (bg_pil.height - image_pil.height) // 2)
# 如果前景图有透明通道,使用alpha通道合成
if image_pil.mode == 'RGBA':
result.paste(image_pil, (x, y), image_pil)
else:
result.paste(image_pil, (x, y))
return self.pil2tensor(result)
def process_image(self, image, scale_by, upscale_method, left, top, right, bottom, color, transparent, background=None):
# 首先进行缩放
if scale_by != 1.0:
image = self.scale_image(image, scale_by, upscale_method)
# 添加padding
padded_image = self.add_padding(image, left, top, right, bottom, color, transparent)
# 如果有背景图,进行合成
if background is not None:
result = []
for i in range(len(padded_image)):
# 处理每一帧
frame = padded_image[i:i+1]
composited = self.composite_with_background(frame, background)
result.append(composited)
padded_image = torch.cat(result, dim=0)
# 创建mask
mask = self.create_mask(image, left, top, right, bottom)
return (padded_image, mask)
class AD_ImageConcat:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image1": ("IMAGE",),
"image2": ("IMAGE",),
"direction": (["horizontal", "vertical"], {"default": "horizontal"}),
"match_size": ("BOOLEAN", {"default": True}),
"method": (["lanczos", "bicubic", "bilinear", "nearest"], {"default": "lanczos"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "concat_images"
CATEGORY = "AD/image"
def concat_images(self, image1, image2, direction="horizontal", match_size=False, method="lanczos"):
try:
# 转换为 PIL 图像
img1 = tensor_to_image(image1[0])
img2 = tensor_to_image(image2[0])
# 确保两张图片的模式相同
if img1.mode != img2.mode:
if 'A' in img1.mode or 'A' in img2.mode:
img1 = img1.convert('RGBA')
img2 = img2.convert('RGBA')
else:
img1 = img1.convert('RGB')
img2 = img2.convert('RGB')
# 如果需要匹配尺寸
if match_size:
if direction == "horizontal":
# 横向拼接,匹配高度
if img1.height != img2.height:
new_height = img2.height
new_width = int(img1.width * (new_height / img1.height))
img1 = img1.resize(
(new_width, new_height),
get_sampler_by_name(method)
)
else: # vertical
# 纵向拼接,匹配宽度
if img1.width != img2.width:
new_width = img2.width
new_height = int(img1.height * (new_width / img1.width))
img1 = img1.resize(
(new_width, new_height),
get_sampler_by_name(method)
)
# 创建新图像
if direction == "horizontal":
new_width = img1.width + img2.width
new_height = max(img1.height, img2.height)
else: # vertical
new_width = max(img1.width, img2.width)
new_height = img1.height + img2.height
# 创建新的画布
mode = img1.mode
new_image = Image.new(mode, (new_width, new_height))
# 计算粘贴位置(居中对齐)
if direction == "horizontal":
y1 = (new_height - img1.height) // 2
y2 = (new_height - img2.height) // 2
new_image.paste(img1, (0, y1))
new_image.paste(img2, (img1.width, y2))
else: # vertical
x1 = (new_width - img1.width) // 2
x2 = (new_width - img2.width) // 2
new_image.paste(img1, (x1, 0))
new_image.paste(img2, (x2, img1.height))
# 转换回 tensor
tensor = image_to_tensor(new_image)
tensor = tensor.unsqueeze(0)
tensor = tensor.permute(0, 2, 3, 1)
return (tensor,)
except Exception as e:
print(f"Error concatenating images: {str(e)}")
return (image1,)
# 添加颜色常量
COLORS = [
"white", "black", "red", "green", "blue", "yellow", "purple", "orange",
"gray", "brown", "pink", "cyan", "custom"
]
# 颜色映射
color_mapping = {
"white": "#FFFFFF",
"black": "#000000",
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
"yellow": "#FFFF00",
"purple": "#800080",
"orange": "#FFA500",
"gray": "#808080",
"brown": "#A52A2A",
"pink": "#FFC0CB",
"cyan": "#00FFFF"
}
def get_color_values(color_name, color_hex, mapping):
"""获取颜色值"""
if color_name == "custom":
return color_hex
return mapping.get(color_name, "#000000")
# 添加图像处理工具函数
def get_sampler_by_name(method: str) -> int:
"""Get PIL resampling method by name."""
samplers = {
"lanczos": Image.LANCZOS,
"bicubic": Image.BICUBIC,
"hamming": Image.HAMMING,
"bilinear": Image.BILINEAR,
"box": Image.BOX,
"nearest": Image.NEAREST
}
return samplers.get(method, Image.LANCZOS)
class AD_ColorImage:
"""Create a solid color image with advanced options."""
def __init__(self):
pass
# 预定义颜色映射
COLOR_PRESETS = {
"white": "#FFFFFF",
"black": "#000000",
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
"yellow": "#FFFF00",
"purple": "#800080",
"orange": "#FFA500",
"gray": "#808080",
"custom": "custom"
}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {
"default": 512,
"min": 1,
"max": 8192,
"step": 1
}),
"height": ("INT", {
"default": 512,
"min": 1,
"max": 8192,
"step": 1
}),
"color": (list(cls.COLOR_PRESETS.keys()), {
"default": "white"
}),
"hex_color": ("STRING", {
"default": "#FFFFFF",
"multiline": False
}),
"alpha": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
}),
},
"optional": {
"reference_image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "create_color_image"
CATEGORY = "AD/image"
def hex_to_rgb(self, hex_color: str) -> tuple:
"""Convert hex color to RGB tuple."""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def create_color_image(
self,
width: int,
height: int,
color: str,
hex_color: str,
alpha: float,
reference_image = None
):
try:
# 如果有参考图片,使用其尺寸
if reference_image is not None:
_, height, width, _ = reference_image.shape
# 获取颜色值
if color == "custom":
rgb_color = self.hex_to_rgb(hex_color)
else:
rgb_color = self.hex_to_rgb(self.COLOR_PRESETS[color])
# 创建图像
canvas = Image.new(
"RGBA",
(width, height),
(*rgb_color, int(alpha * 255))
)
# 转换为 tensor
tensor = image_to_tensor(canvas)
tensor = tensor.unsqueeze(0)
tensor = tensor.permute(0, 2, 3, 1)
return (tensor,)
except Exception as e:
print(f"Error creating color image: {str(e)}")
canvas = Image.new(
"RGB",
(width, height),
(0, 0, 0)
)
tensor = image_to_tensor(canvas)
tensor = tensor.unsqueeze(0)
tensor = tensor.permute(0, 2, 3, 1)
return (tensor,)
def tensor_to_image(tensor):
"""Convert tensor to PIL Image."""
if len(tensor.shape) == 4:
tensor = tensor.squeeze(0) # 移除 batch 维度
return t.ToPILImage()(tensor.permute(2, 0, 1))
def image_to_tensor(image):
"""Convert PIL Image to tensor."""
tensor = t.ToTensor()(image)
return tensor
NODE_CLASS_MAPPINGS = {
"AD_advanced-padding": AD_PaddingAdvanced,
"AD_image-concat": AD_ImageConcat,
"AD_color-image": AD_ColorImage,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AD_advanced-padding": "AD Advanced Padding",
"AD_image-concat": "AD Image Concatenation",
"AD_color-image": "AD Color Image",
}
+99
View File
@@ -0,0 +1,99 @@
"""
@author: ealkanat
@title: ComfyUI Easy Padding
@description: A simple custom node for creates padding for given image
@version: 1.0.2
@project: https://github.com/erkana/comfyui_easy_padding
@author: https://github.com/erkana
"""
import PIL.Image as Image
import PIL.ImageDraw as ImageDraw
import numpy as np
import torch
class AddPaddingBase:
def __init__(self):
pass
FUNCTION = "resize"
CATEGORY = "🌻 Addoor/image"
def add_padding(self, image, left, top, right, bottom, color="#ffffff", transparent=False):
padded_images = []
image = [self.tensor2pil(img) for img in image]
for img in image:
padded_image = Image.new("RGBA" if transparent else "RGB",
(img.width + left + right, img.height + top + bottom),
(0, 0, 0, 0) if transparent else self.hex_to_tuple(color))
padded_image.paste(img, (left, top))
padded_images.append(self.pil2tensor(padded_image))
return torch.cat(padded_images, dim=0)
def create_mask(self, image, left, top, right, bottom):
masks = []
image = [self.tensor2pil(img) for img in image]
for img in image:
shape = (left, top, img.width + left, img.height + top)
mask_image = Image.new("L", (img.width + left + right, img.height + top + bottom), 255)
draw = ImageDraw.Draw(mask_image)
draw.rectangle(shape, fill=0)
masks.append(self.pil2tensor(mask_image))
return torch.cat(masks, dim=0)
def hex_to_float(self, color):
if not isinstance(color, str):
raise ValueError("Color must be a hex string")
color = color.strip("#")
return int(color, 16) / 255.0
def hex_to_tuple(self, color):
if not isinstance(color, str):
raise ValueError("Color must be a hex string")
color = color.strip("#")
return tuple([int(color[i:i + 2], 16) for i in range(0, len(color), 2)])
# Tensor to PIL
def tensor2pil(self, image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# PIL to Tensor
def pil2tensor(self, image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class AddPadding(AddPaddingBase):
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"left": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
"top": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
"right": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
"bottom": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
"color": ("STRING", {"default": "#ffffff"}),
"transparent": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("IMAGE", "MASK")
def resize(self, image, left, top, right, bottom, color, transparent):
return (self.add_padding(image, left, top, right, bottom, color, transparent),
self.create_mask(image, left, top, right, bottom),)
NODE_CLASS_MAPPINGS = {
"comfyui-easy-padding": AddPadding,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"comfyui-easy-padding": "ComfyUI Easy Padding",
}
+126
View File
@@ -0,0 +1,126 @@
"""
@name: "ComfyUI FofrToolkit",
@version: (1,0,0),
@author: "fofr",
@description: "Experimental toolkit for comfyui.",
@project: "https://github.com/fofr/comfyui-fofr-toolkit",
@url: "https://github.com/fofr",
"""
class ToolkitIncrementer:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"current_index": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"control_after_generate": True,
},
),
},
"optional": {
"max": (
"INT",
{"default": 10, "min": 0, "max": 0xFFFFFFFFFFFFFFFF},
),
},
}
RETURN_TYPES = ("INT", "STRING")
RETURN_NAMES = ("INT", "STRING")
FUNCTION = "increment"
CATEGORY = "🌻 Addoor/Utilities"
def increment(self, current_index, max=0):
if max == 0:
result = current_index
else:
result = current_index % (max + 1)
return (result, str(result))
class ToolkitWidthAndHeightFromAspectRatio:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"aspect_ratio": (
[
"1:1",
"1:2",
"2:1",
"2:3",
"3:2",
"3:4",
"4:3",
"4:5",
"5:4",
"9:16",
"16:9",
"9:21",
"21:9",
],
{"default": "1:1"},
),
"target_size": ("INT", {"default": 1024, "min": 64, "max": 8192}),
},
"optional": {
"multiple_of": ("INT", {"default": 8, "min": 1, "max": 1024}),
},
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "width_and_height_from_aspect_ratio"
CATEGORY = "🌻 Addoor/Utilities"
def width_and_height_from_aspect_ratio(
self, aspect_ratio, target_size, multiple_of=8
):
w, h = map(int, aspect_ratio.split(":"))
scale = (target_size**2 / (w * h)) ** 0.5
width = round(w * scale / multiple_of) * multiple_of
height = round(h * scale / multiple_of) * multiple_of
return (width, height)
class ToolkitWidthAndHeightForImageScaling:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"target_size": (
"INT",
{"default": 1024, "min": 64, "max": 8192},
),
},
"optional": {
"multiple_of": ("INT", {"default": 8, "min": 1, "max": 1024}),
},
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "scale_image_to_target"
CATEGORY = "🌻 Addoor/Utilities"
def scale_image_to_target(self, image, target_size, multiple_of=8):
h, w = image.shape[1:3]
scale = (target_size**2 / (w * h)) ** 0.5
width = round(w * scale / multiple_of) * multiple_of
height = round(h * scale / multiple_of) * multiple_of
return (width, height)
NODE_CLASS_MAPPINGS = {
"Incrementer 🪴": ToolkitIncrementer,
"Width and height from aspect ratio 🪴": ToolkitWidthAndHeightFromAspectRatio,
"Width and height for scaling image to ideal resolution 🪴": ToolkitWidthAndHeightForImageScaling,
}
+172
View File
@@ -0,0 +1,172 @@
"""
@author: palant
@title: ComfyUI-imageResize
@description: Custom node for image resizing.
@version: 1.0.0
@project: https://github.com/palant/image-resize-comfyui
@author: https://github.com/palant
"""
import torch
class ImageResize:
def __init__(self):
pass
ACTION_TYPE_RESIZE = "resize only"
ACTION_TYPE_CROP = "crop to ratio"
ACTION_TYPE_PAD = "pad to ratio"
RESIZE_MODE_DOWNSCALE = "reduce size only"
RESIZE_MODE_UPSCALE = "increase size only"
RESIZE_MODE_ANY = "any"
RETURN_TYPES = ("IMAGE", "MASK",)
FUNCTION = "resize"
CATEGORY = "🌻 Addoor/image"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pixels": ("IMAGE",),
"action": ([s.ACTION_TYPE_RESIZE, s.ACTION_TYPE_CROP, s.ACTION_TYPE_PAD],),
"smaller_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
"larger_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
"scale_factor": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"resize_mode": ([s.RESIZE_MODE_DOWNSCALE, s.RESIZE_MODE_UPSCALE, s.RESIZE_MODE_ANY],),
"side_ratio": ("STRING", {"default": "4:3"}),
"crop_pad_position": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"pad_feathering": ("INT", {"default": 20, "min": 0, "max": 8192, "step": 1}),
},
"optional": {
"mask_optional": ("MASK",),
},
}
@classmethod
def VALIDATE_INPUTS(s, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, **_):
if side_ratio is not None:
if action != s.ACTION_TYPE_RESIZE and s.parse_side_ratio(side_ratio) is None:
return f"Invalid side ratio: {side_ratio}"
if smaller_side is not None and larger_side is not None and scale_factor is not None:
if int(smaller_side > 0) + int(larger_side > 0) + int(scale_factor > 0) > 1:
return f"At most one scaling rule (smaller_side, larger_side, scale_factor) should be enabled by setting a non-zero value"
if scale_factor is not None:
if resize_mode == s.RESIZE_MODE_DOWNSCALE and scale_factor > 1.0:
return f"For resize_mode {s.RESIZE_MODE_DOWNSCALE}, scale_factor should be less than one but got {scale_factor}"
if resize_mode == s.RESIZE_MODE_UPSCALE and scale_factor > 0.0 and scale_factor < 1.0:
return f"For resize_mode {s.RESIZE_MODE_UPSCALE}, scale_factor should be larger than one but got {scale_factor}"
return True
@classmethod
def parse_side_ratio(s, side_ratio):
try:
x, y = map(int, side_ratio.split(":", 1))
if x < 1 or y < 1:
raise Exception("Ratio factors have to be positive numbers")
return float(x) / float(y)
except:
return None
def resize(self, pixels, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, crop_pad_position, pad_feathering, mask_optional=None):
validity = self.VALIDATE_INPUTS(action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio)
if validity is not True:
raise Exception(validity)
height, width = pixels.shape[1:3]
if mask_optional is None:
mask = torch.zeros(1, height, width, dtype=torch.float32)
else:
mask = mask_optional
if mask.shape[1] != height or mask.shape[2] != width:
mask = torch.nn.functional.interpolate(mask.unsqueeze(0), size=(height, width), mode="bicubic").squeeze(0).clamp(0.0, 1.0)
crop_x, crop_y, pad_x, pad_y = (0.0, 0.0, 0.0, 0.0)
if action == self.ACTION_TYPE_CROP:
target_ratio = self.parse_side_ratio(side_ratio)
if height * target_ratio < width:
crop_x = width - height * target_ratio
else:
crop_y = height - width / target_ratio
elif action == self.ACTION_TYPE_PAD:
target_ratio = self.parse_side_ratio(side_ratio)
if height * target_ratio > width:
pad_x = height * target_ratio - width
else:
pad_y = width / target_ratio - height
if smaller_side > 0:
if width + pad_x - crop_x > height + pad_y - crop_y:
scale_factor = float(smaller_side) / (height + pad_y - crop_y)
else:
scale_factor = float(smaller_side) / (width + pad_x - crop_x)
if larger_side > 0:
if width + pad_x - crop_x > height + pad_y - crop_y:
scale_factor = float(larger_side) / (width + pad_x - crop_x)
else:
scale_factor = float(larger_side) / (height + pad_y - crop_y)
if (resize_mode == self.RESIZE_MODE_DOWNSCALE and scale_factor >= 1.0) or (resize_mode == self.RESIZE_MODE_UPSCALE and scale_factor <= 1.0):
scale_factor = 0.0
if scale_factor > 0.0:
pixels = torch.nn.functional.interpolate(pixels.movedim(-1, 1), scale_factor=scale_factor, mode="bicubic", antialias=True).movedim(1, -1).clamp(0.0, 1.0)
mask = torch.nn.functional.interpolate(mask.unsqueeze(0), scale_factor=scale_factor, mode="bicubic", antialias=True).squeeze(0).clamp(0.0, 1.0)
height, width = pixels.shape[1:3]
crop_x *= scale_factor
crop_y *= scale_factor
pad_x *= scale_factor
pad_y *= scale_factor
if crop_x > 0.0 or crop_y > 0.0:
remove_x = (round(crop_x * crop_pad_position), round(crop_x * (1 - crop_pad_position))) if crop_x > 0.0 else (0, 0)
remove_y = (round(crop_y * crop_pad_position), round(crop_y * (1 - crop_pad_position))) if crop_y > 0.0 else (0, 0)
pixels = pixels[:, remove_y[0]:height - remove_y[1], remove_x[0]:width - remove_x[1], :]
mask = mask[:, remove_y[0]:height - remove_y[1], remove_x[0]:width - remove_x[1]]
elif pad_x > 0.0 or pad_y > 0.0:
add_x = (round(pad_x * crop_pad_position), round(pad_x * (1 - crop_pad_position))) if pad_x > 0.0 else (0, 0)
add_y = (round(pad_y * crop_pad_position), round(pad_y * (1 - crop_pad_position))) if pad_y > 0.0 else (0, 0)
new_pixels = torch.zeros(pixels.shape[0], height + add_y[0] + add_y[1], width + add_x[0] + add_x[1], pixels.shape[3], dtype=torch.float32)
new_pixels[:, add_y[0]:height + add_y[0], add_x[0]:width + add_x[0], :] = pixels
pixels = new_pixels
new_mask = torch.ones(mask.shape[0], height + add_y[0] + add_y[1], width + add_x[0] + add_x[1], dtype=torch.float32)
new_mask[:, add_y[0]:height + add_y[0], add_x[0]:width + add_x[0]] = mask
mask = new_mask
if pad_feathering > 0:
for i in range(mask.shape[0]):
for j in range(pad_feathering):
feather_strength = (1 - j / pad_feathering) * (1 - j / pad_feathering)
if add_x[0] > 0 and j < width:
for k in range(height):
mask[i, k, add_x[0] + j] = max(mask[i, k, add_x[0] + j], feather_strength)
if add_x[1] > 0 and j < width:
for k in range(height):
mask[i, k, width + add_x[0] - j - 1] = max(mask[i, k, width + add_x[0] - j - 1], feather_strength)
if add_y[0] > 0 and j < height:
for k in range(width):
mask[i, add_y[0] + j, k] = max(mask[i, add_y[0] + j, k], feather_strength)
if add_y[1] > 0 and j < height:
for k in range(width):
mask[i, height + add_y[0] - j - 1, k] = max(mask[i, height + add_y[0] - j - 1, k], feather_strength)
return (pixels, mask)
NODE_CLASS_MAPPINGS = {
"ImageResize": ImageResize
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ImageResize": "Image Resize"
}
+62
View File
@@ -0,0 +1,62 @@
"""
Addoor Nodes for ComfyUI
Provides nodes for image processing and other utilities
"""
import os
import glob
import logging
import importlib
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 首先定义映射字典
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# 获取当前目录下所有的 .py 文件
current_dir = os.path.dirname(os.path.abspath(__file__))
py_files = glob.glob(os.path.join(current_dir, "*.py"))
# 直接注册所有节点
for file_path in py_files:
# 跳过 __init__.py
if "__init__.py" in file_path:
continue
try:
# 获取模块名(不含.py)
module_name = os.path.basename(file_path)[:-3]
# 使用相对导入
module = importlib.import_module(f".{module_name}", package=__package__)
# 如果模块有节点映射,则更新
if hasattr(module, 'NODE_CLASS_MAPPINGS'):
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
if hasattr(module, 'NODE_DISPLAY_NAME_MAPPINGS'):
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
logger.info(f"Imported {module_name} successfully")
except ImportError as e:
logger.error(f"Error importing {module_name}: {str(e)}")
except Exception as e:
logger.error(f"Error processing {module_name}: {str(e)}")
# 如果允许测试节点,导入测试节点
allow_test_nodes = True
if allow_test_nodes:
try:
from .excluded.experimental_nodes import *
# 更新映射
if 'NODE_CLASS_MAPPINGS' in locals():
NODE_CLASS_MAPPINGS.update(locals().get('NODE_CLASS_MAPPINGS', {}))
if 'NODE_DISPLAY_NAME_MAPPINGS' in locals():
NODE_DISPLAY_NAME_MAPPINGS.update(locals().get('NODE_DISPLAY_NAME_MAPPINGS', {}))
except ModuleNotFoundError:
pass
logger.debug(f"Registered nodes: {list(NODE_CLASS_MAPPINGS.keys())}")
WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']