01633623546a commited on
Commit
b277ac5
1 Parent(s): 02bd911

Upload 5 files

Browse files
Files changed (5) hide show
  1. LICENSE +21 -0
  2. README.md +51 -0
  3. __init__.py +3 -0
  4. image_resize.png +0 -0
  5. image_resize.py +163 -0
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Wladimir Palant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image Resize for ComfyUI
2
+
3
+ This custom node provides various tools for resizing images. The goal is resizing without distorting proportions, yet without having to perform any calculations with the size of the original image. If a mask is present, it is resized and modified along with the image.
4
+
5
+ ![A ComfyUI node titled “Image resize” with inputs pixels and mask_optional, outputs IMAGE and MASK as well as a variety of widgets: action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, crop_pad_position, pad_feathering](image_resize.png)
6
+
7
+ ## Installation
8
+
9
+ To install, clone this repository into `ComfyUI/custom_nodes` folder with `git clone https://github.com/palant/image-resize-comfyui` and restart ComfyUI.
10
+
11
+ ## Node configuration
12
+
13
+ ### action
14
+
15
+ In the `resize only` mode, the image will only be resized while keeping its side ratio. The `side_ratio` setting is ignored then.
16
+
17
+ In the `crop to ratio` mode, parts of the image will be removed after resizing as necessary to make its side ratio match `side_ratio` setting.
18
+
19
+ In the `pad to ratio` mode, transparent padding will be added to the image after resizing as necessary to make its side ratio match `side_ratio` setting.
20
+
21
+ ### smaller_side, larger_side, scale_factor
22
+
23
+ These settings determine the image’s target size. Only one of these settings can be enabled (set to a non-zero value).
24
+
25
+ With `smaller_side` set, the target size is determined by the smaller side of the image. E.g. with the `action` being `resize only` and the original image being 512x768 pixels large, `smaller_side` set to 1024 will resize the image to 1024x1536 pixels.
26
+
27
+ With `larger_side` set, the target size is determined by the larger side of the image. E.g. with the `action` being `resize only` and the original image being 512x768 pixels large, `larger_side` set to 1024 will resize the image to 683x1024 pixels.
28
+
29
+ Finally, `scale_factor` can be set as an explicit scaling factor. Values below 1.0 will reduce image size, above 1.0 increase it.
30
+
31
+ If neither setting is set, the image is not resized but merely cropped/padded as necessary.
32
+
33
+ ### resize_mode
34
+
35
+ In the `reduce size only` mode, images already smaller than the target size will not be resized. In the `increase size only` mode, images already larger than the target size will not be resized. The `any` mode causes the image to be always resized, regardless of whether downscaling or upscaling is required.
36
+
37
+ ### side_ratio
38
+
39
+ If the `action` setting enables cropping or padding of the image, this setting determines the required side ratio of the image. The format is `width:height`, e.g. `4:3` or `2:3`.
40
+
41
+ In case you want to resize the image to an explicit size, you can also set this size here, e.g. `512:768`. You then set `smaller_side` setting to `512` and the resulting image will always be 512x768 pixels large.
42
+
43
+ ### crop_pad_position
44
+
45
+ If the image is cropped, this setting determines which side is cropped. The value `0.0` means that only the right/bottom side is cropped. The value `1.0` means that only left/top side is cropped. The value `0.3` means that 30% are being cropped on the left/top side and 70% on the right/top side.
46
+
47
+ If the image is padded, this setting determines where the padding is being inserted. The value `0.0` means that all padding is inserted on the right/bottom side. The value `1.0` means that all padding is inserted on the left/top side. The value `0.3` means that 30% of the padding are inserted on the left/top side and 70% on the right/top side.
48
+
49
+ ### pad_feathering
50
+
51
+ If the image is padded, this setting causes mask transparency to partially expand into the original image for the given number of pixels. This helps avoid borders if the image is later inpainted.
__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .image_resize import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
2
+
3
+ __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
image_resize.png ADDED
image_resize.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ class ImageResize:
4
+ def __init__(self):
5
+ pass
6
+
7
+
8
+ ACTION_TYPE_RESIZE = "resize only"
9
+ ACTION_TYPE_CROP = "crop to ratio"
10
+ ACTION_TYPE_PAD = "pad to ratio"
11
+ RESIZE_MODE_DOWNSCALE = "reduce size only"
12
+ RESIZE_MODE_UPSCALE = "increase size only"
13
+ RESIZE_MODE_ANY = "any"
14
+ RETURN_TYPES = ("IMAGE", "MASK",)
15
+ FUNCTION = "resize"
16
+ CATEGORY = "image"
17
+
18
+
19
+ @classmethod
20
+ def INPUT_TYPES(s):
21
+ return {
22
+ "required": {
23
+ "pixels": ("IMAGE",),
24
+ "action": ([s.ACTION_TYPE_RESIZE, s.ACTION_TYPE_CROP, s.ACTION_TYPE_PAD],),
25
+ "smaller_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
26
+ "larger_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
27
+ "scale_factor": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1}),
28
+ "resize_mode": ([s.RESIZE_MODE_DOWNSCALE, s.RESIZE_MODE_UPSCALE, s.RESIZE_MODE_ANY],),
29
+ "side_ratio": ("STRING", {"default": "4:3"}),
30
+ "crop_pad_position": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
31
+ "pad_feathering": ("INT", {"default": 20, "min": 0, "max": 8192, "step": 1}),
32
+ },
33
+ "optional": {
34
+ "mask_optional": ("MASK",),
35
+ },
36
+ }
37
+
38
+
39
+ @classmethod
40
+ def VALIDATE_INPUTS(s, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, **_):
41
+ if side_ratio is not None:
42
+ if action != s.ACTION_TYPE_RESIZE and s.parse_side_ratio(side_ratio) is None:
43
+ return f"Invalid side ratio: {side_ratio}"
44
+
45
+ if smaller_side is not None and larger_side is not None and scale_factor is not None:
46
+ if int(smaller_side > 0) + int(larger_side > 0) + int(scale_factor > 0) > 1:
47
+ return f"At most one scaling rule (smaller_side, larger_side, scale_factor) should be enabled by setting a non-zero value"
48
+
49
+ if scale_factor is not None:
50
+ if resize_mode == s.RESIZE_MODE_DOWNSCALE and scale_factor > 1.0:
51
+ return f"For resize_mode {s.RESIZE_MODE_DOWNSCALE}, scale_factor should be less than one but got {scale_factor}"
52
+ if resize_mode == s.RESIZE_MODE_UPSCALE and scale_factor > 0.0 and scale_factor < 1.0:
53
+ return f"For resize_mode {s.RESIZE_MODE_UPSCALE}, scale_factor should be larger than one but got {scale_factor}"
54
+
55
+ return True
56
+
57
+
58
+ @classmethod
59
+ def parse_side_ratio(s, side_ratio):
60
+ try:
61
+ x, y = map(int, side_ratio.split(":", 1))
62
+ if x < 1 or y < 1:
63
+ raise Exception("Ratio factors have to be positive numbers")
64
+ return float(x) / float(y)
65
+ except:
66
+ return None
67
+
68
+
69
+ def resize(self, pixels, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, crop_pad_position, pad_feathering, mask_optional=None):
70
+ validity = self.VALIDATE_INPUTS(action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio)
71
+ if validity is not True:
72
+ raise Exception(validity)
73
+
74
+ height, width = pixels.shape[1:3]
75
+ if mask_optional is None:
76
+ mask = torch.zeros(1, height, width, dtype=torch.float32)
77
+ else:
78
+ mask = mask_optional
79
+ if mask.shape[1] != height or mask.shape[2] != width:
80
+ mask = torch.nn.functional.interpolate(mask.unsqueeze(0), size=(height, width), mode="bicubic").squeeze(0).clamp(0.0, 1.0)
81
+
82
+ crop_x, crop_y, pad_x, pad_y = (0.0, 0.0, 0.0, 0.0)
83
+ if action == self.ACTION_TYPE_CROP:
84
+ target_ratio = self.parse_side_ratio(side_ratio)
85
+ if height * target_ratio < width:
86
+ crop_x = width - height * target_ratio
87
+ else:
88
+ crop_y = height - width / target_ratio
89
+ elif action == self.ACTION_TYPE_PAD:
90
+ target_ratio = self.parse_side_ratio(side_ratio)
91
+ if height * target_ratio > width:
92
+ pad_x = height * target_ratio - width
93
+ else:
94
+ pad_y = width / target_ratio - height
95
+
96
+ if smaller_side > 0:
97
+ if width + pad_x - crop_x > height + pad_y - crop_y:
98
+ scale_factor = float(smaller_side) / (height + pad_y - crop_y)
99
+ else:
100
+ scale_factor = float(smaller_side) / (width + pad_x - crop_x)
101
+ if larger_side > 0:
102
+ if width + pad_x - crop_x > height + pad_y - crop_y:
103
+ scale_factor = float(larger_side) / (width + pad_x - crop_x)
104
+ else:
105
+ scale_factor = float(larger_side) / (height + pad_y - crop_y)
106
+
107
+ if (resize_mode == self.RESIZE_MODE_DOWNSCALE and scale_factor >= 1.0) or (resize_mode == self.RESIZE_MODE_UPSCALE and scale_factor <= 1.0):
108
+ scale_factor = 0.0
109
+
110
+ if scale_factor > 0.0:
111
+ pixels = torch.nn.functional.interpolate(pixels.movedim(-1, 1), scale_factor=scale_factor, mode="bicubic", antialias=True).movedim(1, -1).clamp(0.0, 1.0)
112
+ mask = torch.nn.functional.interpolate(mask.unsqueeze(0), scale_factor=scale_factor, mode="bicubic", antialias=True).squeeze(0).clamp(0.0, 1.0)
113
+ height, width = pixels.shape[1:3]
114
+
115
+ crop_x *= scale_factor
116
+ crop_y *= scale_factor
117
+ pad_x *= scale_factor
118
+ pad_y *= scale_factor
119
+
120
+ if crop_x > 0.0 or crop_y > 0.0:
121
+ remove_x = (round(crop_x * crop_pad_position), round(crop_x * (1 - crop_pad_position))) if crop_x > 0.0 else (0, 0)
122
+ remove_y = (round(crop_y * crop_pad_position), round(crop_y * (1 - crop_pad_position))) if crop_y > 0.0 else (0, 0)
123
+ pixels = pixels[:, remove_y[0]:height - remove_y[1], remove_x[0]:width - remove_x[1], :]
124
+ mask = mask[:, remove_y[0]:height - remove_y[1], remove_x[0]:width - remove_x[1]]
125
+ elif pad_x > 0.0 or pad_y > 0.0:
126
+ add_x = (round(pad_x * crop_pad_position), round(pad_x * (1 - crop_pad_position))) if pad_x > 0.0 else (0, 0)
127
+ add_y = (round(pad_y * crop_pad_position), round(pad_y * (1 - crop_pad_position))) if pad_y > 0.0 else (0, 0)
128
+
129
+ new_pixels = torch.zeros(pixels.shape[0], height + add_y[0] + add_y[1], width + add_x[0] + add_x[1], pixels.shape[3], dtype=torch.float32)
130
+ new_pixels[:, add_y[0]:height + add_y[0], add_x[0]:width + add_x[0], :] = pixels
131
+ pixels = new_pixels
132
+
133
+ new_mask = torch.ones(mask.shape[0], height + add_y[0] + add_y[1], width + add_x[0] + add_x[1], dtype=torch.float32)
134
+ new_mask[:, add_y[0]:height + add_y[0], add_x[0]:width + add_x[0]] = mask
135
+ mask = new_mask
136
+
137
+ if pad_feathering > 0:
138
+ for i in range(mask.shape[0]):
139
+ for j in range(pad_feathering):
140
+ feather_strength = (1 - j / pad_feathering) * (1 - j / pad_feathering)
141
+ if add_x[0] > 0 and j < width:
142
+ for k in range(height):
143
+ mask[i, k, add_x[0] + j] = max(mask[i, k, add_x[0] + j], feather_strength)
144
+ if add_x[1] > 0 and j < width:
145
+ for k in range(height):
146
+ mask[i, k, width + add_x[0] - j - 1] = max(mask[i, k, width + add_x[0] - j - 1], feather_strength)
147
+ if add_y[0] > 0 and j < height:
148
+ for k in range(width):
149
+ mask[i, add_y[0] + j, k] = max(mask[i, add_y[0] + j, k], feather_strength)
150
+ if add_y[1] > 0 and j < height:
151
+ for k in range(width):
152
+ mask[i, height + add_y[0] - j - 1, k] = max(mask[i, height + add_y[0] - j - 1, k], feather_strength)
153
+
154
+ return (pixels, mask)
155
+
156
+
157
+ NODE_CLASS_MAPPINGS = {
158
+ "ImageResize": ImageResize
159
+ }
160
+
161
+ NODE_DISPLAY_NAME_MAPPINGS = {
162
+ "ImageResize": "Image Resize"
163
+ }