commit DistortDisplace node

This commit is contained in:
chflame163
2026-01-08 17:50:29 +08:00
parent d256c3a057
commit a1875e824c
9 changed files with 521 additions and 53 deletions
+34
View File
@@ -2149,6 +2149,19 @@ On the basis of MaskEdgeUltraDetail, the following changes have been made:
* max_megapixels: Set the maximum size for VitMate operations.
### <a id="table1">MaskEdgeUltraDetailV3</a>
The upgraded version of MaskEdgeUltraDetailV2 to processes different partitions through inputting trimap masks, generating an overall mask that includes more refined and translucent parts.
![image](image/mask_edge_ultra_detail_v3_example.jpg)
On the basis of MaskEdgeUltraDetailV2, the following changes have been made:
![image](image/mask_edge_ultra_detail_v3_node.jpg)
* transparent_trimap: Using different vitmatte parameters within this area can generate more refined matte mask. It is typically used to handle areas such as translucent objects or hair strands.
* mask_edge_erode: The edge of the mask part erodes inwardly. The larger the value, the greater the range of inward repair.
* mask_edge_dilate: The edge of the mask expands outward. The larger the value, the greater the outward repair range.
* transparent_trimap_edge_erode: The edge of the transparent_trimap mask erodes inwardly. The larger the value, the greater the range of inward correction.
* transparent_trimap_edge_dilate: The edge of the transparent_trimap mask expands outward. The larger the value, the greater the outward repair range.
* trimap_blur: The degree of blur at the edge where the trimap mask and the mask are fused.
### <a id="table1">MaskByColor</a>
Generate a mask based on the selected color.
@@ -2522,6 +2535,27 @@ Node Options:
* grain_scale: Noise size.
* grain_sat: Color saturation of noise.
### <a id="table1">DistortDisplace</a>
Generate displacement deformation effects for material images.
![image](image/distort_displace_example.jpg)
Node Options:
![image](image/distort_displace_node.jpg)
* image: The original image, with material distortion based on the grayscale information of this image.
* material_image: Material image. The size of this image should be consistent with that of the image, otherwise it will be resized forcibly.
* mask: Optional mask input. The output will only include the deformed result of the map in the masked part.
* distort_strength: The strength of distortion.
* smoothness: The smoothness of distortion.
* anit_aliasing: The value of anti-aliasing. Higher values will result in a significant decrease in generation speed.
* shadow_blend_mode: The shadow part blending mode.
* shadow_strength: The shadow part blending opacity.
* highlight_blend_mode: The highlight part blending mode.
* highlight_strength: The highlight part blending opacity.
Outputs:
* image: The output image.
* displaced_material: The deformation result of the material image.
## Annotation for <a id="table1">notes</a>
<sup>1</sup> The layer_image, layer_mask and the background_image(if have input), These three items must be of the same size.
+35
View File
@@ -128,6 +128,8 @@ os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
## 更新说明
<font size="4">**如果本插件更新后出现依赖包错误,请双击运行插件目录下的```install_requirements.bat```(官方便携包),或 ```install_requirements_aki.bat```(秋叶整合包) 重新安装依赖包。
* 添加 [DistortDisplace](#DistortDisplace) 节点, 为材质图片生成置换变形效果。
* 添加 [MaskEdgeUltraDetailV3](#MaskEdgeUltraDetailV3) 节点, 通过输入trimap遮罩对不同分区处理,生成包括更加精细半透明部分的整体遮罩。
* 添加 [ImageCompositeHandleMask](#ImageCompositeHandleMask) 节点, 用于生成局部羽化遮罩以及对应的裁切数据。
* 添加 [DrawRoundedRectangle](#DrawRoundedRectangle) 节点, 用于生成圆角矩形遮罩。
* 添加 [FluxKontextImageScale](#FluxKontextImageScale) 节点,基于官方节点修改,用于将图像大小调整为更适合FluxKontext的大小。对于非标准宽高比的图像,自动调整比例以保持所有画面信息。
@@ -1933,6 +1935,18 @@ MaskEdgeUltraDetail的V2升级版,增加了VITMatte边缘处理方法,此方
* device: 设置是否使用cuda。
* max_megapixels: 设置vitmatte运算的最大尺寸。
### <a id="table1">MaskEdgeUltraDetailV3</a>
MaskEdgeUltraDetailV2的升级版,通过输入trimap遮罩对不同分区处理,生成包括更加精细半透明部分的整体遮罩。
![image](image/mask_edge_ultra_detail_v3_example.jpg)
在MaskEdgeUltraDetailV2的基础上做了如下改变:
![image](image/mask_edge_ultra_detail_v3_node.jpg)
* transparent_trimap: 此区域范围使用不同的vitmatte参数,可生成更精细的局部遮罩。通常用于处理半透明物体或头发丝等区域。
* mask_edge_erode: mask遮罩部分的边缘向内侵蚀范围。数值越大,向内修复的范围越大。
* mask_edge_dilate: mask遮罩部分的边缘向外扩张范围。数值越大,向外修复的范围越大。
* transparent_trimap_edge_erode: transparent_trimap遮罩部分的边缘向内侵蚀范围。数值越大,向内修复的范围越大。
* transparent_trimap_edge_dilate: transparent_trimap遮罩部分的边缘向外扩张范围。数值越大,向外修复的范围越大。
* trimap_blur: trimap遮罩与mask遮罩融合边缘的模糊程度。
### <a id="table1">MaskByColor</a>
根据颜色生成遮罩。
@@ -2261,6 +2275,27 @@ Film节点的升级版, 在之前基础上增加了fastgrain方法,生成噪
* grain_sat: 噪声的色彩饱和度。
### <a id="table1">DistortDisplace</a>
为材质图片生成置换变形效果。
![image](image/distort_displace_example.jpg)
节点选项说明:
![image](image/distort_displace_node.jpg)
* image: 原始图片,材质变形基于此图片的灰度信息。
* material_image: 材质图片。 注意此图片大小要与image一致,否则将强行转换尺寸。
* mask: 可选遮罩输入。将输出仅包括在遮罩部分的贴图变形结果。
* distort_strength: 变形强度。
* smoothness: 变形的平滑度。
* anit_aliasing: 抗锯齿。更高的数值将导致生成速度明显下降。
* shadow_blend_mode: 暗部混合模式。
* shadow_strength: 暗部混合强度。
* highlight_blend_mode: 亮部混合模式。
* highlight_strength: 亮部混合强度。
输出说明:
* image: 输出图片。
* displaced_material: 材质图片变形结果。
## <a id="table1">节点注解</a>
<sup>1</sup> image、mask和background_image(如果有输入)这三项必须是相同的尺寸。
Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

+101
View File
@@ -0,0 +1,101 @@
import copy
import torch
import numpy as np
from .imagefunc import log, pil2tensor, tensor2pil, chop_image_v2, chop_mode_v2, fit_resize_image, displacement_image
class LS_DistortDisplace:
def __init__(self):
self.NODE_NAME = 'DistortDisplace'
@classmethod
def INPUT_TYPES(self):
shadow_blendmode_list = ['linear burn', "multiply"]
highlight_blendmode_list = ['screen', 'linear dodge(add)']
shadow_blendmode_list = shadow_blendmode_list + [x for x in chop_mode_v2 if x not in shadow_blendmode_list]
highlight_blendmode_list = highlight_blendmode_list + [x for x in chop_mode_v2 if x not in highlight_blendmode_list]
return {
"required": {
"image": ("IMAGE", ), #
"material_image": ("IMAGE",), #
"distort_strength": ("FLOAT", {"default": 50, "min": -999, "max": 999, "step": 0.1}),
"smoothness": ("INT", {"default": 8, "min": 0, "max": 99, "step": 1}),
"anti_aliasing": ("INT", {"default": 2, "min": 1, "max": 16, "step": 1}),
"shadow_blend_mode": (shadow_blendmode_list,),
"shadow_strength": ("INT", {"default": 75, "min": 0, "max": 100, "step": 1}), # 透明度
"highlight_blend_mode": (highlight_blendmode_list,),
"highlight_strength": ("INT", {"default": 30, "min": 0, "max": 100, "step": 1}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "IMAGE",)
RETURN_NAMES = ("image", "displaced_material")
FUNCTION = 'distort_displace'
CATEGORY = '😺dzNodes/LayerFilter'
def distort_displace(self, image, material_image, distort_strength, smoothness, anti_aliasing,
shadow_blend_mode, shadow_strength, highlight_blend_mode, highlight_strength,
mask=None):
m_images = []
i_images = []
i_masks = []
ret_images = []
displaced_images = []
for m in material_image:
m_images.append(torch.unsqueeze(m, 0))
for i in image:
i_images.append(torch.unsqueeze(i, 0))
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
i_masks.append(torch.unsqueeze(m, 0))
max_batch = max(len(m_images), len(i_images), len(i_masks))
for i in range(max_batch):
m_img = m_images[i] if i < len(m_images) else m_images[-1]
_image = tensor2pil(m_img).convert('RGB')
i_img = i_images[i] if i < len(i_images) else i_images[-1]
_grayscale = tensor2pil(i_img)
log(f"{self.NODE_NAME} processing:")
displaced_image = displacement_image(_image, _grayscale, distort_strength, smoothness, anti_aliasing)
orig_image = tensor2pil(i_img)
if shadow_strength > 0:
ret_image = chop_image_v2(orig_image, displaced_image, shadow_blend_mode, shadow_strength)
else:
ret_image = orig_image
if highlight_strength > 0:
ret_image = chop_image_v2(ret_image, displaced_image, highlight_blend_mode, highlight_strength)
if mask is not None:
i_msk = i_masks[i] if i < len(i_masks) else i_masks[-1]
_mask = tensor2pil(i_msk).convert('L')
if _mask.size != displaced_image.size:
_mask = fit_resize_image(_mask, displaced_image.width, displaced_image.height,'fill', Image.LANCZOS)
log(f"Warning: {self.NODE_NAME} mask mismatch, fixed to image size!", message_type='warning')
orig_image.paste(ret_image, mask=_mask)
ret_image = orig_image
ret_images.append(pil2tensor(ret_image))
displaced_images.append(pil2tensor(displaced_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(displaced_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: DistortDisplace": LS_DistortDisplace,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: DistortDisplace": "LayerFilter: Distort Displace",
}
+55 -52
View File
@@ -1380,58 +1380,6 @@ def decode_watermark(image:Image, watermark_image_size:int=94) -> Image:
ret_image = normalize_gray(ret_image)
return ret_image
# def generate_text_image(text:str, font_path:str, font_size:int, text_color:str="#FFFFFF",
# vertical:bool=True, stroke_width:int=1, stroke_color:str="#000000",
# spacing:int=0, leading:int=0) -> tuple:
#
# lines = text.split("\n")
# if vertical:
# layout = "vertical"
# else:
# layout = "horizontal"
# char_coordinates = []
# if layout == "vertical":
# x = 0
# y = 0
# for i in range(len(lines)):
# line = lines[i]
# for char in line:
# char_coordinates.append((x, y))
# y += font_size + spacing
# x += font_size + leading
# y = 0
# else:
# x = 0
# y = 0
# for line in lines:
# for char in line:
# char_coordinates.append((x, y))
# x += font_size + spacing
# y += font_size + leading
# x = 0
# if layout == "vertical":
# width = (len(lines) * (font_size + spacing)) - spacing
# height = ((len(max(lines, key=len)) + 1) * (font_size + spacing)) + spacing
# else:
# width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
# height = ((len(lines) - 1) * (font_size + spacing)) + font_size
#
# image = Image.new('RGBA', size=(width, height), color=stroke_color)
# draw = ImageDraw.Draw(image)
# font = ImageFont.truetype(font_path, font_size)
# index = 0
# for i, line in enumerate(lines):
# for j, char in enumerate(line):
# x, y = char_coordinates[index]
# if stroke_width > 0:
# draw.text((x - stroke_width, y), char, font=font, fill=stroke_color)
# draw.text((x + stroke_width, y), char, font=font, fill=stroke_color)
# draw.text((x, y - stroke_width), char, font=font, fill=stroke_color)
# draw.text((x, y + stroke_width), char, font=font, fill=stroke_color)
# draw.text((x, y), char, font=font, fill=text_color)
# index += 1
# return (image.convert('RGB'), image.split()[3])
def generate_text_image(width:int, height:int, text:str, font_file:str, text_scale:float=1, font_color:str="#FFFFFF",) -> Image:
image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(image)
@@ -1444,6 +1392,61 @@ def generate_text_image(width:int, height:int, text:str, font_file:str, text_sca
draw.text((x, y), text, font=font, fill=font_color)
return image
def displacement_image(image: Image, displacement_map: Image, strength: float, smoothness: int, anti_aliasing: int) -> Image:
if image.mode != 'RGB':
image = image.convert('RGB')
if displacement_map.mode != 'L':
displacement_map = displacement_map.convert('L')
orig_w, orig_h = image.size
if displacement_map.size != image.size:
displacement_map = displacement_map.resize(image.size, Image.LANCZOS)
if smoothness:
displacement_map = gaussian_blur(displacement_map, smoothness)
if anti_aliasing > 1:
up_size = (orig_w * anti_aliasing, orig_h * anti_aliasing)
image = image.resize(up_size, Image.LANCZOS)
displacement_map = displacement_map.resize(up_size, Image.LANCZOS)
img = np.asarray(image)
disp = np.asarray(displacement_map).astype(np.float32)
h, w = disp.shape
ys, xs = np.meshgrid(
np.arange(h, dtype=np.int32),
np.arange(w, dtype=np.int32),
indexing="ij"
)
offset = (disp / 255.0 * strength * anti_aliasing).astype(np.int32)
new_x = xs + offset
new_y = ys + offset
def mirror(coord, size):
coord = np.where(coord < 0, -coord, coord)
coord = np.where(coord >= size, 2 * size - coord - 1, coord)
return coord
new_x = mirror(new_x, w)
new_y = mirror(new_y, h)
out = img[new_y, new_x]
ret = Image.fromarray(out, mode="RGB")
if anti_aliasing > 1:
ret = gaussian_blur(ret, int(anti_aliasing / 3))
ret = ret.resize((orig_w, orig_h), Image.LANCZOS)
return ret
'''Mask Functions'''
def create_mask_from_color_cv2(image:Image, color:str, tolerance:int=0) -> Image:
+1 -1
View File
@@ -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.33"
version = "2.0.34"
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"]
+295
View File
@@ -0,0 +1,295 @@
{
"id": "2faaee3f-fb4f-4675-bf73-0188eded5ad4",
"revision": 0,
"last_node_id": 43,
"last_link_id": 90,
"nodes": [
{
"id": 9,
"type": "LoadImage",
"pos": [
-6656.189793454538,
-1030.5373971645267
],
"size": [
360.73952709359037,
489.6573516743524
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
43,
87
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.6.0",
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"768x1344_dress.png",
"image"
]
},
{
"id": 10,
"type": "LoadImage",
"pos": [
-6653.694433010127,
-1579.8143566299393
],
"size": [
352.59643720054373,
490.82062740849506
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
88
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.6.0",
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"plaid.jpg",
"image"
]
},
{
"id": 25,
"type": "LayerMask: SegmentAnythingUltra V2",
"pos": [
-6120.023112208488,
-1027.5182287606453
],
"size": [
420.491796875,
366
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 43
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": null
},
{
"name": "mask",
"type": "MASK",
"links": [
89
]
}
],
"properties": {
"cnr_id": "ComfyUI_LayerStyle_Advance",
"ver": "86cc56bbcffd9a3e61c0dc62c2eb857344c23fce",
"Node name for S&R": "LayerMask: SegmentAnythingUltra V2"
},
"widgets_values": [
"sam_vit_h (2.56GB)",
"GroundingDINO_SwinT_OGC (694MB)",
0.3,
"VITMatte",
6,
6,
0.15,
0.99,
true,
"dress",
"cuda",
2,
false
],
"color": "rgba(27, 80, 119, 0.7)"
},
{
"id": 41,
"type": "LayerFilter: DistortDisplace",
"pos": [
-5663.784182811386,
-1392.847340659025
],
"size": [
296.525390625,
242
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 87
},
{
"name": "material_image",
"type": "IMAGE",
"link": 88
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": 89
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
90
]
},
{
"name": "displaced_material",
"type": "IMAGE",
"links": null
}
],
"properties": {
"cnr_id": "comfyui_layerstyle",
"ver": "d256c3a0573d854f4eb382672397c3361cf12084",
"Node name for S&R": "LayerFilter: DistortDisplace"
},
"widgets_values": [
50,
8,
2,
"linear burn",
75,
"screen",
30
],
"color": "rgba(34, 67, 111, 0.7)"
},
{
"id": 22,
"type": "PreviewImage",
"pos": [
-5218.693562230295,
-1574.0943976697952
],
"size": [
602.5899407651559,
1000.8520524954201
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 90
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.6.0",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[
43,
9,
0,
25,
0,
"IMAGE"
],
[
87,
9,
0,
41,
0,
"IMAGE"
],
[
88,
10,
0,
41,
1,
"IMAGE"
],
[
89,
25,
1,
41,
2,
"MASK"
],
[
90,
41,
0,
22,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.4305676431342591,
"offset": [
8224.326676621306,
2659.4485264813743
]
},
"frontendVersion": "1.35.9",
"workflowRendererVersion": "LG",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB