no message
This commit is contained in:
+7
-3
@@ -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']
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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']
|
||||
Reference in New Issue
Block a user