uploaded SmartResize nodes

This commit is contained in:
rubz
2026-03-03 12:13:32 +01:00
parent 3a64d873ab
commit aa2e2bd16c
5 changed files with 374 additions and 1 deletions
+73 -1
View File
@@ -1,2 +1,74 @@
# ComfyUI-SmartResize
Smart image resize nodes for ComfyUI - Batch processing, LoRA prep & SeedVR support
Two focused resize nodes for ComfyUI — both with **Lanczos** interpolation and **multiple_of** snapping.
---
## Nodes
### 📐 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.
**Inputs:**
| Input | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | Single image or batch |
| max_pixels | INT | 1536 | Target size for longest side |
| multiple_of | INT | 8 | Snap dimensions to this multiple (8 = SD/Flux compatible) |
| interpolation | ENUM | lanczos | lanczos / bicubic / bilinear / nearest |
| upscale_if_smaller | BOOLEAN | false | Also upscale images already smaller than max_pixels |
**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.
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 |
| max_pixels | INT | 1536 | Target size for longest side |
| filename_suffix | STRING | _Small | Appended to each output filename |
| 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 |
| skip_if_exists | BOOLEAN | false | Skip files that already exist in output |
**Outputs:** `images` (list), `filenames` (list), `output_paths` (list), `count`
---
## 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.
---
## Installation
**Option A — ComfyUI Manager:**
Search for `ComfyUI-SmartResize` and install.
**Option B — Manual:**
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/YOUR_USERNAME/ComfyUI-SmartResize
```
Restart ComfyUI. Nodes appear under:
- `image/transform` → **Smart Resize 📐**
- `image/batch` → **Batch Resize & Save 📁**
---
## Requirements
- ComfyUI
- Pillow (included with ComfyUI)
- PyTorch (included with ComfyUI)
+8
View File
@@ -0,0 +1,8 @@
from .smart_resize import NODE_CLASS_MAPPINGS as A, NODE_DISPLAY_NAME_MAPPINGS as A_NAMES
from .smart_batch_resize import NODE_CLASS_MAPPINGS as B, NODE_DISPLAY_NAME_MAPPINGS as B_NAMES
from .smart_size_multiplier import NODE_CLASS_MAPPINGS as C, NODE_DISPLAY_NAME_MAPPINGS as C_NAMES
NODE_CLASS_MAPPINGS = {**A, **B, **C}
NODE_DISPLAY_NAME_MAPPINGS = {**A_NAMES, **B_NAMES, **C_NAMES}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+132
View File
@@ -0,0 +1,132 @@
import os
from pathlib import Path
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,
}
SUPPORTED_EXT = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff", ".tif"}
def _calc_new_size(w, h, max_pixels, multiple_of):
longest = max(w, h)
if longest <= max_pixels:
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 SmartBatchResize:
"""
Load all images from a folder, resize by longest side, save with suffix.
Original filenames preserved. Output subfolder auto-created.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_folder": ("STRING", {
"default": "D:/images/input",
"multiline": False,
"tooltip": "Folder containing images to resize."
}),
"max_pixels": ("INT", {
"default": 1536, "min": 64, "max": 8192, "step": 8,
"tooltip": "Longest side target in pixels. Aspect ratio is always preserved."
}),
"filename_suffix": ("STRING", {
"default": "_Small",
"multiline": False,
"tooltip": "Appended to each output filename before the extension."
}),
"output_subfolder": ("STRING", {
"default": "resized",
"multiline": False,
"tooltip": "Subfolder created inside input_folder for output files."
}),
"multiple_of": ("INT", {
"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"],),
"skip_if_exists": ("BOOLEAN", {
"default": False,
"tooltip": "Skip files that already exist in the output folder."
}),
}
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT")
RETURN_NAMES = ("images", "filenames", "output_paths", "count")
OUTPUT_IS_LIST = (True, True, True, False)
FUNCTION = "run"
CATEGORY = "SmartResize"
OUTPUT_NODE = True
def run(self, input_folder, max_pixels, filename_suffix,
output_subfolder, multiple_of, interpolation, skip_if_exists):
input_folder = input_folder.strip()
if not os.path.isdir(input_folder):
raise ValueError(f"[SmartBatchResize] Folder not found: {input_folder}")
files = sorted([
f for f in Path(input_folder).iterdir()
if f.is_file() and f.suffix.lower() in SUPPORTED_EXT
])
if not files:
raise ValueError(f"[SmartBatchResize] No images found in: {input_folder}")
out_dir = Path(input_folder) / output_subfolder
out_dir.mkdir(parents=True, exist_ok=True)
interp = INTERP_MAP[interpolation]
images_out, filenames_out, paths_out = [], [], []
for f in files:
out_name = f"{f.stem}{filename_suffix}{f.suffix.lower()}"
out_path = out_dir / out_name
if skip_if_exists and out_path.exists():
print(f"[SmartBatchResize] Skipped (exists): {out_name}")
continue
img = PILImage.open(f).convert("RGB")
w, h = img.size
new_w, new_h = _calc_new_size(w, h, max_pixels, multiple_of)
img_resized = img.resize((new_w, new_h), interp)
save_kwargs = {"quality": 95} if f.suffix.lower() in (".jpg", ".jpeg") else {}
img_resized.save(out_path, **save_kwargs)
print(f"[SmartBatchResize] {f.name} → {out_name} ({w}x{h} → {new_w}x{new_h})")
arr = np.array(img_resized).astype("float32") / 255.0
images_out.append(torch.from_numpy(arr).unsqueeze(0))
filenames_out.append(out_name)
paths_out.append(str(out_path))
if not images_out:
raise ValueError("[SmartBatchResize] No images processed.")
return (images_out, filenames_out, paths_out, len(images_out))
NODE_CLASS_MAPPINGS = {"SmartBatchResize": SmartBatchResize}
NODE_DISPLAY_NAME_MAPPINGS = {"SmartBatchResize": "Smart Batch Resize 📁"}
+109
View File
@@ -0,0 +1,109 @@
import math
from PIL import Image as PILImage
import numpy as np
import torch
class SmartResize:
"""
Resize images so the longest side equals max_pixels.
Supports lanczos interpolation and multiple_of snapping.
Works with single images and batches.
"""
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)"
}),
"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"
}),
"upscale_if_smaller": ("BOOLEAN", {
"default": False,
"tooltip": "Also upscale images that are already smaller than max_pixels"
}),
}
}
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
def run(self, image, max_pixels, multiple_of, interpolation, upscale_if_smaller):
# image shape: [B, H, W, C]
interp = self.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
# 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)
NODE_CLASS_MAPPINGS = {
"SmartResize": SmartResize,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SmartResize": "Smart Resize 📐",
}
+52
View File
@@ -0,0 +1,52 @@
class SmartSizeMultiplier:
"""
Multiplies image dimensions by a factor.
Outputs width, height, and resolution (longest side) as INT.
Use resolution output directly with SeedVR max_resolution.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"multiplier": ("FLOAT", {
"default": 2.0,
"min": 0.1,
"max": 16.0,
"step": 0.1,
"round": 0.01,
"tooltip": "Multiply width and height by this factor. E.g. 2.0 = double resolution."
}),
"multiple_of": ("INT", {
"default": 8,
"min": 1,
"max": 64,
"step": 1,
"tooltip": "Snap output dimensions to a multiple of this value."
}),
}
}
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("width", "height", "resolution")
FUNCTION = "run"
CATEGORY = "SmartResize"
def run(self, image, multiplier, multiple_of):
h, w = image.shape[1], image.shape[2]
new_w = int(round(w * multiplier))
new_h = int(round(h * multiplier))
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)
resolution = max(new_w, new_h)
print(f"[SmartSizeMultiplier] {w}x{h} x {multiplier} -> {new_w}x{new_h} (resolution: {resolution})")
return (new_w, new_h, resolution)
NODE_CLASS_MAPPINGS = {"SmartSizeMultiplier": SmartSizeMultiplier}
NODE_DISPLAY_NAME_MAPPINGS = {"SmartSizeMultiplier": "Smart Size Multiplier 📏"}