diff --git a/README.MD b/README.MD
index b679c6c..f37e281 100644
--- a/README.MD
+++ b/README.MD
@@ -2149,6 +2149,19 @@ On the basis of MaskEdgeUltraDetail, the following changes have been made:
* max_megapixels: Set the maximum size for VitMate operations.
+### MaskEdgeUltraDetailV3
+The upgraded version of MaskEdgeUltraDetailV2 to processes different partitions through inputting trimap masks, generating an overall mask that includes more refined and translucent parts.
+
+
+On the basis of MaskEdgeUltraDetailV2, the following changes have been made:
+
+* 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.
+
### MaskByColor
Generate a mask based on the selected color.
@@ -2522,6 +2535,27 @@ Node Options:
* grain_scale: Noise size.
* grain_sat: Color saturation of noise.
+### DistortDisplace
+Generate displacement deformation effects for material images.
+
+
+Node Options:
+
+* 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 notes
1 The layer_image, layer_mask and the background_image(if have input), These three items must be of the same size.
diff --git a/README_CN.MD b/README_CN.MD
index 583d280..64e9c01 100644
--- a/README_CN.MD
+++ b/README_CN.MD
@@ -128,6 +128,8 @@ os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
## 更新说明
**如果本插件更新后出现依赖包错误,请双击运行插件目录下的```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运算的最大尺寸。
+### MaskEdgeUltraDetailV3
+MaskEdgeUltraDetailV2的升级版,通过输入trimap遮罩对不同分区处理,生成包括更加精细半透明部分的整体遮罩。
+
+
+在MaskEdgeUltraDetailV2的基础上做了如下改变:
+
+* 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遮罩融合边缘的模糊程度。
### MaskByColor
根据颜色生成遮罩。
@@ -2261,6 +2275,27 @@ Film节点的升级版, 在之前基础上增加了fastgrain方法,生成噪
* grain_sat: 噪声的色彩饱和度。
+### DistortDisplace
+为材质图片生成置换变形效果。
+
+
+节点选项说明:
+
+* image: 原始图片,材质变形基于此图片的灰度信息。
+* material_image: 材质图片。 注意此图片大小要与image一致,否则将强行转换尺寸。
+* mask: 可选遮罩输入。将输出仅包括在遮罩部分的贴图变形结果。
+* distort_strength: 变形强度。
+* smoothness: 变形的平滑度。
+* anit_aliasing: 抗锯齿。更高的数值将导致生成速度明显下降。
+* shadow_blend_mode: 暗部混合模式。
+* shadow_strength: 暗部混合强度。
+* highlight_blend_mode: 亮部混合模式。
+* highlight_strength: 亮部混合强度。
+
+输出说明:
+* image: 输出图片。
+* displaced_material: 材质图片变形结果。
+
## 节点注解
1 image、mask和background_image(如果有输入)这三项必须是相同的尺寸。
diff --git a/image/distort_displace_example.jpg b/image/distort_displace_example.jpg
new file mode 100644
index 0000000..028b6fa
Binary files /dev/null and b/image/distort_displace_example.jpg differ
diff --git a/image/distort_displace_node.jpg b/image/distort_displace_node.jpg
new file mode 100644
index 0000000..ddc16d2
Binary files /dev/null and b/image/distort_displace_node.jpg differ
diff --git a/py/distort_displace.py b/py/distort_displace.py
new file mode 100644
index 0000000..02d6e77
--- /dev/null
+++ b/py/distort_displace.py
@@ -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",
+}
diff --git a/py/imagefunc.py b/py/imagefunc.py
index fd43aaf..8f40d79 100644
--- a/py/imagefunc.py
+++ b/py/imagefunc.py
@@ -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:
diff --git a/pyproject.toml b/pyproject.toml
index 2134adb..88e7ec0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"]
diff --git a/workflow/distort_displace_example.json b/workflow/distort_displace_example.json
new file mode 100644
index 0000000..2489a05
--- /dev/null
+++ b/workflow/distort_displace_example.json
@@ -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
+}
\ No newline at end of file
diff --git a/workflow/plaid.jpg b/workflow/plaid.jpg
new file mode 100644
index 0000000..6c0b241
Binary files /dev/null and b/workflow/plaid.jpg differ