Normalize Product: centers/scales a catalogue product into a consistent frame so a fixed set of detail crops lands on the same spot across every SKU. Default "keep resolution (pad)" mode crops to the product and pads with the margin at native resolution (no downscaling / no quality loss); an optional "fixed canvas (scale)" mode targets an exact WxH. Detects the product against the light background locally - no AI, no API key. Resize (Long Side): scales an image so its longest side hits a target, preserving aspect ratio, with an only-downscale toggle. Outputs the image plus the resulting width/height. Manual Detail Sheet improvements: - external numbered on/off buttons (replacing the fiddly in-canvas toggle dot); only active boxes are drawn, so toggling never fights dragging - boxes are now true 1:1 squares in image pixels (widget + server-side) - live per-box crop-preview thumbnails so you can confirm each detail is inside its box before running - the node now sends the exact image it received back to its widget on execute, so it can show/box an upstream-processed image (e.g. from Normalize Product) that has no live thumbnail of its own Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
import logging
|
|
|
|
import numpy as np
|
|
import torch
|
|
from PIL import Image
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
RESAMPLE_FILTERS = {
|
|
"lanczos": Image.LANCZOS,
|
|
"bicubic": Image.BICUBIC,
|
|
"bilinear": Image.BILINEAR,
|
|
"nearest": Image.NEAREST,
|
|
}
|
|
|
|
|
|
class SupersideResizeLongSideNode:
|
|
"""
|
|
Resize (Long Side) Node: scale an image so its longest side equals (or is
|
|
capped at) a target size, preserving aspect ratio. Handy for keeping the
|
|
biggest dimension of catalogue images under a limit before further
|
|
processing, without distorting them.
|
|
"""
|
|
|
|
CATEGORY = "Superside"
|
|
|
|
RESAMPLE_OPTIONS = ["lanczos", "bicubic", "bilinear", "nearest"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"max_long_side": (
|
|
"INT",
|
|
{"default": 2048, "min": 16, "max": 16384, "step": 8,
|
|
"tooltip": "Target size, in pixels, for the image's longest side. The shorter side scales proportionally."},
|
|
),
|
|
},
|
|
"optional": {
|
|
"only_downscale": (
|
|
"BOOLEAN",
|
|
{"default": True,
|
|
"tooltip": "ON: only shrink images whose long side is bigger than the target; smaller images are left untouched (never upscaled). OFF: always set the long side to exactly the target, scaling up small images too."},
|
|
),
|
|
"resample": (
|
|
cls.RESAMPLE_OPTIONS,
|
|
{"default": "lanczos",
|
|
"tooltip": "Resampling filter. Lanczos gives the sharpest results for photos."},
|
|
),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "INT", "INT")
|
|
RETURN_NAMES = ("image", "width", "height")
|
|
FUNCTION = "resize"
|
|
DESCRIPTION = (
|
|
"Scale an image so its longest side hits a target size, keeping "
|
|
"aspect ratio. Optionally only downscale (never enlarge). Also "
|
|
"outputs the resulting width and height."
|
|
)
|
|
|
|
def resize(self, image, max_long_side=2048, only_downscale=True, resample="lanczos"):
|
|
try:
|
|
resample_filter = RESAMPLE_FILTERS.get(resample, Image.LANCZOS)
|
|
|
|
image_np = image.cpu().numpy() if isinstance(image, torch.Tensor) else np.asarray(image)
|
|
if image_np.ndim == 3:
|
|
image_np = image_np[None, ...]
|
|
|
|
# All frames in a ComfyUI IMAGE batch share dimensions, so the
|
|
# target size is computed once from the first frame.
|
|
h, w = image_np.shape[1], image_np.shape[2]
|
|
long_side = max(w, h)
|
|
|
|
if only_downscale and long_side <= max_long_side:
|
|
new_w, new_h = w, h
|
|
else:
|
|
scale = max_long_side / float(long_side)
|
|
new_w = max(1, int(round(w * scale)))
|
|
new_h = max(1, int(round(h * scale)))
|
|
|
|
if (new_w, new_h) == (w, h):
|
|
out_tensor = image if isinstance(image, torch.Tensor) else torch.from_numpy(image_np)
|
|
return (out_tensor, w, h)
|
|
|
|
frames = []
|
|
for frame in image_np:
|
|
if frame.dtype != np.uint8:
|
|
frame_u8 = (frame * 255).astype(np.uint8) if frame.max() <= 1.0 else frame.astype(np.uint8)
|
|
else:
|
|
frame_u8 = frame
|
|
pil = Image.fromarray(frame_u8)
|
|
resized = pil.resize((new_w, new_h), resample_filter)
|
|
frames.append(np.array(resized).astype(np.float32) / 255.0)
|
|
|
|
out_tensor = torch.from_numpy(np.stack(frames, axis=0))
|
|
logger.info(f"Resized image from {w}x{h} to {new_w}x{new_h} (long side -> {max_long_side})")
|
|
return (out_tensor, new_w, new_h)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Resize long side failed: {str(e)}")
|
|
raise RuntimeError(f"Resize long side failed: {str(e)}") from e
|