From 701cf7888af55a10f74f89a32633794c6ce8bded Mon Sep 17 00:00:00 2001 From: rgthree Date: Thu, 15 May 2025 23:13:20 -0400 Subject: [PATCH] Add a simpler image resize node. --- .style.yapf | 9 ++-- __init__.py | 2 + py/image_resize.py | 117 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 py/image_resize.py diff --git a/.style.yapf b/.style.yapf index 5cfeed1..9657de3 100644 --- a/.style.yapf +++ b/.style.yapf @@ -1,8 +1,11 @@ # https://github.com/google/yapf [style] based_on_style = google -indent_width = 2 +INDENT_WIDTH = 2 CONTINUATION_INDENT_WIDTH = 2 COLUMN_LIMIT = 100 -EACH_DICT_ENTRY_ON_SEPARATE_LINE = true -INDENT_DICTIONARY_VALUE = false \ No newline at end of file +DISABLE_ENDING_COMMA_HEURISTIC = true +DEDENT_CLOSING_BRACKETS = true +FORCE_MULTILINE_DICT = false +coalesce_brackets=True +ALLOW_SPLIT_BEFORE_DICT_VALUE = false \ No newline at end of file diff --git a/__init__.py b/__init__.py index 863aecc..6600301 100644 --- a/__init__.py +++ b/__init__.py @@ -41,6 +41,7 @@ from .py.image_comparer import RgthreeImageComparer from .py.power_lora_loader import RgthreePowerLoraLoader from .py.power_primitive import RgthreePowerPrimitive from .py.image_or_latent_size import RgthreeImageOrLatentSize +from .py.image_resize import RgthreeImageResize NODE_CLASS_MAPPINGS = { RgthreeBigContext.NAME: RgthreeBigContext, @@ -65,6 +66,7 @@ NODE_CLASS_MAPPINGS = { RgthreePowerLoraLoader.NAME: RgthreePowerLoraLoader, RgthreePowerPrimitive.NAME: RgthreePowerPrimitive, RgthreeImageOrLatentSize.NAME: RgthreeImageOrLatentSize, + RgthreeImageResize.NAME: RgthreeImageResize, } if get_config_value('unreleased.dynamic_context.enabled') is True: diff --git a/py/image_resize.py b/py/image_resize.py new file mode 100644 index 0000000..a21001b --- /dev/null +++ b/py/image_resize.py @@ -0,0 +1,117 @@ +import torch +import comfy.utils +import nodes + +from .constants import get_category, get_name + + +class RgthreeImageResize: + """Image Resize.""" + + NAME = get_name("Image Resize") + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "image": ("IMAGE",), + "measurement": (["pixels", "percentage"],), + "width": ( + "INT", { + "default": 0, + "min": 0, + "max": nodes.MAX_RESOLUTION, + "step": 1, + "tooltip": ( + "The width of the desired resize. A pixel value if measurement is 'pixels' or a" + " 100% scale percentage value if measurement is 'percentage'. Passing '0' will" + " calculate the dimension based on the height." + ), + }, + ), + "height": ("INT", { + "default": 0, + "min": 0, + "max": nodes.MAX_RESOLUTION, + "step": 1 + }), + "fit": (["crop", "pad", "contain"], { + "tooltip": ( + "'crop' resizes so the image covers the desired width and height, and center-crops the" + " excess, returning exactly the desired width and height." + "\n'pad' resizes so the image fits inside the desired width and height, and fills the" + " empty space returning exactly the desired width and height." + "\n'contain' resizes so the image fits inside the desired width and height, and" + " returns the image with it's new size, with one side liekly smaller than the desired." + "\n\nNote, if either width or height is '0', the effective fit is 'contain'." + ) + }, + ), + "method": (nodes.ImageScale.upscale_methods,), + }, + } + + RETURN_TYPES = ("IMAGE", "INT", "INT",) + RETURN_NAMES = ("IMAGE", "WIDTH", "HEIGHT",) + FUNCTION = "main" + DESCRIPTION = """Resize the image.""" + + def main(self, image, measurement, width, height, method, fit): + """Resizes the image.""" + _, H, W, _ = image.shape + + if measurement == "percentage": + width = round(width * W / 100) + height = round(height * H / 100) + + if (width == 0 and height == 0) or (width == W and height == H): + return (image, W, H) + + # If one dimension is 0, then calculate the desired value from the ratio of the set dimension. + # This also implies a 'contain' fit since the width and height will be scaled with a locked + # aspect ratio. + if width == 0 or height == 0: + width = round(height / H * W) if width == 0 else width + height = round(width / W * H) if height == 0 else height + fit = "contain" + + # At this point, width and height are our output height, but our resize sizes will be different. + resized_width = width + resized_height = height + if fit == "crop": + # If we resize against the opposite ratio, then choose the ratio that has the overhang. + if (height / H * W) > width: + resized_width = round(height / H * W) + elif (width / W * H) > height: + resized_height = round(width / W * H) + elif fit == "contain" or fit == "pad": + # If we resize against the opposite ratio, then choose the ratio that has the overhang. + if (height / H * W) > width: + resized_height = round(width / W * H) + elif (width / W * H) > height: + resized_width = round(height / H * W) + + out_image = comfy.utils.common_upscale( + image.clone().movedim(-1, 1), resized_width, resized_height, method, crop="disabled" + ).movedim(1, -1) + OB, OH, OW, OC = out_image.shape + + if fit != "contain": + # First, we crop, then we pad; no need to check fit (other than not 'contain') since the size + # should already be correct. + if OW > width: + out_image = out_image.narrow(-2, (OW - width) // 2, width) + if OH > height: + out_image = out_image.narrow(-3, (OH - height) // 2, height) + + OB, OH, OW, OC = out_image.shape + if width != OW or height != OH: + padded_image = torch.zeros((OB, height, width, OC), dtype=image.dtype, device=image.device) + x = (width - OW) // 2 + y = (height - OH) // 2 + for b in range(OB): + padded_image[b, y:y + OH, x:x + OW, :] = out_image[b] + out_image = padded_image + + return (out_image, out_image.shape[2], out_image.shape[1])