diff --git a/README.MD b/README.MD index f9ec258..48499d2 100644 --- a/README.MD +++ b/README.MD @@ -13,6 +13,7 @@ Nodes are divided into four groups according to their functions: LayerStyle, Lay [中文说明点这里](./README_CN.MD) ## Update +* Commit [ImageMaskScaleAs](#ImageMaskScaleAs) node to adjust the image or mask size based on the reference image. * Commit [ImageScaleRestore](#ImageScaleRestore) node to work with CropByMask for local upscale and repair works. * Commit [CropByMask](#CropByMask) and [RestoreCropBox](#RestoreCropBox) nodes. The combination of these two can partially crop and redraw the image before restoring it. * Commit [ColorAdapter](#ColorAdapter) node, that can automatically adjust the color tone of the image. @@ -188,6 +189,7 @@ Node options: * [note](#notes) # LayerColor +![image](image/layercolor_title.png) ![image](image/layercolor_nodes.png) ### LUT Apply @@ -437,6 +439,28 @@ Outputs: * mask: If have mask input, the scaled mask will be output. * original_size: The original size data of the image is used for subsequent node recovery. +### ImageMaskScaleAs +Scale the image or mask to the size of the reference image (or reference mask). +![image](image/image_mask_scale_as_example.png) + +Node options: +![image](image/image_mask_scale_as_node.png) +* scale_as*: Reference size. It can be an image or a mask. +* image: Image to be scaled. this option is optional input. if there is no input, a black image will be output. +* mask: Mask to be scaled. this option is optional input. if there is no input, a black mask will be output. +* fit: Scale aspect ratio mode. when the width to height ratio of the original image does not match the scaled size, there are three modes to choose from, +The _letterbox_ mode retains the complete frame and fills in the blank spaces with black; +The _crop_ mode retains the complete short edge, and any excess of the long edge will be cut off; +The _fill_ mode does not maintain frame ratio and fills the screen with width and height. +* method: Scaling sampling methods, including lanczos, bicubic, hamming, bilinear, box, and nearest. + +*Only limited to input images and masks. forcing the integration of other types of inputs will result in node errors. + +Outputs: +* image: If there is an image input, the scaled image will be output. +* mask: If there is a mask input, the scaled mask will be output. +* original_size: The original size data of the image is used for subsequent node recovery. + ### ImageBlend A simple node for composit layer image and background image, multiple blend modes are available for option, and transparency can be set. ![image](image/image_blend_example.png) diff --git a/README_CN.MD b/README_CN.MD index 60087d5..7fee911 100644 --- a/README_CN.MD +++ b/README_CN.MD @@ -10,6 +10,7 @@ ComfyUI * [LayerFilter](#LayerFilter)½Úµã×éÌṩͼÏñЧ¹ûÂ˾µ¡£ ## ¸üÐÂ˵Ã÷ +* Ìí¼Ó[ImageMaskScaleAs](#ImageMaskScaleAs) ½Úµã£¬¿É¸ù¾Ý²Î¿¼Í¼Æ¬µ÷ÕûͼÏñ»òÕÚÕÖ´óС¡£ * Ìí¼Ó[ImageScaleRestore](#ImageScaleRestore) ½Úµã£¬ÓÃÓÚÅäºÏCropByMask½øÐоֲ¿·Å´óÐÞ¸´¹¤×÷¡£ * Ìí¼Ó[CropByMask](#CropByMask) ºÍ [RestoreCropBox](#RestoreCropBox)½Úµã¡£´Ë¶þÕßÅäºÏ¿É½«Í¼Æ¬¾Ö²¿²ÃÇÐÖØ»æÈ»ºó»¹Ô­¡£ * Ìí¼Ó[ColorAdapter](#ColorAdapter) ½Úµã£¬¿É×Ô¶¯µ÷ÕûͼƬɫµ÷¡£ @@ -182,6 +183,7 @@ ComfyUI * [½Úµã×¢½â](#½Úµã×¢½â) # LayerColor +![image](image/layercolor_title.png) ![image](image/layercolor_nodes.png) ### LUT Apply @@ -427,6 +429,25 @@ mask * mask: Èç¹ûÓÐmaskÊäÈ룬½«Êä³öËõ·ÅºóµÄmask¡£ * original_size: ͼÏñµÄԭʼ´óСÊý¾Ý£¬ÓÃÓÚºóÐø½Úµã½øÐлָ´¡£ +### ImageMaskScaleAs +½«Í¼Ïñ»òÕÚÕÖËõ·Åµ½²Î¿¼Í¼Ïñ£¨»òÕÚÕÖ£©µÄ´óС¡£ +![image](image/image_mask_scale_as_example.png) + +½ÚµãÑ¡Ïî˵Ã÷: +![image](image/image_mask_scale_as_node.png) +* scale_as*: ²Î¿¼´óС¡£¿ÉÒÔÊÇͼÏñimage£¬Ò²¿ÉÒÔÊÇÕÚÕÖmask¡£ +* image: ´ýËõ·ÅµÄͼÏñ¡£´ËÑ¡ÏîΪ¿ÉÑ¡ÊäÈ룬Èç¹ûûÓÐÊäÈ뽫Êä³ö´¿ºÚͼƬ¡£ +* mask: ´ýËõ·ÅµÄÕÚÕÖ¡£´ËÑ¡ÏîΪ¿ÉÑ¡ÊäÈ룬Èç¹ûûÓÐÊäÈ뽫Êä³ö´¿ºÚÕÚÕÖ¡£ +* fit: Ëõ·Å»­·ù¿í¸ß±Èģʽ¡£µ±Ô­Í¼ÓëËõ·Å³ß´ç»­·ù¿í¸ß±ÈÀý²»Ò»ÖÂʱ£¬ÓÐ3ÖÖģʽ¿ÉÒÔÑ¡Ôñ, letterboxģʽ±£ÁôÍêÕûµÄ»­·ù£¬¿Õ°×´¦ÓúÚÉ«²¹×㣻cropģʽ±£ÁôÍêÕûµÄ¶Ì±ß£¬³¤±ß³¬³ö²¿·Ö½«±»Çгý£»fillģʽ²»±£³Ö»­·ù±ÈÀý£¬¿í¸ß¸÷×ÔÌîÂú»­Ãæ¡£ +* method: Ëõ·ÅµÄ²ÉÑù·½·¨£¬°üÀ¨lanczos¡¢bicubic¡¢hamming¡¢bilinear¡¢boxºÍnearest¡£ + +*½öÏÞÊäÈëimageºÍmask, Èç¹ûÇ¿ÖÆ½ÓÈëÆäËûÀàÐÍÊäÈ룬½«µ¼Ö½ڵã´íÎó¡£ + +Êä³ö: +* image: Èç¹ûÓÐimageÊäÈ룬½«Êä³öËõ·ÅºóµÄͼÏñ¡£ +* mask: Èç¹ûÓÐmaskÊäÈ룬½«Êä³öËõ·ÅºóµÄÕÚÕÖ¡£ +* original_size: ͼÏñµÄԭʼ´óСÊý¾Ý£¬ÓÃÓÚºóÐø½Úµã½øÐлָ´¡£ + ### ImageBlend Ò»¸öÓÃÓںϳÉͼ²ãµÄ¼òµ¥½Úµã£¬Ìṩ¶àÖÖ»ìºÏģʽ¹©Ñ¡Ôñ£¬¿ÉÉèÖÃ͸Ã÷¶È¡£ ![image](image/image_blend_example.png) diff --git a/image/image_mask_scale_as_example.png b/image/image_mask_scale_as_example.png new file mode 100644 index 0000000..d53426b Binary files /dev/null and b/image/image_mask_scale_as_example.png differ diff --git a/image/image_mask_scale_as_node.png b/image/image_mask_scale_as_node.png new file mode 100644 index 0000000..17ed6a5 Binary files /dev/null and b/image/image_mask_scale_as_node.png differ diff --git a/image/layercolor_nodes.png b/image/layercolor_nodes.png index 5941142..3e7604b 100644 Binary files a/image/layercolor_nodes.png and b/image/layercolor_nodes.png differ diff --git a/image/layercolor_title.png b/image/layercolor_title.png new file mode 100644 index 0000000..5941142 Binary files /dev/null and b/image/layercolor_title.png differ diff --git a/image/layerutility_nodes.png b/image/layerutility_nodes.png index 548e815..b6476c8 100644 Binary files a/image/layerutility_nodes.png and b/image/layerutility_nodes.png differ diff --git a/py/image_mask_scale_as.py b/py/image_mask_scale_as.py new file mode 100644 index 0000000..78317fe --- /dev/null +++ b/py/image_mask_scale_as.py @@ -0,0 +1,74 @@ +from .imagefunc import * + +any = AnyType("*") + +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",) + RETURN_NAMES = ("image", "mask", "original_size") + FUNCTION = 'image_mask_scale_as' + CATEGORY = '😺dzNodes/LayerUtility' + OUTPUT_NODE = True + + def image_mask_scale_as(self, scale_as, fit, method, + image=None, mask = None, + ): + + _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 + if mask is not None: + _mask = tensor2pil(mask).convert('L') + orig_width, orig_height = _mask.size + if image is not None: + _image = tensor2pil(image).convert('RGB') + orig_width, orig_height = _image.size + 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 image is not None: + _image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler) + if mask is not None: + _mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L') + + return (pil2tensor(_image), image2mask(_mask), [orig_width, orig_height],) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageMaskScaleAs": ImageMaskScaleAs +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageMaskScaleAs": "LayerUtility: ImageMaskScaleAs" +} \ No newline at end of file diff --git a/py/imagefunc.py b/py/imagefunc.py index 6167c04..c378662 100644 --- a/py/imagefunc.py +++ b/py/imagefunc.py @@ -127,6 +127,33 @@ def motion_blur(image:Image, angle:int, blur:int) -> Image: ret_image = cv22pil(blurred) return ret_image +def fit_resize_image(image:Image, target_width:int, target_height:int, fit:str, resize_sampler:str) -> Image: + image = image.convert('RGB') + orig_width, orig_height = image.size + if image is not None: + if fit == 'letterbox': + if orig_width / orig_height > target_width / target_height: # 更宽,上下留黑 + fit_width = target_width + fit_height = int(target_width / orig_width * orig_height) + else: # 更瘦,左å�³ç•™é»‘ + fit_height = target_height + fit_width = int(target_height / orig_height * orig_width) + fit_image = image.resize((fit_width, fit_height), resize_sampler) + ret_image = Image.new('RGB', size=(target_width, target_height), color='black') + ret_image.paste(fit_image, box=((target_width - fit_width)//2, (target_height - fit_height)//2)) + elif fit == 'crop': + if orig_width / orig_height > target_width / target_height: # 更宽,è£�å·¦å�³ + fit_width = int(orig_height * target_width / target_height) + fit_image = image.crop( + ((orig_width - fit_width)//2, 0, (orig_width - fit_width)//2 + fit_width, orig_height)) + else: # 更瘦,è£�上下 + fit_height = int(orig_width * target_height / target_width) + fit_image = image.crop( + (0, (orig_height-fit_height)//2, orig_width, (orig_height-fit_height)//2 + fit_height)) + ret_image = fit_image.resize((target_width, target_height), resize_sampler) + else: + ret_image = image.resize((target_width, target_height), resize_sampler) + return ret_image def __rotate_expand(image:Image, angle:float, SSAA:int=0, method:str="lanczos") -> Image: images = pil2tensor(image) @@ -536,3 +563,8 @@ def has_letters(string:str) -> bool: return True else: return False + +class AnyType(str): + """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" + def __ne__(self, __value: object) -> bool: + return False \ No newline at end of file diff --git a/py/print_info.py b/py/print_info.py index f76a724..797288c 100644 --- a/py/print_info.py +++ b/py/print_info.py @@ -1,11 +1,7 @@ import json import torch from .imagefunc import tensor2pil, log - -class AnyType(str): - """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" - def __ne__(self, __value: object) -> bool: - return False +from .imagefunc import AnyType any = AnyType("*") diff --git a/workflow/image_mask_scale_as_example.json b/workflow/image_mask_scale_as_example.json new file mode 100644 index 0000000..d691a8f --- /dev/null +++ b/workflow/image_mask_scale_as_example.json @@ -0,0 +1,313 @@ +{ + "last_node_id": 65, + "last_link_id": 146, + "nodes": [ + { + "id": 42, + "type": "LoadImage", + "pos": [ + 133, + 364 + ], + "size": { + "0": 315, + "1": 314 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 137 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": [], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "D04QPvc (26).jpg", + "image" + ] + }, + { + "id": 59, + "type": "LayerMask: MaskPreview", + "pos": [ + 1543, + 510 + ], + "size": { + "0": 359.6678771972656, + "1": 408.945556640625 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "mask", + "type": "MASK", + "link": 142 + } + ], + "properties": { + "Node name for S&R": "LayerMask: MaskPreview" + } + }, + { + "id": 16, + "type": "Image Remove Background Rembg (mtb)", + "pos": [ + 485, + 735 + ], + "size": { + "0": 294, + "1": 230 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 22 + } + ], + "outputs": [ + { + "name": "Image (rgba)", + "type": "IMAGE", + "links": null, + "shape": 3 + }, + { + "name": "Mask", + "type": "MASK", + "links": [ + 145 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "Image", + "type": "IMAGE", + "links": [], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "Image Remove Background Rembg (mtb)" + }, + "widgets_values": [ + false, + 240, + 10, + 10, + false, + "#000000" + ] + }, + { + "id": 58, + "type": "PreviewImage", + "pos": [ + 1143, + 506 + ], + "size": { + "0": 372.0478820800781, + "1": 417.78558349609375 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 140 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 64, + "type": "LayerUtility: ImageMaskScaleAs", + "pos": [ + 783, + 507 + ], + "size": { + "0": 315, + "1": 122 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "scale_as", + "type": "*", + "link": 137 + }, + { + "name": "image", + "type": "IMAGE", + "link": 144 + }, + { + "name": "mask", + "type": "MASK", + "link": 145 + } + ], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": [ + 140 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "mask", + "type": "MASK", + "links": [ + 142 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "original_size", + "type": "BOX", + "links": [], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "LayerUtility: ImageMaskScaleAs" + }, + "widgets_values": [ + "letterbox", + "lanczos" + ] + }, + { + "id": 2, + "type": "LoadImage", + "pos": [ + 131, + 726 + ], + "size": { + "0": 315, + "1": 314 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 22, + 144 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "512x512with_background (15).png", + "image" + ] + } + ], + "links": [ + [ + 22, + 2, + 0, + 16, + 0, + "IMAGE" + ], + [ + 137, + 42, + 0, + 64, + 0, + "*" + ], + [ + 140, + 64, + 0, + 58, + 0, + "IMAGE" + ], + [ + 142, + 64, + 1, + 59, + 0, + "MASK" + ], + [ + 144, + 2, + 0, + 64, + 1, + "IMAGE" + ], + [ + 145, + 16, + 1, + 64, + 2, + "MASK" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file