uploaded SmartResize nodes
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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 📐",
|
||||
}
|
||||
@@ -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 📏"}
|
||||
Reference in New Issue
Block a user