227 lines
6.3 KiB
Python
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)
|