diff --git a/README.MD b/README.MD index 6b2ded9..5f23705 100644 --- a/README.MD +++ b/README.MD @@ -12,6 +12,7 @@ Nodes are divided into four groups according to their functions: LayerStyle, Lay [中文说明点这里](./README_CN.MD) ## Update +* Commit [MaskGradient](#MaskGradient) node, can create a gradient in the mask. * Commit [GetColorTone](#GetColorTone) node, can obtain the main color or average color of the image. Commit [MaskGrow](#MaskGrow) and [MaskEdgeShrink](#MaskEdgeShrink) nodes. * Commit [MaskBoxDetect](#MaskBoxDetect) node, which can automatically detect the position through the mask and output it to the composite node. @@ -223,6 +224,19 @@ Comparison of MaskGrow and MaskEdgeShrink ![image](image/mask_edge_compare.png) +### MaskGradient +Create a gradient for the mask from one side. +![image](image/mask_gradient_example.png) + +Node options: +![image](image/mask_gradient_node.png) +* invert_mask: Whether to reverse the mask. +* gradient_side: Generate gradient from which edge. There are four directions: top, bottom, left and right. +* gradient_scale: Gradient distance. The default value of 100 indicates that one side of the gradient is completely transparent and the other side is completely opaque. The smaller the value, the shorter the distance from transparent to opaque. +* gradient_offset: Gradient position offset. +* opacity: The opacity of the gradient. + + ### MaskPreview Preview the input mask ![image](image/mask_invert.png) diff --git a/README_CN.MD b/README_CN.MD index 5668564..414384d 100644 --- a/README_CN.MD +++ b/README_CN.MD @@ -9,6 +9,7 @@ ComfyUI * [LayerFilter](#LayerFilter)½Úµã×éÌṩͼÏñЧ¹ûÂ˾µ¡£ ## ¸üÐÂ˵Ã÷ +* Ìí¼Ó[MaskGradient](#MaskGradient)½Úµã£¬¿Éʹmask²úÉú½¥±ä¡£ * Ìí¼Ó[GetColorTone](#GetColorTone)½Úµã£¬¿ÉÒÔ»ñȡͼƬµÄÖ÷É«»òƽ¾ùÉ«¡£Ìí¼Ó[MaskGrow](#MaskGrow)ºÍ[MaskEdgeShrink](#MaskEdgeShrink)½Úµã¡£ * Ìí¼Ó[MaskBoxDetect](#MaskBoxDetect)½Úµã£¬¿ÉÒÔͨ¹ýmask×Ô¶¯Ì½²âλÖò¢Êä³öµ½ºÏ³É½Úµã¡£Ìí¼Ó[XY to Percent](#Percent)½Úµã£¬½«¾ø¶Ô×ø±êת»»Îªpercent×ø±ê¡£Ìí¼Ó[GaussianBlur](#GaussianBlur)½Úµã¡£Ìí¼Ó[GetImageSize](#GetImageSize)½Úµã¡£ * Ìí¼Ó[ExtendCanvas](#ExtendCanvas)½Úµã¡£ @@ -214,6 +215,18 @@ ComfyUI MaskGrowÓëMaskEdgeShrinkЧ¹û¶Ô±È ![image](image/mask_edge_compare.png) +### MaskGradient +ʹmask´ÓÒ»²à²úÉú½¥±ä¡£ +![image](image/mask_gradient_example.png) + +½ÚµãÑ¡Ïî˵Ã÷: +![image](image/mask_gradient_node.png) +* invert_mask: ÊÇ·ñ·´×ªÕÚÕÖ¡£ +* gradient_side: ´ÓÄĸö±ß²úÉú½¥±ä¡£ÓÐËĸö·½Ïò£º¶¥²àtop¡¢µ×²àbottom¡¢×ó²àleft¡¢ÓÒ²àright¡£ +* gradient_scale: ½¥±ä¾àÀ롣ĬÈÏÖµ100±íʾ½¥±ä²úÉúÒ»²àÍêȫ͸Ã÷£¬ÁíÒ»²àÍêÈ«²»Í¸Ã÷¡£ÊýֵԽС£¬´Ó͸Ã÷µ½²»Í¸Ã÷µÄ¾àÀëÔ½¶Ì¡£ +* gradient_offset: ½¥±äλÖÃÆ«ÒÆ¡£ +* opacity: ½¥±äµÄ²»Í¸Ã÷¶È¡£ + ### MaskPreview Ô¤ÀÀmask ![image](image/mask_invert.png) diff --git a/image/mask_gradient_example.png b/image/mask_gradient_example.png new file mode 100644 index 0000000..2cdcca1 Binary files /dev/null and b/image/mask_gradient_example.png differ diff --git a/image/mask_gradient_node.png b/image/mask_gradient_node.png new file mode 100644 index 0000000..fe3aa2d Binary files /dev/null and b/image/mask_gradient_node.png differ diff --git a/py/gradient_image.py b/py/gradient_image.py index 827cf3a..46370d7 100644 --- a/py/gradient_image.py +++ b/py/gradient_image.py @@ -28,7 +28,7 @@ class GradientImage: def gradient_image(self, width, height, angle, start_color, end_color, ): - ret_image = gradint(start_color, end_color, width, height, angle) + ret_image = gradient(start_color, end_color, width, height, angle) return (pil2tensor(ret_image), ) diff --git a/py/gradient_overlay.py b/py/gradient_overlay.py index 4b04c51..e671a74 100644 --- a/py/gradient_overlay.py +++ b/py/gradient_overlay.py @@ -54,7 +54,7 @@ class GradientOverlay: _mask = Image.new('L', _layer.size, 'white') log('Warning: mask mismatch, droped!') - _gradient = gradint(start_color, end_color, _layer.width, _layer.height, float(angle)) + _gradient = gradient(start_color, end_color, _layer.width, _layer.height, float(angle)) # å�ˆæˆ�layer _comp = chop_image(_layer, _gradient, blend_mode, opacity) diff --git a/py/imagefunc.py b/py/imagefunc.py index 84ddd79..65c4b3f 100644 --- a/py/imagefunc.py +++ b/py/imagefunc.py @@ -188,7 +188,7 @@ def create_gradient(start_color_inhex:str, end_color_inhex:str, width:int, heigh draw.line((0, i, width, i), fill=color) return ret_image -def gradint(start_color_inhex:str, end_color_inhex:str, width:int, height:int, angle:float, ) -> Image: +def gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, angle:float, ) -> Image: radius = int((width + height) / 4) g = create_gradient(start_color_inhex, end_color_inhex, radius, radius) _canvas = Image.new('RGB', size=(radius, radius*3), color=start_color_inhex) diff --git a/py/mask_gradient.py b/py/mask_gradient.py new file mode 100644 index 0000000..cf3b284 --- /dev/null +++ b/py/mask_gradient.py @@ -0,0 +1,133 @@ +import copy + +from .imagefunc import * + +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' + OUTPUT_NODE = True + + def mask_gradient(self, mask, invert_mask, gradient_side, gradient_scale, gradient_offset, opacity, ): + + if invert_mask: + mask = 1 - mask + _mask = mask2image(mask).convert('L') + _canvas = copy.copy(_mask) + width = _mask.width + height = _mask.height + _gradient = gradient('#000000', '#FFFFFF', + _mask.width, _mask.height, 0) + (box_x, box_y, box_width, box_height) = min_bounding_rect(_mask) + # preview_image = mask2image(mask).convert('RGB') + # preview_image = draw_rect(preview_image, box_x, box_y, box_width, box_height, + # line_color = "#F00000", line_width = int(preview_image.height / 60)) + 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), 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, 0), mask = _gradient.convert('L')) + # opacity + if opacity < 100: + _canvas = chop_image(_mask, _canvas, 'normal', opacity) + ret_mask = image2mask(_canvas) + return (ret_mask,) + +NODE_CLASS_MAPPINGS = { + "LayerMask: MaskGradient": MaskGradient +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskGradient": "LayerMask: MaskGradient" +} \ No newline at end of file diff --git a/workflow/mask_gradient_example.json b/workflow/mask_gradient_example.json new file mode 100644 index 0000000..5e3d2ee --- /dev/null +++ b/workflow/mask_gradient_example.json @@ -0,0 +1,442 @@ +{ + "last_node_id": 20, + "last_link_id": 37, + "nodes": [ + { + "id": 12, + "type": "LayerUtility: ImageBlendAdvance", + "pos": [ + 1709, + -215 + ], + "size": { + "0": 315, + "1": 338 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "background_image", + "type": "IMAGE", + "link": 35 + }, + { + "name": "layer_image", + "type": "IMAGE", + "link": 17 + }, + { + "name": "layer_mask", + "type": "MASK", + "link": 33 + } + ], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": [ + 15 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "mask", + "type": "MASK", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LayerUtility: ImageBlendAdvance" + }, + "widgets_values": [ + false, + "normal", + 78, + 50, + 100, + "vertical", + 0.8, + 1, + 0, + "lanczos", + 0 + ] + }, + { + "id": 19, + "type": "ImageScale", + "pos": [ + 1306, + -209 + ], + "size": { + "0": 315, + "1": 130 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 34 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 35 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ImageScale" + }, + "widgets_values": [ + "nearest-exact", + 1920, + 1080, + "disabled" + ] + }, + { + "id": 18, + "type": "LayerMask: MaskGradient", + "pos": [ + 1318, + 189 + ], + "size": { + "0": 315, + "1": 154 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "mask", + "type": "MASK", + "link": 36 + } + ], + "outputs": [ + { + "name": "mask", + "type": "MASK", + "links": [ + 32, + 33 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "LayerMask: MaskGradient" + }, + "widgets_values": [ + true, + "top", + 74, + 397, + 100 + ] + }, + { + "id": 9, + "type": "LoadImage", + "pos": [ + 936, + -209 + ], + "size": { + "0": 315, + "1": 314 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 34 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": [], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "beach (2).jpg", + "image" + ] + }, + { + "id": 16, + "type": "LayerMask: MaskPreview", + "pos": [ + 1709, + 177 + ], + "size": [ + 313.1912463378908, + 338.8637414550783 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "mask", + "type": "MASK", + "link": 32 + } + ], + "properties": { + "Node name for S&R": "LayerMask: MaskPreview" + } + }, + { + "id": 10, + "type": "LayerUtility: ImageBlendAdvance", + "pos": [ + 2077, + -213 + ], + "size": { + "0": 315, + "1": 338 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "background_image", + "type": "IMAGE", + "link": 15 + }, + { + "name": "layer_image", + "type": "IMAGE", + "link": 10 + }, + { + "name": "layer_mask", + "type": "MASK", + "link": 37 + } + ], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": [ + 18 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "mask", + "type": "MASK", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LayerUtility: ImageBlendAdvance" + }, + "widgets_values": [ + true, + "normal", + 100, + 50, + 40, + "None", + 0.8, + 1, + 0, + "lanczos", + 0 + ] + }, + { + "id": 13, + "type": "PreviewImage", + "pos": [ + 2452, + -213 + ], + "size": [ + 743.0909667968749, + 703.6272277832031 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 18 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 2, + "type": "LoadImage", + "pos": [ + 931, + 178 + ], + "size": { + "0": 315, + "1": 314 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 10, + 17 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": [ + 36, + 37 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "erji_alpha (8).png", + "image" + ] + } + ], + "links": [ + [ + 10, + 2, + 0, + 10, + 1, + "IMAGE" + ], + [ + 15, + 12, + 0, + 10, + 0, + "IMAGE" + ], + [ + 17, + 2, + 0, + 12, + 1, + "IMAGE" + ], + [ + 18, + 10, + 0, + 13, + 0, + "IMAGE" + ], + [ + 32, + 18, + 0, + 16, + 0, + "MASK" + ], + [ + 33, + 18, + 0, + 12, + 2, + "MASK" + ], + [ + 34, + 9, + 0, + 19, + 0, + "IMAGE" + ], + [ + 35, + 19, + 0, + 12, + 0, + "IMAGE" + ], + [ + 36, + 2, + 1, + 18, + 0, + "MASK" + ], + [ + 37, + 2, + 1, + 10, + 2, + "MASK" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file