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. +![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. + ### 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. +![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 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遮罩对不同分区处理,生成包括更加精细半透明部分的整体遮罩。 +![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遮罩融合边缘的模糊程度。 ### MaskByColor 根据颜色生成遮罩。 @@ -2261,6 +2275,27 @@ Film节点的升级版, 在之前基础上增加了fastgrain方法,生成噪 * grain_sat: 噪声的色彩饱和度。 +### DistortDisplace +为材质图片生成置换变形效果。 +![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: 材质图片变形结果。 + ## 节点注解 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