diff --git a/README.md b/README.md index 8bb395a..8998f67 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # ComfyUI-SmartResize -Two focused resize nodes for ComfyUI — both with **Lanczos** interpolation and **multiple_of** snapping. +Smart image resize nodes for ComfyUI — batch processing, LoRA prep & SeedVR support. + +All nodes appear under the **Craftopia** category in the node menu. --- @@ -9,9 +11,13 @@ Two focused resize nodes for ComfyUI — both with **Lanczos** interpolation and ### 📐 Smart Resize Resize any image (or batch) so the **longest side** equals `max_pixels`, with aspect ratio preserved. -Use this as a **pipeline node** — IMAGE in, IMAGE out. Perfect for downscaling SeedVR upscales before LoRA training, or any workflow step where you need a clean downscale without saving to disk. +Use this as a **pipeline node** — IMAGE in, IMAGE out. No files are saved to disk. + +**Typical usecases:** +- Downscale a SeedVR (or other upscaler) result back to your target training size — the upscaler adds fine detail, Smart Resize brings it to the right dimensions for LoRA training +- Any workflow step that needs a clean resize before the next node +- Normalize mixed-resolution batches to a consistent size -**Inputs:** | Input | Type | Default | Description | |---|---|---|---| | image | IMAGE | — | Single image or batch | @@ -20,55 +26,71 @@ Use this as a **pipeline node** — IMAGE in, IMAGE out. Perfect for downscaling | interpolation | ENUM | lanczos | lanczos / bicubic / bilinear / nearest | | upscale_if_smaller | BOOLEAN | false | Also upscale images already smaller than max_pixels | -**Outputs:** `IMAGE`, `width`, `height` +**Outputs:** `image`, `width`, `height` --- -### 📁 Batch Resize & Save -Load **all images from a folder**, resize each one (longest side), and save them with the original filename + a suffix into a subfolder. +### 📁 Smart Batch Resize +Load **all images from a folder**, resize each one by longest side, and save with the original filename + a suffix into a subfolder. Original filenames are always preserved. Use this for **bulk preprocessing** — e.g. preparing a LoRA dataset from a folder of high-res images. -**Inputs:** | Input | Type | Default | Description | |---|---|---|---| -| input_folder | STRING | D:/images/mijnmap | Source folder path | +| input_folder | STRING | D:/images/input | Source folder path | | max_pixels | INT | 1536 | Target size for longest side | -| filename_suffix | STRING | _Small | Appended to each output filename | +| filename_suffix | STRING | _Small | Appended to each output filename before the extension | | output_subfolder | STRING | resized | Subfolder created inside input_folder | | multiple_of | INT | 8 | Snap dimensions to this multiple | -| interpolation | ENUM | lanczos | lanczos / nearest / bilinear / bicubic | +| interpolation | ENUM | lanczos | lanczos / bicubic / bilinear / nearest | | skip_if_exists | BOOLEAN | false | Skip files that already exist in output | **Outputs:** `images` (list), `filenames` (list), `output_paths` (list), `count` --- +### 📏 Smart Size Multiplier +Multiply image dimensions by a factor and output the results as integers. + +SeedVR takes a single `resolution` INT (the longest side) — not a width and height separately. Standard math nodes give you a FLOAT or require multiple steps to get there. This node does it cleanly in one step: give it your image and a multiplier, and it outputs `width`, `height`, and `resolution` (longest side) ready to connect directly to SeedVR's `max_resolution` input. + +Also useful for LoRA dataset prep — using mixed resolutions in your training set generally produces better results than training on a single fixed size. Smart Size Multiplier makes it easy to dynamically calculate the right target size per image rather than hardcoding a value. + +| Input | Type | Default | Description | +|---|---|---|---| +| image | IMAGE | — | Source image | +| multiplier | FLOAT | 2.0 | Multiply width and height by this factor | +| multiple_of | INT | 8 | Snap dimensions to this multiple | + +**Outputs:** `width`, `height`, `resolution` (longest side as INT → directly into SeedVR) + +--- + ## Why these nodes? -Existing nodes like `JWImageResizeByLongerSide` lack **Lanczos** interpolation (the best choice for downscaling). Other batch nodes don't preserve original filenames or offer `multiple_of` snapping. - -These nodes fill that gap with a minimal, focused feature set. +- ComfyUI's built-in `Resize Images by Longer Edge` [BETA] has no Lanczos and no `multiple_of` snapping +- `JWImageResizeByLongerSide` (comfyui-various) has no Lanczos in the official release +- No existing node combines batch folder loading + longest-side resize + original filename preservation +- No existing node outputs a ready-to-use `resolution` INT for SeedVR --- ## Installation -**Option A — ComfyUI Manager:** -Search for `ComfyUI-SmartResize` and install. +**ComfyUI Manager:** search for `ComfyUI-SmartResize` and install. -**Option B — Manual:** +**Manual:** ```bash cd ComfyUI/custom_nodes -git clone https://github.com/YOUR_USERNAME/ComfyUI-SmartResize +git clone https://github.com/CraftopiaStudio/ComfyUI-SmartResize ``` -Restart ComfyUI. Nodes appear under: -- `image/transform` → **Smart Resize 📐** -- `image/batch` → **Batch Resize & Save 📁** +Restart ComfyUI. All nodes appear under **Craftopia** in the node menu: + +- `Craftopia` → **Smart Resize 📐** +- `Craftopia` → **Smart Batch Resize 📁** +- `Craftopia` → **Smart Size Multiplier 📏** --- ## Requirements -- ComfyUI -- Pillow (included with ComfyUI) -- PyTorch (included with ComfyUI) +Pillow, NumPy, PyTorch — all included with ComfyUI. No extra dependencies. diff --git a/smart_batch_resize.py b/smart_batch_resize.py index 6dda692..e6f2124 100644 --- a/smart_batch_resize.py +++ b/smart_batch_resize.py @@ -74,7 +74,7 @@ class SmartBatchResize: RETURN_NAMES = ("images", "filenames", "output_paths", "count") OUTPUT_IS_LIST = (True, True, True, False) FUNCTION = "run" - CATEGORY = "SmartResize" + CATEGORY = "Craftopia" OUTPUT_NODE = True def run(self, input_folder, max_pixels, filename_suffix, diff --git a/smart_resize.py b/smart_resize.py index afc426e..b188014 100644 --- a/smart_resize.py +++ b/smart_resize.py @@ -1,48 +1,53 @@ -import math from PIL import Image as PILImage import numpy as np import torch +INTERP_MAP = { + "lanczos": PILImage.LANCZOS, + "bicubic": PILImage.BICUBIC, + "bilinear": PILImage.BILINEAR, + "nearest": PILImage.NEAREST, +} + + +def _calc_new_size(w, h, max_pixels, multiple_of, upscale_if_smaller): + longest = max(w, h) + if longest <= max_pixels and not upscale_if_smaller: + new_w, new_h = w, h + else: + scale = max_pixels / longest + new_w = int(w * scale) + new_h = int(h * scale) + if multiple_of > 1: + new_w = max(multiple_of, round(new_w / multiple_of) * multiple_of) + new_h = max(multiple_of, round(new_h / multiple_of) * multiple_of) + return new_w, new_h + + class SmartResize: """ - Resize images so the longest side equals max_pixels. - Supports lanczos interpolation and multiple_of snapping. - Works with single images and batches. + Resize IMAGE by longest side. Lanczos included. Multiple_of snapping. + Works with single images and batches. IMAGE in → IMAGE out. """ - INTERP_MAP = { - "lanczos": PILImage.LANCZOS, - "bicubic": PILImage.BICUBIC, - "bilinear": PILImage.BILINEAR, - "nearest": PILImage.NEAREST, - } - @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE",), "max_pixels": ("INT", { - "default": 1536, - "min": 64, - "max": 8192, - "step": 8, - "tooltip": "Longest side will be resized to this value (aspect ratio preserved)" + "default": 1536, "min": 64, "max": 8192, "step": 8, + "tooltip": "Longest side target in pixels. Aspect ratio is always preserved." }), "multiple_of": ("INT", { - "default": 8, - "min": 1, - "max": 64, - "step": 1, - "tooltip": "Round dimensions to a multiple of this value (8 = SD compatible)" - }), - "interpolation": (["lanczos", "bicubic", "bilinear", "nearest"], { - "default": "lanczos" + "default": 8, "min": 1, "max": 64, "step": 1, + "tooltip": "Snap both dimensions to a multiple of this value. Use 8 for SD/Flux." }), + "interpolation": (["lanczos", "bicubic", "bilinear", "nearest"],), "upscale_if_smaller": ("BOOLEAN", { "default": False, - "tooltip": "Also upscale images that are already smaller than max_pixels" + "tooltip": "Upscale images that are already smaller than max_pixels." }), } } @@ -50,60 +55,31 @@ class SmartResize: RETURN_TYPES = ("IMAGE", "INT", "INT") RETURN_NAMES = ("image", "width", "height") FUNCTION = "run" - CATEGORY = "image/transform" - - def _resize_one(self, img_tensor, max_pixels, multiple_of, interp, upscale_if_smaller): - """Resize a single [H, W, C] tensor.""" - h, w = img_tensor.shape[0], img_tensor.shape[1] - longest = max(w, h) - - # Skip if already small enough and upscale disabled - if longest <= max_pixels and not upscale_if_smaller: - # Still snap to multiple_of - new_w = max(multiple_of, round(w / multiple_of) * multiple_of) if multiple_of > 1 else w - new_h = max(multiple_of, round(h / multiple_of) * multiple_of) if multiple_of > 1 else h - if new_w == w and new_h == h: - return img_tensor, w, h - else: - scale = max_pixels / longest - new_w = int(w * scale) - new_h = int(h * scale) - - # Snap to multiple_of - if multiple_of > 1: - new_w = max(multiple_of, round(new_w / multiple_of) * multiple_of) - new_h = max(multiple_of, round(new_h / multiple_of) * multiple_of) - - # Convert tensor [H, W, C] float32 → PIL → resize → tensor - arr = (img_tensor.numpy() * 255).astype(np.uint8) - pil = PILImage.fromarray(arr, mode="RGB") - pil_resized = pil.resize((new_w, new_h), interp) - arr_resized = np.array(pil_resized).astype(np.float32) / 255.0 - return torch.from_numpy(arr_resized), new_w, new_h + CATEGORY = "Craftopia" def run(self, image, max_pixels, multiple_of, interpolation, upscale_if_smaller): - # image shape: [B, H, W, C] - interp = self.INTERP_MAP[interpolation] + interp = INTERP_MAP[interpolation] results = [] last_w, last_h = 0, 0 for i in range(image.shape[0]): - resized, w, h = self._resize_one( - image[i], max_pixels, multiple_of, interp, upscale_if_smaller - ) - results.append(resized) - last_w, last_h = w, h + frame = image[i] # [H, W, C] + h, w = frame.shape[0], frame.shape[1] + new_w, new_h = _calc_new_size(w, h, max_pixels, multiple_of, upscale_if_smaller) - # Stack back to [B, H, W, C] — all frames must be same size - # (they will be if all input frames are same size, which is the ComfyUI norm) - output = torch.stack(results, dim=0) - return (output, last_w, last_h) + if new_w == w and new_h == h: + results.append(frame) + else: + arr = (frame.numpy() * 255).astype("uint8") + pil = PILImage.fromarray(arr, mode="RGB") + pil = pil.resize((new_w, new_h), interp) + arr = np.array(pil).astype("float32") / 255.0 + results.append(torch.from_numpy(arr)) + + last_w, last_h = new_w, new_h + + return (torch.stack(results, dim=0), last_w, last_h) -NODE_CLASS_MAPPINGS = { - "SmartResize": SmartResize, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "SmartResize": "Smart Resize 📐", -} +NODE_CLASS_MAPPINGS = {"SmartResize": SmartResize} +NODE_DISPLAY_NAME_MAPPINGS = {"SmartResize": "Smart Resize 📐"} diff --git a/smart_size_multiplier.py b/smart_size_multiplier.py index 189f3e1..77043a6 100644 --- a/smart_size_multiplier.py +++ b/smart_size_multiplier.py @@ -31,7 +31,7 @@ class SmartSizeMultiplier: RETURN_TYPES = ("INT", "INT", "INT") RETURN_NAMES = ("width", "height", "resolution") FUNCTION = "run" - CATEGORY = "SmartResize" + CATEGORY = "Craftopia" def run(self, image, multiplier, multiple_of): h, w = image.shape[1], image.shape[2]