Files

227 lines
6.3 KiB
Python

"""
Image and mask resize utilities for the ComfyUI Auto Size plugin.
Provides unified resize logic with support for various crop and scale methods.
"""
from __future__ import annotations
import torch
import torch.nn.functional as F
try:
from .presets import INTERPOLATION_MODES
except ImportError:
from utils.presets import INTERPOLATION_MODES
# ============================================================================
# Core Resize Logic
# ============================================================================
def _calculate_scaled_dimensions(
src_w: int, src_h: int, target_w: int, target_h: int, cover: bool
) -> tuple[int, int]:
"""
Calculate new dimensions for scaling.
Args:
src_w: Source width.
src_h: Source height.
target_w: Target width.
target_h: Target height.
cover: If True, scale to cover target (for cropping);
if False, scale to fit within target (for letterboxing).
Returns:
(new_width, new_height) for the scaled image.
"""
src_ratio = src_w / src_h
tgt_ratio = target_w / target_h
# For cover: use the dimension that gives larger result (to ensure full coverage)
# For fit: use the dimension that gives smaller result (to ensure it fits inside)
use_width_as_ref = (src_ratio > tgt_ratio) != cover
if use_width_as_ref:
new_w = target_w
new_h = int(target_w / src_ratio)
else:
new_h = target_h
new_w = int(target_h * src_ratio)
return new_w, new_h
def _get_crop_offset(
crop_method: str, new_w: int, new_h: int, target_w: int, target_h: int
) -> tuple[int, int]:
"""
Calculate crop offset based on crop method.
Args:
crop_method: One of "Center Crop", "Top-Left Crop", "Bottom-Right Crop".
new_w: Scaled width before cropping.
new_h: Scaled height before cropping.
target_w: Target width.
target_h: Target height.
Returns:
(top, left) offset for cropping.
"""
if crop_method == "Top-Left Crop":
return 0, 0
if crop_method == "Bottom-Right Crop":
return new_h - target_h, new_w - target_w
# Center crop (default)
return (new_h - target_h) // 2, (new_w - target_w) // 2
def _resize_tensor(
tensor: torch.Tensor,
target_width: int,
target_height: int,
crop_method: str,
scale_method: str,
) -> torch.Tensor:
"""
Core resize function for tensors in [B, C, H, W] format.
Args:
tensor: Input tensor in [B, C, H, W] format.
target_width: Target width in pixels.
target_height: Target height in pixels.
crop_method: Resize strategy (crop, fit, or stretch).
scale_method: Interpolation method name.
Returns:
Resized tensor in [B, C, H, W] format.
"""
batch, channels, src_h, src_w = tensor.shape
# Get interpolation mode
mode = INTERPOLATION_MODES.get(scale_method, "bicubic")
align_corners = None if mode == "nearest" else False
if crop_method == "Stretch to Fill":
# Direct resize to target dimensions
return F.interpolate(
tensor,
size=(target_height, target_width),
mode=mode,
align_corners=align_corners,
)
if crop_method == "Scale to Fit":
# Scale proportionally, pad with black (letterbox)
new_w, new_h = _calculate_scaled_dimensions(
src_w, src_h, target_width, target_height, cover=False
)
resized = F.interpolate(
tensor,
size=(new_h, new_w),
mode=mode,
align_corners=align_corners,
)
# Create black canvas and center the image
result = torch.zeros(
(batch, channels, target_height, target_width),
dtype=tensor.dtype,
device=tensor.device,
)
pad_top = (target_height - new_h) // 2
pad_left = (target_width - new_w) // 2
result[:, :, pad_top : pad_top + new_h, pad_left : pad_left + new_w] = resized
return result
# Crop modes: scale to cover, then crop
new_w, new_h = _calculate_scaled_dimensions(
src_w, src_h, target_width, target_height, cover=True
)
resized = F.interpolate(
tensor,
size=(new_h, new_w),
mode=mode,
align_corners=align_corners,
)
# Crop to target size
top, left = _get_crop_offset(crop_method, new_w, new_h, target_width, target_height)
return resized[:, :, top : top + target_height, left : left + target_width]
# ============================================================================
# Public API
# ============================================================================
def resize_image(
image_tensor: torch.Tensor,
target_width: int,
target_height: int,
crop_method: str,
scale_method: str,
) -> torch.Tensor:
"""
Resize an image tensor.
Args:
image_tensor: Input tensor in [B, H, W, C] format.
target_width: Target width in pixels.
target_height: Target height in pixels.
crop_method: Resize strategy.
scale_method: Interpolation method.
Returns:
Resized tensor in [B, H, W, C] format.
"""
# Convert [B, H, W, C] -> [B, C, H, W]
img = image_tensor.permute(0, 3, 1, 2)
resized = _resize_tensor(
img,
target_width,
target_height,
crop_method,
scale_method,
)
# Convert back [B, C, H, W] -> [B, H, W, C]
return resized.permute(0, 2, 3, 1)
def resize_mask(
mask_tensor: torch.Tensor,
target_width: int,
target_height: int,
crop_method: str,
scale_method: str,
) -> torch.Tensor:
"""
Resize a mask tensor.
Args:
mask_tensor: Input tensor in [B, H, W] format.
target_width: Target width in pixels.
target_height: Target height in pixels.
crop_method: Resize strategy.
scale_method: Interpolation method.
Returns:
Resized tensor in [B, H, W] format.
"""
# Add channel dimension [B, H, W] -> [B, 1, H, W]
mask = mask_tensor.unsqueeze(1)
resized = _resize_tensor(
mask,
target_width,
target_height,
crop_method,
scale_method,
)
# Remove channel dimension [B, 1, H, W] -> [B, H, W]
return resized.squeeze(1)