commit ImageCompositeHandleMask node
This commit is contained in:
@@ -147,6 +147,7 @@ When this error has occurred, please check the network environment.
|
||||
|
||||
<font size="4">**If the dependency package error after updating, please double clicking ```repair_dependency.bat``` (for Official ComfyUI Protable) or ```repair_dependency_aki.bat``` (for ComfyUI-aki-v1.x) in the plugin folder to reinstall the dependency packages. </font><br />
|
||||
|
||||
* Commit [ImageCompositeHandleMask](#ImageCompositeHandleMask) node, used to generate local feathering masks and cropping data.
|
||||
* Commit [DrawRoundedRectangle](#DrawRoundedRectangle) node, used to generate rounded rectangle masks.
|
||||
* Commit [FluxKontextImageScale](#FluxKontextImageScale) node, based on official node modifications, used to resizes the image to one that is more optimal for flux kontext. For images with different aspect ratio, the scale will be adjusted appropriately to maintain all information.
|
||||
* Commit [MaskBoxExtend](#MaskBoxExtend) node, used to generate BBOX mask extension range and output as Mask.
|
||||
@@ -788,6 +789,43 @@ Node options:
|
||||
* anti_aliasing: Anti aliasing, ranging from 0 to 16, the larger the value, the less obvious the aliasing. An excessively high value will significantly reduce the processing speed of the node.
|
||||
* [note](#notes)
|
||||
|
||||
### <a id="table1">ImageCompositeHandleMask</a>
|
||||
Used to generate local feathering masks and corresponding cropping data. The node provides mask output that can be used for subsequent workflows.
|
||||

|
||||
|
||||
Node Options:
|
||||

|
||||
* background_image: The background image.
|
||||
* layer_image: Layer image for composite.
|
||||
* layer_mask: Mask for layer_image.
|
||||
* invert_mask: Whether to reverse the mask.
|
||||
* opacity: Opacity of image composite.
|
||||
* x_percent: Horizontal position of the layer on the background image, expressed as a percentage, with 0 on the far left and 100 on the far right. It can be less than 0 or more than 100, indicating that some of the layer's content is outside the screen.
|
||||
* y_percent: Vertical position of the layer on the background image, expressed as a percentage, with 0 on the top and 100 on the bottom. For example, setting it to 50 indicates vertical center, 20 indicates upper center, and 80 indicates lower center.
|
||||
* scale: Layer magnification, 1.0 represents the original size.
|
||||
* mirror: Mirror flipping. Provide two flipping modes, horizontal flipping and vertical flipping.
|
||||
* rotate: Layer rotation degree.
|
||||
* anti_aliasing: Anti aliasing, ranging from 0 to 8, the larger the value, the less obvious the aliasing. An excessively high value will significantly reduce the processing speed of the node.
|
||||
* handle_detect: There are two methods for detecting the position of feathering masks: mask_area and layer-bbox. ```mask_area``` detects the effective area of the mask for the layer object, while ```layer-bbox``` detects the outer BBox of the layer object.
|
||||
* top_handle: The amplitude of feathering at the top side of the mask. The value is the percentage of the average edge length of the mask.
|
||||
* bottom_handle: The amplitude of feathering at the bottom side of the mask. The value is the percentage of the average edge length of the mask.
|
||||
* left_handle: The amplitude of feathering at the left side of the mask. The value is the percentage of the average edge length of the mask.
|
||||
* right_handle: The amplitude of feathering at the right side of the mask. The value is the percentage of the average edge length of the mask.
|
||||
* handle_mask_outradius: Feather mask fillet radius.
|
||||
* top_reserve: Cut the top to preserve size.
|
||||
* bottom_reserve: Cut the bottom to preserve size.
|
||||
* left_reserve: Cut the left to preserve size.
|
||||
* right_reserve: Cut the right to preserve size.
|
||||
* round_to_multiple: Round the trimming edge length multiple. For example, setting it to 8 will force the width and height to be multiples of 8.
|
||||
|
||||
Outputs:
|
||||
* image: The composited image.
|
||||
* mask: The composited mask.
|
||||
* layer_bbox_mask: Composite object BBox mask.
|
||||
* handle_mask: The mask after feather process.
|
||||
* handle_crop_bbox: Feather mask cropping data.
|
||||
* handle_overrange: Does the feathering mask exceed the range of the background image. The output format is a string including "top", "bottom", "left", and "right".
|
||||
|
||||
### <a id="table1">CropByMask</a>
|
||||
|
||||
Crop the image according to the mask range, and set the size of the surrounding borders to be retained.
|
||||
|
||||
@@ -128,6 +128,7 @@ os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
|
||||
## 更新说明
|
||||
<font size="4">**如果本插件更新后出现依赖包错误,请双击运行插件目录下的```install_requirements.bat```(官方便携包),或 ```install_requirements_aki.bat```(秋叶整合包) 重新安装依赖包。
|
||||
|
||||
* 添加 [ImageCompositeHandleMask](#ImageCompositeHandleMask) 节点, 用于生成局部羽化遮罩以及对应的裁切数据。
|
||||
* 添加 [DrawRoundedRectangle](#DrawRoundedRectangle) 节点, 用于生成圆角矩形遮罩。
|
||||
* 添加 [FluxKontextImageScale](#FluxKontextImageScale) 节点,基于官方节点修改,用于将图像大小调整为更适合FluxKontext的大小。对于非标准宽高比的图像,自动调整比例以保持所有画面信息。
|
||||
* 添加 [MaskBoxExtend](#MaskBoxExtend) 节点,用于生成遮罩BBOX扩展范围并输出为Mask。
|
||||
@@ -712,6 +713,43 @@ Color of Shadow & Highlight 节点的复制品,去掉了节点名称中的"&"
|
||||
* anti_aliasing: 抗锯齿,范围从0-16,数值越大,锯齿越不明显。过高的数值将显著降低节点的处理速度。
|
||||
* [节点注解](#节点注解)
|
||||
|
||||
### <a id="table1">ImageCompositeHandleMask</a>
|
||||
用于生成局部羽化遮罩以及对应的裁切数据。节点提供了mask输出可用于后续工作流。
|
||||

|
||||
|
||||
节点选项说明:
|
||||

|
||||
* background_image: 背景图像。
|
||||
* layer_image: 用于合成的层图像。
|
||||
* layer_mask: 层图像的遮罩。
|
||||
* invert_mask: 是否反转遮罩。
|
||||
* opacity: 不透明度。
|
||||
* x_percent: 图层在背景图上的水平位置,用百分比表示,最左侧是0,最右侧是100,可以是小于0或者超过100,那表示图层有部分内容在画面之外。
|
||||
* y_percent: 图层在背景图上的垂直位置,用百分比表示,最上侧是0,最下侧是100。例如设置为50表示垂直居中,20是偏上,80则是偏下。
|
||||
* scale: 图层放大倍数,1.0 表示原大。
|
||||
* mirror: 镜像翻转。提供2种翻转模式, 水平翻转和垂直翻转。
|
||||
* rotate: 图层旋转度数。
|
||||
* anti_aliasing: 抗锯齿,范围从0-16,数值越大,锯齿越不明显。过高的数值将显著降低节点的处理速度。
|
||||
* handle_detect: 羽化遮罩位置检测方法,有mask_area和layer_bbox两种。mask_area检测合成对象的遮罩有效区域,layer_bbox检测合成对象外框。
|
||||
* top_handle: 遮罩顶部羽化幅度。数值为遮罩平均边长的百分比。
|
||||
* bottom_handle: 遮罩底部羽化幅度。数值为遮罩平均边长的百分比。
|
||||
* left_handle: 遮罩左侧羽化幅度。数值为遮罩平均边长的百分比。
|
||||
* right_handle: 遮罩右侧羽化幅度。数值为遮罩平均边长的百分比。
|
||||
* handle_mask_outradius: 羽化遮罩圆角半径。
|
||||
* top_reserve: 裁切顶端保留大小。
|
||||
* bottom_reserve: 裁切底部保留大小。
|
||||
* left_reserve: 裁切左侧保留大小。
|
||||
* right_reserve: 裁切右侧保留大小。
|
||||
* round_to_multiple: 使裁切边长倍数取整。例如设置为8,宽和高将强制设置为8的倍数。
|
||||
|
||||
输出:
|
||||
* image: 合成后的图片。
|
||||
* mask: 合成后的遮罩。
|
||||
* layer_bbox_mask: 合成对象外框遮罩。
|
||||
* handle_mask: 羽化遮罩。
|
||||
* handle_crop_bbox: 羽化遮罩裁切数据。
|
||||
* handle_overrange: 羽化遮罩是否超出背景图范围。输出格式为包括"top","bottom","left","right"的字符串。
|
||||
|
||||
### <a id="table1">CropByMask</a>
|
||||
将图片按照mask范围裁切,可设置四周边框保留大小。这个节点与[RestoreCropBox](#RestoreCropBox)和[ImageScaleRestore](#ImageScaleRestore)配合使用,可以对图片的局部进行裁切,放大修改后贴回原处。
|
||||

|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 802 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
@@ -0,0 +1,359 @@
|
||||
from .imagefunc import *
|
||||
|
||||
def composite_layer(background_image: Image, layer_image: Image, x_center: int, y_center: int, scale: float = 1.0,
|
||||
rotate: float = 0, aa: int = 1, opacity: int = 100) -> list:
|
||||
orig_layer_width, orig_layer_height = layer_image.size
|
||||
if aa > 1:
|
||||
w, h = layer_image.size
|
||||
layer_image = layer_image.resize((w * aa, h * aa), Image.LANCZOS)
|
||||
|
||||
if scale != 1.0:
|
||||
w, h = layer_image.size
|
||||
layer_image = layer_image.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
|
||||
|
||||
if rotate != 0:
|
||||
layer_image = layer_image.rotate(rotate, expand=True, resample=Image.BICUBIC)
|
||||
|
||||
if aa > 1:
|
||||
layer_image = layer_image.resize((layer_image.width // aa, layer_image.height // aa), Image.LANCZOS)
|
||||
|
||||
r, g, b, a = layer_image.split()
|
||||
alpha = a.copy()
|
||||
if 0 <= opacity < 100:
|
||||
alpha = alpha.point(lambda i: int(i * opacity * 0.01))
|
||||
layer_image = Image.merge("RGBA", (r, g, b, alpha))
|
||||
|
||||
left = int(x_center - layer_image.width / 2)
|
||||
top = int(y_center - layer_image.height / 2)
|
||||
|
||||
# composite
|
||||
bg = background_image.copy()
|
||||
bg.alpha_composite(layer_image, (left, top))
|
||||
# draw masks
|
||||
whiteimage = Image.new("L", alpha.size, 'white')
|
||||
layer_mask = Image.merge("RGBA", (whiteimage, whiteimage, whiteimage, a))
|
||||
mask = Image.new("RGBA", bg.size, 'black')
|
||||
mask.alpha_composite(layer_mask, (left, top))
|
||||
bbox = Image.new("RGBA", bg.size, 'black')
|
||||
bbox.alpha_composite(whiteimage.convert("RGBA"), (left, top))
|
||||
|
||||
return [bg.convert("RGB"), mask.convert("L"), bbox.convert("L")]
|
||||
|
||||
def sdf_rounded_rect_4corner(px, py, x1, y1, x2, y2, r):
|
||||
"""
|
||||
r = [r_tl, r_tr, r_br, r_bl] 四个角不同半径
|
||||
顺序:左上、右上、右下、左下
|
||||
"""
|
||||
cx = (x1 + x2) * 0.5
|
||||
cy = (y1 + y2) * 0.5
|
||||
hw = (x2 - x1) * 0.5
|
||||
hh = (y2 - y1) * 0.5
|
||||
|
||||
dx = px - cx
|
||||
dy = py - cy
|
||||
|
||||
# 按象限选择对应圆角半径
|
||||
r_tl, r_tr, r_br, r_bl = r
|
||||
|
||||
# 每个象限对应的半径
|
||||
r_corner = np.where(
|
||||
(dx < 0) & (dy < 0), r_tl,
|
||||
np.where(
|
||||
(dx > 0) & (dy < 0), r_tr,
|
||||
np.where(
|
||||
(dx > 0) & (dy > 0), r_br,
|
||||
r_bl
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# 剩余半宽高
|
||||
ex = hw - r_corner
|
||||
ey = hh - r_corner
|
||||
|
||||
dx2 = np.abs(dx) - ex
|
||||
dy2 = np.abs(dy) - ey
|
||||
|
||||
ox = np.maximum(dx2, 0)
|
||||
oy = np.maximum(dy2, 0)
|
||||
|
||||
outside = np.hypot(ox, oy)
|
||||
inside = np.minimum(np.maximum(dx2, dy2), 0)
|
||||
|
||||
return outside + inside - r_corner
|
||||
|
||||
|
||||
def smoothstep(t):
|
||||
return t * t * (3 - 2 * t)
|
||||
|
||||
|
||||
def rounded_rect_gradient_mask_numpy(image, outer_box, inner_box, outer_radius):
|
||||
w, h = image.size
|
||||
|
||||
if outer_box[0] <= 0:
|
||||
outer_box = [-outer_radius, outer_box[1], outer_box[2], outer_box[3]]
|
||||
if outer_box[1] <= 0:
|
||||
outer_box = [outer_box[0], -outer_radius, outer_box[2], outer_box[3]]
|
||||
if outer_box[2] >= w:
|
||||
outer_box = [outer_box[0], outer_box[1], w+outer_radius, outer_box[3]]
|
||||
if outer_box[3] >= h:
|
||||
outer_box = [outer_box[0], outer_box[1], outer_box[2], h+outer_radius]
|
||||
|
||||
xs = np.arange(w) + 0.5
|
||||
ys = np.arange(h) + 0.5
|
||||
px, py = np.meshgrid(xs, ys)
|
||||
|
||||
ox1, oy1, ox2, oy2 = outer_box
|
||||
ix1, iy1, ix2, iy2 = inner_box
|
||||
|
||||
# ---- 自动禁用圆角边 ----
|
||||
inner_radius = outer_radius // 2
|
||||
# 四个角默认使用同一半径
|
||||
ro = np.array([outer_radius, outer_radius, outer_radius, outer_radius], dtype=np.float32)
|
||||
ri = np.array([inner_radius, inner_radius, inner_radius, inner_radius], dtype=np.float32)
|
||||
|
||||
# 左边重叠:左上、左下角 = 0
|
||||
if ox1 == ix1:
|
||||
ro[0] = ro[3] = 0
|
||||
ri[0] = ri[3] = 0
|
||||
|
||||
# 右边重叠:右上、右下角 = 0
|
||||
if ox2 == ix2:
|
||||
ro[1] = ro[2] = 0
|
||||
ri[1] = ri[2] = 0
|
||||
|
||||
# 上边重叠:左上、右上角 = 0
|
||||
if oy1 == iy1:
|
||||
ro[0] = ro[1] = 0
|
||||
ri[0] = ri[1] = 0
|
||||
|
||||
# 下边重叠:左下、右下角 = 0
|
||||
if oy2 == iy2:
|
||||
ro[3] = ro[2] = 0
|
||||
ri[3] = ri[2] = 0
|
||||
|
||||
# ---- 计算 SDF ----
|
||||
sd_outer = sdf_rounded_rect_4corner(px, py, ox1, oy1, ox2, oy2, ro)
|
||||
sd_inner = sdf_rounded_rect_4corner(px, py, ix1, iy1, ix2, iy2, ri)
|
||||
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
|
||||
inside_inner = sd_inner < 0
|
||||
outside_outer = sd_outer > 0
|
||||
transition = ~(inside_inner | outside_outer)
|
||||
|
||||
mask[inside_inner] = 1.0
|
||||
mask[outside_outer] = 0.0
|
||||
|
||||
sd_i = sd_inner[transition]
|
||||
sd_o = sd_outer[transition]
|
||||
|
||||
t = 1.0 - (sd_i / (sd_i - sd_o + 1e-9))
|
||||
t = np.clip(t, 0, 1)
|
||||
|
||||
t = smoothstep(t)
|
||||
|
||||
mask[transition] = t
|
||||
|
||||
return Image.fromarray((mask * 255).astype(np.uint8), mode="L")
|
||||
|
||||
class LS_ImageCompositeHandleMask:
|
||||
|
||||
def __init__(self):
|
||||
self.NODE_NAME = 'ImageCompositeHandleMask'
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
mirror_mode = ['None', 'horizontal', 'vertical']
|
||||
multiple_list = ['8', '16', '32', '64', '128', '256', '512', 'None']
|
||||
handle_detect_list = ['mask_area', 'layer_bbox']
|
||||
return {
|
||||
"required": {
|
||||
"background_image": ("IMAGE",),
|
||||
"layer_image": ("IMAGE",),
|
||||
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
|
||||
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
|
||||
"x_percent": ("FLOAT", {"default": 50, "min": -999, "max": 999, "step": 0.01}),
|
||||
"y_percent": ("FLOAT", {"default": 50, "min": -999, "max": 999, "step": 0.01}),
|
||||
"scale": ("FLOAT", {"default": 1.0, "min": 0.001, "max": 1e4, "step": 0.001}),
|
||||
"rotate": ("FLOAT", {"default": 0, "min": -360, "max": 360, "step": 0.01}),
|
||||
"mirror": (mirror_mode,),
|
||||
"anti_aliasing": ("INT", {"default": 0, "min": 1, "max": 8, "step": 1}),
|
||||
"handle_detect": (handle_detect_list,),
|
||||
"top_handle": ("FLOAT", {"default": 0.3, "min": 0, "max": 5, "step": 0.01}),
|
||||
"bottom_handle": ("FLOAT", {"default": 0.3, "min": 0, "max": 5, "step": 0.01}),
|
||||
"left_handle": ("FLOAT", {"default": 0.3, "min": 0, "max": 5, "step": 0.01}),
|
||||
"right_handle": ("FLOAT", {"default": 0.3, "min": 0, "max": 5, "step": 0.01}),
|
||||
"handle_mask_outradius": ("INT", {"default": 128, "min": 8, "max": 9999, "step": 1}),
|
||||
"top_reserve": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}),
|
||||
"bottom_reserve": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}),
|
||||
"left_reserve": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}),
|
||||
"right_reserve": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}),
|
||||
"round_to_multiple": (multiple_list,),
|
||||
},
|
||||
"optional": {
|
||||
"layer_mask": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "MASK", "MASK", "BOX", "STRING")
|
||||
RETURN_NAMES = ("image", "mask", "layer_bbox_mask", "handle_mask", "handle_crop_box", "handle_overrange")
|
||||
FUNCTION = 'image_composite_handle_mask'
|
||||
CATEGORY = '😺dzNodes/LayerMask'
|
||||
|
||||
def image_composite_handle_mask(self, background_image, layer_image, invert_mask, opacity,
|
||||
x_percent, y_percent, scale, rotate, mirror, anti_aliasing,
|
||||
handle_detect, top_handle, bottom_handle, left_handle, right_handle,
|
||||
handle_mask_outradius,
|
||||
top_reserve, bottom_reserve, left_reserve, right_reserve,
|
||||
round_to_multiple, layer_mask=None,):
|
||||
|
||||
ret_images = []
|
||||
ret_masks = []
|
||||
ret_layer_bbox_masks = []
|
||||
ret_handle_masks = []
|
||||
handle_overrange = "None"
|
||||
|
||||
b_images = []
|
||||
l_images = []
|
||||
l_masks = []
|
||||
|
||||
for b in background_image:
|
||||
b_images.append(torch.unsqueeze(b, 0))
|
||||
for l in layer_image:
|
||||
l_images.append(torch.unsqueeze(l, 0))
|
||||
m = tensor2pil(l)
|
||||
if m.mode == 'RGBA':
|
||||
l_masks.append(m.split()[-1])
|
||||
else:
|
||||
l_masks.append(Image.new('L', m.size, 'white'))
|
||||
if layer_mask is not None:
|
||||
if layer_mask.dim() == 2:
|
||||
layer_mask = torch.unsqueeze(layer_mask, 0)
|
||||
l_masks = []
|
||||
for m in layer_mask:
|
||||
if invert_mask:
|
||||
m = 1 - m
|
||||
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
|
||||
|
||||
max_batch = max(len(b_images), len(l_images), len(l_masks))
|
||||
for i in range(max_batch):
|
||||
background_image = b_images[i] if i < len(b_images) else b_images[-1]
|
||||
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
|
||||
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
|
||||
# preprocess
|
||||
_canvas = tensor2pil(background_image).convert('RGBA')
|
||||
_layer = tensor2pil(layer_image).convert('RGBA')
|
||||
if _mask.size != _layer.size:
|
||||
_mask = Image.new('L', _layer.size, 'white')
|
||||
log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning')
|
||||
r, g, b, a = _layer.split()
|
||||
if 0 <= opacity < 1.0:
|
||||
_mask = _mask.point(lambda i: int(i * opacity))
|
||||
_layer = Image.merge('RGBA', (r, g, b, _mask))
|
||||
# mirror
|
||||
if mirror == 'horizontal':
|
||||
_layer = _layer.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
_mask = _mask.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
elif mirror == 'vertical':
|
||||
_layer = _layer.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
_mask = _mask.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
|
||||
x_center = int(_canvas.width * x_percent / 100)
|
||||
y_center = int(_canvas.height * y_percent / 100)
|
||||
if anti_aliasing == 0:
|
||||
anti_aliasing = 1
|
||||
ret_image, ret_mask, bbox_mask = composite_layer(_canvas, _layer, x_center, y_center,
|
||||
scale, rotate, anti_aliasing, opacity)
|
||||
|
||||
# clac crop box
|
||||
if handle_detect == "mask_area":
|
||||
mask_box = mask_area(ret_mask)
|
||||
else:
|
||||
mask_box = mask_area(bbox_mask)
|
||||
|
||||
x1 = int(mask_box[0]) - left_reserve
|
||||
y1 = int(mask_box[1]) - top_reserve
|
||||
x2 = int(x1 + mask_box[2]) + right_reserve
|
||||
y2 = int(y1 + mask_box[3]) + bottom_reserve
|
||||
if x1 < 0:
|
||||
x1 = 0
|
||||
if x2 > ret_image.width:
|
||||
x2 = ret_image.width
|
||||
if y1 < 0:
|
||||
y1 = 0
|
||||
if y2 > ret_image.height:
|
||||
y2 = ret_image.height
|
||||
mask_box = (x1, y1, x2, y2)
|
||||
|
||||
side_length = ((x2-x1) + (y2-y1)) // 2
|
||||
handle_x1 = int(x1 - left_handle * side_length - 1)
|
||||
handle_x2 = int(x2 + right_handle * side_length + 1)
|
||||
handle_y1 = int(y1 - top_handle * side_length - 1)
|
||||
handle_y2 = int(y2 + bottom_handle * side_length + 1)
|
||||
handle_width = handle_x2 - handle_x1
|
||||
handle_height = handle_y2 - handle_y1
|
||||
|
||||
if round_to_multiple != 'None':
|
||||
multiple = int(round_to_multiple)
|
||||
handle_width = num_round_up_to_multiple(handle_x2 - handle_x1, multiple)
|
||||
handle_height = num_round_up_to_multiple(handle_y2 - handle_y1, multiple)
|
||||
handle_x1 = handle_x1 - (handle_width - (handle_x2 - handle_x1)) // 2
|
||||
handle_y1 = handle_y1 - (handle_height - (handle_y2 - handle_y1)) // 2
|
||||
handle_x2 = handle_x1 + handle_width
|
||||
handle_y2 = handle_y1 + handle_height
|
||||
|
||||
if handle_x1 <0:
|
||||
handle_x1 = 0
|
||||
handle_x2 = num_round_up_to_multiple(handle_x2, multiple)
|
||||
if handle_x2 > ret_image.size[0]:
|
||||
handle_x1 = handle_x2 - num_round_up_to_multiple(handle_x2 - handle_x1, multiple)
|
||||
handle_x2 = ret_image.size[0]
|
||||
if handle_y1 <0:
|
||||
handle_y1 = 0
|
||||
handle_y2 = num_round_up_to_multiple(handle_y2, multiple)
|
||||
if handle_y2 > ret_image.size[1]:
|
||||
handle_y1 = handle_y2 - num_round_up_to_multiple(handle_y2 - handle_y1, multiple)
|
||||
handle_y2 = ret_image.size[1]
|
||||
|
||||
crop_box = (handle_x1, handle_y1, handle_x2, handle_y2)
|
||||
|
||||
# draw handle mask
|
||||
handle_mask = rounded_rect_gradient_mask_numpy(ret_mask, crop_box, mask_box, handle_mask_outradius)
|
||||
|
||||
# check handle overrange
|
||||
if handle_x1 <= 0 or handle_x2 >= ret_image.size[0] or handle_y1 <= 0 or handle_y2 >= ret_image.size[1]:
|
||||
top = ""
|
||||
bottom = ""
|
||||
left = ""
|
||||
right = ""
|
||||
if handle_y1 <= 0 :
|
||||
top = "top,"
|
||||
if handle_y2 >= ret_image.size[1] :
|
||||
bottom = "bottom,"
|
||||
if handle_x1 <= 0 :
|
||||
left = "left,"
|
||||
if handle_x2 >= ret_image.size[0] :
|
||||
right = "right"
|
||||
handle_overrange = f"{top}{bottom}{left}{right}"
|
||||
log(f"{self.NODE_NAME} handle overrange: {handle_overrange}")
|
||||
|
||||
ret_images.append(pil2tensor(ret_image))
|
||||
ret_masks.append(image2mask(ret_mask))
|
||||
ret_layer_bbox_masks.append(image2mask(bbox_mask))
|
||||
ret_handle_masks.append(image2mask(handle_mask))
|
||||
|
||||
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
|
||||
return (torch.cat(ret_images, dim=0),torch.cat(ret_masks, dim=0), torch.cat(ret_layer_bbox_masks, dim=0),
|
||||
torch.cat(ret_handle_masks, dim=0), list(crop_box), handle_overrange)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LayerUtility: ImageCompositeHandleMask": LS_ImageCompositeHandleMask,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LayerUtility: ImageCompositeHandleMask": "LayerUtility: Image Composite Handle Mask",
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui_layerstyle"
|
||||
description = "A set of nodes for ComfyUI it generate image like Adobe Photoshop's Layer Style. the Drop Shadow is first completed node, and follow-up work is in progress."
|
||||
version = "2.0.26"
|
||||
version = "2.0.27"
|
||||
license = {text = "MIT License"}
|
||||
dependencies = ["numpy", "pillow", "torch", "matplotlib", "Scipy", "scikit_image", "scikit_learn", "opencv-contrib-python", "pymatting", "timm", "colour-science", "transformers", "blend_modes", "huggingface_hub", "loguru"]
|
||||
|
||||
|
||||
@@ -0,0 +1,628 @@
|
||||
{
|
||||
"id": "7e351344-cbbd-4b6d-9ab4-733acaedcf3b",
|
||||
"revision": 0,
|
||||
"last_node_id": 33,
|
||||
"last_link_id": 135,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 29,
|
||||
"type": "LayerMask: MaskPreview",
|
||||
"pos": [
|
||||
-1210.48486328125,
|
||||
1281.5628662109375
|
||||
],
|
||||
"size": [
|
||||
356.89923095703125,
|
||||
258
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 133
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui_layerstyle",
|
||||
"ver": "89014d833d2ad0998d724090eff1456692692a1d",
|
||||
"Node name for S&R": "LayerMask: MaskPreview"
|
||||
},
|
||||
"widgets_values": [],
|
||||
"color": "rgba(27, 80, 119, 0.7)"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "LayerMask: MaskPreview",
|
||||
"pos": [
|
||||
-743.678466796875,
|
||||
1272.4127197265625
|
||||
],
|
||||
"size": [
|
||||
288.8282470703125,
|
||||
270.1563720703125
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 29
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui_layerstyle",
|
||||
"ver": "89014d833d2ad0998d724090eff1456692692a1d",
|
||||
"Node name for S&R": "LayerMask: MaskPreview"
|
||||
},
|
||||
"widgets_values": [],
|
||||
"color": "rgba(27, 80, 119, 0.7)"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
-1217.8658447265625,
|
||||
951.4215087890625
|
||||
],
|
||||
"size": [
|
||||
363.9338684082031,
|
||||
261.3555908203125
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 130
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.62",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
-749.3758544921875,
|
||||
916.8765258789062
|
||||
],
|
||||
"size": [
|
||||
296.3067932128906,
|
||||
287.94805908203125
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 114
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.62",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "LayerUtility: CropByMask V2",
|
||||
"pos": [
|
||||
-738.6014404296875,
|
||||
447.8634033203125
|
||||
],
|
||||
"size": [
|
||||
278.0650329589844,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 129
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 132
|
||||
},
|
||||
{
|
||||
"name": "crop_box",
|
||||
"shape": 7,
|
||||
"type": "BOX",
|
||||
"link": 134
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "croped_image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
114,
|
||||
122
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "croped_mask",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
29,
|
||||
123
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "crop_box",
|
||||
"type": "BOX",
|
||||
"links": [
|
||||
124
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "box_preview",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui_layerstyle",
|
||||
"ver": "89014d833d2ad0998d724090eff1456692692a1d",
|
||||
"Node name for S&R": "LayerUtility: CropByMask V2"
|
||||
},
|
||||
"widgets_values": [
|
||||
false,
|
||||
"mask_area",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"None"
|
||||
],
|
||||
"color": "rgba(38, 73, 116, 0.7)"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-1721.1669921875,
|
||||
297.2882385253906
|
||||
],
|
||||
"size": [
|
||||
265.1226806640625,
|
||||
326
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
126
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.62",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1344x768_beach.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-1727.592041015625,
|
||||
689.5145263671875
|
||||
],
|
||||
"size": [
|
||||
277.9189453125,
|
||||
326
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
127
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
135
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.62",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"512x512.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"type": "LayerMask: ImageCompositeHandleMask",
|
||||
"pos": [
|
||||
-1219.4482421875,
|
||||
293.64581298828125
|
||||
],
|
||||
"size": [
|
||||
371.4576110839844,
|
||||
590
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "background_image",
|
||||
"type": "IMAGE",
|
||||
"link": 126
|
||||
},
|
||||
{
|
||||
"name": "layer_image",
|
||||
"type": "IMAGE",
|
||||
"link": 127
|
||||
},
|
||||
{
|
||||
"name": "layer_mask",
|
||||
"shape": 7,
|
||||
"type": "MASK",
|
||||
"link": 135
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
129,
|
||||
130,
|
||||
131
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "layer_bbox_mask",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "handle_mask",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
132,
|
||||
133
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "handle_crop_box",
|
||||
"type": "BOX",
|
||||
"links": [
|
||||
134
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "handle_overrange",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui_layerstyle",
|
||||
"ver": "89014d833d2ad0998d724090eff1456692692a1d",
|
||||
"Node name for S&R": "LayerMask: ImageCompositeHandleMask"
|
||||
},
|
||||
"widgets_values": [
|
||||
true,
|
||||
100,
|
||||
50,
|
||||
50,
|
||||
1,
|
||||
-10,
|
||||
"None",
|
||||
1,
|
||||
"mask_area",
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
128,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"8"
|
||||
],
|
||||
"color": "rgba(27, 80, 119, 0.7)"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"type": "LayerUtility: RestoreCropBox",
|
||||
"pos": [
|
||||
68.38394165039062,
|
||||
473.526611328125
|
||||
],
|
||||
"size": [
|
||||
280.4097595214844,
|
||||
118
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "background_image",
|
||||
"type": "IMAGE",
|
||||
"link": 131
|
||||
},
|
||||
{
|
||||
"name": "croped_image",
|
||||
"type": "IMAGE",
|
||||
"link": 122
|
||||
},
|
||||
{
|
||||
"name": "crop_box",
|
||||
"type": "BOX",
|
||||
"link": 124
|
||||
},
|
||||
{
|
||||
"name": "croped_mask",
|
||||
"shape": 7,
|
||||
"type": "MASK",
|
||||
"link": 123
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
125
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui_layerstyle",
|
||||
"ver": "89014d833d2ad0998d724090eff1456692692a1d",
|
||||
"Node name for S&R": "LayerUtility: RestoreCropBox"
|
||||
},
|
||||
"widgets_values": [
|
||||
false
|
||||
],
|
||||
"color": "rgba(38, 73, 116, 0.7)"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
-95.73606872558594,
|
||||
1018.9447631835938
|
||||
],
|
||||
"size": [
|
||||
573.1544189453125,
|
||||
358.45465087890625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 125
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.62",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
29,
|
||||
12,
|
||||
1,
|
||||
19,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
114,
|
||||
12,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
122,
|
||||
12,
|
||||
0,
|
||||
30,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
123,
|
||||
12,
|
||||
1,
|
||||
30,
|
||||
3,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
124,
|
||||
12,
|
||||
2,
|
||||
30,
|
||||
2,
|
||||
"BOX"
|
||||
],
|
||||
[
|
||||
125,
|
||||
30,
|
||||
0,
|
||||
31,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
126,
|
||||
2,
|
||||
0,
|
||||
32,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
127,
|
||||
3,
|
||||
0,
|
||||
32,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
129,
|
||||
32,
|
||||
0,
|
||||
12,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
130,
|
||||
32,
|
||||
0,
|
||||
28,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
131,
|
||||
32,
|
||||
0,
|
||||
30,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
132,
|
||||
32,
|
||||
3,
|
||||
12,
|
||||
1,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
133,
|
||||
32,
|
||||
3,
|
||||
29,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
134,
|
||||
32,
|
||||
4,
|
||||
12,
|
||||
2,
|
||||
"BOX"
|
||||
],
|
||||
[
|
||||
135,
|
||||
3,
|
||||
1,
|
||||
32,
|
||||
2,
|
||||
"MASK"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Process local part of image composite",
|
||||
"bounding": [
|
||||
-406.0556335449219,
|
||||
333.6806640625,
|
||||
436.49578857421875,
|
||||
420.3355407714844
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.5730855330116974,
|
||||
"offset": [
|
||||
2131.027252025057,
|
||||
60.6777304707427
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.26.13",
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
Reference in New Issue
Block a user