Skip to content

LayerUtility: ImageScaleByAspectRatio

Documentation

  • Class name: LayerUtility: ImageScaleByAspectRatio
  • Category: 😺dzNodes/LayerUtility
  • Output node: False

This node is designed to adjust the scale of an image based on its aspect ratio, offering various scaling options to fit, fill, or match specific dimensions while maintaining the original or a custom aspect ratio. It supports different resizing methods to optimize the visual quality of the scaled image.

Input types

Required

  • aspect_ratio
    • Specifies the aspect ratio to use for scaling the image. It can be set to 'original' to use the image's current aspect ratio, 'custom' to use a specified ratio, or a predefined ratio in the format 'width:height'.
    • Comfy dtype: COMBO[STRING]
    • Python dtype: str
  • proportional_width
    • The width part of the custom aspect ratio, used when 'aspect_ratio' is set to 'custom'.
    • Comfy dtype: INT
    • Python dtype: int
  • proportional_height
    • The height part of the custom aspect ratio, used when 'aspect_ratio' is set to 'custom'.
    • Comfy dtype: INT
    • Python dtype: int
  • fit
    • Determines how the image should be fitted to the target dimensions, affecting how the image is scaled and cropped.
    • Comfy dtype: COMBO[STRING]
    • Python dtype: str
  • method
    • The method used for resizing the image, such as 'bicubic', 'hamming', 'bilinear', 'box', or 'nearest', each offering different quality and performance characteristics.
    • Comfy dtype: COMBO[STRING]
    • Python dtype: str
  • round_to_multiple
    • If specified, rounds the scaled dimensions to the nearest multiple of this value, which can be useful for aligning images to certain size constraints.
    • Comfy dtype: COMBO[STRING]
    • Python dtype: str
  • scale_to_longest_side
    • Determines if the image should be scaled based on the longest side, providing a boolean option to enable or disable this feature.
    • Comfy dtype: BOOLEAN
    • Python dtype: bool
  • longest_side
    • Specifies the target length for the longest side of the image when 'scale_to_longest_side' is enabled, determining the final dimensions of the scaled image.
    • Comfy dtype: INT
    • Python dtype: int

Optional

  • image
    • The original image to be scaled. This can be a tensor representation of an image.
    • Comfy dtype: IMAGE
    • Python dtype: torch.Tensor
  • mask
    • An optional mask to be scaled alongside the image, typically used for segmentation tasks.
    • Comfy dtype: MASK
    • Python dtype: torch.Tensor

Output types

  • image
    • Comfy dtype: IMAGE
    • The scaled image tensor, adjusted according to the specified aspect ratio, dimensions, and scaling method.
    • Python dtype: torch.Tensor
  • mask
    • Comfy dtype: MASK
    • The scaled mask tensor, corresponding to the 'mask' input, adjusted to match the dimensions of the scaled image.
    • Python dtype: torch.Tensor
  • original_size
    • Comfy dtype: BOX
    • The original dimensions of the image before scaling.
    • Python dtype: list
  • width
    • Comfy dtype: INT
    • The width of the scaled image.
    • Python dtype: int
  • height
    • Comfy dtype: INT
    • The height of the scaled image.
    • Python dtype: int

Usage tips

  • Infra type: GPU
  • Common nodes: unknown

Source code

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,)