添加图片描边节点
This commit is contained in:
@@ -1,239 +0,0 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from enum import Enum
|
||||
|
||||
class ColorMode(Enum):
|
||||
HSLA = "HSLA颜色" # 色相-饱和度-亮度-透明度
|
||||
HSVA = "HSVA颜色" # 色相-饱和度-明度-透明度
|
||||
CMYK = "CMYK颜色" # 青-品红-黄-黑
|
||||
|
||||
class ColorBackgroundGenerator:
|
||||
"""Current Date and Time (UTC): 2025-02-19 13:59:10
|
||||
Current User's Login: 1761696257"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"图层设置": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 1.0}),
|
||||
"宽度": ("INT", {"default": 512, "min": 64, "max": 8192}),
|
||||
"高度": ("INT", {"default": 512, "min": 64, "max": 8192}),
|
||||
"颜色模式": (list(mode.value for mode in ColorMode), {"default": "HSLA颜色"}),
|
||||
# 通用透明度
|
||||
"透明度": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
# HSLA inputs
|
||||
"HSLA色相": ("INT", {"default": 0, "min": 0, "max": 360}),
|
||||
"HSLA饱和度": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
"HSLA亮度": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
# HSVA inputs
|
||||
"HSVA色相": ("INT", {"default": 0, "min": 0, "max": 360}),
|
||||
"HSVA饱和度": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
"HSVA明度": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
# CMYK inputs
|
||||
"CMYK青色": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
"CMYK品红": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
"CMYK黄色": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
"CMYK黑色": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
# 颜色选择器控制
|
||||
"使用取色器": ("BOOLEAN", {"default": False}),
|
||||
"颜色选择器": ("COLOR", {"default": "#FFFFFF"}),
|
||||
},
|
||||
"optional": {
|
||||
# 图片输入
|
||||
"输入图片": ("IMAGE",),
|
||||
"遮罩": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT", "INT", "FLOAT")
|
||||
RETURN_NAMES = ("图像", "红", "绿", "蓝", "透明度")
|
||||
FUNCTION = "generate_background"
|
||||
CATEGORY = "🍺DD系列节点"
|
||||
|
||||
def hex_to_rgba(self, hex_color):
|
||||
"""将十六进制颜色转换为RGBA,忽略原始透明度"""
|
||||
hex_color = hex_color.lstrip('#')
|
||||
if len(hex_color) == 8:
|
||||
r, g, b, _ = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4, 6))
|
||||
return r, g, b
|
||||
else:
|
||||
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
return r, g, b
|
||||
|
||||
def hsla_to_rgba(self, h, s, l, a=1.0):
|
||||
h = h/360
|
||||
def hue_to_rgb(p, q, t):
|
||||
if t < 0: t += 1
|
||||
if t > 1: t -= 1
|
||||
if t < 1/6: return p + (q - p) * 6 * t
|
||||
if t < 1/2: return q
|
||||
if t < 2/3: return p + (q - p) * (2/3 - t) * 6
|
||||
return p
|
||||
|
||||
if s == 0:
|
||||
r = g = b = l
|
||||
else:
|
||||
q = l * (1 + s) if l < 0.5 else l + s - l * s
|
||||
p = 2 * l - q
|
||||
r = hue_to_rgb(p, q, h + 1/3)
|
||||
g = hue_to_rgb(p, q, h)
|
||||
b = hue_to_rgb(p, q, h - 1/3)
|
||||
|
||||
return int(r * 255), int(g * 255), int(b * 255), a
|
||||
|
||||
def hsva_to_rgba(self, h, s, v, a=1.0):
|
||||
h = h/360
|
||||
i = int(h*6)
|
||||
f = h*6 - i
|
||||
p = v * (1-s)
|
||||
q = v * (1-f*s)
|
||||
t = v * (1-(1-f)*s)
|
||||
|
||||
if i % 6 == 0:
|
||||
r, g, b = v, t, p
|
||||
elif i % 6 == 1:
|
||||
r, g, b = q, v, p
|
||||
elif i % 6 == 2:
|
||||
r, g, b = p, v, t
|
||||
elif i % 6 == 3:
|
||||
r, g, b = p, q, v
|
||||
elif i % 6 == 4:
|
||||
r, g, b = t, p, v
|
||||
else:
|
||||
r, g, b = v, p, q
|
||||
|
||||
return int(r * 255), int(g * 255), int(b * 255), a
|
||||
|
||||
def cmyk_to_rgba(self, c, m, y, k, a=1.0):
|
||||
r = int(255 * (1-c) * (1-k))
|
||||
g = int(255 * (1-m) * (1-k))
|
||||
b = int(255 * (1-y) * (1-k))
|
||||
return r, g, b, a
|
||||
|
||||
def ensure_rgba(self, image):
|
||||
"""确保图像为RGBA格式"""
|
||||
if image.shape[-1] == 3: # RGB格式
|
||||
# 添加alpha通道(完全不透明)
|
||||
alpha = torch.ones((*image.shape[:-1], 1), device=image.device)
|
||||
return torch.cat([image, alpha], dim=-1)
|
||||
return image
|
||||
|
||||
def adjust_mask_size(self, mask, height, width):
|
||||
"""调整遮罩大小的辅助函数"""
|
||||
if mask.shape[-2:] != (height, width): # 使用最后两个维度比较尺寸
|
||||
# ComfyUI中的MASK通常是2D或3D的,需要正确处理维度
|
||||
if len(mask.shape) == 2:
|
||||
mask = mask.unsqueeze(0).unsqueeze(0) # (H,W) -> (1,1,H,W)
|
||||
elif len(mask.shape) == 3:
|
||||
mask = mask.unsqueeze(1) # (B,H,W) -> (B,1,H,W)
|
||||
|
||||
# 调整大小
|
||||
mask = torch.nn.functional.interpolate(
|
||||
mask,
|
||||
size=(height, width),
|
||||
mode='nearest'
|
||||
)
|
||||
|
||||
# 恢复原始维度
|
||||
if len(mask.shape) == 4:
|
||||
mask = mask.squeeze(0).squeeze(0) # (1,1,H,W) -> (H,W)
|
||||
return mask
|
||||
|
||||
def get_output_size(self, 宽度, 高度, 输入图片=None):
|
||||
"""确定输出尺寸"""
|
||||
output_height, output_width = 高度, 宽度
|
||||
|
||||
# 如果有输入图片,使用输入图片的尺寸
|
||||
if 输入图片 is not None:
|
||||
if len(输入图片.shape) == 4: # BCHW或BHWC格式
|
||||
if 输入图片.shape[1] == 3 or 输入图片.shape[1] == 4: # BCHW格式
|
||||
output_height = 输入图片.shape[2]
|
||||
output_width = 输入图片.shape[3]
|
||||
else: # BHWC格式
|
||||
output_height = 输入图片.shape[1]
|
||||
output_width = 输入图片.shape[2]
|
||||
|
||||
return output_height, output_width
|
||||
|
||||
def generate_background(self, 图层设置, 宽度, 高度, 颜色模式,
|
||||
透明度, HSLA色相, HSLA饱和度, HSLA亮度,
|
||||
HSVA色相, HSVA饱和度, HSVA明度,
|
||||
CMYK青色, CMYK品红, CMYK黄色, CMYK黑色,
|
||||
使用取色器, 颜色选择器,
|
||||
输入图片=None, 遮罩=None):
|
||||
|
||||
# 确定输出尺寸
|
||||
output_height, output_width = self.get_output_size(宽度, 高度, 输入图片)
|
||||
|
||||
# 生成颜色值
|
||||
if 使用取色器:
|
||||
r, g, b = self.hex_to_rgba(颜色选择器)
|
||||
a = 透明度
|
||||
else:
|
||||
if 颜色模式 == "HSLA颜色":
|
||||
r, g, b, a = self.hsla_to_rgba(HSLA色相, HSLA饱和度, HSLA亮度, 透明度)
|
||||
elif 颜色模式 == "HSVA颜色":
|
||||
r, g, b, a = self.hsva_to_rgba(HSVA色相, HSVA饱和度, HSVA明度, 透明度)
|
||||
elif 颜色模式 == "CMYK颜色":
|
||||
r, g, b, a = self.cmyk_to_rgba(CMYK青色, CMYK品红, CMYK黄色, CMYK黑色, 透明度)
|
||||
|
||||
# 确保颜色值在有效范围内
|
||||
r = max(0, min(255, r))
|
||||
g = max(0, min(255, g))
|
||||
b = max(0, min(255, b))
|
||||
a = max(0.0, min(1.0, a))
|
||||
|
||||
# 创建颜色图层 (BHWC格式)
|
||||
color = np.array([r, g, b, int(a * 255)]) / 255.0
|
||||
color_image = np.ones((1, output_height, output_width, 4)) * color
|
||||
color_image = torch.from_numpy(color_image).float()
|
||||
|
||||
# 如果没有输入图片,直接返回颜色图层
|
||||
if 输入图片 is None:
|
||||
return (color_image, r, g, b, a)
|
||||
|
||||
# 处理输入图片
|
||||
if 输入图片.shape[1] == 3 or 输入图片.shape[1] == 4: # 如果是BCHW格式
|
||||
输入图片 = 输入图片.permute(0, 2, 3, 1)
|
||||
输入图片 = self.ensure_rgba(输入图片)
|
||||
|
||||
# 调整输入图片大小
|
||||
if 输入图片.shape[1:3] != (output_height, output_width):
|
||||
输入图片 = torch.nn.functional.interpolate(
|
||||
输入图片.permute(0, 3, 1, 2),
|
||||
size=(output_height, output_width),
|
||||
mode='bilinear',
|
||||
align_corners=False
|
||||
).permute(0, 2, 3, 1)
|
||||
|
||||
# 处理遮罩
|
||||
if 遮罩 is not None:
|
||||
遮罩 = self.adjust_mask_size(遮罩, output_height, output_width)
|
||||
遮罩 = 遮罩.view(1, output_height, output_width, 1).expand(-1, -1, -1, 4)
|
||||
|
||||
# 根据图层设置决定混合方式
|
||||
if 图层设置 == 0: # 输入图片在底层
|
||||
# 遮罩区域显示颜色图层,非遮罩区域显示输入图片
|
||||
result = 输入图片 * (1 - 遮罩) + color_image * 遮罩
|
||||
else: # 输入图片在顶层
|
||||
# 遮罩区域显示输入图片,非遮罩区域显示颜色图层
|
||||
result = color_image * (1 - 遮罩) + 输入图片 * 遮罩
|
||||
else:
|
||||
# 如果没有遮罩,根据图层设置决定叠加顺序
|
||||
if 图层设置 == 0: # 输入图片在底层
|
||||
alpha = 输入图片[..., 3:4]
|
||||
result = color_image * alpha + 输入图片 * (1 - alpha)
|
||||
else: # 输入图片在顶层
|
||||
alpha = color_image[..., 3:4]
|
||||
result = 输入图片 * alpha + color_image * (1 - alpha)
|
||||
|
||||
return (result, r, g, b, a)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DD-ColorBackgroundGenerator": ColorBackgroundGenerator
|
||||
}
|
||||
|
||||
# 节点显示名称映射 - 使用英文(中文通过locales提供)
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DD-ColorBackgroundGenerator": "DD Color Background Generator"
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image, ImageOps, ImageFilter, ImageDraw
|
||||
import cv2
|
||||
|
||||
class DDImageStroke:
|
||||
"""
|
||||
DD 图片描边 - 为图片添加描边效果
|
||||
支持透明图和普通图片的边缘描边,可自定义颜色、大小、位置和透明度
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"图片": ("IMAGE",),
|
||||
"反转遮罩": ("BOOLEAN", {"default": True}),
|
||||
"位置": (["外描边", "内描边", "居中描边"], {"default": "外描边"}),
|
||||
"大小": ("INT", {"default": 5, "min": 1, "max": 200, "step": 1}),
|
||||
"不透明度": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}),
|
||||
"描边颜色": ("COLOR", {"default": "#FFFFFF"}),
|
||||
},
|
||||
"optional": {
|
||||
"遮罩": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("描边图片",)
|
||||
FUNCTION = "add_stroke"
|
||||
CATEGORY = "🍺DD系列节点"
|
||||
|
||||
def hex_to_rgb(self, hex_color):
|
||||
"""将十六进制颜色转换为RGB"""
|
||||
try:
|
||||
hex_color = hex_color.lstrip('#')
|
||||
if len(hex_color) == 6:
|
||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
elif len(hex_color) == 3:
|
||||
# 支持短格式的十六进制颜色,如 #FFF
|
||||
return tuple(int(hex_color[i], 16) * 17 for i in range(3))
|
||||
else:
|
||||
return (255, 255, 255) # 格式错误时返回白色
|
||||
except:
|
||||
return (255, 255, 255) # 解析失败时返回白色
|
||||
|
||||
def int_to_rgb(self, color_int):
|
||||
"""将整数颜色值转换为RGB"""
|
||||
try:
|
||||
# 确保颜色值在有效范围内
|
||||
color_int = max(0, min(0xFFFFFF, int(color_int)))
|
||||
|
||||
# 提取RGB分量
|
||||
r = (color_int >> 16) & 0xFF
|
||||
g = (color_int >> 8) & 0xFF
|
||||
b = color_int & 0xFF
|
||||
|
||||
return (r, g, b)
|
||||
except:
|
||||
return (255, 255, 255) # 解析失败时返回白色
|
||||
|
||||
def tensor_to_pil(self, tensor, mask=None, invert_mask=True):
|
||||
"""将tensor转换为PIL图像,支持遮罩"""
|
||||
# tensor格式: [batch, height, width, channels]
|
||||
if len(tensor.shape) == 4:
|
||||
tensor = tensor[0] # 取第一张图片
|
||||
|
||||
# 转换为numpy数组并调整范围到0-255
|
||||
np_image = (tensor.cpu().numpy() * 255).astype(np.uint8)
|
||||
|
||||
# 创建PIL图像
|
||||
if np_image.shape[2] == 3: # RGB
|
||||
pil_image = Image.fromarray(np_image, 'RGB')
|
||||
elif np_image.shape[2] == 4: # RGBA
|
||||
pil_image = Image.fromarray(np_image, 'RGBA')
|
||||
else: # 灰度图
|
||||
pil_image = Image.fromarray(np_image[:, :, 0], 'L')
|
||||
|
||||
# 判断图像类型以决定处理方式
|
||||
is_transparent_image = False
|
||||
|
||||
# 如果提供了遮罩,将RGB转换为RGBA(这是透明图逻辑)
|
||||
if mask is not None:
|
||||
is_transparent_image = True
|
||||
if pil_image.mode == 'RGB':
|
||||
# 遮罩格式: [batch, height, width] 或 [height, width]
|
||||
if len(mask.shape) == 3:
|
||||
mask = mask[0] # 取第一个遮罩
|
||||
|
||||
# 转换遮罩为numpy数组
|
||||
mask_np = (mask.cpu().numpy() * 255).astype(np.uint8)
|
||||
|
||||
# 根据反转设置处理遮罩
|
||||
if invert_mask:
|
||||
mask_np = 255 - mask_np # 反转遮罩
|
||||
|
||||
# 创建RGBA图像
|
||||
pil_image = pil_image.convert('RGBA')
|
||||
|
||||
# 应用遮罩作为alpha通道
|
||||
pil_image.putalpha(Image.fromarray(mask_np, 'L'))
|
||||
elif pil_image.mode == 'RGBA':
|
||||
# 原本就是RGBA图像(透明图)
|
||||
is_transparent_image = True
|
||||
else:
|
||||
# 普通RGB图片,保持RGB格式
|
||||
is_transparent_image = False
|
||||
|
||||
return pil_image, is_transparent_image
|
||||
|
||||
def pil_to_tensor(self, pil_image):
|
||||
"""将PIL图像转换为tensor"""
|
||||
# 根据图像模式转换
|
||||
if pil_image.mode == 'RGB':
|
||||
# 普通图片保持RGB格式,添加alpha通道用于输出
|
||||
pil_image = pil_image.convert('RGBA')
|
||||
elif pil_image.mode != 'RGBA':
|
||||
pil_image = pil_image.convert('RGBA')
|
||||
|
||||
# 转换为numpy数组
|
||||
np_image = np.array(pil_image).astype(np.float32) / 255.0
|
||||
|
||||
# 转换为tensor格式: [1, height, width, channels]
|
||||
tensor = torch.from_numpy(np_image).unsqueeze(0)
|
||||
|
||||
return tensor
|
||||
|
||||
def create_stroke_mask_cv2(self, image, stroke_size, position):
|
||||
"""使用OpenCV创建描边遮罩,性能更好"""
|
||||
height, width = image.size[1], image.size[0]
|
||||
|
||||
# 获取alpha通道作为遮罩
|
||||
if image.mode == 'RGBA':
|
||||
alpha = np.array(image.split()[-1])
|
||||
has_transparency = True
|
||||
else:
|
||||
# 对于普通图片,创建一个全白的遮罩代表整个图像区域
|
||||
alpha = np.full((height, width), 255, dtype=np.uint8)
|
||||
has_transparency = False
|
||||
|
||||
# 创建结构元素
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (stroke_size * 2 + 1, stroke_size * 2 + 1))
|
||||
|
||||
if position == "外描边":
|
||||
if has_transparency:
|
||||
# 透明图:膨胀后减去原图(传统外描边)
|
||||
dilated = cv2.dilate(alpha, kernel, iterations=1)
|
||||
stroke_mask = cv2.subtract(dilated, alpha)
|
||||
else:
|
||||
# 普通图片:外描边没有意义,返回空遮罩
|
||||
stroke_mask = np.zeros((height, width), dtype=np.uint8)
|
||||
|
||||
elif position == "内描边":
|
||||
if has_transparency:
|
||||
# 透明图:原图减去腐蚀后的结果(传统内描边)
|
||||
eroded = cv2.erode(alpha, kernel, iterations=1)
|
||||
stroke_mask = cv2.subtract(alpha, eroded)
|
||||
else:
|
||||
# 普通图片:从边缘向内创建描边区域
|
||||
# 创建边缘遮罩:整个图像减去内缩区域
|
||||
inner_mask = np.zeros((height, width), dtype=np.uint8)
|
||||
margin = stroke_size
|
||||
if margin * 2 < min(width, height): # 确保有足够空间
|
||||
inner_mask[margin:height-margin, margin:width-margin] = 255
|
||||
stroke_mask = cv2.subtract(alpha, inner_mask)
|
||||
|
||||
else: # 居中描边
|
||||
if has_transparency:
|
||||
# 透明图:膨胀后减去腐蚀后的结果(传统居中描边)
|
||||
half_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,
|
||||
(max(1, stroke_size) + 1, max(1, stroke_size) + 1))
|
||||
dilated = cv2.dilate(alpha, half_kernel, iterations=1)
|
||||
eroded = cv2.erode(alpha, half_kernel, iterations=1)
|
||||
stroke_mask = cv2.subtract(dilated, eroded)
|
||||
else:
|
||||
# 普通图片:在边缘位置创建描边
|
||||
# 创建两个不同大小的内缩区域,取差值
|
||||
outer_mask = np.zeros((height, width), dtype=np.uint8)
|
||||
inner_mask = np.zeros((height, width), dtype=np.uint8)
|
||||
|
||||
outer_margin = max(1, stroke_size // 2)
|
||||
inner_margin = stroke_size
|
||||
|
||||
if outer_margin * 2 < min(width, height):
|
||||
outer_mask[outer_margin:height-outer_margin, outer_margin:width-outer_margin] = 255
|
||||
if inner_margin * 2 < min(width, height):
|
||||
inner_mask[inner_margin:height-inner_margin, inner_margin:width-inner_margin] = 255
|
||||
|
||||
stroke_mask = cv2.subtract(outer_mask, inner_mask)
|
||||
|
||||
return Image.fromarray(stroke_mask, 'L')
|
||||
|
||||
def create_stroke_for_normal_image(self, image, stroke_size, position, stroke_color):
|
||||
"""为普通图片(非透明图)创建描边效果"""
|
||||
width, height = image.size
|
||||
|
||||
# 创建一个新的RGBA图像用于绘制描边
|
||||
result = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
||||
|
||||
# 将原图粘贴到结果图像上
|
||||
if image.mode == 'RGB':
|
||||
# 将RGB图像转换为RGBA并设置为完全不透明
|
||||
rgba_image = image.convert('RGBA')
|
||||
result.paste(rgba_image, (0, 0))
|
||||
else:
|
||||
result.paste(image, (0, 0))
|
||||
|
||||
# 根据描边位置创建描边
|
||||
draw = ImageDraw.Draw(result)
|
||||
|
||||
if position == "外描边":
|
||||
# 普通图片的外描边:在图像边界外绘制(这里我们扩展画布)
|
||||
# 创建扩展的画布
|
||||
new_width = width + stroke_size * 2
|
||||
new_height = height + stroke_size * 2
|
||||
extended_result = Image.new('RGBA', (new_width, new_height), (0, 0, 0, 0))
|
||||
|
||||
# 先绘制描边(整个扩展区域)
|
||||
extended_draw = ImageDraw.Draw(extended_result)
|
||||
extended_draw.rectangle([0, 0, new_width-1, new_height-1], fill=stroke_color)
|
||||
|
||||
# 再粘贴原图到中心位置
|
||||
extended_result.paste(result, (stroke_size, stroke_size))
|
||||
|
||||
return extended_result
|
||||
|
||||
elif position == "内描边":
|
||||
# 普通图片的内描边:在图像内部边缘绘制描边
|
||||
# 绘制描边矩形框
|
||||
for i in range(stroke_size):
|
||||
draw.rectangle([i, i, width-1-i, height-1-i], outline=stroke_color, width=1)
|
||||
|
||||
else: # 居中描边
|
||||
# 普通图片的居中描边:一半在内,一半在外
|
||||
half_size = stroke_size // 2
|
||||
|
||||
# 创建稍微扩展的画布
|
||||
new_width = width + half_size * 2
|
||||
new_height = height + half_size * 2
|
||||
extended_result = Image.new('RGBA', (new_width, new_height), (0, 0, 0, 0))
|
||||
|
||||
# 先粘贴原图到中心
|
||||
extended_result.paste(result, (half_size, half_size))
|
||||
|
||||
# 绘制描边
|
||||
extended_draw = ImageDraw.Draw(extended_result)
|
||||
for i in range(stroke_size):
|
||||
extended_draw.rectangle([i, i, new_width-1-i, new_height-1-i], outline=stroke_color, width=1)
|
||||
|
||||
return extended_result
|
||||
|
||||
return result
|
||||
|
||||
def add_stroke(self, 图片, 反转遮罩, 位置, 大小, 不透明度, 描边颜色, 遮罩=None):
|
||||
"""
|
||||
为图片添加描边效果
|
||||
|
||||
Args:
|
||||
图片: 输入图片tensor
|
||||
反转遮罩: 是否反转遮罩(True/False)
|
||||
位置: 描边位置("外描边", "内描边", "居中描边")
|
||||
大小: 描边大小(像素)
|
||||
不透明度: 描边不透明度(1-100)
|
||||
描边颜色: 描边颜色(COLOR类型,如"#FFFFFF")
|
||||
遮罩: 可选的遮罩tensor,用于定义透明区域
|
||||
|
||||
Returns:
|
||||
描边后的图片tensor
|
||||
"""
|
||||
try:
|
||||
# 批处理
|
||||
batch_size = 图片.shape[0]
|
||||
results = []
|
||||
|
||||
for i in range(batch_size):
|
||||
# 获取对应的遮罩(如果有)
|
||||
current_mask = None
|
||||
if 遮罩 is not None:
|
||||
if len(遮罩.shape) == 3 and 遮罩.shape[0] > i:
|
||||
current_mask = 遮罩[i:i+1]
|
||||
elif len(遮罩.shape) == 2:
|
||||
current_mask = 遮罩.unsqueeze(0)
|
||||
elif len(遮罩.shape) == 3 and 遮罩.shape[0] == 1:
|
||||
current_mask = 遮罩
|
||||
|
||||
# 转换为PIL图像,应用遮罩(支持反转)
|
||||
pil_image, is_transparent_image = self.tensor_to_pil(图片[i:i+1], current_mask, 反转遮罩)
|
||||
|
||||
# 解析描边颜色(支持COLOR类型、整数和字符串格式)
|
||||
if isinstance(描边颜色, str):
|
||||
# COLOR类型会传入字符串格式的十六进制颜色
|
||||
stroke_rgb = self.hex_to_rgb(描边颜色)
|
||||
elif isinstance(描边颜色, (int, float)):
|
||||
# 向后兼容整数格式
|
||||
stroke_rgb = self.int_to_rgb(描边颜色)
|
||||
else:
|
||||
# 默认使用白色
|
||||
stroke_rgb = (255, 255, 255)
|
||||
|
||||
# 创建描边颜色(包含透明度)
|
||||
# 将1-100的不透明度转换为0-255的alpha值
|
||||
alpha_value = int((不透明度 / 100.0) * 255)
|
||||
stroke_color = stroke_rgb + (alpha_value,)
|
||||
|
||||
# 根据图像类型使用不同的描边逻辑
|
||||
if is_transparent_image:
|
||||
# 透明图:使用传统的遮罩+合成方法
|
||||
# 确保图像有alpha通道
|
||||
if pil_image.mode != 'RGBA':
|
||||
pil_image = pil_image.convert('RGBA')
|
||||
|
||||
# 使用OpenCV创建描边遮罩(性能更好)
|
||||
try:
|
||||
stroke_mask = self.create_stroke_mask_cv2(pil_image, 大小, 位置)
|
||||
except:
|
||||
# 如果OpenCV方法失败,使用PIL方法作为后备
|
||||
stroke_mask = self.create_stroke_mask_pil(pil_image, 大小, 位置)
|
||||
|
||||
# 创建描边图层
|
||||
stroke_layer = Image.new('RGBA', pil_image.size, (0, 0, 0, 0))
|
||||
|
||||
# 创建纯色图层
|
||||
color_layer = Image.new('RGBA', pil_image.size, stroke_color)
|
||||
|
||||
# 应用描边遮罩
|
||||
stroke_layer = Image.composite(color_layer, stroke_layer, stroke_mask)
|
||||
|
||||
# 根据位置决定图层顺序
|
||||
if 位置 == "外描边":
|
||||
# 外描边在底层
|
||||
result = Image.alpha_composite(stroke_layer, pil_image)
|
||||
else:
|
||||
# 内描边和居中描边在顶层
|
||||
result = Image.alpha_composite(pil_image, stroke_layer)
|
||||
else:
|
||||
# 普通图片:使用直接绘制方法
|
||||
result = self.create_stroke_for_normal_image(pil_image, 大小, 位置, stroke_color)
|
||||
|
||||
# 转换回tensor
|
||||
result_tensor = self.pil_to_tensor(result)
|
||||
results.append(result_tensor)
|
||||
|
||||
# 合并批处理结果
|
||||
final_result = torch.cat(results, dim=0)
|
||||
|
||||
return (final_result,)
|
||||
|
||||
except Exception as e:
|
||||
print(f"DD图片描边节点错误: {str(e)}")
|
||||
# 出错时返回原图
|
||||
return (图片,)
|
||||
|
||||
def create_stroke_mask_pil(self, image, stroke_size, position):
|
||||
"""使用PIL创建描边遮罩(备用方法)"""
|
||||
width, height = image.size
|
||||
|
||||
# 获取alpha通道作为遮罩
|
||||
if image.mode == 'RGBA':
|
||||
alpha = image.split()[-1]
|
||||
has_transparency = True
|
||||
else:
|
||||
# 对于普通图片,创建一个全白的遮罩代表整个图像区域
|
||||
alpha = Image.new('L', image.size, 255)
|
||||
has_transparency = False
|
||||
|
||||
# 根据位置类型创建描边遮罩
|
||||
if position == "外描边":
|
||||
if has_transparency:
|
||||
# 透明图:扩展原图像边界
|
||||
dilated = alpha.filter(ImageFilter.MaxFilter(stroke_size * 2 + 1))
|
||||
stroke_mask = Image.new('L', image.size, 0)
|
||||
stroke_mask.paste(dilated, (0, 0))
|
||||
# 减去原图像区域
|
||||
stroke_mask = Image.composite(Image.new('L', image.size, 0), stroke_mask, alpha)
|
||||
else:
|
||||
# 普通图片:外描边没有意义,返回空遮罩
|
||||
stroke_mask = Image.new('L', image.size, 0)
|
||||
|
||||
elif position == "内描边":
|
||||
if has_transparency:
|
||||
# 透明图:在原图像内部创建描边
|
||||
eroded = alpha.filter(ImageFilter.MinFilter(stroke_size * 2 + 1))
|
||||
stroke_mask = Image.composite(alpha, Image.new('L', image.size, 0), eroded)
|
||||
else:
|
||||
# 普通图片:从边缘向内创建描边区域
|
||||
# 创建边缘遮罩:整个图像减去内缩区域
|
||||
inner_mask = Image.new('L', image.size, 0)
|
||||
margin = stroke_size
|
||||
if margin * 2 < min(width, height): # 确保有足够空间
|
||||
bbox = (margin, margin, width - margin, height - margin)
|
||||
inner_mask.paste(255, bbox)
|
||||
# 整个图像减去内部区域 = 边缘区域
|
||||
stroke_mask = Image.composite(alpha, Image.new('L', image.size, 0), inner_mask)
|
||||
|
||||
else: # 居中描边
|
||||
if has_transparency:
|
||||
# 透明图:一半在内,一半在外
|
||||
half_size = max(1, stroke_size // 2)
|
||||
dilated = alpha.filter(ImageFilter.MaxFilter(half_size * 2 + 1))
|
||||
eroded = alpha.filter(ImageFilter.MinFilter(half_size * 2 + 1))
|
||||
stroke_mask = Image.composite(dilated, Image.new('L', image.size, 0), eroded)
|
||||
else:
|
||||
# 普通图片:在边缘位置创建描边
|
||||
# 创建两个不同大小的内缩区域,取差值
|
||||
outer_mask = Image.new('L', image.size, 0)
|
||||
inner_mask = Image.new('L', image.size, 0)
|
||||
|
||||
outer_margin = max(1, stroke_size // 2)
|
||||
inner_margin = stroke_size
|
||||
|
||||
if outer_margin * 2 < min(width, height):
|
||||
bbox = (outer_margin, outer_margin, width - outer_margin, height - outer_margin)
|
||||
outer_mask.paste(255, bbox)
|
||||
if inner_margin * 2 < min(width, height):
|
||||
bbox = (inner_margin, inner_margin, width - inner_margin, height - inner_margin)
|
||||
inner_mask.paste(255, bbox)
|
||||
|
||||
# outer_mask - inner_mask = 边缘环形区域
|
||||
stroke_mask = Image.composite(outer_mask, Image.new('L', image.size, 0), inner_mask)
|
||||
|
||||
return stroke_mask
|
||||
|
||||
# 节点类映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DD-ImageStroke": DDImageStroke
|
||||
}
|
||||
|
||||
# 节点显示名称映射
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DD-ImageStroke": "DD Image Stroke"
|
||||
}
|
||||
Reference in New Issue
Block a user