From 022aa1d73af19dba1e2fd8d9e6cdcdec83336f55 Mon Sep 17 00:00:00 2001 From: cavinHuang Date: Fri, 14 Jun 2024 19:23:49 +0800 Subject: [PATCH] feat: add docs --- docs/LayerFilter%3A LightLeak.md | 124 ++++++++ docs/LayerFilter%3A MotionBlur.md | 75 +++++ ...Ultra.md => LayerMask%3A BiRefNetUltra.md} | 0 docs/LayerMask%3A MaskBoxDetect.md | 145 +++++++++ docs/LayerMask%3A MaskByColor.md | 133 ++++++++ docs/LayerMask%3A MaskByDifferent.md | 120 +++++++ docs/LayerMask%3A MaskEdgeShrink.md | 121 +++++++ docs/LayerMask%3A MaskEdgeUltraDetail V2.md | 158 ++++++++++ docs/LayerMask%3A MaskEdgeUltraDetail.md | 140 +++++++++ docs/LayerMask%3A MaskGradient.md | 192 ++++++++++++ docs/LayerMask%3A MaskGrow.md | 89 ++++++ docs/LayerMask%3A MaskInvert.md | 65 ++++ docs/LayerMask%3A MaskMotionBlur.md | 89 ++++++ docs/LayerMask%3A MaskPreview.md | 49 +++ docs/LayerMask%3A MaskStroke.md | 100 ++++++ docs/LayerMask%3A MediapipeFacialSegment.md | 147 +++++++++ docs/LayerMask%3A PersonMaskUltra V2.md | 254 +++++++++++++++ docs/LayerMask%3A PersonMaskUltra.md | 217 +++++++++++++ docs/LayerMask%3A PixelSpread.md | 120 +++++++ docs/LayerMask%3A RemBgUltra.md | 98 ++++++ docs/LayerMask%3A RmBgUltra V2.md | 134 ++++++++ docs/LayerMask%3A SegformerB2ClothesUltra.md | 274 ++++++++++++++++ docs/LayerMask%3A SegmentAnythingUltra V2.md | 173 ++++++++++ docs/LayerMask%3A SegmentAnythingUltra.md | 146 +++++++++ docs/LayerStyle%3A InnerGlow V2.md | 199 ++++++++++++ docs/LayerStyle%3A InnerGlow.md | 196 ++++++++++++ docs/LayerStyle%3A InnerShadow V2.md | 192 ++++++++++++ docs/LayerStyle%3A InnerShadow.md | 199 ++++++++++++ docs/LayerStyle%3A OuterGlow V2.md | 178 +++++++++++ docs/LayerStyle%3A OuterGlow.md | 174 +++++++++++ docs/LayerUtility%3A CreateQRCode.md | 81 +++++ docs/LayerUtility%3A DecodeQRCode.md | 76 +++++ docs/LayerUtility%3A ImageChannelMerge.md | 115 +++++++ docs/LayerUtility%3A ImageChannelSplit.md | 95 ++++++ docs/LayerUtility%3A ImageCombineAlpha.md | 80 +++++ docs/LayerUtility%3A ImageHub.md | 271 ++++++++++++++++ docs/LayerUtility%3A ImageMaskScaleAs.md | 166 ++++++++++ docs/LayerUtility%3A ImageOpacity.md | 125 ++++++++ docs/LayerUtility%3A ImageRemoveAlpha.md | 97 ++++++ docs/LayerUtility%3A ImageRewardFilter.md | 108 +++++++ ...erUtility%3A ImageScaleByAspectRatio V2.md | 295 ++++++++++++++++++ ...LayerUtility%3A ImageScaleByAspectRatio.md | 267 ++++++++++++++++ docs/LayerUtility%3A ImageScaleRestore V2.md | 213 +++++++++++++ docs/LayerUtility%3A ImageScaleRestore.md | 222 +++++++++++++ docs/LayerUtility%3A ImageShift.md | 151 +++++++++ docs/LayerUtility%3A LaMa.md | 167 ++++++++++ docs/LayerUtility%3A LayerImageTransform.md | 162 ++++++++++ docs/LayerUtility%3A LayerMaskTransform.md | 148 +++++++++ docs/LayerUtility%3A LoadPSD.md | 152 +++++++++ docs/LayerUtility%3A PrintInfo.md | 73 +++++ docs/LayerUtility%3A PromptEmbellish.md | 137 ++++++++ docs/LayerUtility%3A PromptTagger.md | 122 ++++++++ docs/LayerUtility%3A PurgeVRAM.md | 75 +++++ docs/LayerUtility%3A QWenImage2Prompt.md | 61 ++++ docs/LayerUtility%3A SaveImagePlus.md | 251 +++++++++++++++ docs/LayerUtility: RestoreCropBox.md | 126 ++++++++ needAddFile.json | 110 +------ 57 files changed, 8138 insertions(+), 109 deletions(-) create mode 100644 docs/LayerFilter%3A LightLeak.md create mode 100644 docs/LayerFilter%3A MotionBlur.md rename docs/{LayerMask%3A%20BiRefNetUltra.md => LayerMask%3A BiRefNetUltra.md} (100%) create mode 100644 docs/LayerMask%3A MaskBoxDetect.md create mode 100644 docs/LayerMask%3A MaskByColor.md create mode 100644 docs/LayerMask%3A MaskByDifferent.md create mode 100644 docs/LayerMask%3A MaskEdgeShrink.md create mode 100644 docs/LayerMask%3A MaskEdgeUltraDetail V2.md create mode 100644 docs/LayerMask%3A MaskEdgeUltraDetail.md create mode 100644 docs/LayerMask%3A MaskGradient.md create mode 100644 docs/LayerMask%3A MaskGrow.md create mode 100644 docs/LayerMask%3A MaskInvert.md create mode 100644 docs/LayerMask%3A MaskMotionBlur.md create mode 100644 docs/LayerMask%3A MaskPreview.md create mode 100644 docs/LayerMask%3A MaskStroke.md create mode 100644 docs/LayerMask%3A MediapipeFacialSegment.md create mode 100644 docs/LayerMask%3A PersonMaskUltra V2.md create mode 100644 docs/LayerMask%3A PersonMaskUltra.md create mode 100644 docs/LayerMask%3A PixelSpread.md create mode 100644 docs/LayerMask%3A RemBgUltra.md create mode 100644 docs/LayerMask%3A RmBgUltra V2.md create mode 100644 docs/LayerMask%3A SegformerB2ClothesUltra.md create mode 100644 docs/LayerMask%3A SegmentAnythingUltra V2.md create mode 100644 docs/LayerMask%3A SegmentAnythingUltra.md create mode 100644 docs/LayerStyle%3A InnerGlow V2.md create mode 100644 docs/LayerStyle%3A InnerGlow.md create mode 100644 docs/LayerStyle%3A InnerShadow V2.md create mode 100644 docs/LayerStyle%3A InnerShadow.md create mode 100644 docs/LayerStyle%3A OuterGlow V2.md create mode 100644 docs/LayerStyle%3A OuterGlow.md create mode 100644 docs/LayerUtility%3A CreateQRCode.md create mode 100644 docs/LayerUtility%3A DecodeQRCode.md create mode 100644 docs/LayerUtility%3A ImageChannelMerge.md create mode 100644 docs/LayerUtility%3A ImageChannelSplit.md create mode 100644 docs/LayerUtility%3A ImageCombineAlpha.md create mode 100644 docs/LayerUtility%3A ImageHub.md create mode 100644 docs/LayerUtility%3A ImageMaskScaleAs.md create mode 100644 docs/LayerUtility%3A ImageOpacity.md create mode 100644 docs/LayerUtility%3A ImageRemoveAlpha.md create mode 100644 docs/LayerUtility%3A ImageRewardFilter.md create mode 100644 docs/LayerUtility%3A ImageScaleByAspectRatio V2.md create mode 100644 docs/LayerUtility%3A ImageScaleByAspectRatio.md create mode 100644 docs/LayerUtility%3A ImageScaleRestore V2.md create mode 100644 docs/LayerUtility%3A ImageScaleRestore.md create mode 100644 docs/LayerUtility%3A ImageShift.md create mode 100644 docs/LayerUtility%3A LaMa.md create mode 100644 docs/LayerUtility%3A LayerImageTransform.md create mode 100644 docs/LayerUtility%3A LayerMaskTransform.md create mode 100644 docs/LayerUtility%3A LoadPSD.md create mode 100644 docs/LayerUtility%3A PrintInfo.md create mode 100644 docs/LayerUtility%3A PromptEmbellish.md create mode 100644 docs/LayerUtility%3A PromptTagger.md create mode 100644 docs/LayerUtility%3A PurgeVRAM.md create mode 100644 docs/LayerUtility%3A QWenImage2Prompt.md create mode 100644 docs/LayerUtility%3A SaveImagePlus.md create mode 100644 docs/LayerUtility: RestoreCropBox.md diff --git a/docs/LayerFilter%3A LightLeak.md b/docs/LayerFilter%3A LightLeak.md new file mode 100644 index 0000000..d8cc3ca --- /dev/null +++ b/docs/LayerFilter%3A LightLeak.md @@ -0,0 +1,124 @@ +# Documentation +- Class name: LightLeak +- Category: 😺dzNodes/LayerFilter +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +模拟胶片漏光效果。请下载light_leak.pkl(百度网盘)或[light_leak.pkl(Google Drive)(https://drive.google.com/file/d/1DcH2Zkyj7W3OiAeeGpJk1eaZpdJwdCL-/view?usp=sharing)将文件复制到ComfyUI/models/layerstyle 文件夹。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- light + - 提供了32种光斑。random为随机选择。 + - 可选值: ['random', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', + '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32'] + - Comfy dtype: STRING + - Python dtype: str + +- corner + - 光斑出现的角落,有左上、右上、左下和右下4个选项。 + - 可选值: ['left_top', 'right_top', 'left_bottom', 'right_bottom'] + - Comfy dtype: STRING + - Python dtype: str + +- hue + - 光斑的色相。 + - Comfy dtype: INT + - Python dtype: int + +- saturation + - 光斑的色彩饱和度。 + - Comfy dtype: INT + - Python dtype: int + +- opacity + - 光斑的不透明度。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class LightLeak: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + light_list = ['random', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', + '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', + '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', + '31', '32'] + corner_list = ['left_top', 'right_top', 'left_bottom', 'right_bottom'] + return { + "required": { + "image": ("IMAGE", ), + "light": (light_list,), + "corner": (corner_list,), + "hue": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "saturation": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}) + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'light_leak' + CATEGORY = '😺dzNodes/LayerFilter' + + def light_leak(self, image, light, corner, hue, saturation, opacity): + + ret_images = [] + light_leak_images = load_light_leak_images() + if light == 'random': + random.seed(time.time()) + light_index = random.randint(0,31) + else: + light_index = int(light) - 1 + + for i in image: + i = torch.unsqueeze(i, 0) + _canvas = tensor2pil(i).convert('RGB') + _light = light_leak_images[light_index] + if _canvas.width < _canvas.height: + _light = _light.transpose(Image.ROTATE_90).transpose(Image.FLIP_TOP_BOTTOM) + if corner == 'right_top': + _light = _light.transpose(Image.FLIP_LEFT_RIGHT) + elif corner == 'left_bottom': + _light = _light.transpose(Image.FLIP_TOP_BOTTOM) + elif corner == 'right_bottom': + _light = _light.transpose(Image.ROTATE_180) + if hue != 0 or saturation != 0: + _h, _s, _v = _light.convert('HSV').split() + if hue != 0: + _h = image_hue_offset(_h, hue) + if saturation != 0: + _s = image_gray_offset(_s, saturation) + _light = image_channel_merge((_h, _s, _v), 'HSV') + resize_sampler = Image.BILINEAR + _light = fit_resize_image(_light, _canvas.width, _canvas.height, fit='crop', resize_sampler=resize_sampler) + ret_image = chop_image(_canvas, _light, blend_mode=blend_mode, opacity=opacity) + ret_images.append(pil2tensor(ret_image)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerFilter%3A MotionBlur.md b/docs/LayerFilter%3A MotionBlur.md new file mode 100644 index 0000000..9008c84 --- /dev/null +++ b/docs/LayerFilter%3A MotionBlur.md @@ -0,0 +1,75 @@ +# Documentation +- Class name: MotionBlur +- Category: 😺dzNodes/LayerFilter +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +用于在图像上应用运动模糊效果的节点。 + +# Input types + +## Required + +- image + - 输入的图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- angle + - 模糊的角度。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 模糊量。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- image + - 输出的图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MotionBlur: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "angle": ("INT", {"default": 0, "min": -90, "max": 90, "step": 1}), # 角度 + "blur": ("INT", {"default": 20, "min": 1, "max": 999, "step": 1}), # 模糊 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'motion_blur' + CATEGORY = '😺dzNodes/LayerFilter' + + def motion_blur(self, image, angle, blur): + + ret_images = [] + + for i in image: + + _canvas = tensor2pil(torch.unsqueeze(i, 0)).convert('RGB') + + ret_images.append(pil2tensor(motion_blur(_canvas, angle, blur))) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A%20BiRefNetUltra.md b/docs/LayerMask%3A BiRefNetUltra.md similarity index 100% rename from docs/LayerMask%3A%20BiRefNetUltra.md rename to docs/LayerMask%3A BiRefNetUltra.md diff --git a/docs/LayerMask%3A MaskBoxDetect.md b/docs/LayerMask%3A MaskBoxDetect.md new file mode 100644 index 0000000..a294bab --- /dev/null +++ b/docs/LayerMask%3A MaskBoxDetect.md @@ -0,0 +1,145 @@ +# Documentation +- Class name: MaskBoxDetect +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +探测mask所在区域,并输出位置和大小。 + +# Input types + +## Required + +- mask + - 遮罩图像。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- detect + - 探测方法,min_bounding_rect是大块形状最小外接矩形, max_inscribed_rect是大块形状最大内接矩形, mask_area是遮罩像素有效区域。 + - 可选值: ['min_bounding_rect', 'max_inscribed_rect', 'mask_area'] + - Comfy dtype: STRING + - Python dtype: str + +- x_adjust + - 修正探测之后的水平偏移。 + - Comfy dtype: INT + - Python dtype: int + +- y_adjust + - 修正探测之后的垂直偏移。 + - Comfy dtype: INT + - Python dtype: int + +- scale_adjust + - 修正探测之后的缩放偏移。 + - Comfy dtype: FLOAT + - Python dtype: float + +# Output types + +- box_preview + - 探测结果预览图。红色表示探测到的结果,绿色表示加上修正后的输出结果。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- x_percent + - x轴百分比位置。 + - Comfy dtype: FLOAT + - Python dtype: float + +- y_percent + - y轴百分比位置。 + - Comfy dtype: FLOAT + - Python dtype: float + +- width + - 盒子宽度。 + - Comfy dtype: INT + - Python dtype: int + +- height + - 盒子高度。 + - Comfy dtype: INT + - Python dtype: int + +- x + - 左上角位置x坐标输出。 + - Comfy dtype: INT + - Python dtype: int + +- y + - 左上角位置y坐标输出。 + - Comfy dtype: INT + - Python dtype: int + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskBoxDetect: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + detect_mode = ['min_bounding_rect', 'max_inscribed_rect', 'mask_area'] + return { + "required": { + "mask": ("MASK", ), + "detect": (detect_mode,), # 探测类型:最小外接矩形/最大内接矩形/掩码区域 + "x_adjust": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), # x轴修正 + "y_adjust": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), # y轴修正 + "scale_adjust": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100, "step": 0.01}), # 比例修正 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "FLOAT", "FLOAT", "INT", "INT", "INT", "INT",) + RETURN_NAMES = ("box_preview", "x_percent", "y_percent", "width", "height", "x", "y",) + FUNCTION = 'mask_box_detect' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_box_detect(self, mask, detect, x_adjust, y_adjust, scale_adjust): + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + if mask.shape[0] > 0: + mask = torch.unsqueeze(mask[0], 0) + + _mask = mask2image(mask).convert('RGB') + + _mask = gaussian_blur(_mask, 20).convert('L') + x = 0 + y = 0 + width = 0 + height = 0 + + if detect == "min_bounding_rect": + (x, y, width, height) = min_bounding_rect(_mask) + elif detect == "max_inscribed_rect": + (x, y, width, height) = max_inscribed_rect(_mask) + else: + (x, y, width, height) = mask_area(_mask) + log(f"{NODE_NAME}: Box detected. x={x},y={y},width={width},height={height}") + _width = width + _height = height + if scale_adjust != 1.0: + _width = int(width * scale_adjust) + _height = int(height * scale_adjust) + x = x - int((_width - width) / 2) + y = y - int((_height - height) / 2) + x += x_adjust + y += y_adjust + x_percent = (x + _width / 2) / _mask.width * 100 + y_percent = (y + _height / 2) / _mask.height * 100 + preview_image = tensor2pil(mask).convert('RGB') + preview_image = draw_rect(preview_image, x - x_adjust, y - y_adjust, width, height, line_color="#F00000", line_width=int(preview_image.height / 60)) + preview_image = draw_rect(preview_image, x, y, width, height, line_color="#00F000", line_width=int(preview_image.height / 40)) + log(f"{NODE_NAME} Processed.", message_type='finish') + return ( pil2tensor(preview_image), round(x_percent, 2), round(y_percent, 2), _width, _height, x, y,) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskByColor.md b/docs/LayerMask%3A MaskByColor.md new file mode 100644 index 0000000..40eaa16 --- /dev/null +++ b/docs/LayerMask%3A MaskByColor.md @@ -0,0 +1,133 @@ +# Documentation +- Class name: MaskByColor +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +根据颜色创建Mask的节点。 + +# Input types + +## Required + +- image + - 输入图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- color + - 颜色选择器。点击色块选择颜色,可以使用选色器面板上的吸管拾取屏幕颜色。注意:使用吸管时,需将浏览器窗口最大化。 + - Comfy dtype: COLOR + - Python dtype: str + - 默认值: #FFFFFF + +- color_in_HEX + - 输入色值。如果此项有输入,则优先使用,忽略color选取的颜色。 + - Comfy dtype: STRING + - Python dtype: str + - 默认值: "" + +- threshold + - 遮罩范围阈值,数值越大,遮罩范围越大。 + - Comfy dtype: INT + - Python dtype: int + +- fix_gap + - 修补遮罩中的空隙。如果遮罩中有比较明显的空隙,适当调高此数值。 + - Comfy dtype: INT + - Python dtype: int + +- fix_threshold + - 修补遮罩的阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- invert_mask + - 是否反转遮罩。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +## Optional + +- mask + - 遮罩输入。此输入是可选项,如果有遮罩则仅遮罩内的颜色被纳入范围。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- mask + - 输出Mask。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskByColor: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE", ), + "color": ("COLOR", {"default": "#FFFFFF"},), + "color_in_HEX": ("STRING", {"default": ""}), + "threshold": ("INT", { "default": 0, "min": 0, "max": 100, "step": 1, }), + "fix_gap": ("INT", {"default": 2, "min": 0, "max": 32, "step": 1}), + "fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}), + "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask + }, + "optional": { + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = "mask_by_color" + CATEGORY = '😺dzNodes/LayerMask' + + def mask_by_color(self, image, color, color_in_HEX, threshold, + fix_gap, fix_threshold, invert_mask, mask=None): + + if color_in_HEX != "" and color_in_HEX.startswith('#') and len(color_in_HEX) == 7: + color = color_in_HEX + + ret_masks = [] + l_images = [] + l_masks = [] + + for l in 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 mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + for i in range(len(l_images)): + img = l_images[i] if i < len(l_images) else l_images[-1] + img = tensor2pil(img) + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + + mask = Image.new('L', _mask.size, 'black') + mask.paste(create_mask_from_color_tensor(img, color, threshold), mask=_mask) + mask = image2mask(mask) + if invert_mask: + mask = 1 - mask + if fix_gap: + mask = mask_fix(mask, 1, fix_gap, fix_threshold, fix_threshold) + ret_masks.append(mask) + + return (torch.cat(ret_masks, dim=0), ) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskByDifferent.md b/docs/LayerMask%3A MaskByDifferent.md new file mode 100644 index 0000000..55a4873 --- /dev/null +++ b/docs/LayerMask%3A MaskByDifferent.md @@ -0,0 +1,120 @@ +# Documentation +- Class name: MaskByDifferent +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +计算两张图像不同之处,并输出为遮罩。 + +# Input types + +## Required + +- image_1 + - 第一张输入图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- image_2 + - 第二张输入图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- gain + - 差异增益。调高此值,微弱的差异将更显著的呈现。 + - Comfy dtype: FLOAT + - Python dtype: float + +- fix_gap + - 修正间隙。更高的值将修补更大的缝隙。 + - Comfy dtype: INT + - Python dtype: int + +- fix_threshold + - 修正阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- main_subject_detect + - 此项设为True将开启主体侦测,忽略主体之外的差异。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +# Output types + +- mask + - 输出mask。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskByDifferent: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image_1": ("IMAGE", ), # + "image_2": ("IMAGE",), # + "gain": ("FLOAT", {"default": 1.5, "min": 0.1, "max": 100, "step": 0.1}), + "fix_gap": ("INT", {"default": 4, "min": 0, "max": 32, "step": 1}), + "fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}), + "main_subject_detect": ("BOOLEAN", {"default": False}), + }, + "optional": { + } + } + + RETURN_TYPES = ( "MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_by_different' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_by_different(self, image_1, image_2, gain, fix_gap, fix_threshold, main_subject_detect): + + image1s = [] + image2s = [] + ret_masks = [] + for i in image_1: + image1s.append(torch.unsqueeze(i, 0)) + for i in image_2: + image2s.append(torch.unsqueeze(i, 0)) + max_batch = max(len(image1s), len(image2s)) + blank_mask = image2mask(Image.new('L', size=tensor2pil(image1s[0]).size, color='black')) + if tensor2pil(image1s[0]).size != tensor2pil(image2s[0]).size: + log(f"Error: {NODE_NAME} skipped, because the image size is not match.", message_type='error') + return (torch.cat([blank_mask], dim=0)) + for i in range(max_batch): + t1 = image1s[i] if i < len(image1s) else image1s[-1] + t2 = image2s[i] if i < len(image2s) else image2s[-1] + t1 = pil2tensor(tensor2pil(t1).convert('RGB')) + t2 = pil2tensor(tensor2pil(t2).convert('RGB')) + t = torch.abs(t1 - t2) * gain + _mask = mask_fix(t, 1, fix_gap, fix_threshold, fix_threshold) + _mask = tensor2pil(_mask) + if main_subject_detect: + subject_mask1 = RMBG(tensor2pil(t1)) + subject_mask2 = RMBG(tensor2pil(t2)) + subject_mask = chop_image(subject_mask1, subject_mask2, blend_mode='add', opacity=100) + grow = (subject_mask.width + subject_mask.height) // 100 + subject_mask = mask2image(expand_mask(image2mask(subject_mask), grow * 2, grow)) + black = Image.new('L', size=_mask.size, color='black') + white = Image.new('L', size=_mask.size, color='white') + black.paste(_mask, mask=subject_mask.convert('L')) + black.paste(white, mask=subject_mask1.convert('L')) + black.paste(white, mask=subject_mask2.convert('L')) + _mask = black + + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskEdgeShrink.md b/docs/LayerMask%3A MaskEdgeShrink.md new file mode 100644 index 0000000..0b5bc6e --- /dev/null +++ b/docs/LayerMask%3A MaskEdgeShrink.md @@ -0,0 +1,121 @@ +# Documentation +- Class name: MaskEdgeShrink +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +使mask边缘平滑地过渡收缩,并保留边缘细节。 + +# Input types + +## Required + +- mask + - 输入的遮罩 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- invert_mask + - 反转遮罩 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- shrink_level + - 收缩级别。 + - Comfy dtype: INT + - Python dtype: int + +- soft + - 软化级别。 + - Comfy dtype: INT + - Python dtype: int + +- edge_shrink + - 边缘收缩。 + - Comfy dtype: INT + - Python dtype: int + +- edge_reserve + - 透明度保留。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- mask + - 输出的遮罩 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskEdgeShrink: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "mask": ("MASK", ), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "shrink_level": ("INT", {"default": 4, "min": 0, "max": 16, "step": 1}), + "soft": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}), + "edge_shrink": ("INT", {"default": 1, "min": 0, "max": 999, "step": 1}), + "edge_reserve": ("INT", {"default": 25, "min": 0, "max": 100, "step": 1}), # 透明度 + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_edge_shrink' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_edge_shrink(self, mask, invert_mask, shrink_level, soft, edge_shrink, edge_reserve): + + l_masks = [] + ret_masks = [] + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + glow_range = shrink_level * soft + blur = 12 + + for i in range(len(l_masks)): + _mask = l_masks[i] + _canvas = Image.new('RGB', size=_mask.size, color='black') + _layer = Image.new('RGB', size=_mask.size, color='white') + loop_grow = glow_range + inner_mask = _mask + for x in range(shrink_level): + _color = step_color('#FFFFFF', '#000000', shrink_level, x) + glow_mask = expand_mask(image2mask(inner_mask), -loop_grow, blur / (x+0.1)) #扩张,模糊 + # 合成 + color_image = Image.new("RGB", _layer.size, color=_color) + alpha = tensor2pil(mask_invert(glow_mask)).convert('L') + _glow = chop_image(_layer, color_image, 'subtract', int(step_value(1, 100, shrink_level, x))) + _layer.paste(_glow, mask=alpha) + loop_grow = loop_grow - int(glow_range / shrink_level) + # 合成layer + _edge = tensor2pil(expand_mask(image2mask(_mask), -edge_shrink, 0)).convert('RGB') + _layer = chop_image(_layer, _edge, 'normal', edge_reserve) + _layer.paste(_canvas, mask=ImageChops.invert(_mask)) + + ret_masks.append(image2mask(_layer)) + + log(f"{NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskEdgeUltraDetail V2.md b/docs/LayerMask%3A MaskEdgeUltraDetail V2.md new file mode 100644 index 0000000..00544cd --- /dev/null +++ b/docs/LayerMask%3A MaskEdgeUltraDetail V2.md @@ -0,0 +1,158 @@ +# Documentation +- Class name: MaskEdgeUltraDetailV2 +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +处理较粗糙的遮罩使其获得超精细边缘。 本节点结合了spacepxl的[ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters)的Alpha Matte和Guided Filter Alpha两者功能,感谢原作者。 + +MaskEdgeUltraDetail的V2升级版,增加了VITMatte边缘处理方法,此方法适合处理半透明区域。(注意:超过2K尺寸的图片使用此方法将占用大量内存) + +# Input types + +## Required + +- image + - 输入的图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 输入的遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- method + - 提供PyMatting和OpenCV-GuidedFilter两种方法处理边缘。PyMatting处理速度较慢,但是对于视频,建议使用这种方法获得更平滑的遮罩序列。 + - Comfy dtype: LIST + - Python dtype: str + +- mask_grow + - 遮罩扩张幅度。正值是向外扩张,负值是向内收缩。对于较粗糙的遮罩,通常使用负值使其边缘收缩以获得更好的效果。 + - Comfy dtype: INT + - Python dtype: int + +- fix_gap + - 修补遮罩中的空隙。如果遮罩中有比较明显的空隙,适当调高此数值。 + - Comfy dtype: INT + - Python dtype: int + +- fix_threshold + - 修补遮罩的阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- detail_range + - 边缘细节范围。 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + + + +# Output types + +- image + - 输出的图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 输出的遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskEdgeUltraDetailV2: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + + return { + "required": { + "image": ("IMAGE",), + "mask": ("MASK",), + "method": (method_list,), + "mask_grow": ("INT", {"default": 0, "min": 0, "max": 256, "step": 1}), + "fix_gap": ("INT", {"default": 0, "min": 0, "max": 32, "step": 1}), + "fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}), + "edge_erode": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "edte_dilate": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01, "display": "slider"}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01, "display": "slider"}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = "mask_edge_ultra_detail_v2" + CATEGORY = '😺dzNodes/LayerMask' + + def mask_edge_ultra_detail_v2(self, image, mask, method, mask_grow, fix_gap, fix_threshold, + edge_erode, edte_dilate, black_point, white_point,): + ret_images = [] + ret_masks = [] + l_images = [] + l_masks = [] + + if method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + for m in mask: + l_masks.append(torch.unsqueeze(m, 0)) + if len(l_images) != len(l_masks) or tensor2pil(l_images[0]).size != tensor2pil(l_masks[0]).size: + log(f"Error: {NODE_NAME} skipped, because mask does'nt match image.", message_type='error') + return (image, mask,) + detail_range = edge_erode + edte_dilate + for i in range(len(l_images)): + _image = l_images[i] + orig_image = tensor2pil(_image).convert('RGB') + _image = pil2tensor(orig_image) + _mask = l_masks[i] + if mask_grow != 0: + _mask = expand_mask(_mask, mask_grow, mask_grow//2) + if fix_gap: + _mask = mask_fix(_mask, 1, fix_gap, fix_threshold, fix_threshold) + log(f"{NODE_NAME} Processing...") + if method == 'GuidedFilter': + _mask = guided_filter_alpha(_image, _mask, detail_range//6) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(_image, _mask, detail_range//8, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, edge_erode, edte_dilate) + _mask = generate_VITMatte(orig_image, _trimap, local_files_only=local_files_only) + _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point)) + + ret_image = RGB2RGBA(orig_image, _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskEdgeUltraDetail.md b/docs/LayerMask%3A MaskEdgeUltraDetail.md new file mode 100644 index 0000000..13ff277 --- /dev/null +++ b/docs/LayerMask%3A MaskEdgeUltraDetail.md @@ -0,0 +1,140 @@ +# Documentation +- Class name: MaskEdgeUltraDetail +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +处理较粗糙的遮罩使其获得超精细边缘。 本节点结合了spacepxl的[ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters)的Alpha Matte和Guided Filter Alpha两者功能,感谢原作者。 + +# Input types + +## Required + +- image + - 输入的图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 输入的遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- method + - 提供PyMatting和OpenCV-GuidedFilter两种方法处理边缘。PyMatting处理速度较慢,但是对于视频,建议使用这种方法获得更平滑的遮罩序列。 + - Comfy dtype: LIST + - Python dtype: str + +- mask_grow + - 遮罩扩张幅度。正值是向外扩张,负值是向内收缩。对于较粗糙的遮罩,通常使用负值使其边缘收缩以获得更好的效果。 + - Comfy dtype: INT + - Python dtype: int + +- fix_gap + - 修补遮罩中的空隙。如果遮罩中有比较明显的空隙,适当调高此数值。 + - Comfy dtype: INT + - Python dtype: int + +- fix_threshold + - 修补遮罩的阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- detail_range + - 边缘细节范围。 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +# Output types + +- image + - 输出的图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 输出的遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskEdgeUltraDetail: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + method_list = ['PyMatting', 'OpenCV-GuidedFilter'] + return { + "required": { + "image": ("IMAGE",), + "mask": ("MASK",), + "method": (method_list,), + "mask_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), + "fix_gap": ("INT", {"default": 0, "min": 0, "max": 32, "step": 1}), + "fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}), + "detail_range": ("INT", {"default": 12, "min": 1, "max": 256, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = "mask_edge_ultra_detail" + CATEGORY = '😺dzNodes/LayerMask' + + def mask_edge_ultra_detail(self, image, mask, method, mask_grow, fix_gap, fix_threshold, + detail_range, black_point, white_point,): + ret_images = [] + ret_masks = [] + l_images = [] + l_masks = [] + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + for m in mask: + l_masks.append(torch.unsqueeze(m, 0)) + if len(l_images) != len(l_masks) or tensor2pil(l_images[0]).size != tensor2pil(l_masks[0]).size: + log(f"Error: {NODE_NAME} skipped, because mask does'nt match image.", message_type='error') + return (image, mask,) + + for i in range(len(l_images)): + _image = l_images[i] + orig_image = tensor2pil(_image).convert('RGB') + _image = pil2tensor(orig_image) + _mask = l_masks[i] + if mask_grow != 0: + _mask = expand_mask(_mask, mask_grow, mask_grow//2) + if fix_gap: + _mask = mask_fix(_mask, 1, fix_gap, fix_threshold, fix_threshold) + if method == 'OpenCV-GuidedFilter': + _mask = guided_filter_alpha(_image, _mask, detail_range) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + else: + _mask = tensor2pil(mask_edge_detail(_image, _mask, detail_range, black_point, white_point)) + + ret_image = RGB2RGBA(orig_image, _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskGradient.md b/docs/LayerMask%3A MaskGradient.md new file mode 100644 index 0000000..f308a7b --- /dev/null +++ b/docs/LayerMask%3A MaskGradient.md @@ -0,0 +1,192 @@ +# Documentation +- Class name: MaskGradient +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +使mask从一侧产生渐变。请注意此节点与CreateGradientMask的区别。 + +# Input types + +## Required + +- mask + - 输入的掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- invert_mask + - 反转遮罩。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- gradient_side + - 从哪个边产生渐变。有四个方向:顶侧top、底侧bottom、左侧left、右侧right。 + - Comfy dtype: LIST + - Python dtype: str + +- gradient_scale + - 渐变距离。默认值100表示渐变产生一侧完全透明,另一侧完全不透明。数值越小,从透明到不透明的距离越短。 + - Comfy dtype: INT + - Python dtype: int + +- gradient_offset + - 渐变偏移。 + - Comfy dtype: INT + - Python dtype: int + +- opacity + - 透明度。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- mask + - 输出的掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskGradient: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + side = ['top', 'bottom', 'left', 'right'] + return { + "required": { + "mask": ("MASK",), + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "gradient_side": (side,), + "gradient_scale": ("INT", {"default": 100, "min": 1, "max": 9999, "step": 1}), + "gradient_offset": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_gradient' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_gradient(self, mask, invert_mask, gradient_side, gradient_scale, gradient_offset, opacity, ): + + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + l_masks = [] + ret_masks = [] + + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + for i in range(len(l_masks)): + _mask = l_masks[i] + _canvas = copy.copy(_mask) + width = _mask.width + height = _mask.height + _gradient = gradient('#000000', '#FFFFFF', + 1024, 1024, 0) + # (box_x, box_y, box_width, box_height) = min_bounding_rect(_mask) + (box_x, box_y, box_width, box_height) = mask_area(_mask) + log(f"{NODE_NAME}: Box detected. x={box_x},y={box_y},width={box_width},height={box_height}") + if box_width < 1 or box_height < 1: + log(f"Error: {NODE_NAME} skipped, because the mask is does'nt have valid area", message_type='error') + return (mask,) + + if gradient_side == 'top': + boxsize = (width, box_height) + _gradient = _gradient.transpose(Image.FLIP_TOP_BOTTOM) + _gradient = _gradient.resize(boxsize) + _black = Image.new('RGB', size = boxsize, color = 'black') + if gradient_scale != 100: + _box = Image.new('RGB', size = boxsize, color = 'black') + _gradient = _gradient.resize((width, int(box_height * gradient_scale / 100))) + _box.paste(_gradient, box = (0, 0)) + _gradient = _box + if gradient_offset != 0: + _box = Image.new('RGB', size = boxsize, color = 'black') + _boxwhite = Image.new('RGB', size = boxsize, color = 'white') + _box.paste(_gradient, box=(0, gradient_offset)) + _box.paste(_boxwhite, box = (0, gradient_offset - _box.height)) + _gradient = _box + if gradient_offset > box_height: + _gradient = Image.new('RGB', size = boxsize, color = 'white') + _canvas.paste(_black, box = (0, box_y), mask = _gradient.convert('L')) + elif gradient_side == 'bottom': + boxsize = (width, box_height) + _gradient = _gradient.resize((width, box_height)) + _black = Image.new('RGB', size = boxsize, color = 'black') + if gradient_scale != 100: + _box = Image.new('RGB', size = boxsize, color = 'black') + _gradient = _gradient.resize((width, int(box_height * gradient_scale / 100))) + _box.paste(_gradient, box = (0, box_height - _gradient.height)) + _gradient = _box + if gradient_offset != 0: + _box = Image.new('RGB', size = boxsize, color = 'black') + _boxwhite = Image.new('RGB', size = boxsize, color = 'white') + _box.paste(_gradient, box=(0, gradient_offset)) + _box.paste(_boxwhite, box = (0, gradient_offset + _box.height)) + _gradient = _box + if gradient_offset < -box_height: + _gradient = Image.new('RGB', size=boxsize, color='white') + _canvas.paste(_black, box = (0, box_y + 1), mask = _gradient.convert('L')) + elif gradient_side == 'left': + boxsize = (box_width, height) + _gradient = _gradient.transpose(Image.ROTATE_270) + _gradient = _gradient.resize(boxsize) + _black = Image.new('RGB', size = boxsize, color = 'black') + if gradient_scale != 100: + _box = Image.new('RGB', size = boxsize, color = 'black') + _gradient = _gradient.resize((int(box_width * gradient_scale / 100), height)) + _box.paste(_gradient, box = (0, 0)) + _gradient = _box + if gradient_offset != 0: + _box = Image.new('RGB', size = boxsize, color = 'black') + _boxwhite = Image.new('RGB', size = boxsize, color = 'white') + _box.paste(_gradient, box=(gradient_offset, 0)) + _box.paste(_boxwhite, box = (gradient_offset - _box.width, 0)) + _gradient = _box + if gradient_offset > box_width: + _gradient = Image.new('RGB', size=boxsize, color='white') + _canvas.paste(_black, box = (box_x, 0), mask = _gradient.convert('L')) + elif gradient_side == 'right': + boxsize = (box_width, height) + _gradient = _gradient.transpose(Image.ROTATE_90) + _gradient = _gradient.resize(boxsize) + _black = Image.new('RGB', size = boxsize, color = 'black') + if gradient_scale != 100: + _box = Image.new('RGB', size = boxsize, color = 'black') + _gradient = _gradient.resize((int(box_width * gradient_scale / 100), height)) + _box.paste(_gradient, box = (box_width - _gradient.width, 0)) + _gradient = _box + if gradient_offset != 0: + _box = Image.new('RGB', size = boxsize, color = 'black') + _boxwhite = Image.new('RGB', size = boxsize, color = 'white') + _box.paste(_gradient, box=(gradient_offset, 0)) + _box.paste(_boxwhite, box = (gradient_offset + _box.width, 0)) + _gradient = _box + if gradient_offset < -box_width: + _gradient = Image.new('RGB', size=boxsize, color='white') + + _canvas.paste(_black, box = (box_x + 1, 0), mask = _gradient.convert('L')) + # opacity + if opacity < 100: + _canvas = chop_image(_mask, _canvas, 'normal', opacity) + ret_masks.append(image2mask(_canvas)) + + log(f"{NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskGrow.md b/docs/LayerMask%3A MaskGrow.md new file mode 100644 index 0000000..6fa3b72 --- /dev/null +++ b/docs/LayerMask%3A MaskGrow.md @@ -0,0 +1,89 @@ +# Documentation +- Class name: MaskGrow +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +对mask进行扩张收缩边缘和模糊处理 + +# Input types + +## Required + +- mask + - 输入的掩遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- invert_mask + - 反转遮罩。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- grow + - 扩展量。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 模糊量。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- mask + - 输出的掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskGrow: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "mask": ("MASK", ), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "grow": ("INT", {"default": 4, "min": -999, "max": 999, "step": 1}), + "blur": ("INT", {"default": 4, "min": 0, "max": 999, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_grow' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_grow(self, mask, invert_mask, grow, blur,): + + l_masks = [] + ret_masks = [] + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + for i in range(len(l_masks)): + + _mask = l_masks[i] + ret_masks.append(expand_mask(image2mask(_mask), grow, blur) ) + + log(f"{NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskInvert.md b/docs/LayerMask%3A MaskInvert.md new file mode 100644 index 0000000..a7ac864 --- /dev/null +++ b/docs/LayerMask%3A MaskInvert.md @@ -0,0 +1,65 @@ +# Documentation +- Class name: MaskInvert +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +对Mask进行反转处理的节点。 + +# Input types + +## Required + +- mask + - 输入的遮罩 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- mask + - 输出的遮罩 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskInvert: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "mask": ("MASK", ), # + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_invert' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_invert(self,mask): + l_masks = [] + ret_masks = [] + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + for m in mask: + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + for i in range(len(l_masks)): + _mask = l_masks[i] + ret_masks.append(mask_invert(image2mask(_mask))) + + return (torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskMotionBlur.md b/docs/LayerMask%3A MaskMotionBlur.md new file mode 100644 index 0000000..29d7032 --- /dev/null +++ b/docs/LayerMask%3A MaskMotionBlur.md @@ -0,0 +1,89 @@ +# Documentation +- Class name: MaskMotionBlur +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +对Mask应用运动模糊效果的节点。 + +# Input types + +## Required + +- mask + - 输入的遮罩 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- invert_mask + - 反转mask。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- blur + - 模糊量。 + - Comfy dtype: INT + - Python dtype: int + +- angle + - 模糊角度。 + - Comfy dtype: FLOAT + - Python dtype: float + +# Output types + +- mask + - 输出的掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskMotionBlur: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "mask": ("MASK",), + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blur": ("INT", {"default": 20, "min": 1, "max": 9999, "step": 1}), + "angle": ("FLOAT", {"default": 0, "min": -360, "max": 360, "step": 0.1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_motion_blur' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_motion_blur(self, mask, invert_mask, blur, angle,): + + l_masks = [] + ret_masks = [] + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + for i in range(len(l_masks)): + _mask = l_masks[i] + _blurimage = motion_blur(_mask, angle, blur) + ret_masks.append(image2mask(_blurimage)) + + log(f"{NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskPreview.md b/docs/LayerMask%3A MaskPreview.md new file mode 100644 index 0000000..5f0c7bc --- /dev/null +++ b/docs/LayerMask%3A MaskPreview.md @@ -0,0 +1,49 @@ +# Documentation +- Class name: MaskPreview +- Category: 😺dzNodes/LayerMask +- Output node: True +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +用于生成Mask预览图像的节点。 + +# Input types + +## Required + +- mask + - 输入的掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- 无直接输出,通过保存预览图像来间接输出。 + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskPreview(SaveImage): + def __init__(self): + self.output_dir = folder_paths.get_temp_directory() + self.type = "temp" + self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz1234567890") for x in range(5)) + self.compress_level = 4 + + @classmethod + def INPUT_TYPES(self): + return { + "required": {"mask": ("MASK",), }, + } + + FUNCTION = "mask_preview" + CATEGORY = '😺dzNodes/LayerMask' + OUTPUT_NODE = True + + def mask_preview(self, mask): + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3) + return self.save_images(preview, "MaskPreview") +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MaskStroke.md b/docs/LayerMask%3A MaskStroke.md new file mode 100644 index 0000000..68ab8fb --- /dev/null +++ b/docs/LayerMask%3A MaskStroke.md @@ -0,0 +1,100 @@ +# Documentation +- Class name: MaskStroke +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +对Mask应用描边效果的节点。 + +# Input types + +## Required + +- mask + - 输入的遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- invert_mask + - 反转mask。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- stroke_grow + - 收缩值。 + - Comfy dtype: INT + - Python dtype: int + +- stroke_width + - 扩张值。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 模糊量。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- mask + - 输出的掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class MaskStroke: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "mask": ("MASK", ), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "stroke_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), # 收缩值 + "stroke_width": ("INT", {"default": 20, "min": 0, "max": 999, "step": 1}), # 扩张值 + "blur": ("INT", {"default": 6, "min": 0, "max": 100, "step": 1}), # 模糊 + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_stroke' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_stroke(self, mask, invert_mask, stroke_grow, stroke_width, blur,): + + l_masks = [] + ret_masks = [] + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + for i in range(len(l_masks)): + _mask = l_masks[i] + grow_offset = int(stroke_width / 2) + inner_stroke = stroke_grow - grow_offset + outer_stroke = inner_stroke + stroke_width + inner_mask = expand_mask(image2mask(_mask), inner_stroke, blur) + outer_mask = expand_mask(image2mask(_mask), outer_stroke, blur) + stroke_mask = subtract_mask(outer_mask, inner_mask) + ret_masks.append(stroke_mask) + + log(f"{NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A MediapipeFacialSegment.md b/docs/LayerMask%3A MediapipeFacialSegment.md new file mode 100644 index 0000000..bcb8837 --- /dev/null +++ b/docs/LayerMask%3A MediapipeFacialSegment.md @@ -0,0 +1,147 @@ +# Documentation +- Class name: FacialFeatureSegment +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +使用Mediapipe模型检测人脸五官,分割左右眉、眼睛、嘴唇和牙齿。 + +# Input types + +## Required + +- image + - 输入的图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- left_eye + - 是否包含左眼。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- left_eyebrow + - 是否包含左眉毛。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- right_eye + - 是否包含右眼。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- right_eyebrow + - 是否包含右眉毛。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- lips + - 是否包含嘴唇。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- tooth + - 是否包含牙齿。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +# Output types + +- image + - 输出的图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 输出的遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code + +```python +class FacialFeatureSegment: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE",), # + "left_eye": ("BOOLEAN", {"default": True}), + "left_eyebrow": ("BOOLEAN", {"default": True}), + "right_eye": ("BOOLEAN", {"default": True}), + "right_eyebrow": ("BOOLEAN", {"default": True}), + "lips": ("BOOLEAN", {"default": True}), + "tooth": ("BOOLEAN", {"default": True}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = 'facial_feature_segment' + CATEGORY = '😺dzNodes/LayerMask' + + def facial_feature_segment(self, image, + left_eye, left_eyebrow, right_eye, right_eyebrow, lips, tooth + ): + + # 定义面部特征索引 + left_eye_indices = [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246] + right_eye_indices = [263, 249, 390, 373, 374, 380, 381, 382, 362, 398, 384, 385, 386, 387, 388, 466] + left_eyebrow_indices = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46] + right_eyebrow_indices = [336, 296, 334, 293, 300, 276, 283, 282, 295, 285] + tooth_indices = [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308, 415, 310, 311, 312, 13, 82, 81, 80, 191, 78] + lips_indices = [61, 76, 62, 78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308, 324, 318, 402, 317, 14, 87, 178, + 88, 95, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291, 375, 321, 405, 314, 17, 84, 181, 91, 146, + 61] + + ret_images = [] + ret_masks = [] + scale_factor = 4 + + for i in image: + face_image = tensor2pil(i.unsqueeze(0)).convert('RGB') + width, height = face_image.size + width *= scale_factor + height *= scale_factor + cv2_image = pil2cv2(face_image) + mp_face_mesh = mp.solutions.face_mesh + fase_mesh = mp_face_mesh.FaceMesh(static_image_mode=True, max_num_faces=1, min_detection_confidence=0.5) + results = fase_mesh.process(cv2_image) + mask = np.zeros((height, width), dtype=np.uint8) + + + if results.multi_face_landmarks: + for face_landmarks in results.multi_face_landmarks: + # 绘制各个面部特征 + if left_eye: + draw_feature(left_eye_indices, mask, face_landmarks, width, height) + if right_eye: + draw_feature(right_eye_indices, mask, face_landmarks, width, height) + if left_eyebrow: + draw_feature(left_eyebrow_indices, mask, face_landmarks, width, height) + if right_eyebrow: + draw_feature(right_eyebrow_indices, mask, face_landmarks, width, height) + if lips: + draw_feature(lips_indices, mask, face_landmarks, width, height) + if tooth: + draw_feature(tooth_indices, mask, face_landmarks, width, height) + + mask = cv22pil(mask).convert('L') + mask = gaussian_blur(mask, 2) + mask = mask.resize(face_image.size, Image.BILINEAR) + ret_images.append(pil2tensor(RGB2RGBA(face_image, mask))) + ret_masks.append(image2mask(mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A PersonMaskUltra V2.md b/docs/LayerMask%3A PersonMaskUltra V2.md new file mode 100644 index 0000000..4db13cf --- /dev/null +++ b/docs/LayerMask%3A PersonMaskUltra V2.md @@ -0,0 +1,254 @@ +# Documentation +- Class name: PersonMaskUltraV2 +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +PersonMaskUltra的V2升级版,增加了VITMatte边缘处理方法。(注意:超过2K尺寸的图片使用此方法将占用大量内存) + +# Input types + +## Required + +- images + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- face + - 面部。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- hair + - 头发。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- body + - 身体。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- clothes + - 衣服。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- accessories + - 配饰。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- background + - 背景。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- confidence + - 置信度。 + - Comfy dtype: FLOAT + - Python dtype: float + +- detail_method + - 细节处理方法。 + - Comfy dtype: ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter'] + - Python dtype: str + +- detail_erode + - 细节腐蚀。 + - Comfy dtype: INT + - Python dtype: int + +- detail_dilate + - 细节膨胀。 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 黑点。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 白点。 + - Comfy dtype: FLOAT + - Python dtype: float + +- process_detail + - 处理细节。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class PersonMaskUltraV2: + + def __init__(self): + # download the model if we need it + get_a_person_mask_generator_model_path() + + @classmethod + def INPUT_TYPES(self): + + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + + return { + "required": + { + "images": ("IMAGE",), + "face": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}), + "hair": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "body": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "clothes": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "accessories": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "background": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "confidence": ("FLOAT", {"default": 0.4, "min": 0.05, "max": 0.95, "step": 0.01},), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "detail_dilate": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01, "display": "slider"}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01, "display": "slider"}), + "process_detail": ("BOOLEAN", {"default": True}), + }, + "optional": + { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = 'person_mask_ultra_v2' + CATEGORY = '😺dzNodes/LayerMask' + + def get_mediapipe_image(self, image: Image): + import mediapipe as mp + # Convert image to NumPy array + numpy_image = np.asarray(image) + image_format = mp.ImageFormat.SRGB + # Convert BGR to RGB (if necessary) + if numpy_image.shape[-1] == 4: + image_format = mp.ImageFormat.SRGBA + elif numpy_image.shape[-1] == 3: + image_format = mp.ImageFormat.SRGB + + numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB) + return mp.Image(image_format=image_format, data=numpy_image) + + def person_mask_ultra_v2(self, images, face, hair, body, clothes, + accessories, background, confidence, + detail_method, detail_erode, detail_dilate, + black_point, white_point, process_detail): + + import mediapipe as mp + a_person_mask_generator_model_path = get_a_person_mask_generator_model_path() + a_person_mask_generator_model_buffer = None + with open(a_person_mask_generator_model_path, "rb") as f: + a_person_mask_generator_model_buffer = f.read() + image_segmenter_base_options = mp.tasks.BaseOptions(model_asset_buffer=a_person_mask_generator_model_buffer) + options = mp.tasks.vision.ImageSegmenterOptions( + base_options=image_segmenter_base_options, + running_mode=mp.tasks.vision.RunningMode.IMAGE, + output_category_mask=True) + # Create the image segmenter + ret_images = [] + ret_masks = [] + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter: + for image in images: + _image = torch.unsqueeze(image, 0) + orig_image = tensor2pil(_image).convert('RGB') + # Convert the Tensor to a PIL image + i = 255. * image.cpu().numpy() + image_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + # create our foreground and background arrays for storing the mask results + mask_background_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8) + mask_background_array[:] = (0, 0, 0, 255) + mask_foreground_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8) + mask_foreground_array[:] = (255, 255, 255, 255) + # Retrieve the masks for the segmented image + media_pipe_image = self.get_mediapipe_image(image=image_pil) + segmented_masks = segmenter.segment(media_pipe_image) + masks = [] + if background: + masks.append(segmented_masks.confidence_masks[0]) + if hair: + masks.append(segmented_masks.confidence_masks[1]) + if body: + masks.append(segmented_masks.confidence_masks[2]) + if face: + masks.append(segmented_masks.confidence_masks[3]) + if clothes: + masks.append(segmented_masks.confidence_masks[4]) + if accessories: + masks.append(segmented_masks.confidence_masks[5]) + image_data = media_pipe_image.numpy_view() + image_shape = image_data.shape + # convert the image shape from "rgb" to "rgba" aka add the alpha channel + if image_shape[-1] == 3: + image_shape = (image_shape[0], image_shape[1], 4) + mask_background_array = np.zeros(image_shape, dtype=np.uint8) + mask_background_array[:] = (0, 0, 0, 255) + mask_foreground_array = np.zeros(image_shape, dtype=np.uint8) + mask_foreground_array[:] = (255, 255, 255, 255) + mask_arrays = [] + if len(masks) == 0: + mask_arrays.append(mask_background_array) + else: + for i, mask in enumerate(masks): + condition = np.stack((mask.numpy_view(),) * image_shape[-1], axis=-1) > confidence + mask_array = np.where(condition, mask_foreground_array, mask_background_array) + mask_arrays.append(mask_array) + # Merge our masks taking the maximum from each + merged_mask_arrays = reduce(np.maximum, mask_arrays) + # Create the image + mask_image = Image.fromarray(merged_mask_arrays) + # convert PIL image to tensor image + tensor_mask = mask_image.convert("RGB") + tensor_mask = np.array(tensor_mask).astype(np.float32) / 255.0 + tensor_mask = torch.from_numpy(tensor_mask)[None,] + _mask = tensor_mask.squeeze(3)[..., 0] + + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(pil2tensor(orig_image), _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil( + mask_edge_detail(pil2tensor(orig_image), _mask, + detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_dilate) + _mask = generate_VITMatte(orig_image, _trimap, local_files_only=local_files_only) + _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point)) + else: + _mask = mask2image(_mask) + + ret_image = RGB2RGBA(orig_image, _mask) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A PersonMaskUltra.md b/docs/LayerMask%3A PersonMaskUltra.md new file mode 100644 index 0000000..b187475 --- /dev/null +++ b/docs/LayerMask%3A PersonMaskUltra.md @@ -0,0 +1,217 @@ +# Documentation +- Class name: PersonMaskUltra +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +为人物生成脸、头发、身体皮肤、衣服或配饰的遮罩。与之前的A Person Mask Generator节点相比,这个节点具有超高的边缘细节。 本节点的模型代码来自[a-person-mask-generator](https://github.com/djbielejeski/a-person-mask-generator),边缘处理代码来自spacepxl的[ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters),感谢原作者。 + +# Input types + +## Required + +- images + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- face + - 面部。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- hair + - 头发。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- body + - 身体。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- clothes + - 衣服。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- accessories + - 配饰(例如背包)识别。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- background + - 背景。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- confidence + - 识别阈值,更低的值将输出更多的遮罩范围。 + - Comfy dtype: FLOAT + - Python dtype: float + +- detail_range + - 细节范围。 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 边缘白色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- process_detail + - 此处设为False将跳过边缘处理以节省运行时间。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 遮罩 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class PersonMaskUltra: + + def __init__(self): + # download the model if we need it + get_a_person_mask_generator_model_path() + + @classmethod + def INPUT_TYPES(self): + return { + "required": + { + "images": ("IMAGE",), + "face": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}), + "hair": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "body": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "clothes": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "accessories": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "background": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "confidence": ("FLOAT", {"default": 0.4, "min": 0.05, "max": 0.95, "step": 0.01},), + "detail_range": ("INT", {"default": 16, "min": 1, "max": 256, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}), + "process_detail": ("BOOLEAN", {"default": True}), + }, + "optional": + { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = 'person_mask_ultra' + CATEGORY = '😺dzNodes/LayerMask' + + def get_mediapipe_image(self, image: Image): + import mediapipe as mp + # Convert image to NumPy array + numpy_image = np.asarray(image) + image_format = mp.ImageFormat.SRGB + # Convert BGR to RGB (if necessary) + if numpy_image.shape[-1] == 4: + image_format = mp.ImageFormat.SRGBA + elif numpy_image.shape[-1] == 3: + image_format = mp.ImageFormat.SRGB + numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB) + return mp.Image(image_format=image_format, data=numpy_image) + + def person_mask_ultra(self, images, face, hair, body, clothes, + accessories, background, confidence, + detail_range, black_point, white_point, process_detail): + import mediapipe as mp + a_person_mask_generator_model_path = get_a_person_mask_generator_model_path() + a_person_mask_generator_model_buffer = None + with open(a_person_mask_generator_model_path, "rb") as f: + a_person_mask_generator_model_buffer = f.read() + image_segmenter_base_options = mp.tasks.BaseOptions(model_asset_buffer=a_person_mask_generator_model_buffer) + options = mp.tasks.vision.ImageSegmenterOptions( + base_options=image_segmenter_base_options, + running_mode=mp.tasks.vision.RunningMode.IMAGE, + output_category_mask=True) + # Create the image segmenter + ret_images = [] + ret_masks = [] + with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter: + for image in images: + # image = torch.unsqueeze(image, 0) + orig_image = tensor2pil(image.unsqueeze(0)).convert('RGB') + # Convert the Tensor to a PIL image + # i = 255. * image.cpu().numpy() + # image_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + image_pil = tensor2pil(image.unsqueeze(0)).convert('RGB') + # create our foreground and background arrays for storing the mask results + mask_background_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8) + mask_background_array[:] = (0, 0, 0, 255) + mask_foreground_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8) + mask_foreground_array[:] = (255, 255, 255, 255) + # Retrieve the masks for the segmented image + media_pipe_image = self.get_mediapipe_image(image=image_pil) + segmented_masks = segmenter.segment(media_pipe_image) + masks = [] + if background: + masks.append(segmented_masks.confidence_masks[0]) + if hair: + masks.append(segmented_masks.confidence_masks[1]) + if body: + masks.append(segmented_masks.confidence_masks[2]) + if face: + masks.append(segmented_masks.confidence_masks[3]) + if clothes: + masks.append(segmented_masks.confidence_masks[4]) + if accessories: + masks.append(segmented_masks.confidence_masks[5]) + image_data = media_pipe_image.numpy_view() + image_shape = image_data.shape + # convert the image shape from "rgb" to "rgba" aka add the alpha channel + if image_shape[-1] == 3: + image_shape = (image_shape[0], image_shape[1], 4) + mask_background_array = np.zeros(image_shape, dtype=np.uint8) + mask_background_array[:] = (0, 0, 0, 255) + mask_foreground_array = np.zeros(image_shape, dtype=np.uint8) + mask_foreground_array[:] = (255, 255, 255, 255) + mask_arrays = [] + if len(masks) == 0: + mask_arrays.append(mask_background_array) + else: + for i, mask in enumerate(masks): + condition = np.stack((mask.numpy_view(),) * image_shape[-1], axis=-1) > confidence + mask_array = np.where(condition, mask_foreground_array, mask_background_array) + mask_arrays.append(mask_array) + # Merge our masks taking the maximum from each + merged_mask_arrays = reduce(np.maximum, mask_arrays) + # Create the image + mask_image = Image.fromarray(merged_mask_arrays) + # convert PIL image to tensor image + tensor_mask = mask_image.convert("RGB") + tensor_mask = np.array(tensor_mask).astype(np.float32) / 255.0 + tensor_mask = torch.from_numpy(tensor_mask)[None,] + tensor_mask = tensor_mask.squeeze(3)[..., 0] + _mask = tensor2pil(tensor_mask).convert('L') + if process_detail: + _mask = tensor2pil(mask_edge_detail(pil2tensor(orig_image), pil2tensor(_mask), detail_range, black_point, white_point)) + ret_image = RGB2RGBA(orig_image, _mask) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A PixelSpread.md b/docs/LayerMask%3A PixelSpread.md new file mode 100644 index 0000000..bf43200 --- /dev/null +++ b/docs/LayerMask%3A PixelSpread.md @@ -0,0 +1,120 @@ +# Documentation +- Class name: PixelSpread +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +对图像的遮罩边缘部分进行像素扩张预处理,可有效改善图像合成的边缘。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- invert_mask + - 反转mask。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- mask_grow + - 遮罩扩张幅度。 + - Comfy dtype: INT + - Python dtype: int + +## Optional + +- mask + - 遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class PixelSpread: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask + "mask_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), + }, + "optional": { + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("image", ) + FUNCTION = 'pixel_spread' + CATEGORY = '😺dzNodes/LayerMask' + + def pixel_spread(self, image, invert_mask, mask_grow, mask=None): + + l_images = [] + l_masks = [] + ret_images = [] + + for l in image: + i = tensor2pil(torch.unsqueeze(l, 0)) + l_images.append(i) + if i.mode == 'RGBA': + l_masks.append(i.split()[-1]) + else: + l_masks.append(Image.new('L', i.size, 'white')) + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + max_batch = max(len(l_images), len(l_masks)) + + for i in range(max_batch): + _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] + if mask_grow != 0: + _mask = expand_mask(image2mask(_mask), mask_grow, 0) # 扩张,模糊 + _mask = mask2image(_mask) + # i1 = pil2tensor(_image.convert('RGB')) + # _mask = _mask.convert('RGB') + if _image.size != _mask.size: + log(f"Error: {NODE_NAME} skipped, because the mask is not match image.", message_type='error') + return (image,) + ret_image = pixel_spread(_image.convert('RGB'), _mask.convert('RGB')) + ret_images.append(pil2tensor(ret_image)) + # + # i_dup = copy.deepcopy(i1.cpu().numpy().astype(np.float64)) + # a_dup = copy.deepcopy(pil2tensor(_mask).cpu().numpy().astype(np.float64)) + # fg = copy.deepcopy(i1.cpu().numpy().astype(np.float64)) + # + # for index, img in enumerate(i_dup): + # alpha = a_dup[index][:, :, 0] + # fg[index], _ = estimate_foreground_ml(img, np.array(alpha), return_background=True) + # + # ret_images.append(torch.from_numpy(fg.astype(np.float32))) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A RemBgUltra.md b/docs/LayerMask%3A RemBgUltra.md new file mode 100644 index 0000000..5e78125 --- /dev/null +++ b/docs/LayerMask%3A RemBgUltra.md @@ -0,0 +1,98 @@ +# Documentation +- Class name: RemBgUltra +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +去除背景。与类似的背景移除节点相比,这个节点具有超高的边缘细节。 本节点结合了spacepxl的[ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters)的Alpha Matte节点,以及ZHO-ZHO-ZHO的[ComfyUI-BRIA_AI-RMBG](https://github.com/ZHO-ZHO-ZHO/ComfyUI-BRIA_AI-RMBG)的功能,感谢原作者。 + +*将[BRIA Background Removal v1.4](https://huggingface.co/briaai/RMBG-1.4)模型文件(model.pth)下载至ComfyUI/models/rmbg/RMBG-1.4文件夹。该模型由 BRIA AI 开发,可作为非商业用途的开源模型。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- detail_range + - 边缘细节范围。 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 边缘白色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- process_detail + - 此处设为False将跳过边缘处理以节省运行时间。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class RemBgUltra: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + + return { + "required": { + "image": ("IMAGE",), + "detail_range": ("INT", {"default": 8, "min": 1, "max": 256, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}), + "process_detail": ("BOOLEAN", {"default": True}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = "rembg_ultra" + CATEGORY = '😺dzNodes/LayerMask' + + def rembg_ultra(self, image, detail_range, black_point, white_point, process_detail): + ret_images = [] + ret_masks = [] + + for i in image: + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + orig_image = tensor2pil(i).convert('RGB') + _mask = RMBG(orig_image) + if process_detail: + _mask = tensor2pil(mask_edge_detail(i, pil2tensor(_mask), detail_range, black_point, white_point)) + ret_image = RGB2RGBA(orig_image, _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) \ No newline at end of file diff --git a/docs/LayerMask%3A RmBgUltra V2.md b/docs/LayerMask%3A RmBgUltra V2.md new file mode 100644 index 0000000..07c39b1 --- /dev/null +++ b/docs/LayerMask%3A RmBgUltra V2.md @@ -0,0 +1,134 @@ +# Documentation +- Class name: RmBgUltraV2 +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +RemBgUltra的V2升级版,增加了VITMatte边缘处理方法。(注意:超过2K尺寸的图片使用此方法将占用大量内存) + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- detail_method + - 细节处理方法。 + - Comfy dtype: ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter'] + - Python dtype: str + +- detail_erode + - 遮罩边缘向内侵蚀范围。数值越大,向内修复的范围越大。 + - Comfy dtype: INT + - Python dtype: int + +- detail_dilate + - 遮罩边缘向外扩张范围。数值越大,向外修复的范围越大。 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 边缘白色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- process_detail + - 此处设为False将跳过边缘处理以节省运行时间。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +## Optional + +- 无 + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class RmBgUltraV2: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + + return { + "required": { + "image": ("IMAGE",), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "detail_dilate": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01, "display": "slider"}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01, "display": "slider"}), + "process_detail": ("BOOLEAN", {"default": True}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = "rmbg_ultra_v2" + CATEGORY = '😺dzNodes/LayerMask' + + def rmbg_ultra_v2(self, image, detail_method, detail_erode, detail_dilate, + black_point, white_point, process_detail): + ret_images = [] + ret_masks = [] + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + for i in image: + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + orig_image = tensor2pil(i).convert('RGB') + _mask = RMBG(orig_image) + _mask = pil2tensor(_mask) + + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_dilate) + _mask = generate_VITMatte(orig_image, _trimap, local_files_only=local_files_only) + _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point)) + else: + _mask = mask2image(_mask) + + ret_image = RGB2RGBA(orig_image, _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A SegformerB2ClothesUltra.md b/docs/LayerMask%3A SegformerB2ClothesUltra.md new file mode 100644 index 0000000..01cac28 --- /dev/null +++ b/docs/LayerMask%3A SegformerB2ClothesUltra.md @@ -0,0 +1,274 @@ +# Documentation +- Class name: Segformer_B2_Clothes +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +为人物生成脸、头发、手臂、腿以及服饰的遮罩,主要用于分割服装。模型分割代码来自StartHua,感谢原作者。 与comfyui_segformer_b2_clothes节点相比,这个节点具有超高的边缘细节。(注意:生成边缘超过2K尺寸的图片使用VITMatte方法将占用大量内存) + +*从https://huggingface.co/mattmdjaga/segformer_b2_clothes下载全部文件至ComfyUI/models/segformer_b2_clothes文件夹。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- face + - 脸。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- hair + - 头发。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- hat + - 帽子。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- sunglass + - 太阳镜。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- left_arm + - 左臂。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- right_arm + - 右臂。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- left_leg + - 左腿。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- right_leg + - 右腿。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- upper_clothes + - 上衣。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- skirt + - 裙子。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- pants + - 裤子。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- dress + - 连衣裙。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- belt + - 腰带。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- shoe + - 鞋子。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- bag + - 包。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- scarf + - 围巾。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- detail_method + - 边缘处理方法。提供了VITMatte, VITMatte(local), PyMatting, GuidedFilter。如果首次使用VITMatte后模型已经下载,之后可以使用VITMatte(local)。 + - Comfy dtype: ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter'] + - Python dtype: str + +- detail_erode + - 遮罩边缘向内侵蚀范围。数值越大,向内修复的范围越大。 + - Comfy dtype: INT + - Python dtype: int + +- detail_dilate + - 遮罩边缘向外扩张范围。数值越大,向外修复的范围越大 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 边缘白色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- process_detail + - 此处设为False将跳过边缘处理以节省运行时间。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class Segformer_B2_Clothes: + + def __init__(self): + pass + + # Labels: 0: "Background", 1: "Hat", 2: "Hair", 3: "Sunglasses", 4: "Upper-clothes", 5: "Skirt", + # 6: "Pants", 7: "Dress", 8: "Belt", 9: "Left-shoe", 10: "Right-shoe", 11: "Face", + # 12: "Left-leg", 13: "Right-leg", 14: "Left-arm", 15: "Right-arm", 16: "Bag", 17: "Scarf" + + @classmethod + def INPUT_TYPES(cls): + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + return {"required": + { + "image":("IMAGE",), + "face": ("BOOLEAN", {"default": False}), + "hair": ("BOOLEAN", {"default": False}), + "hat": ("BOOLEAN", {"default": False}), + "sunglass": ("BOOLEAN", {"default": False}), + "left_arm": ("BOOLEAN", {"default": False}), + "right_arm": ("BOOLEAN", {"default": False}), + "left_leg": ("BOOLEAN", {"default": False}), + "right_leg": ("BOOLEAN", {"default": False}), + "upper_clothes": ("BOOLEAN", {"default": False}), + "skirt": ("BOOLEAN", {"default": False}), + "pants": ("BOOLEAN", {"default": False}), + "dress": ("BOOLEAN", {"default": False}), + "belt": ("BOOLEAN", {"default": False}), + "shoe": ("BOOLEAN", {"default": False}), + "bag": ("BOOLEAN", {"default": False}), + "scarf": ("BOOLEAN", {"default": False}), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 12, "min": 1, "max": 255, "step": 1}), + "detail_dilate": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01, "display": "slider"}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01, "display": "slider"}), + "process_detail": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = "segformer_ultra" + CATEGORY = '😺dzNodes/LayerMask' + + def segformer_ultra(self, image, + face, hat, hair, sunglass, upper_clothes, skirt, pants, dress, belt, shoe, + left_leg, right_leg, left_arm, right_arm, bag, scarf, detail_method, + detail_erode, detail_dilate, black_point, white_point, process_detail + ): + + ret_images = [] + ret_masks = [] + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + for i in image: + pred_seg, cloth = get_segmentation(i) + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + orig_image = tensor2pil(i).convert('RGB') + + labels_to_keep = [0] + if not hat: + labels_to_keep.append(1) + if not hair: + labels_to_keep.append(2) + if not sunglass: + labels_to_keep.append(3) + if not upper_clothes: + labels_to_keep.append(4) + if not skirt: + labels_to_keep.append(5) + if not pants: + labels_to_keep.append(6) + if not dress: + labels_to_keep.append(7) + if not belt: + labels_to_keep.append(8) + if not shoe: + labels_to_keep.append(9) + labels_to_keep.append(10) + if not face: + labels_to_keep.append(11) + if not left_leg: + labels_to_keep.append(12) + if not right_leg: + labels_to_keep.append(13) + if not left_arm: + labels_to_keep.append(14) + if not right_arm: + labels_to_keep.append(15) + if not bag: + labels_to_keep.append(16) + if not scarf: + labels_to_keep.append(17) + + mask = np.isin(pred_seg, labels_to_keep).astype(np.uint8) + + # 创建agnostic-mask图像 + mask_image = Image.fromarray((1 - mask) * 255) + mask_image = mask_image.convert("L") + _mask = pil2tensor(mask_image) + + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_dilate) + _mask = generate_VITMatte(orig_image, _trimap, local_files_only=local_files_only) + _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point)) + else: + _mask = mask2image(_mask) + + ret_image = RGB2RGBA(orig_image, _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A SegmentAnythingUltra V2.md b/docs/LayerMask%3A SegmentAnythingUltra V2.md new file mode 100644 index 0000000..ed39a75 --- /dev/null +++ b/docs/LayerMask%3A SegmentAnythingUltra V2.md @@ -0,0 +1,173 @@ +# Documentation +- Class name: SegmentAnythingUltraV2 +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +SegmentAnythingUltra的V2升级版,增加了VITMatte边缘处理方法。(注意:超过2K尺寸的图片使用此方法将占用大量内存) + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- sam_model + - SAM模型。 + - Comfy dtype: list_sam_model() + - Python dtype: str + +- grounding_dino_model + - GroundingDINO模型。 + - Comfy dtype: list_groundingdino_model() + - Python dtype: str + +- threshold + - 阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- detail_method + - 边缘处理方法。提供了VITMatte, VITMatte(local), PyMatting, GuidedFilter。如果首次使用VITMatte后模型已经下载,之后可以使用VITMatte(local)。 + - Comfy dtype: ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter'] + - Python dtype: str + +- detail_erode + - 遮罩边缘向内侵蚀范围。数值越大,向内修复的范围越大。 + - Comfy dtype: INT + - Python dtype: int + +- detail_dilate + - 遮罩边缘向外扩张范围。数值越大,向外修复的范围越大。 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 黑点。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 白点。 + - Comfy dtype: FLOAT + - Python dtype: float + +- process_detail + - 处理细节。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- prompt + - 提示词。 + - Comfy dtype: STRING + - Python dtype: str + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class SegmentAnythingUltraV2: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + + return { + "required": { + "image": ("IMAGE",), + "sam_model": (list_sam_model(), ), + "grounding_dino_model": (list_groundingdino_model(),), + "threshold": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.01}), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "detail_dilate": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "black_point": ("FLOAT", {"default": 0.15, "min": 0.01, "max": 0.98, "step": 0.01, "display": "slider"}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01, "display": "slider"}), + "process_detail": ("BOOLEAN", {"default": True}), + "prompt": ("STRING", {"default": "subject"}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = "segment_anything_ultra_v2" + CATEGORY = '😺dzNodes/LayerMask' + + def segment_anything_ultra_v2(self, image, sam_model, grounding_dino_model, threshold, + detail_method, detail_erode, detail_dilate, + black_point, white_point, process_detail, + prompt, ): + global SAM_MODEL + global DINO_MODEL + global previous_sam_model + global previous_dino_model + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + if previous_sam_model != sam_model: + SAM_MODEL = load_sam_model(sam_model) + previous_sam_model = sam_model + if previous_dino_model != grounding_dino_model: + DINO_MODEL = load_groundingdino_model(grounding_dino_model) + previous_dino_model = grounding_dino_model + ret_images = [] + ret_masks = [] + + for i in image: + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + _image = tensor2pil(i).convert('RGBA') + boxes = groundingdino_predict(DINO_MODEL, _image, prompt, threshold) + if boxes.shape[0] == 0: + break + (_, _mask) = sam_segment(SAM_MODEL, _image, boxes) + _mask = _mask[0] + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_dilate) + _mask = generate_VITMatte(_image, _trimap, local_files_only=local_files_only) + _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point)) + else: + _mask = mask2image(_mask) + _image = RGB2RGBA(tensor2pil(i).convert('RGB'), _mask.convert('L')) + + ret_images.append(pil2tensor(_image)) + ret_masks.append(image2mask(_mask)) + if len(ret_masks) == 0: + _, height, width, _ = image.size() + empty_mask = torch.zeros((1, height, width), dtype=torch.uint8, device="cpu") + return (empty_mask, empty_mask) + + log(f"{NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerMask%3A SegmentAnythingUltra.md b/docs/LayerMask%3A SegmentAnythingUltra.md new file mode 100644 index 0000000..4b4937d --- /dev/null +++ b/docs/LayerMask%3A SegmentAnythingUltra.md @@ -0,0 +1,146 @@ +# Documentation +- Class name: SegmentAnythingUltra +- Category: 😺dzNodes/LayerMask +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +对[ComfyUI Segment Anything](https://github.com/storyicon/comfyui_segment_anything)的改进,结合了spacepxl的[ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters)的Alpha Matte节点,使遮罩有更具细节的边缘,感谢原作者。 + +*请参照ComfyUI Segment Anything的安装方法安装模型。如果已经正确安装了ComfyUI Segment Anything,可跳过此步骤。 +* 从 [这里](https://huggingface.co/bert-base-uncased/tree/main) 下载 config.json,model.safetensors,tokenizer_config.json,tokenizer.json 和 vocab.txt 5个文件到 ```ComfyUI/models/bert-base-uncased```文件夹。 +* 下载 [GroundingDINO_SwinT_OGC config file](https://huggingface.co/ShilongLiu/GroundingDINO/resolve/main/GroundingDINO_SwinT_OGC.cfg.py), [GroundingDINO_SwinT_OGC model](https://huggingface.co/ShilongLiu/GroundingDINO/resolve/main/groundingdino_swint_ogc.pth), +[GroundingDINO_SwinB config file](https://huggingface.co/ShilongLiu/GroundingDINO/resolve/main/GroundingDINO_SwinB.cfg.py), [GroundingDINO_SwinB model](https://huggingface.co/ShilongLiu/GroundingDINO/resolve/main/groundingdino_swinb_cogcoor.pth) 到 ```ComfyUI/models/grounding-dino```文件夹。 +* 下载 [sam_vit_h](https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth),[sam_vit_l](https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth), +[sam_vit_b](https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth), [sam_hq_vit_h](https://huggingface.co/lkeab/hq-sam/resolve/main/sam_hq_vit_h.pth), +[sam_hq_vit_l](https://huggingface.co/lkeab/hq-sam/resolve/main/sam_hq_vit_l.pth), [sam_hq_vit_b](https://huggingface.co/lkeab/hq-sam/resolve/main/sam_hq_vit_b.pth), +[mobile_sam](https://github.com/ChaoningZhang/MobileSAM/blob/master/weights/mobile_sam.pt) 这几个文件到```ComfyUI/models/sams```文件夹。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- sam_model + - SAM模型。 + - Comfy dtype: list_sam_model() + - Python dtype: str + +- grounding_dino_model + - GroundingDINO模型。 + - Comfy dtype: list_groundingdino_model() + - Python dtype: str + +- threshold + - SAM阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- detail_range + - 边缘细节范围。 + - Comfy dtype: INT + - Python dtype: int + +- black_point + - 边缘黑色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- white_point + - 边缘白色采样阈值。 + - Comfy dtype: FLOAT + - Python dtype: float + +- process_detail + - 此处设为False将跳过边缘处理以节省运行时间。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- prompt + - SAM的prompt输入。 + - Comfy dtype: STRING + - Python dtype: str + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 掩码。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class SegmentAnythingUltra: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + + return { + "required": { + "image": ("IMAGE",), + "sam_model": (list_sam_model(), ), + "grounding_dino_model": (list_groundingdino_model(),), + "threshold": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.01}), + "detail_range": ("INT", {"default": 16, "min": 1, "max": 256, "step": 1}), + "black_point": ("FLOAT", {"default": 0.15, "min": 0.01, "max": 0.98, "step": 0.01}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}), + "process_detail": ("BOOLEAN", {"default": True}), + "prompt": ("STRING", {"default": "subject"}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = "segment_anything_ultra" + CATEGORY = '😺dzNodes/LayerMask' + + def segment_anything_ultra(self, image, sam_model, grounding_dino_model, threshold, + detail_range, black_point, white_point, process_detail, + prompt, ): + global SAM_MODEL + global DINO_MODEL + if SAM_MODEL is None: SAM_MODEL = load_sam_model(sam_model) + if DINO_MODEL is None: DINO_MODEL = load_groundingdino_model(grounding_dino_model) + ret_images = [] + ret_masks = [] + + for i in image: + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + item = tensor2pil(i).convert('RGBA') + boxes = groundingdino_predict(DINO_MODEL, item, prompt, threshold) + if boxes.shape[0] == 0: + break + (_, _mask) = sam_segment(SAM_MODEL, item, boxes) + _mask = _mask[0] + if process_detail: + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range, black_point, white_point)) + else: + _mask = mask2image(_mask) + _image = RGB2RGBA(tensor2pil(i).convert('RGB'), _mask.convert('L')) + + ret_images.append(pil2tensor(_image)) + ret_masks.append(image2mask(_mask)) + if len(ret_masks) == 0: + _, height, width, _ = image.size() + empty_mask = torch.zeros((1, height, width), dtype=torch.uint8, device="cpu") + return (empty_mask, empty_mask) + + log(f"{NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerStyle%3A InnerGlow V2.md b/docs/LayerStyle%3A InnerGlow V2.md new file mode 100644 index 0000000..b86e6fb --- /dev/null +++ b/docs/LayerStyle%3A InnerGlow V2.md @@ -0,0 +1,199 @@ +# Documentation +- Class name: InnerGlowV2 +- Category: 😺dzNodes/LayerStyle +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +生成内发光。 + +# Input types + +## Required + +- background_image + - 背景图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- layer_image + - 图层图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- invert_mask + - 反转蒙版。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- blend_mode + - 混合模式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - screen + - linear dodge(add) + - color dodge + - lighten + - dodge + - hard light + - linear light + - normal + - multply + - subtract + - difference + - darker + - color_burn + - linear_burn + - overlay + - soft_light + - vivid_light + - pin_light + - hard_mix + +- opacity + - 透明度。 + - Comfy dtype: INT + - Python dtype: int + +- brightness + - 亮度。 + - Comfy dtype: INT + - Python dtype: int + +- glow_range + - 辉光范围。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 模糊程度。 + - Comfy dtype: INT + - Python dtype: int + +- light_color + - 光源中心颜色。 + - Comfy dtype: STRING + - Python dtype: str + +- glow_color + - 辉光外围颜色。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- layer_mask + - 图层蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 最终处理后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class InnerGlowV2: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + modes = copy.copy(BLEND_MODES) + chop_mode_list = ["screen", "linear dodge(add)", "color dodge", "lighten", "dodge", "hard light", "linear light"] + for i in chop_mode_list: + modes.pop(i) + chop_mode_list.extend(list(modes.keys())) + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode_list,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代 + "glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张 + "light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色 + "glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'inner_glow_v2' + CATEGORY = '😺dzNodes/LayerStyle' + + def inner_glow_v2(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + brightness, glow_range, blur, light_color, glow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + 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]) + 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')) + if len(l_masks) == 0: + log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + 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('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning') + + blur_factor = blur / 20.0 + grow = glow_range + inner_mask = _mask + for x in range(brightness): + blur = int(grow * blur_factor) + _color = step_color(glow_color, light_color, brightness, x) + glow_mask = expand_mask(image2mask(inner_mask), -grow, blur) #扩张,模糊 + # 合成glow + color_image = Image.new("RGB", _layer.size, color=_color) + alpha = tensor2pil(mask_invert(glow_mask)).convert('L') + _glow = chop_image_v2(_layer, color_image, blend_mode, int(step_value(1, opacity, brightness, x))) + _layer.paste(_glow, mask=alpha) + grow = grow - int(glow_range/brightness) + # 合成layer + _layer.paste(_canvas, mask=ImageChops.invert(_mask)) + ret_images.append(pil2tensor(_layer)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerStyle%3A InnerGlow.md b/docs/LayerStyle%3A InnerGlow.md new file mode 100644 index 0000000..f92b86d --- /dev/null +++ b/docs/LayerStyle%3A InnerGlow.md @@ -0,0 +1,196 @@ +# Documentation +- Class name: InnerGlow +- Category: 😺dzNodes/LayerStyle +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +生成内发光。 + +# Input types + +## Required + +- background_image + - 背景图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- layer_image + - 用于合成的层图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- invert_mask + - 反转蒙版。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- blend_mode + - 发光的混合模式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - screen + - add + - lighter + - normal + - multply + - subtract + - difference + - darker + - color_burn + - color_dodge + - linear_burn + - linear_dodge + - overlay + - soft_light + - hard_light + - vivid_light + - pin_light + - linear_light + - hard_mix + +- opacity + - 透明度。 + - Comfy dtype: INT + - Python dtype: int + +- brightness + - 亮度。 + - Comfy dtype: INT + - Python dtype: int + +- glow_range + - 辉光范围。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 模糊程度。 + - Comfy dtype: INT + - Python dtype: int + +- light_color + - 光源中心颜色。 + - Comfy dtype: STRING + - Python dtype: str + +- glow_color + - 辉光外围颜色。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- layer_mask + - 层图像的遮罩,发光按此生成。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 最终处理后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code + +```python +class InnerGlow: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + chop_mode = ['screen', 'add', 'lighter', 'normal', 'multply', 'subtract','difference','darker', + 'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay', + 'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix'] + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代 + "glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张 + "light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色 + "glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'inner_glow' + CATEGORY = '😺dzNodes/LayerStyle' + + def inner_glow(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + brightness, glow_range, blur, light_color, glow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + 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]) + 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')) + if len(l_masks) == 0: + log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + 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('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning') + + blur_factor = blur / 20.0 + grow = glow_range + inner_mask = _mask + for x in range(brightness): + blur = int(grow * blur_factor) + _color = step_color(glow_color, light_color, brightness, x) + glow_mask = expand_mask(image2mask(inner_mask), -grow, blur) #扩张,模糊 + # 合成glow + color_image = Image.new("RGB", _layer.size, color=_color) + alpha = tensor2pil(mask_invert(glow_mask)).convert('L') + _glow = chop_image(_layer, color_image, blend_mode, int(step_value(1, opacity, brightness, x))) + _layer.paste(_glow, mask=alpha) + grow = grow - int(glow_range/brightness) + # 合成layer + _layer.paste(_canvas, mask=ImageChops.invert(_mask)) + ret_images.append(pil2tensor(_layer)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerStyle%3A InnerShadow V2.md b/docs/LayerStyle%3A InnerShadow V2.md new file mode 100644 index 0000000..4a23c7f --- /dev/null +++ b/docs/LayerStyle%3A InnerShadow V2.md @@ -0,0 +1,192 @@ +# Documentation +- Class name: InnerShadowV2 +- Category: 😺dzNodes/LayerStyle +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +生成内阴影效果。 + + +# Input types + +## Required + +- background_image + - 背景图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- layer_image + - 图层图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- invert_mask + - 反转蒙版。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- blend_mode + - 混合模式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - screen + - linear dodge(add) + - color dodge + - lighten + - dodge + - hard light + - linear light + - normal + - multply + - subtract + - difference + - darker + - color_burn + - linear_burn + - overlay + - soft_light + - vivid_light + - pin_light + - hard_mix + +- opacity + - 透明度。 + - Comfy dtype: INT + - Python dtype: int + +- distance_x + - X轴偏移像素数。 + - Comfy dtype: INT + - Python dtype: int + +- distance_y + - Y轴偏移像素数。 + - Comfy dtype: INT + - Python dtype: int + +- grow + - 扩张。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 模糊。 + - Comfy dtype: INT + - Python dtype: int + +- shadow_color + - 阴影颜色。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- layer_mask + - 图层蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 最终处理后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class InnerShadowV2: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode_v2,), # 混合模式 + "opacity": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), # 透明度 + "distance_x": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # x_偏移 + "distance_y": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # y_偏移 + "grow": ("INT", {"default": 2, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 15, "min": 0, "max": 100, "step": 1}), # 模糊 + "shadow_color": ("STRING", {"default": "#000000"}), # 背景颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'inner_shadow_v2' + CATEGORY = '😺dzNodes/LayerStyle' + + def inner_shadow_v2(self, background_image, layer_image, + invert_mask, blend_mode, opacity, distance_x, distance_y, + grow, blur, shadow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + 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]) + 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')) + if len(l_masks) == 0: + log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + distance_x = -distance_x + distance_y = -distance_y + shadow_color = Image.new("RGB", tensor2pil(l_images[0]).size, color=shadow_color) + 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('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning') + + if distance_x != 0 or distance_y != 0: + __mask = shift_image(_mask, distance_x, distance_y) # 位移 + shadow_mask = expand_mask(image2mask(__mask), grow, blur) #扩张,模糊 + # 合成阴影 + alpha = tensor2pil(shadow_mask).convert('L') + _shadow = chop_image_v2(_layer, shadow_color, blend_mode, opacity) + _layer.paste(_shadow, mask=ImageChops.invert(alpha)) + # 合成layer + _canvas.paste(_layer, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerStyle%3A InnerShadow.md b/docs/LayerStyle%3A InnerShadow.md new file mode 100644 index 0000000..b9ebef0 --- /dev/null +++ b/docs/LayerStyle%3A InnerShadow.md @@ -0,0 +1,199 @@ +# Documentation +- Class name: InnerShadow +- Category: 😺dzNodes/LayerStyle +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +生成内阴影效果。 + +# Input types + +## Required + +- background_image1 + - 背景图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- layer_image1 + - 用于合成的层图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- invert_mask + - 是否反转遮罩。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- blend_mode3 + - 阴影的混合模式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - screen + - add + - lighter + - normal + - multply + - subtract + - difference + - darker + - color_burn + - color_dodge + - linear_burn + - linear_dodge + - overlay + - soft_light + - hard_light + - vivid_light + - pin_light + - linear_light + - hard_mix + +- opacity + - 阴影的不透明度。 + - Comfy dtype: INT + - Python dtype: int + +- distance_x + - 阴影的水平方向偏移量。 + - Comfy dtype: INT + - Python dtype: int + +- distance_y + - 阴影的垂直方向偏移量。 + - Comfy dtype: INT + - Python dtype: int + +- grow + - 阴影扩张幅度。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 阴影模糊程度。 + - Comfy dtype: INT + - Python dtype: int + +- shadow_color4 + - 阴影颜色。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- layer_mask1,2 + - 层图像的遮罩,阴影按此生成。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 最终处理后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class InnerShadow: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + chop_mode = [ + 'screen', 'add', 'lighter', 'normal', 'multply', 'subtract', 'difference', + 'darker', 'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay', + 'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix' + ] + + return { + "required": { + "background_image1": ("IMAGE", ), # + "layer_image1": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 是否反转遮罩 + "blend_mode3": (chop_mode,), # 阴影的混合模式 + "opacity": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), # 阴影的不透明度 + "distance_x": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # 阴影的水平方向偏移量 + "distance_y": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # 阴影的垂直方向偏移量 + "grow": ("INT", {"default": 2, "min": -9999, "max": 9999, "step": 1}), # 阴影扩张幅度 + "blur": ("INT", {"default": 15, "min": 0, "max": 100, "step": 1}), # 阴影模糊程度 + "shadow_color4": ("STRING", {"default": "#000000"}), # 阴影颜色 + }, + "optional": { + "layer_mask1,2": ("MASK",), # + } + } + + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'inner_shadow' + CATEGORY = '😺dzNodes/LayerStyle' + + def inner_shadow(self, background_image1, layer_image1, + invert_mask, blend_mode3, opacity, distance_x, distance_y, + grow, blur, shadow_color4, + layer_mask1=None, layer_mask2=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + for b in background_image1: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image1: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask1 is not None or layer_mask2 is not None: + masks = [layer_mask1, layer_mask2] + l_masks = [] + for m in masks: + if m is not None: + if m.dim() == 2: + m = torch.unsqueeze(m, 0) + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image1,) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + distance_x = -distance_x + distance_y = -distance_y + shadow_color = Image.new("RGB", tensor2pil(l_images[0]).size, color=shadow_color4) + 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('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning') + + if distance_x != 0 or distance_y != 0: + __mask = shift_image(_mask, distance_x, distance_y) # 位移 + shadow_mask = expand_mask(image2mask(__mask), grow, blur) #扩张,模糊 + # 合成阴影 + alpha = tensor2pil(shadow_mask).convert('L') + _shadow = chop_image(_layer, shadow_color, blend_mode3, opacity) + _layer.paste(_shadow, mask=ImageChops.invert(alpha)) + # 合成layer + _canvas.paste(_layer, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerStyle%3A OuterGlow V2.md b/docs/LayerStyle%3A OuterGlow V2.md new file mode 100644 index 0000000..6e93474 --- /dev/null +++ b/docs/LayerStyle%3A OuterGlow V2.md @@ -0,0 +1,178 @@ +# Documentation +- Class name: OuterGlowV2 +- Category: 😺dzNodes/LayerStyle +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +外部辉光效果V2。 + +# Input types + +## Required + +- background_image + - 背景图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- layer_image + - 图层图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- invert_mask + - 反转mask。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- blend_mode + - 混合模式。 + - Comfy dtype: ['screen', 'linear dodge(add)', 'color dodge', 'lighten', 'dodge', 'hard light', 'linear light', '其他模式'] + - Python dtype: str + +- opacity + - 透明度。 + - Comfy dtype: INT + - Python dtype: int + +- brightness + - 亮度。 + - Comfy dtype: INT + - Python dtype: int + +- glow_range + - 辉光范围。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 模糊。 + - Comfy dtype: INT + - Python dtype: int + +- light_color + - 光源中心颜色。 + - Comfy dtype: STRING + - Python dtype: str + +- glow_color + - 辉光外围颜色。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- layer_mask + - 图层蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class OuterGlowV2: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + modes = copy.copy(BLEND_MODES) + chop_mode_list = ["screen", "linear dodge(add)", "color dodge", "lighten", "dodge", "hard light", "linear light"] + for i in chop_mode_list: + modes.pop(i) + chop_mode_list.extend(list(modes.keys())) + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode_list,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代 + "glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张 + "light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色 + "glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'outer_glow_v2' + CATEGORY = '😺dzNodes/LayerStyle' + + def outer_glow_v2(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + brightness, glow_range, blur, light_color, glow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + + 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]) + 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')) + if len(l_masks) == 0: + log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + blur_factor = blur / 20.0 + 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('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning') + grow = glow_range + for x in range(brightness): + blur = int(grow * blur_factor) + _color = step_color(glow_color, light_color, brightness, x) + glow_mask = expand_mask(image2mask(_mask), grow, blur) #扩张,模糊 + # 合成glow + color_image = Image.new("RGB", _layer.size, color=_color) + alpha = tensor2pil(glow_mask).convert('L') + _glow = chop_image_v2(_canvas, color_image, blend_mode, int(step_value(1, opacity, brightness, x))) + _canvas.paste(_glow.convert('RGB'), mask=alpha) + grow = grow - int(glow_range/brightness) + # 合成layer + _canvas.paste(_layer, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerStyle%3A OuterGlow.md b/docs/LayerStyle%3A OuterGlow.md new file mode 100644 index 0000000..ad40c25 --- /dev/null +++ b/docs/LayerStyle%3A OuterGlow.md @@ -0,0 +1,174 @@ +# Documentation +- Class name: OuterGlow +- Category: 😺dzNodes/LayerStyle +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +生成外发光。 + +# Input types + +## Required + +- background_image + - 背景图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- layer_image + - 图层图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- invert_mask + - 反转mask。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- blend_mode + - 混合模式。 + - Comfy dtype: ['screen', 'add', 'lighter', 'normal', 'multply', 'subtract', 'difference', 'darker', 'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay', 'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix'] + - Python dtype: str + +- opacity + - 透明度。 + - Comfy dtype: INT + - Python dtype: int + +- brightness + - 亮度。 + - Comfy dtype: INT + - Python dtype: int + +- glow_range + - 辉光范围。 + - Comfy dtype: INT + - Python dtype: int + +- blur + - 模糊。 + - Comfy dtype: INT + - Python dtype: int + +- light_color + - 光源中心颜色。 + - Comfy dtype: STRING + - Python dtype: str + +- glow_color + - 辉光外围颜色。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- layer_mask + - 层图像的遮罩,外发光按此生成。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class OuterGlow: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + chop_mode = ['screen', 'add', 'lighter', 'normal', 'multply', 'subtract', 'difference', 'darker', + 'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay', + 'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix'] + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代 + "glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张 + "light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色 + "glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'outer_glow' + CATEGORY = '😺dzNodes/LayerStyle' + + def outer_glow(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + brightness, glow_range, blur, light_color, glow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + + 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]) + 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')) + if len(l_masks) == 0: + log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + blur_factor = blur / 20.0 + 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('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning') + grow = glow_range + for x in range(brightness): + blur = int(grow * blur_factor) + _color = step_color(glow_color, light_color, brightness, x) + glow_mask = expand_mask(image2mask(_mask), grow, blur) #扩张,模糊 + # 合成glow + color_image = Image.new("RGB", _layer.size, color=_color) + alpha = tensor2pil(glow_mask).convert('L') + _glow = chop_image(_canvas, color_image, blend_mode, int(step_value(1, opacity, brightness, x))) + _canvas.paste(_glow.convert('RGB'), mask=alpha) + grow = grow - int(glow_range/brightness) + # 合成layer + _canvas.paste(_layer, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A CreateQRCode.md b/docs/LayerUtility%3A CreateQRCode.md new file mode 100644 index 0000000..7092115 --- /dev/null +++ b/docs/LayerUtility%3A CreateQRCode.md @@ -0,0 +1,81 @@ +# Documentation +- Class name: CreateQRCode +- Category: 😺dzNodes/LayerUtility/SystemIO +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +生成二维码工具。 + +# Input types + +## Required + +- size + - 大小。 + - Comfy dtype: INT + - Python dtype: int + +- border + - 边框。 + - Comfy dtype: INT + - Python dtype: int + +- text + - 文本。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- 无 + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class CreateQRCode: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "size": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}), + "border": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}), + "text": ("STRING", {"default": "", "multiline": True}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("image", ) + FUNCTION = 'create_qrcode' + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + + def create_qrcode(self, size, border, text): + import qrcode + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_H, + box_size=20, + border=border, + ) + qr.add_data(text.encode('utf-8')) + qr.make(fit=True) + ret_image = qr.make_image(fill_color="black", back_color="white") + ret_image = ret_image.resize((size, size), Image.BICUBIC) + + return (pil2tensor(ret_image.convert("RGB")), ) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A DecodeQRCode.md b/docs/LayerUtility%3A DecodeQRCode.md new file mode 100644 index 0000000..2ba4f6a --- /dev/null +++ b/docs/LayerUtility%3A DecodeQRCode.md @@ -0,0 +1,76 @@ +# Documentation +- Class name: DecodeQRCode +- Category: 😺dzNodes/LayerUtility/SystemIO +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +解码二维码工具。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- pre_blur + - 预模糊。 + - Comfy dtype: INT + - Python dtype: int + +## Optional + +- 无 + +# Output types + +- string + - 文本。 + - Comfy dtype: STRING + - Python dtype: str + +# Usage tips +- Infra type: CPU + +# Source code +```python +class DecodeQRCode: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE",), + "pre_blur": ("INT", {"default": 2, "min": 0, "max": 16, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("STRING", ) + RETURN_NAMES = ("string", ) + FUNCTION = 'decode_qrcode' + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + + def decode_qrcode(self, image, pre_blur): + ret_texts = [] + from pyzbar.pyzbar import decode + for i in image: + _image = torch.unsqueeze(i, 0) + _image = tensor2pil(_image) + if pre_blur: + _image = gaussian_blur(_image, pre_blur) + qrmessage = decode(_image) + if len(qrmessage) > 0: + ret_texts.append(qrmessage[0][0].decode('utf-8')) + else: + ret_texts.append("Cannot recognize QR") + + return (ret_texts, ) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageChannelMerge.md b/docs/LayerUtility%3A ImageChannelMerge.md new file mode 100644 index 0000000..d8823d5 --- /dev/null +++ b/docs/LayerUtility%3A ImageChannelMerge.md @@ -0,0 +1,115 @@ +# Documentation +- Class name: ImageChannelMerge +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +将各通道合并为一张图片。 + +# Input types + +## Required + +- channel_1 + - 通道1。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- channel_2 + - 通道2。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- channel_3 + - 通道3。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mode + - 通道模式。包含RGBA, YCbCr, LAB和HSV。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - RGBA + - YCbCr + - LAB + - HSV + +## Optional + +- channel_4 + - 通道4。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageChannelMerge: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + channel_mode = ['RGBA', 'YCbCr', 'LAB', 'HSV'] + return { + "required": { + "channel_1": ("IMAGE", ), # + "channel_2": ("IMAGE",), # + "channel_3": ("IMAGE",), # + "mode": (channel_mode,), # 通道设置 + }, + "optional": { + "channel_4": ("IMAGE",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'image_channel_merge' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_channel_merge(self, channel_1, channel_2, channel_3, mode, channel_4=None): + + c1_images = [] + c2_images = [] + c3_images = [] + c4_images = [] + ret_images = [] + + width, height = tensor2pil(torch.unsqueeze(channel_1[0], 0)).size + for c in channel_1: + c1_images.append(torch.unsqueeze(c, 0)) + for c in channel_2: + c2_images.append(torch.unsqueeze(c, 0)) + for c in channel_3: + c3_images.append(torch.unsqueeze(c, 0)) + if channel_4 is not None: + for c in channel_4: + c4_images.append(torch.unsqueeze(c, 0)) + else: + c4_images.append(Image.new('L', size=(width, height), color='white')) + + max_batch = max(len(c1_images), len(c2_images), len(c3_images), len(c4_images)) + for i in range(max_batch): + c_1 = c1_images[i] if i < len(c1_images) else c1_images[-1] + c_2 = c2_images[i] if i < len(c2_images) else c2_images[-1] + c_3 = c3_images[i] if i < len(c3_images) else c3_images[-1] + c_4 = c4_images[i] if i < len(c4_images) else c4_images[-1] + ret_image = image_channel_merge((tensor2pil(c_1), tensor2pil(c_2), tensor2pil(c_3), tensor2pil(c_4)), mode) + + ret_images.append(pil2tensor(ret_image)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageChannelSplit.md b/docs/LayerUtility%3A ImageChannelSplit.md new file mode 100644 index 0000000..4eca2a2 --- /dev/null +++ b/docs/LayerUtility%3A ImageChannelSplit.md @@ -0,0 +1,95 @@ +# Documentation +- Class name: ImageChannelSplit +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +将图像通道拆分为单独的图片。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mode + - 通道模式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - RGBA + - YCbCr + - LAB + - HSV + +# Output types + +- channel_1 + - 通道1。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- channel_2 + - 通道2。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- channel_3 + - 通道3。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- channel_4 + - 通道4。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageChannelSplit: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + channel_mode = ['RGBA', 'YCbCr', 'LAB', 'HSV'] + return { + "required": { + "image": ("IMAGE", ), # + "mode": (channel_mode,), # 通道设置 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE",) + RETURN_NAMES = ("channel_1", "channel_2", "channel_3", "channel_4",) + FUNCTION = 'image_channel_split' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_channel_split(self, image, mode): + + c1_images = [] + c2_images = [] + c3_images = [] + c4_images = [] + + for i in image: + i = torch.unsqueeze(i, 0) + _image = tensor2pil(i).convert('RGBA') + channel1, channel2, channel3, channel4 = image_channel_split(_image, mode) + c1_images.append(pil2tensor(channel1)) + c2_images.append(pil2tensor(channel2)) + c3_images.append(pil2tensor(channel3)) + c4_images.append(pil2tensor(channel4)) + + log(f"{NODE_NAME} Processed {len(c1_images)} image(s).", message_type='finish') + return (torch.cat(c1_images, dim=0), torch.cat(c2_images, dim=0), torch.cat(c3_images, dim=0), torch.cat(c4_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageCombineAlpha.md b/docs/LayerUtility%3A ImageCombineAlpha.md new file mode 100644 index 0000000..85a4437 --- /dev/null +++ b/docs/LayerUtility%3A ImageCombineAlpha.md @@ -0,0 +1,80 @@ +# Documentation +- Class name: ImageCombineAlpha +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +将图片与遮罩合并为包含alpha通道的RGBA模式的图片。 + +# Input types + +## Required + +- RGB_image + - RGB 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- RGBA_image + - RGBA 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageCombineAlpha: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "RGB_image": ("IMAGE", ), # + "mask": ("MASK",), # + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("RGBA_image",) + FUNCTION = 'image_combine_alpha' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_combine_alpha(self, RGB_image, mask): + + ret_images = [] + input_images = [] + input_masks = [] + + for i in RGB_image: + input_images.append(torch.unsqueeze(i, 0)) + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + input_masks.append(torch.unsqueeze(m, 0)) + + max_batch = max(len(input_images), len(input_masks)) + for i in range(max_batch): + _image = input_images[i] if i < len(input_images) else input_images[-1] + _mask = input_masks[i] if i < len(input_masks) else input_masks[-1] + r, g, b, _ = image_channel_split(tensor2pil(_image).convert('RGB'), 'RGB') + ret_image = image_channel_merge((r, g, b, tensor2pil(_mask).convert('L')), 'RGBA') + + ret_images.append(pil2tensor(ret_image)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageHub.md b/docs/LayerUtility%3A ImageHub.md new file mode 100644 index 0000000..fc358a9 --- /dev/null +++ b/docs/LayerUtility%3A ImageHub.md @@ -0,0 +1,271 @@ +# Documentation +- Class name: ImageHub +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +从多路输入图片和遮罩中切换其中一组输出,支持9组输入。所有的输入项都是可选项。如果一组输入中只有image或者只有mask, 缺失项将输出为None。 + +# Input types + +## Required + +- output + - 输出。 + - Comfy dtype: INT + - Python dtype: int + +- random_output + - 随机输出。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +## Optional + +- input1_image + - 输入1图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input1_mask + - 输入1蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- input2_image + - 输入2图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input2_mask + - 输入2蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- input3_image + - 输入3图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input3_mask + - 输入3蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- input4_image + - 输入4图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input4_mask + - 输入4蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- input5_image + - 输入5图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input5_mask + - 输入5蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- input6_image + - 输入6图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input6_mask + - 输入6蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- input7_image + - 输入7图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input7_mask + - 输入7蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- input8_image + - 输入8图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input8_mask + - 输入8蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- input9_image + - 输入9图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- input9_mask + - 输入9蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 输出图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 输出蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageHub: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "output": ("INT", {"default": 1, "min": 1, "max": 9, "step": 1}), + "random_output": ("BOOLEAN", {"default": False}), + }, + "optional": { + "input1_image": ("IMAGE",), + "input1_mask": ("MASK",), + "input2_image": ("IMAGE",), + "input2_mask": ("MASK",), + "input3_image": ("IMAGE",), + "input3_mask": ("MASK",), + "input4_image": ("IMAGE",), + "input4_mask": ("MASK",), + "input5_image": ("IMAGE",), + "input5_mask": ("MASK",), + "input6_image": ("IMAGE",), + "input6_mask": ("MASK",), + "input7_image": ("IMAGE",), + "input7_mask": ("MASK",), + "input8_image": ("IMAGE",), + "input8_mask": ("MASK",), + "input9_image": ("IMAGE",), + "input9_mask": ("MASK",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask") + FUNCTION = 'image_hub' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_hub(self, output, random_output, + input1_image=None, input1_mask=None, + input2_image=None, input2_mask=None, + input3_image=None, input3_mask=None, + input4_image=None, input4_mask=None, + input5_image=None, input5_mask=None, + input6_image=None, input6_mask=None, + input7_image=None, input7_mask=None, + input8_image=None, input8_mask=None, + input9_image=None, input9_mask=None, + ): + + output_list = [] + if input1_image is not None or input1_mask is not None: + output_list.append(1) + if input2_image is not None or input2_mask is not None: + output_list.append(2) + if input3_image is not None or input3_mask is not None: + output_list.append(3) + if input4_image is not None or input4_mask is not None: + output_list.append(4) + if input5_image is not None or input5_mask is not None: + output_list.append(5) + if input6_image is not None or input6_mask is not None: + output_list.append(6) + if input7_image is not None or input7_mask is not None: + output_list.append(7) + if input8_image is not None or input8_mask is not None: + output_list.append(8) + if input9_image is not None or input9_mask is not None: + output_list.append(9) + + log(f"output_list={output_list}") + if len(output_list) == 0: + log(f"{NODE_NAME} is skip, because No Input.", message_type='error') + return (None, None) + + if random_output: + index = random.randint(1, len(output_list)) + output = output_list[index - 1] + + ret_image = None + ret_mask = None + if output == 1: + if input1_image is not None: + ret_image = input1_image + if input1_mask is not None: + ret_mask = input1_mask + elif output == 2: + if input2_image is not None: + ret_image = input2_image + if input2_mask is not None: + ret_mask = input2_mask + elif output == 3: + if input3_image is not None: + ret_image = input3_image + if input3_mask is not None: + ret_mask = input3_mask + elif output == 4: + if input4_image is not None: + ret_image = input4_image + if input4_mask is not None: + ret_mask = input4_mask + elif output == 5: + if input5_image is not None: + ret_image = input5_image + if input5_mask is not None: + ret_mask = input5_mask + elif output == 6: + if input6_image is not None: + ret_image = input6_image + if input6_mask is not None: + ret_mask = input6_mask + elif output == 7: + if input7_image is not None: + ret_image = input7_image + if input7_mask is not None: + ret_mask = input7_mask + elif output == 8: + if input8_image is not None: + ret_image = input8_image + if input8_mask is not None: + ret_mask = input8_mask + else: + if input9_image is not None: + ret_image = input9_image + if input9_mask is not None: + ret_mask = input9_mask + + if ret_image is None and ret_mask is None: + log(f"{NODE_NAME} have {output_list} inputs, output is {output}, but there is no corresponding input.", message_type="error") + elif ret_image is None: + log(f"{NODE_NAME} have {output_list} inputs, output is {output}, but image is None.", message_type='finish') + elif ret_mask is None: + log(f"{NODE_NAME} have {output_list} inputs, output is {output}, but mask is None.", message_type='finish') + else: + log(f"{NODE_NAME} have {output_list} inputs, output is {output}.", message_type='finish') + + return (ret_image, ret_mask) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageMaskScaleAs.md b/docs/LayerUtility%3A ImageMaskScaleAs.md new file mode 100644 index 0000000..5bfa8c5 --- /dev/null +++ b/docs/LayerUtility%3A ImageMaskScaleAs.md @@ -0,0 +1,166 @@ +# Documentation +- Class name: ImageMaskScaleAs +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +将图像或遮罩缩放到参考图像(或遮罩)的大小。 + +# Input types + +## Required + +- scale_as + - 参考大小。可以是图像image,也可以是遮罩mask。 + - Comfy dtype: ANY + - Python dtype: torch.Tensor + +- fit + - 缩放画幅宽高比模式。当原图与缩放尺寸画幅宽高比例不一致时,有3种模式可以选择, letterbox模式保留完整的画幅,空白处用黑色补足;crop模式保留完整的短边,长边超出部分将被切除;fill模式不保持画幅比例,宽高各自填满画面。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - letterbox + - crop + - fill + +- method + - 缩放的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - lanczos + - bicubic + - hamming + - bilinear + - box + - nearest + +## Optional + +- image + - 待缩放的图像。此选项为可选输入,如果没有输入将输出纯黑图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 待缩放的遮罩。此选项为可选输入,如果没有输入将输出纯黑遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 缩放后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 缩放后的蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- original_size + - 原始尺寸。 + - Comfy dtype: BOX + - Python dtype: List[int] + +- width + - 目标宽度。 + - Comfy dtype: INT + - Python dtype: int + +- height + - 目标高度。 + - Comfy dtype: INT + - Python dtype: int + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageMaskScaleAs: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + fit_mode = ['letterbox', 'crop', 'fill'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + + return { + "required": { + "scale_as": (any, {}), + "fit": (fit_mode,), + "method": (method_mode,), + }, + "optional": { + "image": ("IMAGE",), # + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT") + RETURN_NAMES = ("image", "mask", "original_size", "widht", "height",) + FUNCTION = 'image_mask_scale_as' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_mask_scale_as(self, scale_as, fit, method, + image=None, mask=None, + ): + if scale_as.shape[0] > 0: + _asimage = tensor2pil(scale_as[0]) + else: + _asimage = tensor2pil(scale_as) + target_width, target_height = _asimage.size + _mask = Image.new('L', size=_asimage.size, color='black') + _image = Image.new('RGB', size=_asimage.size, color='black') + orig_width = 4 + orig_height = 4 + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + ret_images = [] + ret_masks = [] + + if image is not None: + for i in image: + i = torch.unsqueeze(i, 0) + _image = tensor2pil(i).convert('RGB') + orig_width, orig_height = _image.size + _image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler) + ret_images.append(pil2tensor(_image)) + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + m = torch.unsqueeze(m, 0) + _mask = tensor2pil(m).convert('L') + orig_width, orig_height = _mask.size + _mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L') + ret_masks.append(image2mask(_mask)) + if len(ret_images) > 0 and len(ret_masks) > 0: + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,) + elif len(ret_images) > 0 and len(ret_masks) == 0: + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), None, [orig_width, orig_height], target_width, target_height,) + elif len(ret_images) == 0 and len(ret_masks) > 0: + log(f"{NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish') + return (None, torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,) + else: + log(f"Error: {NODE_NAME} skipped, because the available image or mask is not found.", message_type='error') + return (None, None, [orig_width, orig_height], 0, 0,) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageOpacity.md b/docs/LayerUtility%3A ImageOpacity.md new file mode 100644 index 0000000..f38830f --- /dev/null +++ b/docs/LayerUtility%3A ImageOpacity.md @@ -0,0 +1,125 @@ +# Documentation +- Class name: ImageOpacity +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +调整图像不透明度。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- opacity + - 透明度。 + - Comfy dtype: INT + - Python dtype: int + +- invert_mask + - 反转蒙版。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +## Optional + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 处理后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 处理后的蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageOpacity: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + }, + "optional": { + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = 'image_opacity' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_opacity(self, image, opacity, invert_mask, + mask=None, + ): + + ret_images = [] + ret_masks = [] + l_images = [] + l_masks = [] + for l in 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', size=m.size, color='white')) + + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + max_batch = max(len(l_images), len(l_masks)) + + for i in range(max_batch): + _image = l_images[i] if i < len(l_images) else l_images[-1] + _image = tensor2pil(_image) + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + if invert_mask: + _color = Image.new("L", _image.size, color=('white')) + _mask = ImageChops.invert(_mask) + else: + _color = Image.new("L", _image.size, color=('black')) + + alpha = 1 - opacity / 100.0 + ret_mask = Image.blend(_mask, _color, alpha) + R, G, B, = _image.convert('RGB').split() + if invert_mask: + ret_mask = ImageChops.invert(ret_mask) + ret_image = Image.merge('RGBA', (R, G, B, ret_mask)) + + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(ret_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageRemoveAlpha.md b/docs/LayerUtility%3A ImageRemoveAlpha.md new file mode 100644 index 0000000..6b2c3af --- /dev/null +++ b/docs/LayerUtility%3A ImageRemoveAlpha.md @@ -0,0 +1,97 @@ +# Documentation +- Class name: ImageRemoveAlpha +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +移除图片的alpha通道,将图片转换为RGB模式。可选择填充背景以及设置背景颜色。 + +# Input types + +## Required + +- RGBA_image + - 输入的图像,支持RGBA或RGB模式。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- fill_background + - 填充背景。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- background_color + - 背景颜色。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- mask + - 可选输入遮罩。如果有输入遮罩将优先使用, 忽略RGBA_image自带的alpha。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- RGB_image + - 处理后的RGB图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageRemoveAlpha: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "RGBA_image": ("IMAGE", ), # + "fill_background": ("BOOLEAN", {"default": False}), + "background_color": ("STRING", {"default": "#000000"}), + }, + "optional": { + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("RGB_image", ) + FUNCTION = 'image_remove_alpha' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_remove_alpha(self, RGBA_image, fill_background, background_color, mask=None): + + ret_images = [] + + for index, img in enumerate(RGBA_image): + _image = tensor2pil(img) + + if fill_background: + if mask is not None: + m = mask[index].unsqueeze(0) if index < len(mask) else mask[-1].unsqueeze(0) + alpha = tensor2pil(m).convert('L') + elif _image.mode == "RGBA": + alpha = _image.split()[-1] + else: + log(f"Error: {NODE_NAME} skipped, because the input image is not RGBA and mask is None.", + message_type='error') + return (RGBA_image,) + ret_image = Image.new('RGB', size=_image.size, color=background_color) + ret_image.paste(_image, mask=alpha) + ret_images.append(pil2tensor(ret_image)) + + else: + ret_images.append(pil2tensor(tensor2pil(i).convert('RGB'))) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), ) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageRewardFilter.md b/docs/LayerUtility%3A ImageRewardFilter.md new file mode 100644 index 0000000..ff7e4ba --- /dev/null +++ b/docs/LayerUtility%3A ImageRewardFilter.md @@ -0,0 +1,108 @@ +# Documentation +- Class name: ImageRewardFilter +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +对批量图片评分并输出排名靠前的图片。这个节点使用了[ImageReward](https://github.com/THUDM/ImageReward)作为图片评分,感谢原作者。 + +# Input types + +## Required + +- images + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- prompt + - 可选输入。将prompt在此输入将作为依据判定其与图片的符合程度。 + - Comfy dtype: STRING + - Python dtype: str + +- output_num + - 输出的图片数量。此数值应小于图片批量。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- images + - 按评分顺序从高到低输出的批量图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- obsolete_images + - 淘汰的图片。同样按评分顺序从高到低输出。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageRewardFilter: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "images": ("IMAGE", ), + "prompt": ("STRING", {"multiline": False}), + "output_num": ("INT", {"default": 3, "min": 1, "max": 999999, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE",) + RETURN_NAMES = ("images", 'obsolete_images',) + FUNCTION = 'image_reward_filter' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_reward_filter(self, images, prompt, output_num,): + log(f"len(images)= {len(images)}, output_num={output_num}") + if output_num > len(images): + log(f"Error: {NODE_NAME} skipped, because 'output_num' is greater then input images.", message_type='error') + return (images,) + + scores = [] + ret_images = [] + obsolete_images = [] + + if not torch.cuda.is_available() : + device = "cpu" + else: + device = "cuda" + + import ImageReward as RM + reward_model = RM.load("ImageReward-v1.0") + reward_model = reward_model.to(device=device) + + with torch.no_grad(): + for i in range(len(images)): + score = reward_model.score(prompt, tensor2pil(images[i])) + scores.append( + { + "score":score, + "image_index":i + } + ) + scores = sorted(scores, key=lambda s: s['score'], reverse=True) + + for i in range(len(images)): + if i < output_num: + log(f"{NODE_NAME} append image #{i}: {scores[i]['image_index']}, score = {scores[i]['score']}.") + ret_images.append(images[scores[i]['image_index']]) + else: + log(f"{NODE_NAME} obsolete image #{i}: {scores[i]['image_index']}, score = {scores[i]['score']}.") + obsolete_images.append(images[scores[i]['image_index']]) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + + return (ret_images, obsolete_images,) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageScaleByAspectRatio V2.md b/docs/LayerUtility%3A ImageScaleByAspectRatio V2.md new file mode 100644 index 0000000..a5e6ba2 --- /dev/null +++ b/docs/LayerUtility%3A ImageScaleByAspectRatio V2.md @@ -0,0 +1,295 @@ +# Documentation +- Class name: ImageScaleByAspectRatioV2 +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +ImageScaleByAspectRatio的V2升级版 + +# Input types + +## Required + +- aspect_ratio + - 目标宽高比。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - original + - custom + - 1:1 + - 3:2 + - 4:3 + - 16:9 + - 2:3 + - 3:4 + - 9:16 + +- proportional_width + - 自定义宽度比例。 + - Comfy dtype: INT + - Python dtype: int + +- proportional_height + - 自定义高度比例。 + - Comfy dtype: INT + - Python dtype: int + +- fit + - 适配方式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - letterbox + - crop + - fill + +- method + - 采样方式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - lanczos + - bicubic + - hamming + - bilinear + - box + - nearest + +- round_to_multiple + - 四舍五入到倍数。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - 8 + - 16 + - 32 + - 64 + - None + +- scale_to_side + - 允许按长边、短边、宽度、高度或总像素指定尺寸缩放。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - None + - longest + - shortest + - width + - height + - total_pixel(kilo pixel) + +- scale_to_length + - 这里的数值作为scale_to_side指定边的长度, 或者总像素数量(kilo pixels)。 + - Comfy dtype: INT + - Python dtype: int + +## Optional + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 缩放后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 缩放后的蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- original_size + - 原始尺寸。 + - Comfy dtype: BOX + - Python dtype: List[int] + +- width + - 目标宽度。 + - Comfy dtype: INT + - Python dtype: int + +- height + - 目标高度。 + - Comfy dtype: INT + - Python dtype: int + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageScaleByAspectRatioV2: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + ratio_list = ['original', 'custom', '1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16'] + fit_mode = ['letterbox', 'crop', 'fill'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + multiple_list = ['8', '16', '32', '64', 'None'] + scale_to_list = ['None', 'longest', 'shortest', 'width', 'height', 'total_pixel(kilo pixel)'] + return { + "required": { + "aspect_ratio": (ratio_list,), + "proportional_width": ("INT", {"default": 1, "min": 1, "max": 999, "step": 1}), + "proportional_height": ("INT", {"default": 1, "min": 1, "max": 999, "step": 1}), + "fit": (fit_mode,), + "method": (method_mode,), + "round_to_multiple": (multiple_list,), + "scale_to_side": (scale_to_list,), # 是否按长边缩放 + "scale_to_length": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}), + }, + "optional": { + "image": ("IMAGE",), # + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT",) + RETURN_NAMES = ("image", "mask", "original_size", "width", "height",) + FUNCTION = 'image_scale_by_aspect_ratio' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_scale_by_aspect_ratio(self, aspect_ratio, proportional_width, proportional_height, + fit, method, round_to_multiple, scale_to_side, scale_to_length, + image=None, mask = None, + ): + orig_images = [] + orig_masks = [] + orig_width = 0 + orig_height = 0 + target_width = 0 + target_height = 0 + ratio = 1.0 + ret_images = [] + ret_masks = [] + if image is not None: + for i in image: + i = torch.unsqueeze(i, 0) + orig_images.append(i) + orig_width, orig_height = tensor2pil(orig_images[0]).size + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + m = torch.unsqueeze(m, 0) + orig_masks.append(m) + _width, _height = tensor2pil(orig_masks[0]).size + if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height): + log(f"Error: {NODE_NAME} skipped, because the mask is does'nt match image.", message_type='error') + return (None, None, None, 0, 0,) + elif orig_width + orig_height == 0: + orig_width = _width + orig_height = _height + + if orig_width + orig_height == 0: + log(f"Error: {NODE_NAME} skipped, because the image or mask at least one must be input.", message_type='error') + return (None, None, None, 0, 0,) + + if aspect_ratio == 'original': + ratio = orig_width / orig_height + elif aspect_ratio == 'custom': + ratio = proportional_width / proportional_height + else: + s = aspect_ratio.split(":") + ratio = int(s[0]) / int(s[1]) + + # calculate target width and height + if ratio > 1: + if scale_to_side == 'longest': + target_width = scale_to_length + target_height = int(target_width / ratio) + elif scale_to_side == 'shortest': + target_height = scale_to_length + target_width = int(target_height * ratio) + elif scale_to_side == 'width': + target_width = scale_to_length + target_height = int(target_width / ratio) + elif scale_to_side == 'height': + target_height = scale_to_length + target_width = int(target_height * ratio) + elif scale_to_side == 'total_pixel(kilo pixel)': + target_width = math.sqrt(ratio * scale_to_length * 1000) + target_height = target_width / ratio + target_width = int(target_width) + target_height = int(target_height) + else: + target_width = orig_width + target_height = int(target_width / ratio) + else: + if scale_to_side == 'longest': + target_height = scale_to_length + target_width = int(target_height * ratio) + elif scale_to_side == 'shortest': + target_width = scale_to_length + target_height = int(target_width / ratio) + elif scale_to_side == 'width': + target_width = scale_to_length + target_height = int(target_width / ratio) + elif scale_to_side == 'height': + target_height = scale_to_length + target_width = int(target_height * ratio) + elif scale_to_side == 'total_pixel(kilo pixel)': + target_width = math.sqrt(ratio * scale_to_length * 1000) + target_height = target_width / ratio + target_width = int(target_width) + target_height = int(target_height) + else: + target_height = orig_height + target_width = int(target_height * ratio) + + if round_to_multiple != 'None': + multiple = int(round_to_multiple) + target_width = num_round_to_multiple(target_width, multiple) + target_height = num_round_to_multiple(target_height, multiple) + + _mask = Image.new('L', size=(target_width, target_height), color='black') + _image = Image.new('RGB', size=(target_width, target_height), color='black') + + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + if len(orig_images) > 0: + for i in orig_images: + _image = tensor2pil(i).convert('RGB') + _image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler) + ret_images.append(pil2tensor(_image)) + if len(orig_masks) > 0: + for m in orig_masks: + _mask = tensor2pil(m).convert('L') + _mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L') + ret_masks.append(image2mask(_mask)) + if len(ret_images) > 0 and len(ret_masks) >0: + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),[orig_width, orig_height], target_width, target_height,) + elif len(ret_images) > 0 and len(ret_masks) == 0: + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), None, [orig_width, orig_height], target_width, target_height,) + elif len(ret_images) == 0 and len(ret_masks) > 0: + log(f"{NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish') + return (None, torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,) + else: + log(f"Error: {NODE_NAME} skipped, because the available image or mask is not found.", message_type='error') + return (None, None, None, 0, 0,) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageScaleByAspectRatio.md b/docs/LayerUtility%3A ImageScaleByAspectRatio.md new file mode 100644 index 0000000..a235c74 --- /dev/null +++ b/docs/LayerUtility%3A ImageScaleByAspectRatio.md @@ -0,0 +1,267 @@ +# Documentation +- Class name: ImageScaleByAspectRatio +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +将图像或遮罩按宽高比缩放。可设置将缩放后的尺寸按8或者16的倍数取整,可按长边尺寸缩放。 + +# Input types + +## Required + +- aspect_ratio + - 宽高比。此处提供了几个常见画幅比例。也可选"original"保持原图比例或者"custom"自定义比例。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - original + - custom + - 1:1 + - 3:2 + - 4:3 + - 16:9 + - 2:3 + - 3:4 + - 9:16 + +- proportional_width + - 比例宽。如果aspect_ratio选项不是"custom",此处设置将被忽略。 + - Comfy dtype: INT + - Python dtype: int + +- proportional_height + - 比例高。如果aspect_ratio选项不是"custom",此处设置将被忽略。 + - Comfy dtype: INT + - Python dtype: int + +- fit + - 缩放画幅宽高比模式。有3种模式可以选择, letterbox模式保留完整的画幅,空白处用黑色补足;crop模式保留完整的短边,长边超出部分将被切除;fill模式不保持画幅比例,宽高各自填满画面。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - letterbox + - crop + - fill + +- method + - 缩放的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - lanczos + - bicubic + - hamming + - bilinear + - box + - nearest + +- round_to_multiple + - 倍数取整。例如设置为8,宽和高将强制设置为8的倍数。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - 8 + - 16 + - 32 + - 64 + - None + +- scale_to_longest_side + - 是否按长边尺寸缩放。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- longest_side + - scale_to_longest_side被设置为True时,此项将作为是图像长边的长度。 + - Comfy dtype: INT + - Python dtype: int + +## Optional + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 缩放后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 缩放后的蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- original_size + - 原始尺寸。 + - Comfy dtype: BOX + - Python dtype: List[int] + +- width + - 目标宽度。 + - Comfy dtype: INT + - Python dtype: int + +- height + - 目标高度。 + - Comfy dtype: INT + - Python dtype: int + +# Usage tips +- Infra type: CPU + +# Source code + +```python +class ImageScaleByAspectRatio: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + ratio_list = ['original', 'custom', '1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16'] + fit_mode = ['letterbox', 'crop', 'fill'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + multiple_list = ['8', '16', '32', '64', 'None'] + + return { + "required": { + "aspect_ratio": (ratio_list,), + "proportional_width": ("INT", {"default": 2, "min": 1, "max": 999, "step": 1}), + "proportional_height": ("INT", {"default": 1, "min": 1, "max": 999, "step": 1}), + "fit": (fit_mode,), + "method": (method_mode,), + "round_to_multiple": (multiple_list,), + "scale_to_longest_side": ("BOOLEAN", {"default": False}), # 是否按长边缩放 + "longest_side": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}), + }, + "optional": { + "image": ("IMAGE",), # + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT",) + RETURN_NAMES = ("image", "mask", "original_size", "width", "height",) + FUNCTION = 'image_scale_by_aspect_ratio' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_scale_by_aspect_ratio(self, aspect_ratio, proportional_width, proportional_height, + fit, method, round_to_multiple, scale_to_longest_side, longest_side, + image=None, mask = None, + ): + orig_images = [] + orig_masks = [] + orig_width = 0 + orig_height = 0 + target_width = 0 + target_height = 0 + ratio = 1.0 + ret_images = [] + ret_masks = [] + if image is not None: + for i in image: + i = torch.unsqueeze(i, 0) + orig_images.append(i) + orig_width, orig_height = tensor2pil(orig_images[0]).size + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + m = torch.unsqueeze(m, 0) + orig_masks.append(m) + _width, _height = tensor2pil(orig_masks[0]).size + if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height): + log(f"Error: {NODE_NAME} skipped, because the mask is does'nt match image.", message_type='error') + return (None, None, None, 0, 0,) + elif orig_width + orig_height == 0: + orig_width = _width + orig_height = _height + + if orig_width + orig_height == 0: + log(f"Error: {NODE_NAME} skipped, because the image or mask at least one must be input.", message_type='error') + return (None, None, None, 0, 0,) + + if aspect_ratio == 'original': + ratio = orig_width / orig_height + elif aspect_ratio == 'custom': + ratio = proportional_width / proportional_height + else: + s = aspect_ratio.split(":") + ratio = int(s[0]) / int(s[1]) + + # calculate target width and height + if orig_width > orig_height: + if scale_to_longest_side: + target_width = longest_side + else: + target_width = orig_width + target_height = int(target_width / ratio) + else: + if scale_to_longest_side: + target_height = longest_side + else: + target_height = orig_height + target_width = int(target_height * ratio) + + if ratio < 1: + if scale_to_longest_side: + _r = longest_side / target_height + target_height = longest_side + else: + _r = orig_height / target_height + target_height = orig_height + target_width = int(target_width * _r) + + if round_to_multiple != 'None': + multiple = int(round_to_multiple) + target_width = num_round_to_multiple(target_width, multiple) + target_height = num_round_to_multiple(target_height, multiple) + + _mask = Image.new('L', size=(target_width, target_height), color='black') + _image = Image.new('RGB', size=(target_width, target_height), color='black') + + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + if len(orig_images) > 0: + for i in orig_images: + _image = tensor2pil(i).convert('RGB') + _image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler) + ret_images.append(pil2tensor(_image)) + if len(orig_masks) > 0: + for m in orig_masks: + _mask = tensor2pil(m).convert('L') + _mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L') + ret_masks.append(image2mask(_mask)) + if len(ret_images) > 0 and len(ret_masks) >0: + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),[orig_width, orig_height], target_width, target_height,) + elif len(ret_images) > 0 and len(ret_masks) == 0: + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), None,[orig_width, orig_height], target_width, target_height,) + elif len(ret_images) == 0 and len(ret_masks) > 0: + log(f"{NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish') + return (None, torch.cat(ret_masks, dim=0),[orig_width, orig_height], target_width, target_height,) + else: + log(f"Error: {NODE_NAME} skipped, because the available image or mask is not found.", message_type='error') + return (None, None, None, 0, 0,) \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageScaleRestore V2.md b/docs/LayerUtility%3A ImageScaleRestore V2.md new file mode 100644 index 0000000..9437afb --- /dev/null +++ b/docs/LayerUtility%3A ImageScaleRestore V2.md @@ -0,0 +1,213 @@ +# Documentation +- Class name: ImageScaleRestoreV2 +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +V2 将图像或遮罩按宽高比缩放。可设置将缩放后的尺寸按8或者16的倍数取整,可按长边尺寸缩放。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- scale + - 缩放比例。 + - Comfy dtype: FLOAT + - Python dtype: float + +- method + - 采样方式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - lanczos + - bicubic + - hamming + - bilinear + - box + - nearest + +- scale_by + - 缩放方式。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - by_scale + - longest + - shortest + - width + - height + - total_pixel(kilo pixel) + +- scale_by_length + - 缩放长度。 + - Comfy dtype: INT + - Python dtype: int + +## Optional + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- original_size + - 原始尺寸。 + - Comfy dtype: BOX + - Python dtype: List[int] + +# Output types + +- image + - 缩放后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 缩放后的蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- original_size + - 原始尺寸。 + - Comfy dtype: BOX + - Python dtype: List[int] + +- width + - 目标宽度。 + - Comfy dtype: INT + - Python dtype: int + +- height + - 目标高度。 + - Comfy dtype: INT + - Python dtype: int + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageScaleRestoreV2: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + scale_by_list = ['by_scale', 'longest', 'shortest', 'width', 'height', 'total_pixel(kilo pixel)'] + return { + "required": { + "image": ("IMAGE", ), # + "scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "method": (method_mode,), + "scale_by": (scale_by_list,), # 是否按长边缩放 + "scale_by_length": ("INT", {"default": 1024, "min": 4, "max": 99999999, "step": 1}), + }, + "optional": { + "mask": ("MASK",), # + "original_size": ("BOX",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT") + RETURN_NAMES = ("image", "mask", "original_size", "width", "height",) + FUNCTION = 'image_scale_restore' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_scale_restore(self, image, scale, method, + scale_by, scale_by_length, + mask = None, original_size = None + ): + + l_images = [] + l_masks = [] + ret_images = [] + ret_masks = [] + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + max_batch = max(len(l_images), len(l_masks)) + + orig_width, orig_height = tensor2pil(l_images[0]).size + if original_size is not None: + target_width = original_size[0] + target_height = original_size[1] + else: + target_width = int(orig_width * scale) + target_height = int(orig_height * scale) + if scale_by == 'longest': + if orig_width > orig_height: + target_width = scale_by_length + target_height = int(target_width * orig_height / orig_width) + else: + target_height = scale_by_length + target_width = int(target_height * orig_width / orig_height) + if scale_by == 'shortest': + if orig_width < orig_height: + target_width = scale_by_length + target_height = int(target_width * orig_height / orig_width) + else: + target_height = scale_by_length + target_width = int(target_height * orig_width / orig_height) + if scale_by == 'width': + target_width = scale_by_length + target_height = int(target_width * orig_height / orig_width) + if scale_by == 'height': + target_height = scale_by_length + target_width = int(target_height * orig_width / orig_height) + if scale_by == 'total_pixel(kilo pixel)': + r = orig_width / orig_height + target_width = math.sqrt(r * scale_by_length * 1000) + target_height = target_width / r + target_width = int(target_width) + target_height = int(target_height) + if target_width < 4: + target_width = 4 + if target_height < 4: + target_height = 4 + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + for i in range(max_batch): + + _image = l_images[i] if i < len(l_images) else l_images[-1] + + _canvas = tensor2pil(_image).convert('RGB') + ret_image = _canvas.resize((target_width, target_height), resize_sampler) + ret_mask = Image.new('L', size=ret_image.size, color='white') + if mask is not None: + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + ret_mask = _mask.resize((target_width, target_height), resize_sampler) + + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(ret_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageScaleRestore.md b/docs/LayerUtility%3A ImageScaleRestore.md new file mode 100644 index 0000000..dc5dd31 --- /dev/null +++ b/docs/LayerUtility%3A ImageScaleRestore.md @@ -0,0 +1,222 @@ +# Documentation +- Class name: ImageScaleByAspectRatio +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +将图像或遮罩按宽高比缩放。可设置将缩放后的尺寸按8或者16的倍数取整,可按长边尺寸缩放。 + +# Input types + +## Required + +- aspect_ratio + - 宽高比。此处提供了几个常见画幅比例。也可选"original"保持原图比例或者"custom"自定义比例。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - original + - custom + - 1:1 + - 3:2 + - 4:3 + - 16:9 + - 2:3 + - 3:4 + - 9:16 + +- proportional_width + - 比例宽。如果aspect_ratio选项不是"custom",此处设置将被忽略。 + - Comfy dtype: INT + - Python dtype: int + +- proportional_height + - 比例高。如果aspect_ratio选项不是"custom",此处设置将被忽略。 + - Comfy dtype: INT + - Python dtype: int + +- fit + - 缩放画幅宽高比模式。有3种模式可以选择, letterbox模式保留完整的画幅,空白处用黑色补足;crop模式保留完整的短边,长边超出部分将被切除;fill模式不保持画幅比例,宽高各自填满画面。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - letterbox + - crop + - fill + +- method + - 缩放的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - lanczos + - bicubic + - hamming + - bilinear + - box + - nearest + +- round_to_multiple + - 倍数取整。例如设置为8,宽和高将强制设置为8的倍数。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - 8 + - 16 + - 32 + - 64 + - None + +- scale_to_longest_side + - 是否按长边尺寸缩放。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- longest_side + - scale_to_longest_side被设置为True时,此项将作为是图像长边的长度。 + - Comfy dtype: INT + - Python dtype: int + +## Optional + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 缩放后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 缩放后的蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- original_size + - 原始尺寸。 + - Comfy dtype: BOX + - Python dtype: List[int] + +- width + - 目标宽度。 + - Comfy dtype: INT + - Python dtype: int + +- height + - 目标高度。 + - Comfy dtype: INT + - Python dtype: int + +# Usage tips +- Infra type: CPU + +# Source code + +```python +class ImageScaleRestore: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + return { + "required": { + "image": ("IMAGE", ), # + "scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "method": (method_mode,), + "scale_by_longest_side": ("BOOLEAN", {"default": False}), # 是否按长边缩放 + "longest_side": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}), + }, + "optional": { + "mask": ("MASK",), # + "original_size": ("BOX",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT") + RETURN_NAMES = ("image", "mask", "original_size", "width", "height",) + FUNCTION = 'image_scale_restore' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_scale_restore(self, image, scale, method, + scale_by_longest_side, longest_side, + mask = None, original_size = None + ): + + l_images = [] + l_masks = [] + ret_images = [] + ret_masks = [] + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + max_batch = max(len(l_images), len(l_masks)) + + orig_width, orig_height = tensor2pil(l_images[0]).size + if original_size is not None: + target_width = original_size[0] + target_height = original_size[1] + else: + target_width = int(orig_width * scale) + target_height = int(orig_height * scale) + if scale_by_longest_side: + if orig_width > orig_height: + target_width = longest_side + target_height = int(target_width * orig_height / orig_width) + else: + target_height = longest_side + target_width = int(target_height * orig_width / orig_height) + if target_width < 4: + target_width = 4 + if target_height < 4: + target_height = 4 + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + for i in range(max_batch): + + _image = l_images[i] if i < len(l_images) else l_images[-1] + + _canvas = tensor2pil(_image).convert('RGB') + ret_image = _canvas.resize((target_width, target_height), resize_sampler) + ret_mask = Image.new('L', size=ret_image.size, color='white') + if mask is not None: + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + ret_mask = _mask.resize((target_width, target_height), resize_sampler) + + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(ret_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A ImageShift.md b/docs/LayerUtility%3A ImageShift.md new file mode 100644 index 0000000..dee598e --- /dev/null +++ b/docs/LayerUtility%3A ImageShift.md @@ -0,0 +1,151 @@ +# Documentation +- Class name: ImageShift +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +使图片产生位移。此节点支持位移接缝遮罩的输出,方便制作连续贴图。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- shift_x + - X轴移动像素数。 + - Comfy dtype: INT + - Python dtype: int + +- shift_y + - Y轴移动像素数。 + - Comfy dtype: INT + - Python dtype: int + +- cyclic + - 是否循环重复。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- background_color + - 背景颜色。 + - Comfy dtype: STRING + - Python dtype: str + +- border_mask_width + - 边框宽度。 + - Comfy dtype: INT + - Python dtype: int + +- border_mask_blur + - 边框模糊。 + - Comfy dtype: INT + - Python dtype: int + +## Optional + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 移动后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 移动后的蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- border_mask + - 边框蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class ImageShift: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "shift_x": ("INT", {"default": 256, "min": -9999, "max": 9999, "step": 1}), + "shift_y": ("INT", {"default": 256, "min": -9999, "max": 9999, "step": 1}), + "cyclic": ("BOOLEAN", {"default": True}), # 是否循环重复 + "background_color": ("STRING", {"default": "#000000"}), + "border_mask_width": ("INT", {"default": 20, "min": 0, "max": 999, "step": 1}), + "border_mask_blur": ("INT", {"default": 12, "min": 0, "max": 999, "step": 1}), + }, + "optional": { + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "MASK",) + RETURN_NAMES = ("image", "mask", "border_mask") + FUNCTION = 'image_shift' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_shift(self, image, shift_x, shift_y, + cyclic, background_color, + border_mask_width, border_mask_blur, + mask=None + ): + + ret_images = [] + ret_masks = [] + ret_border_masks = [] + + l_images = [] + l_masks = [] + + for l in 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', size=m.size, color='white')) + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + shift_x, shift_y = -shift_x, -shift_y + for i in range(len(l_images)): + _image = l_images[i] + _canvas = tensor2pil(_image).convert('RGB') + _mask = l_masks[i] if len(l_masks) < i else l_masks[-1] + _border = Image.new('L', size=_canvas.size, color='black') + _border = draw_border(_border, border_width=border_mask_width, color='#FFFFFF') + _border = _border.resize(_canvas.size) + _canvas = shift_image(_canvas, shift_x, shift_y, background_color=background_color, cyclic=cyclic) + _mask = shift_image(_mask, shift_x, shift_y, background_color='#000000', cyclic=cyclic) + _border = shift_image(_border, shift_x, shift_y, background_color='#000000', cyclic=cyclic) + _border = gaussian_blur(_border, border_mask_blur) + + ret_images.append(pil2tensor(_canvas)) + ret_masks.append(image2mask(_mask)) + ret_border_masks.append(image2mask(_border)) + + log(f"{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_border_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A LaMa.md b/docs/LayerUtility%3A LaMa.md new file mode 100644 index 0000000..c153801 --- /dev/null +++ b/docs/LayerUtility%3A LaMa.md @@ -0,0 +1,167 @@ +# Documentation +- Class name: LaMa +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +根据图像遮罩擦除物体。本节点是对[IOPaint](https://www.iopaint.com/)的封装,由 SOTA AI 模型提供支持, 感谢原作者。 +提供LaMa, [LDM](https://github.com/CompVis/latent-diffusion), [ZITS](https://github.com/DQiaole/ZITS_inpainting),[MAT](https://github.com/fenglinglwb/MAT), [FcF](https://github.com/SHI-Labs/FcF-Inpainting), [Manga](https://github.com/msxie92/MangaInpainting) 模型以及 SPREAD 擦除方法。请查看链接了解各个模型的介绍。 +请下载模型文件 [lama models(百度网盘)](https://pan.baidu.com/s/1LllR9TJHP1G9uEwWT3Mvkg?pwd=tvzv) 或者 [lama models(Google Drive)](https://drive.google.com/drive/folders/1Aq0a4sybb3SRxi7j1e1_ZbBRjaWDdP9e?usp=sharing), 将文件放到ComfyUI/models/lama + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- lama_model + - LaMa模型。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - lama + - ldm + - zits + - mat + - fcf + - manga + - spread + +- device + - 在正确安装torch和Nvidia CUDA驱动程序后,使用cuda将明显提高运行速度。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - cuda + - cpu + +- invert_mask + - 反转蒙版。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- mask_grow + - 遮罩扩张幅度。正值是向外扩张,负值是向内收缩。 + - Comfy dtype: INT + - Python dtype: int + +- mask_blur + - 蒙版模糊。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- image + - 修复后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class LaMa: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + model_list = ['lama', 'ldm', 'zits', 'mat', 'fcf', 'manga', 'spread'] + device_list = ['cuda', 'cpu'] + return { + "required": { + "image": ("IMAGE", ), # + "mask": ("MASK",), # + "lama_model": (model_list,), + "device": (device_list,), + "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask + "mask_grow": ("INT", {"default": 25, "min": -255, "max": 255, "step": 1}), + "mask_blur": ("INT", {"default": 8, "min": -255, "max": 255, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'lama' + CATEGORY = '😺dzNodes/LayerUtility' + + def lama(self, image, mask, lama_model, device, invert_mask, mask_grow, mask_blur): + + l_images = [] + l_masks = [] + ret_images = [] + + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if mask is not None: + if mask.dim() == 2: + layer_mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (image,) + + max_batch = max(len(l_images), len(l_masks)) + + for i in range(max_batch): + _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] + if mask_grow or mask_blur: + _mask = tensor2pil(expand_mask(image2mask(_mask), mask_grow, mask_blur)) + + if lama_model == 'spread': + ret_image = pixel_spread(tensor2pil(_image).convert('RGB'), ImageChops.invert(_mask.convert('RGB'))) + else: + temp_dir = os.path.join(folder_paths.get_temp_directory(), generate_random_name('_lama_', '_temp', 16)) + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir) + image_dir = os.path.join(temp_dir, 'image') + mask_dir = os.path.join(temp_dir, 'mask') + result_dir = os.path.join(temp_dir, 'result') + try: + os.makedirs(image_dir) + os.makedirs(mask_dir) + os.makedirs(result_dir) + except Exception as e: + print(e) + log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.", message_type='error') + return (image, ) + file_name = os.path.join(generate_random_name('lama_', '_temp', 16) + '.png') + try: + tensor2pil(_image).save(os.path.join(image_dir, file_name)) + _mask.save(os.path.join(mask_dir, file_name)) + except IOError as e: + print(e) + log(f"Error: {NODE_NAME} skipped, because unable to create temporary file.", message_type='error') + return (image, ) + # process + from iopaint import cli + cli.run(model=lama_model, device=device, image=Path(image_dir), mask=Path(mask_dir), output=Path(result_dir)) + ret_image = check_image_file(os.path.join(result_dir, file_name), 500) + shutil.rmtree(temp_dir) + ret_images.append(pil2tensor(ret_image)) + + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A LayerImageTransform.md b/docs/LayerUtility%3A LayerImageTransform.md new file mode 100644 index 0000000..84cfabb --- /dev/null +++ b/docs/LayerUtility%3A LayerImageTransform.md @@ -0,0 +1,162 @@ +# Documentation +- Class name: LayerImageTransform +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +这个节点用于单独对layer_image进行变换,可改变大小,旋转,改变长宽比以及镜像翻转。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- x + - 坐标x值。 + - Comfy dtype: INT + - Python dtype: int + +- y + - 坐标y值。 + - Comfy dtype: INT + - Python dtype: int + +- mirror + - 镜像翻转。提供2种翻转模式, 水平翻转和垂直翻转。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - None + - horizontal + - vertical + +- scale + - 图层放大倍数,1.0 表示原大。 + - Comfy dtype: FLOAT + - Python dtype: float + +- aspect_ratio + - 图层长宽比。1.0 是原始比例,大于此值表示拉长,小于此值表示压扁。 + - Comfy dtype: FLOAT + - Python dtype: float + +- rotate + - 图层旋转度数。 + - Comfy dtype: FLOAT + - Python dtype: float + +- transform_method + - 用于图层放大和旋转的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。不同的采样方法会影响合成的画质和画面处理时间。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - lanczos + - bicubic + - hamming + - bilinear + - box + - nearest + +- anti_aliasing + - 抗锯齿,范围从0-16,数值越大,锯齿越不明显。过高的数值将显著降低节点的处理速度。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- image + - 变换后的图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class LayerImageTransform: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + mirror_mode = ['None', 'horizontal', 'vertical'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + return { + "required": { + "image": ("IMAGE",), # + "x": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}), + "y": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}), + "mirror": (mirror_mode,), # 镜像翻转 + "scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "aspect_ratio": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "rotate": ("FLOAT", {"default": 0, "min": -999999, "max": 999999, "step": 0.01}), + "transform_method": (method_mode,), + "anti_aliasing": ("INT", {"default": 2, "min": 0, "max": 16, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'layer_image_transform' + CATEGORY = '😺dzNodes/LayerUtility' + + def layer_image_transform(self, image, x, y, mirror, scale, aspect_ratio, rotate, + transform_method, anti_aliasing, + ): + + l_images = [] + l_masks = [] + ret_images = [] + + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + + for i in range(len(l_images)): + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _image = tensor2pil(layer_image).convert('RGB') + if i < len(l_masks): + _mask = l_masks[i] + else: + _mask = Image.new('L', size=_image.size, color='white') + _image_canvas = Image.new('RGB', size=_image.size, color='black') + _mask_canvas = Image.new('L', size=_mask.size, color='black') + orig_layer_width = _image.width + orig_layer_height = _image.height + target_layer_width = int(orig_layer_width * scale) + target_layer_height = int(orig_layer_height * scale * aspect_ratio) + # mirror + if mirror == 'horizontal': + _image = _image.transpose(Image.FLIP_LEFT_RIGHT) + _mask = _mask.transpose(Image.FLIP_LEFT_RIGHT) + elif mirror == 'vertical': + _image = _image.transpose(Image.FLIP_TOP_BOTTOM) + _mask = _mask.transpose(Image.FLIP_TOP_BOTTOM) + # scale + _image = _image.resize((target_layer_width, target_layer_height)) + _mask = _mask.resize((target_layer_width, target_layer_height)) + # rotate + _image, _mask, _ = image_rotate_extend_with_alpha(_image, rotate, _mask, transform_method, anti_aliasing) + # composit layer + paste_x = (orig_layer_width - _image.width) // 2 + x + paste_y = (orig_layer_height - _image.height) // 2 + y + _image_canvas.paste(_image, (paste_x, paste_y)) + _mask_canvas.paste(_mask, (paste_x, paste_y)) + if tensor2pil(layer_image).mode == 'RGBA': + _image_canvas = RGB2RGBA(_image_canvas, _mask_canvas) + + ret_images.append(pil2tensor(_image_canvas)) + + log(f"{NODE_NAME} Processed {len(l_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A LayerMaskTransform.md b/docs/LayerUtility%3A LayerMaskTransform.md new file mode 100644 index 0000000..ffe5754 --- /dev/null +++ b/docs/LayerUtility%3A LayerMaskTransform.md @@ -0,0 +1,148 @@ +# Documentation +- Class name: LayerMaskTransform +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +与LayerImageTransform类似,这个节点用于单独对layer_mask进行变换,可改变大小,旋转,改变长宽比以及镜像翻转。 + +# Input types + +## Required + +- mask + - 蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +- x + - 坐标x值。 + - Comfy dtype: INT + - Python dtype: int + +- y + - 坐标y值。 + - Comfy dtype: INT + - Python dtype: int + +- mirror + - 镜像翻转。提供2种翻转模式, 水平翻转和垂直翻转。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - None + - horizontal + - vertical + +- scale + - 图层放大倍数,1.0 表示原大。 + - Comfy dtype: FLOAT + - Python dtype: float + +- aspect_ratio + - 图层长宽比。1.0 是原始比例,大于此值表示拉长,小于此值表示压扁。 + - Comfy dtype: FLOAT + - Python dtype: float + +- rotate + - 图层旋转度数。 + - Comfy dtype: FLOAT + - Python dtype: float + +- transform_method + - 用于图层放大和旋转的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。不同的采样方法会影响合成的画质和画面处理时间。 + - Comfy dtype: STRING_ONEOF + - Python dtype: str + - Options: + - lanczos + - bicubic + - hamming + - bilinear + - box + - nearest + +- anti_aliasing + - 抗锯齿,范围从0-16,数值越大,锯齿越不明显。过高的数值将显著降低节点的处理速度。 + - Comfy dtype: INT + - Python dtype: int + +# Output types + +- mask + - 变换后的蒙版。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class LayerMaskTransform: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + mirror_mode = ['None', 'horizontal', 'vertical'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + return { + "required": { + "mask": ("MASK",), # + "x": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}), + "y": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}), + "mirror": (mirror_mode,), # 镜像翻转 + "scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "aspect_ratio": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "rotate": ("FLOAT", {"default": 0, "min": -999999, "max": 999999, "step": 0.01}), + "transform_method": (method_mode,), + "anti_aliasing": ("INT", {"default": 2, "min": 0, "max": 16, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'layer_mask_transform' + CATEGORY = '😺dzNodes/LayerUtility' + + def layer_mask_transform(self, mask, x, y, mirror, scale, aspect_ratio, rotate, + transform_method, anti_aliasing, + ): + + l_masks = [] + ret_masks = [] + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + l_masks.append(torch.unsqueeze(m, 0)) + for i in range(len(l_masks)): + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + _mask = tensor2pil(_mask).convert('L') + _mask_canvas = Image.new('L', size=_mask.size, color='black') + orig_width = _mask.width + orig_height = _mask.height + target_layer_width = int(orig_width * scale) + target_layer_height = int(orig_height * scale * aspect_ratio) + # mirror + if mirror == 'horizontal': + _mask = _mask.transpose(Image.FLIP_LEFT_RIGHT) + elif mirror == 'vertical': + _mask = _mask.transpose(Image.FLIP_TOP_BOTTOM) + # scale + _mask = _mask.resize((target_layer_width, target_layer_height)) + # rotate + _, _mask, _ = image_rotate_extend_with_alpha(_mask.convert('RGB'), rotate, _mask, transform_method, anti_aliasing) + paste_x = (orig_width - _mask.width) // 2 + x + paste_y = (orig_height - _mask.height) // 2 + y + # composit layer + _mask_canvas.paste(_mask, (paste_x, paste_y)) + + ret_masks.append(image2mask(_mask_canvas)) + + log(f"{NODE_NAME} Processed {len(l_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A LoadPSD.md b/docs/LayerUtility%3A LoadPSD.md new file mode 100644 index 0000000..1bd79e7 --- /dev/null +++ b/docs/LayerUtility%3A LoadPSD.md @@ -0,0 +1,152 @@ +# Documentation +- Class name: LoadPSD +- Category: 😺dzNodes/LayerUtility/SystemIO +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +加载PSD格式文件,并导出图层。 注意这个节点需要安装psd_tools依赖包,如果安装psd_tool中出现ModuleNotFoundError: No module named 'docopt'错误,请下载[docopt的whl](https://www.piwheels.org/project/docopt/)手动安装。 + +# Input types + +## Required + +- image + - 这里列出了ComfyUI/input下的*.psd文件,之前加载过的psd图片可以从这里选择。 + - Comfy dtype: FILE + - 支持上传 PSD 文件。 + +- file_path + - psd文件的完整路径以及文件名。 + - Comfy dtype: STRING + - Python dtype: str + +- include_hidden_layer + - 是否包含隐藏图层。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- find_layer_by + - 查找图层的方法,可选择按图层索引编号或者图层名称查找。图层组被作为一个图层对待。 + - 可选值: ["layer_index", "layer_name"] + - Comfy dtype: STRING + - Python dtype: str + +- layer_index + - 图层索引编号,0是最下面的图层,依次递增。如果include_hidden_layer设置为false,隐藏的图层不计入。设为-1则输出最上层的图层。 + - Comfy dtype: INT + - Python dtype: int + +- layer_name + - 图层名称。注意大小写和标点符号必须完全匹配。 + - Comfy dtype: STRING + - Python dtype: str + +# Output types + +- flat_image + - 平面图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- layer_image + - 图层图像。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- all_layers + - 所有图层。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class LoadPSD(LoadImage): + @classmethod + def INPUT_TYPES(s): + input_dir = folder_paths.get_input_directory() + files = [f for f in os.listdir(input_dir) if os.path.isfile( + os.path.join(input_dir, f)) and f.endswith(".psd")] + fine_layer_method = ["layer_index", "layer_name"] + return {"required":{ + "image": (sorted(files), {"image_upload": True}), + "file_path": ("STRING", {"default": ""}), + "include_hidden_layer": ("BOOLEAN", {"default": False}), + "find_layer_by": (fine_layer_method,), + "layer_index": ("INT", {"default": 0, "min": -1, "max": 999, "step": 1}), + "layer_name": ("STRING", {"default": ""}), + }, + "optional": { + } + } + + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE",) + RETURN_NAMES = ("flat_image", "layer_image", "all_layers",) + FUNCTION = "load_psd" + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + + def load_psd(self, image, file_path, include_hidden_layer, layer_index, find_layer_by, layer_name,): + + from psd_tools import PSDImage + from psd_tools.api.layers import Layer + + NODE_NAME = 'LoadPSD' + number_of_layers = 1 + layer_image = [] + all_layers = [] + if file_path == "": + psd_file_path = folder_paths.get_annotated_filepath(image) + else: + psd_file_path = folder_paths.get_annotated_filepath(file_path) + flat_image = Image.open(psd_file_path).convert("RGB") + width, height = flat_image.size + + if image.endswith(".psd"): + from psd_tools import PSDImage + from psd_tools.api.layers import Layer + log(f"{NODE_NAME} -> Loading PSD file: {psd_file_path}") + psd_image = PSDImage.open(psd_file_path) + layers = [] + for layer in psd_image: + if include_hidden_layer: + if not layer.is_visible(): + layer.visible = True + layers.append(layer) + else: + if layer.is_visible(): + layers.append(layer) + + number_of_layers = len(layers) + for i in range(number_of_layers): + layer_canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + layer_canvas.paste(layers[i].composite(), layers[i].bbox) + all_layers.append(pil2tensor(layer_canvas)) + if find_layer_by == "layer_name": + if layers[i].name == layer_name: + layer_image.append(pil2tensor(layer_canvas)) + log(f"{NODE_NAME} -> Layer {i} : {layer.name} found.") + elif find_layer_by == "layer_index": + if i == layer_index: + layer_image.append(pil2tensor(layer_canvas)) + log(f"{NODE_NAME} -> Layer {i} : {layer.name} found.") + + text = "Layer Not Found!" + font_file = list(FONT_DICT.values())[0] + empty_layer_image = generate_text_image(flat_image.width, flat_image.height, text, font_file, font_color="#F01000") + if layer_image == []: + if layer_index == -1: + layer_image.append(all_layers[-1]) + elif find_layer_by == "layer_name": + log(f'{NODE_NAME} -> Layer "{layer_name}" not found, top layer will be output.', message_type="warning") + elif find_layer_by == "layer_index": + log(f'{NODE_NAME} -> Layer index {layer_index} not found, top layer will be output.', message_type="warning") + layer_image.append(pil2tensor(empty_layer_image)) + else: + layer_image.append(pil2tensor(flat_image)) + all_layers.append(pil2tensor(flat_image)) + + return (torch.cat([pil2tensor(flat_image),], dim=0), torch.cat(layer_image, dim=0), torch.cat(all_layers, dim=0),) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A PrintInfo.md b/docs/LayerUtility%3A PrintInfo.md new file mode 100644 index 0000000..f37c901 --- /dev/null +++ b/docs/LayerUtility%3A PrintInfo.md @@ -0,0 +1,73 @@ +# Documentation +- Class name: PrintInfo +- Category: 😺dzNodes/LayerUtility/Data +- Output node: True +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +用于给工作流调试提供辅助。当运行时,任何接上这个节点的对象的属性将被打印到控制台。 + +这个节点允许任意类型的输入。 + +# Input types + +## Required + +- anything + - 任意输入。 + - Comfy dtype: any + - Python dtype: any + +# Output types + +- text + - 文本。 + - Comfy dtype: STRING + - Python dtype: str + +# Usage tips +- Infra type: CPU + +# Source code +```python +class PrintInfo: + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "anything": (any, {}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "print_info" + CATEGORY = '😺dzNodes/LayerUtility/Data' + OUTPUT_NODE = True + + def print_info(self, anything=None): + value = f'PrintInfo:\nInput type = {type(anything)}' + if isinstance(anything, torch.Tensor): + value += f"\n Input dim = {anything.dim()}, shape[0] = {anything.shape[0]} \n" + for i in range(anything.shape[0]): + t = anything[i] + image = tensor2pil(t) + value += f'\n index {i}: Image.size = {image.size}, Image.mode = {image.mode}, dim = {t.dim()}, ' + for j in range(t.dim()): + value += f'shape[{j}] = {t.shape[j]}, ' + # value += f'\n {t} \n' + elif anything is not None: + try: + value = value + json.dumps(anything) + "\n" + except Exception: + try: + value = value + str(anything) + "\n" + except Exception: + value = 'source exists, but could not be serialized.' + else: + value = 'source does not exist.' + + log(value) + + return (value,) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A PromptEmbellish.md b/docs/LayerUtility%3A PromptEmbellish.md new file mode 100644 index 0000000..79b4a58 --- /dev/null +++ b/docs/LayerUtility%3A PromptEmbellish.md @@ -0,0 +1,137 @@ +# Documentation +- Class name: PromptEmbellish +- Category: 😺dzNodes/LayerUtility/Prompt +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +输入简单的提示词,输出经过润色的提示词,支持输入图片作为参考。这个节点目前使用Google Gemini API作为后端服务,请确保网络环境可以正常使用Gemini。 请在[Google AI Studio](https://makersuite.google.com/app/apikey)申请你的API key, 并将其填到api_key.ini, 这个文件位于插件根目录下, 默认名字是api_key.ini.example, 初次使用这个文件需将文件后缀改为.ini。用文本编辑软件打开,在google_api_key=后面填入你的API key并保存。 + +# Input types + +## Required + +- api + - API列表。 + - Comfy dtype: ['google-gemini'] + - Python dtype: str + +- token_limit + - 令牌限制。 + - Comfy dtype: INT + - Python dtype: int + +- describe + - 描述。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +# Output types + +- text + - 文本。 + - Comfy dtype: STRING + - Python dtype: str + +# Usage tips +- Infra type: CPU + +# Source code +```python +class PromptEmbellish: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + api_list = ['google-gemini'] + return { + "required": { + "api": (api_list,), + "token_limit": ("INT", {"default": 40, "min": 2, "max": 1024, "step": 1}), + "describe": ("STRING", {"default": "", "multiline": True}), + }, + "optional": { + "image": ("IMAGE",), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = 'prompt_embellish' + CATEGORY = '😺dzNodes/LayerUtility/Prompt' + + def prompt_embellish(self, api, token_limit, describe, image=None): + if describe == "" and image is None: + return ("",) + import google.generativeai as genai + ret_text = "" + first_step_prompt = (f"You are creating a prompt for Stable Diffusion to generate an image. " + f"First step:Using '{describe}' as the basic content, " + f"polish and embellish it to describe into text, keep it on {token_limit} tokens." + f"Second step: Generate a Stable Diffusion text prompt for based on first step in at least {token_limit} words." + f"Only respond with the prompt itself, but embellish it." + ) + + genai.configure(api_key=get_api_key('google_api_key'), transport='rest') + if describe != "": + model = genai.GenerativeModel('gemini-pro', + generation_config=gemini_generate_config, + safety_settings=gemini_safety_settings) + log(f"{NODE_NAME}: Request to gemini-pro...") + response = model.generate_content(first_step_prompt) + print(response) + ret_text = response.text + ret_text = ret_text[ret_text.rfind(':') + 1:] + ret_text = ret_text[ret_text.rfind('\n') + 1:] + # log(f"{NODE_NAME}: Text2Image Prompt is:\n\033[1;36m{ret_text}\033[m") + if is_contain_chinese(describe): + translate_prompt = (f"Please translate the text in parentheses into English:({describe})" + ) + response = model.generate_content(translate_prompt) + print(response) + ret_discribe = response.text + else: + ret_discribe = describe + + if image is not None: + if describe != "": + second_step_prompt = (f"You are creating a prompt for Stable Diffusion to generate an image. " + f"First step:Modify and polish the content in parentheses to match this photo," + f"but must keep '{describe}': ({ret_text}) " + f"Second step: Find objects that is similar in parentheses from the content of the first step" + f" and replace it with the content in parentheses: ({describe})" + f"Third step: Generate a Stable Diffusion text prompt for based on second step in at least {token_limit} words." + f"Only respond with the prompt itself, but embellish it." + ) + else: + second_step_prompt = (f"You are creating a prompt for Stable Diffusion to generate an image. " + f"First step: describe this image, " + f"polish and embellish it into text, discrete it in {token_limit} tokens." + f"Second step: Generate a Stable Diffusion text prompt for based on first step in at least {token_limit} words." + f"Only respond with the prompt itself, but embellish it." + ) + _image = tensor2pil(image).convert('RGB') + model = genai.GenerativeModel('gemini-pro-vision', + generation_config=gemini_generate_config, + safety_settings=gemini_safety_settings) + log(f"{NODE_NAME}: Request to gemini-pro-vision...") + response = model.generate_content([second_step_prompt, _image]) + print(response) + ret_text = response.text + ret_text = ret_text[ret_text.rfind(':') + 1:] + ret_text = ret_text.replace('(','').replace(')','') + if describe != "": + ret_text = f"((({ret_discribe}))), {ret_text}" + # log(f"{NODE_NAME}: Text2Image by ImageRefrence Prompt is:\n\033[1;36m{ret_text}\033[m") + log(f"{NODE_NAME}: Prompt is:\n\033[1;36m{ret_text}\033[m") + log(f"{NODE_NAME} Processed.", message_type='finish') + return (ret_text,) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A PromptTagger.md b/docs/LayerUtility%3A PromptTagger.md new file mode 100644 index 0000000..dfc4814 --- /dev/null +++ b/docs/LayerUtility%3A PromptTagger.md @@ -0,0 +1,122 @@ +# Documentation +- Class name: PromptTagger +- Category: 😺dzNodes/LayerUtility/Prompt +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +根据图片反推提示词,可以设置替换词。这个节点目前使用Google Gemini API作为后端服务,请确保网络环境可以正常使用Gemini。 请在[Google AI Studio](https://makersuite.google.com/app/apikey)申请你的API key, 并将其填到api_key.ini, 这个文件位于插件根目录下, 默认名字是api_key.ini.example, 初次使用这个文件需将文件后缀改为.ini。用文本编辑软件打开,在google_api_key=后面填入你的API key并保存。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- api + - API列表。 + - Comfy dtype: ['gemini-pro-vision'] + - Python dtype: str + +- token_limit + - 令牌限制。 + - Comfy dtype: INT + - Python dtype: int + +- exclude_word + - 排除词。 + - Comfy dtype: STRING + - Python dtype: str + +- replace_with_word + - 替换词。 + - Comfy dtype: STRING + - Python dtype: str + +## Optional + +- 无 + +# Output types + +- text + - 文本。 + - Comfy dtype: STRING + - Python dtype: str + +# Usage tips +- Infra type: CPU + +# Source code +```python +class PromptTagger: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + api_list = ['gemini-pro-vision'] + + return { + "required": { + "image": ("IMAGE", ), + "api": (api_list,), + "token_limit": ("INT", {"default": 80, "min": 2, "max": 1024, "step": 1}), + "exclude_word": ("STRING", {"default": ""}), + "replace_with_word": ("STRING", {"default": ""}), + }, + "optional": { + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = 'prompt_tagger' + CATEGORY = '😺dzNodes/LayerUtility/Prompt' + + def prompt_tagger(self, image, api, token_limit, exclude_word, replace_with_word): + import google.generativeai as genai + replace_with_word = replace_with_word.strip() + exclude_word = exclude_word.strip() + _image = tensor2pil(image).convert('RGB') + ret_text = "" + prompt = ("You are creating a prompt for Stable Diffusion to generate an image. " + "First step: describe this image, then put description into text. " + "Second step: generate a text prompt for based on first step. " + "Only respond with the prompt itself, but embellish it. ") + prompt = f"{prompt}As needed keep it under {token_limit} tokens." + + + + if api == 'gemini-pro-vision': + model = genai.GenerativeModel(api, + generation_config=gemini_generate_config, + safety_settings=gemini_safety_settings) + genai.configure(api_key=get_api_key('google_api_key'), transport='rest') + log(f"{NODE_NAME}: Request to gemini-pro-vision...") + response = model.generate_content([prompt, _image]) + ret_text = response.text + ret_text = ret_text[ret_text.rfind(':') + 1:] + log(f"{NODE_NAME}: Gemini response is:\n\033[1;36m{ret_text}\033[m") + if len(exclude_word) > 0: + if len(replace_with_word) > 0: + ret_text = replace_case(exclude_word, replace_with_word, ret_text) + refine_model = genai.GenerativeModel('gemini-pro', + generation_config=gemini_generate_config, + safety_settings=gemini_safety_settings + ) + response = refine_model.generate_content( + f'You are creating a prompt for Stable Diffusion to generate an image. ' + f'First step: Replace "{exclude_word}" and its synonyms with "{replace_with_word}" in the following text:{ret_text}' + f'Second step: Correct the grammar errors for based on first step.') + ret_text = response.text + if len(replace_with_word) > 0: + ret_text = ret_text.replace(replace_with_word, f"({replace_with_word})") + log(f"{NODE_NAME}: Tagger prompt is:\n\033[1;36m{ret_text}\033[m") + + log(f"{NODE_NAME} Processed.", message_type='finish') + return (ret_text,) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A PurgeVRAM.md b/docs/LayerUtility%3A PurgeVRAM.md new file mode 100644 index 0000000..84f06e7 --- /dev/null +++ b/docs/LayerUtility%3A PurgeVRAM.md @@ -0,0 +1,75 @@ +# Documentation +- Class name: PurgeVRAM +- Category: 😺dzNodes/LayerUtility/SystemIO +- Output node: True +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +清理GPU显存。可以接入任意类型的输入,当执行到这个节点时将清理VRAM以及RAM中的垃圾对象。通常放置在推理任务完成的节点之后,例如VAE Decode节点。 + +# Input types + +## Required + +- anything + - 任意输入。 + - Comfy dtype: any + - Python dtype: any + +- purge_cache + - 清除缓存。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- purge_models + - 清除模型。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +## Optional + +- 无 + +# Output types + +- 无 + +# Usage tips +- Infra type: CPU + +# Source code +```python +class PurgeVRAM: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "anything": (any, {}), + "purge_cache": ("BOOLEAN", {"default": True}), + "purge_models": ("BOOLEAN", {"default": True}), + }, + "optional": { + } + } + + RETURN_TYPES = () + FUNCTION = "purge_vram" + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + OUTPUT_NODE = True + + def purge_vram(self, anything, purge_cache, purge_models): + import torch.cuda + import gc + import comfy.model_management + if purge_cache: + if torch.cuda.is_available(): + gc.collect() + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + if purge_models: + comfy.model_management.unload_all_models() + + return (None,) \ No newline at end of file diff --git a/docs/LayerUtility%3A QWenImage2Prompt.md b/docs/LayerUtility%3A QWenImage2Prompt.md new file mode 100644 index 0000000..69ee57e --- /dev/null +++ b/docs/LayerUtility%3A QWenImage2Prompt.md @@ -0,0 +1,61 @@ +# Documentation +- Class name: QWenImage2Prompt +- Category: 😺dzNodes/LayerUtility/Prompt +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +根据图片反推提示词。这个节点是[ComfyUI_VLM_nodes](https://github.com/gokayfem/ComfyUI_VLM_nodes)中的UForm-Gen2 Qwen Node节点的重新封装,感谢原作者。 请从[huggingface](https://huggingface.co/unum-cloud/uform-gen2-qwen-500m)或者[百度网盘](https://pan.baidu.com/s/1ztnVX_Sh800xsWZhMDe-Ww?pwd=esyt)下载模型到ComfyUI/models/LLavacheckpoints/files_for_uform_gen2_qwen文件夹。 + +# Input types + +## Required + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- question + - 对UForm-Gen-QWen模型的提示词。 + - Comfy dtype: STRING + - Python dtype: str + +# Output types + +- text + - 文本。 + - Comfy dtype: STRING + - Python dtype: str + +# Usage tips +- Infra type: CPU + +# Source code +```python +class QWenImage2Prompt: + def __init__(self): + self.chat_model = UformGen2QwenChat() + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "question": ("STRING", {"multiline": False, "default": "describe this image",},), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "uform_gen2_qwen_chat" + CATEGORY = '😺dzNodes/LayerUtility/Prompt' + + def uform_gen2_qwen_chat(self, image, question): + history = [] # Example empty history + pil_image = ToPILImage()(image[0].permute(2, 0, 1)) + temp_path = files_for_uform_gen2_qwen / "temp.png" + pil_image.save(temp_path) + + response = self.chat_model.chat_response(question, history, temp_path) + return (response.split("assistant\n", 1)[1], ) +``` \ No newline at end of file diff --git a/docs/LayerUtility%3A SaveImagePlus.md b/docs/LayerUtility%3A SaveImagePlus.md new file mode 100644 index 0000000..f2dc741 --- /dev/null +++ b/docs/LayerUtility%3A SaveImagePlus.md @@ -0,0 +1,251 @@ +# Documentation +- Class name: SaveImagePlus +- Category: 😺dzNodes/LayerUtility/SystemIO +- Output node: True +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +增强版的保存图片节点。可自定义保存图片的目录,文件名增加时间戳,选择保存格式,设置图片压缩率,设置是否保存工作流,以及可选给图片添加隐形水印(以肉眼无法觉察的方式添加信息,使用配套的ShowBlindWaterMark节点可以解码水印)。可选择是否同时输出工作流的json文件。 + +*输入%date表示当前日期(YY-mm-dd),%time表示当前时间(HH-MM-SS)。可以输入/表示子目录。例如%date/name_%time 将输出图片到YY-mm-dd文件夹下,以name_HH-MM-SS为文件名前缀。 + +# Input types + +## Required + +- images + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- custom_path + - 用户自定义目录,请按正确的格式输入目录名。如果为空则保存在ComfyUI默认的output目录。 + - Comfy dtype: STRING + - Python dtype: str + +- filename_prefix + - 文件名前缀。 + - Comfy dtype: STRING + - Python dtype: str + +- timestamp + - 为文件名加上时间戳,可选择日期、时间到秒和时间到毫秒。 + - Comfy dtype: ["None", "second", "millisecond"] + - Python dtype: str + +- format + - 图片保存格式。目前提供png和jpg两种。注意RGBA模式的图片仅支持png格式。 + - Comfy dtype: ["png", "jpg"] + - Python dtype: str + +- quality + - 图片质量,数值范围10-100,数值越高,图片质量越好,文件的体积也对应增大。 + - Comfy dtype: INT + - Python dtype: int + +- meta_data + - 是否保存元数据即工作流信息到png文件。如果不希望泄露工作流,请把这里设置为false。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- blind_watermark + - 这里输入的文字(不支持多语言)将被转换为二维码作为隐形水印保存,使用ShowBlindWaterMark节点可以解码水印。注意有水印的图片建议保存为png格式,质量较低的jpg格式将导致水印信息丢失。 + - Comfy dtype: STRING + - Python dtype: str + +- save_workflow_as_json + - 是否同时输出工作流为json文件(输出的json与图片在同一目录)。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- preview + - 预览。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +## Hidden + +- prompt + - 提示。 + - Comfy dtype: PROMPT + - Python dtype: str + +- extra_pnginfo + - 额外PNG信息。 + - Comfy dtype: EXTRA_PNGINFO + - Python dtype: dict + +# Output types + +- 无 + +# Usage tips +- Infra type: CPU + +# Source code +```python +class SaveImagePlus: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + self.compress_level = 4 + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"images": ("IMAGE", ), + "custom_path": ("STRING", {"default": ""}), + "filename_prefix": ("STRING", {"default": "comfyui"}), + "timestamp": (["None", "second", "millisecond"],), + "format": (["png", "jpg"],), + "quality": ("INT", {"default": 80, "min": 10, "max": 100, "step": 1}), + "meta_data": ("BOOLEAN", {"default": False}), + "blind_watermark": ("STRING", {"default": ""}), + "save_workflow_as_json": ("BOOLEAN", {"default": False}), + "preview": ("BOOLEAN", {"default": True}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = () + FUNCTION = "save_image_plus" + OUTPUT_NODE = True + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + + def save_image_plus(self, images, custom_path, filename_prefix, timestamp, format, quality, + meta_data, blind_watermark, preview, save_workflow_as_json, + prompt=None, extra_pnginfo=None): + + now = datetime.datetime.now() + custom_path = custom_path.replace("%date", now.strftime("%Y-%m-%d")) + custom_path = custom_path.replace("%time", now.strftime("%H-%M-%S")) + filename_prefix = filename_prefix.replace("%date", now.strftime("%Y-%m-%d")) + filename_prefix = filename_prefix.replace("%time", now.strftime("%H-%M-%S")) + filename_prefix += self.prefix_append + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) + results = list() + temp_sub_dir = generate_random_name('_savepreview_', '_temp', 16) + temp_dir = os.path.join(folder_paths.get_temp_directory(), temp_sub_dir) + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + if blind_watermark != "": + img_mode = img.mode + wm_size = watermark_image_size(img) + import qrcode + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_H, + box_size=20, + border=1, + ) + qr.add_data(blind_watermark.encode('utf-8')) + qr.make(fit=True) + qr_image = qr.make_image(fill_color="black", back_color="white") + qr_image = qr_image.resize((wm_size, wm_size), Image.BICUBIC).convert("L") + + y, u, v, _ = image_channel_split(img, mode='YCbCr') + _u = add_invisibal_watermark(u, qr_image) + wm_img = image_channel_merge((y, _u, v), mode='YCbCr') + + if img.mode == "RGBA": + img = RGB2RGBA(wm_img, img.split()[-1]) + else: + img = wm_img.convert(img_mode) + + metadata = None + if meta_data: + metadata = PngInfo() + if prompt is not None: + metadata.add_text("prompt", json.dumps(prompt)) + if extra_pnginfo is not None: + for x in extra_pnginfo: + metadata.add_text(x, json.dumps(extra_pnginfo[x])) + + if timestamp == "millisecond": + file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}' + elif timestamp == "second": + file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S")}' + else: + file = f'{filename}_{counter:05}' + + + preview_filename = "" + if custom_path != "": + if not os.path.exists(custom_path): + try: + os.makedirs(custom_path) + except Exception as e: + log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.", + message_type='warning') + raise FileNotFoundError(f"cannot create custom_path {custom_path}, {e}") + + full_output_folder = os.path.normpath(custom_path) + # save preview image to temp_dir + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir) + try: + os.makedirs(temp_dir) + except Exception as e: + print(e) + log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.", + message_type='warning') + try: + preview_filename = os.path.join(generate_random_name('saveimage_preview_', '_temp', 16) + '.png') + img.save(os.path.join(temp_dir, preview_filename)) + except Exception as e: + print(e) + log(f"Error: {NODE_NAME} skipped, because unable to create temporary file.", message_type='warning') + + # check if file exists, change filename + while os.path.isfile(os.path.join(full_output_folder, f"{file}.{format}")): + counter += 1 + if timestamp == "millisecond": + file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}_{counter:05}' + elif timestamp == "second": + file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S")}_{counter:05}' + else: + file = f"{filename}_{counter:05}" + + image_file_name = os.path.join(full_output_folder, f"{file}.{format}") + json_file_name = os.path.join(full_output_folder, f"{file}.json") + + if format == "png": + img.save(image_file_name, pnginfo=metadata, compress_level= (100 - quality) // 10) + else: + if img.mode == "RGBA": + img = img.convert("RGB") + img.save(image_file_name, quality=quality) + log(f"{NODE_NAME} -> Saving image to {image_file_name}") + + if save_workflow_as_json: + try: + workflow = (extra_pnginfo or {}).get('workflow') + if workflow is None: + log('No workflow found, skipping saving of JSON') + with open(f'{json_file_name}', 'w') as workflow_file: + json.dump(workflow, workflow_file) + log(f'Saved workflow to {json_file_name}') + except Exception as e: + log( + f'Failed to save workflow as json due to: {e}, proceeding with the remainder of saving execution', message_type="warning") + + if preview: + if custom_path == "": + results.append({ + "filename": f"{file}.{format}", + "subfolder": subfolder, + "type": self.type + }) + else: + results.append({ + "filename": preview_filename, + "subfolder": temp_sub_dir, + "type": "temp" + }) + + counter += 1 + + return { "ui": { "images": results } } +``` \ No newline at end of file diff --git a/docs/LayerUtility: RestoreCropBox.md b/docs/LayerUtility: RestoreCropBox.md new file mode 100644 index 0000000..1aed820 --- /dev/null +++ b/docs/LayerUtility: RestoreCropBox.md @@ -0,0 +1,126 @@ +# Documentation +- Class name: RestoreCropBox +- Category: 😺dzNodes/LayerUtility +- Output node: False +- Repo Ref: https://github.com/chflame163/ComfyUI_LayerStyle + +将被CropByMask裁切后的图片恢复到原图。 + +# Input types + +## Required + +- background_image + - 背景图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- croped_image + - 裁切后的图片。如果中间经过放大处理,恢复前需将尺寸还原。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- invert_mask + - 反转遮罩。 + - Comfy dtype: BOOLEAN + - Python dtype: bool + +- crop_box + - 裁剪框。 + - Comfy dtype: BOX + - Python dtype: tuple + +## Optional + +- croped_mask + - 裁剪后的遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Output types + +- image + - 图片。 + - Comfy dtype: IMAGE + - Python dtype: torch.Tensor + +- mask + - 遮罩。 + - Comfy dtype: MASK + - Python dtype: torch.Tensor + +# Usage tips +- Infra type: CPU + +# Source code +```python +class RestoreCropBox: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), + "croped_image": ("IMAGE",), + "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask# + "crop_box": ("BOX",), + }, + "optional": { + "croped_mask": ("MASK",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = 'restore_crop_box' + CATEGORY = '😺dzNodes/LayerUtility' + + def restore_crop_box(self, background_image, croped_image, invert_mask, crop_box, + croped_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + ret_masks = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in croped_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', size=m.size, color='white')) + if croped_mask is not None: + if croped_mask.dim() == 2: + croped_mask = torch.unsqueeze(croped_mask, 0) + l_masks = [] + for m in croped_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] + croped_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] + + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(croped_image).convert('RGB') + + ret_mask = Image.new('L', size=_canvas.size, color='black') + _canvas.paste(_layer, box=tuple(crop_box), mask=_mask) + ret_mask.paste(_mask, box=tuple(crop_box)) + ret_images.append(pil2tensor(_canvas)) + ret_masks.append(image2mask(ret_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) +``` \ No newline at end of file diff --git a/needAddFile.json b/needAddFile.json index b141f2c..89ffa6e 100644 --- a/needAddFile.json +++ b/needAddFile.json @@ -1538,115 +1538,7 @@ "Create Video from Path" ], "😺dzNodes": [ - "LayerUtility: ImageChannelMerge", - "LayerUtility: ImageChannelMerge", - "LayerUtility: ImageChannelSplit", - "LayerUtility: ImageChannelSplit", - "LayerUtility: ImageCombineAlpha", - "LayerUtility: ImageCombineAlpha", - "LayerUtility: ImageHub", - "LayerUtility: ImageHub", - "LayerUtility: ImageMaskScaleAs", - "LayerUtility: ImageMaskScaleAs", - "LayerUtility: ImageOpacity", - "LayerUtility: ImageOpacity", - "LayerUtility: ImageRemoveAlpha", - "LayerUtility: ImageRemoveAlpha", - "LayerUtility: ImageRewardFilter", - "LayerUtility: ImageRewardFilter", - "LayerUtility: ImageScaleByAspectRatio", - "LayerUtility: ImageScaleByAspectRatio", - "LayerUtility: ImageScaleByAspectRatio V2", - "LayerUtility: ImageScaleByAspectRatio V2", - "LayerUtility: ImageScaleRestore", - "LayerUtility: ImageScaleRestore", - "LayerUtility: ImageScaleRestore V2", - "LayerUtility: ImageScaleRestore V2", - "LayerUtility: ImageShift", - "LayerUtility: ImageShift", - "LayerStyle: InnerGlow", - "LayerStyle: InnerGlow", - "LayerStyle: InnerGlow V2", - "LayerStyle: InnerGlow V2", - "LayerStyle: InnerShadow", - "LayerStyle: InnerShadow", - "LayerStyle: InnerShadow V2", - "LayerStyle: InnerShadow V2", - "LayerUtility: LaMa", - "LayerUtility: LaMa", - "LayerUtility: LayerImageTransform", - "LayerUtility: LayerImageTransform", - "LayerUtility: LayerMaskTransform", - "LayerUtility: LayerMaskTransform", - "LayerFilter: LightLeak", - "LayerFilter: LightLeak", - "LayerUtility: LoadPSD", - "LayerUtility: LoadPSD", - "LayerMask: MaskBoxDetect", - "LayerMask: MaskBoxDetect", - "LayerMask: MaskByColor", - "LayerMask: MaskByColor", - "LayerMask: MaskByDifferent", - "LayerMask: MaskByDifferent", - "LayerMask: MaskEdgeShrink", - "LayerMask: MaskEdgeShrink", - "LayerMask: MaskEdgeUltraDetail", - "LayerMask: MaskEdgeUltraDetail", - "LayerMask: MaskEdgeUltraDetail V2", - "LayerMask: MaskEdgeUltraDetail V2", - "LayerMask: MaskGradient", - "LayerMask: MaskGradient", - "LayerMask: MaskGrow", - "LayerMask: MaskGrow", - "LayerMask: MaskInvert", - "LayerMask: MaskInvert", - "LayerMask: MaskMotionBlur", - "LayerMask: MaskMotionBlur", - "LayerMask: MaskPreview", - "LayerMask: MaskPreview", - "LayerMask: MaskStroke", - "LayerMask: MaskStroke", - "LayerMask: MediapipeFacialSegment", - "LayerMask: MediapipeFacialSegment", - "LayerFilter: MotionBlur", - "LayerFilter: MotionBlur", - "LayerStyle: OuterGlow", - "LayerStyle: OuterGlow", - "LayerStyle: OuterGlow V2", - "LayerStyle: OuterGlow V2", - "LayerMask: PersonMaskUltra", - "LayerMask: PersonMaskUltra", - "LayerMask: PersonMaskUltra V2", - "LayerMask: PersonMaskUltra V2", - "LayerMask: PixelSpread", - "LayerMask: PixelSpread", - "LayerUtility: PrintInfo", - "LayerUtility: PrintInfo", - "LayerUtility: PromptEmbellish", - "LayerUtility: PromptEmbellish", - "LayerUtility: PromptTagger", - "LayerUtility: PromptTagger", - "LayerUtility: PurgeVRAM", - "LayerUtility: PurgeVRAM", - "LayerUtility: CreateQRCode", - "LayerUtility: CreateQRCode", - "LayerUtility: DecodeQRCode", - "LayerUtility: DecodeQRCode", - "LayerUtility: QWenImage2Prompt", - "LayerUtility: QWenImage2Prompt", - "LayerMask: RemBgUltra", - "LayerMask: RemBgUltra", - "LayerMask: RmBgUltra V2", - "LayerMask: RmBgUltra V2", - "LayerUtility: RestoreCropBox", - "LayerUtility: RestoreCropBox", - "LayerUtility: SaveImagePlus", - "LayerUtility: SaveImagePlus", - "LayerMask: SegformerB2ClothesUltra", - "LayerMask: SegformerB2ClothesUltra", "LayerMask: SegmentAnythingUltra", - "LayerMask: SegmentAnythingUltra", - "LayerMask: SegmentAnythingUltra V2", - "LayerMask: SegmentAnythingUltra V2", + "LayerMask: SegmentAnythingUltra V2" ] } \ No newline at end of file