Add Smart Tile model upscale prep cap

This commit is contained in:
TTPlanetPIG
2026-07-03 22:41:30 +08:00
parent e02386d43a
commit e078f6c90f
3 changed files with 175 additions and 7 deletions
+2
View File
@@ -62,6 +62,8 @@ TTP Smart Tile Interactive Crop
By default, `TTP Smart Tile Assemble` uses `assemble_mode=final_only`. Connect `TTP Smart Tile Loop Collect.done` to `TTP Smart Tile Assemble.done` so loop runs return a lightweight preview while `done` is false, then perform the full assemble once after the last tile. Switch `assemble_mode` to `always` only when you want a full recomposite after every tile; if pixel alignment is enabled, unfinished loop runs are automatically treated as `final_only` to avoid repeated expensive alignment passes. `assemble_device` controls the paste/weight accumulation device (`auto`, `cpu`, or `gpu`), and pixel alignment can use the GPU canvas when GPU assemble is active. Use `base_canvas_mode=black` when you want connected source/base images to remain available as references without being pasted underneath the tiles. Enable `small_tile_on_top` when small detail tiles should automatically stack above larger body/background/context tiles in overlap areas. Official `Transfer Color` methods and PIL resize/crop handling remain on their existing paths for compatibility.
`TTP Smart Tile Image Upscale Prep` prepares each loop tile before img2img sampling. It can use a connected ComfyUI `UPSCALE_MODEL` through the same tiled upscale-model path as the built-in upscale node, or fall back to `lanczos`, `bicubic`, `bilinear`, `area`, or nearest resize when no model is connected or `use_upscale_model` is off. `scale` sets the requested enlargement, `max_megapixels` caps the final tile pixel count, and `round_to` snaps the final width/height after the cap. When the cap is active, the node rounds down so the rounded tile stays under the megapixel budget. Tile coordinates are not changed; they remain in original-image space so assemble can map the processed tile back by `sample_box` and `output_scale`.
`TTP Smart Tile Interactive Crop` is the recommended starting point when you want to manually or automatically split a still image by visual regions. Its `image` input follows the official `Load Image` pattern, so uploads go to ComfyUI's input folder and the workflow stores the selected filename instead of embedding the whole image. The editor can generate a standard grid from column/row numbers, replace the full layout with that grid, subdivide the currently selected tile, add painted-mask tiles, and fill uncovered gaps. It stores the tile layout in a hidden widget so the workflow keeps the current plan.
Example layout:
+160 -7
View File
@@ -2,6 +2,7 @@ import json
import os
import base64
import hashlib
import math
import re
import cv2
import numpy as np
@@ -1402,8 +1403,10 @@ def _ttp_first_image_tensor(image_tensor):
def _ttp_resize_pil_to_round(image, scale=2.0, round_to=8, resampling="lanczos"):
filters = {
"nearest": Image.Resampling.NEAREST,
"nearest-exact": Image.Resampling.NEAREST,
"bilinear": Image.Resampling.BILINEAR,
"bicubic": Image.Resampling.BICUBIC,
"area": Image.Resampling.BOX,
"lanczos": Image.Resampling.LANCZOS,
}
target_width = max(1, int(round(image.width * float(scale))))
@@ -1414,6 +1417,120 @@ def _ttp_resize_pil_to_round(image, scale=2.0, round_to=8, resampling="lanczos")
return image.resize((target_width, target_height), filters.get(str(resampling), Image.Resampling.LANCZOS))
def _ttp_resize_pil_to_size(image, width, height, resampling="lanczos"):
filters = {
"nearest": Image.Resampling.NEAREST,
"nearest-exact": Image.Resampling.NEAREST,
"bilinear": Image.Resampling.BILINEAR,
"bicubic": Image.Resampling.BICUBIC,
"area": Image.Resampling.BOX,
"lanczos": Image.Resampling.LANCZOS,
}
return image.resize((max(1, int(width)), max(1, int(height))), filters.get(str(resampling), Image.Resampling.LANCZOS))
def _ttp_round_down_to(value, multiple):
multiple = max(1, int(multiple))
value = max(1, int(math.floor(float(value))))
if multiple <= 1:
return value
rounded = int(math.floor(value / multiple) * multiple)
return max(multiple, rounded)
def _ttp_smart_upscale_target_size(width, height, scale=2.0, round_to=8, max_megapixels=0.0):
width = max(1, int(width))
height = max(1, int(height))
scale = max(0.01, float(scale))
round_to = max(1, int(round_to))
max_pixels = int(float(max_megapixels) * 1000000.0) if float(max_megapixels or 0.0) > 0 else 0
desired_width = max(1, int(round(width * scale)))
desired_height = max(1, int(round(height * scale)))
capped = False
if max_pixels > 0 and desired_width * desired_height > max_pixels:
capped = True
scale = math.sqrt(max_pixels / max(1.0, float(width) * float(height)))
target_width = _ttp_round_down_to(width * scale, round_to)
target_height = _ttp_round_down_to(height * scale, round_to)
while target_width * target_height > max_pixels and (target_width > 1 or target_height > 1):
if target_width / max(1, width) >= target_height / max(1, height) and target_width > 1:
target_width = max(1, target_width - round_to)
elif target_height > 1:
target_height = max(1, target_height - round_to)
else:
target_width = max(1, target_width - round_to)
return max(1, target_width), max(1, target_height), capped
return max(round_to, _ttp_round_to(desired_width, round_to)), max(round_to, _ttp_round_to(desired_height, round_to)), capped
def _ttp_resize_image_tensor(image, width, height, method="lanczos"):
if not hasattr(image, "movedim") or not hasattr(comfy.utils, "common_upscale"):
pil = _ttp_image_tensor_to_pil(_ttp_first_image_tensor(image))
return pil2tensor(_ttp_resize_pil_to_size(pil, int(width), int(height), method))
method = str(method or "lanczos")
method_map = {
"nearest": "nearest-exact",
"nearest-exact": "nearest-exact",
"bilinear": "bilinear",
"bicubic": "bicubic",
"area": "area",
"lanczos": "lanczos",
}
image = _ttp_first_image_tensor(image)
samples = image.movedim(-1, 1)
resized = comfy.utils.common_upscale(samples, int(width), int(height), method_map.get(method, "lanczos"), "disabled")
return torch.clamp(resized.movedim(1, -1), min=0.0, max=1.0).to(dtype=image.dtype)
def _ttp_upscale_image_with_model(upscale_model, image):
if upscale_model is None:
return image
device = comfy.model_management.get_torch_device()
upscale_amount = max(float(getattr(upscale_model, "scale", 1.0) or 1.0), 1.0)
memory_required = comfy.model_management.module_size(upscale_model.model)
memory_required += (512 * 512 * 3) * image.element_size() * upscale_amount * 384.0
memory_required += image.nelement() * image.element_size()
comfy.model_management.free_memory(memory_required, device)
upscale_model.to(device)
in_img = image.movedim(-1, -3).to(device)
tile = 512
overlap = 32
output_device = comfy.model_management.intermediate_device()
try:
while True:
try:
steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps(
in_img.shape[3],
in_img.shape[2],
tile_x=tile,
tile_y=tile,
overlap=overlap,
)
pbar = comfy.utils.ProgressBar(steps)
scaled = comfy.utils.tiled_scale(
in_img,
lambda a: upscale_model(a.float()),
tile_x=tile,
tile_y=tile,
overlap=overlap,
upscale_amount=upscale_amount,
pbar=pbar,
output_device=output_device,
)
break
except Exception as exc:
comfy.model_management.raise_non_oom(exc)
tile //= 2
if tile < 128:
raise exc
finally:
upscale_model.to("cpu")
scaled = torch.clamp(scaled.movedim(-3, -1), min=0.0, max=1.0)
return scaled.to(device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype())
def _ttp_align_pil_to_aspect(image, expected_width, expected_height, mode="center_crop"):
expected_width = max(1, int(expected_width))
expected_height = max(1, int(expected_height))
@@ -3499,8 +3616,13 @@ class TTP_Smart_Tile_Image_Upscale_Prep_Experimental:
"image": ("IMAGE",),
"scale": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 16.0, "step": 0.05}),
"round_to": ("INT", {"default": 8, "min": 1, "max": 256, "step": 1}),
"resampling": (["lanczos", "bicubic", "bilinear", "nearest"], {"default": "lanczos"}),
}
"resampling": (["lanczos", "bicubic", "bilinear", "area", "nearest-exact", "nearest"], {"default": "lanczos"}),
"max_megapixels": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 64.0, "step": 0.05}),
"use_upscale_model": ("BOOLEAN", {"default": True}),
},
"optional": {
"upscale_model": ("UPSCALE_MODEL",),
},
}
RETURN_TYPES = ("IMAGE", "STRING")
@@ -3508,11 +3630,42 @@ class TTP_Smart_Tile_Image_Upscale_Prep_Experimental:
FUNCTION = "upscale_tile"
CATEGORY = "TTP/Smart Tile"
def upscale_tile(self, image, scale=2.0, round_to=8, resampling="lanczos"):
pil = _ttp_image_tensor_to_pil(_ttp_first_image_tensor(image))
upscaled = _ttp_resize_pil_to_round(pil, float(scale), int(round_to), str(resampling))
info = f"{pil.width}x{pil.height} -> {upscaled.width}x{upscaled.height} scale={float(scale):g} round_to={int(round_to)}"
return (pil2tensor(upscaled), info)
def upscale_tile(
self,
image,
scale=2.0,
round_to=8,
resampling="lanczos",
max_megapixels=0.0,
use_upscale_model=True,
upscale_model=None,
):
tile = _ttp_first_image_tensor(image)
width, height = _ttp_image_tensor_size(tile[0])
target_width, target_height, capped = _ttp_smart_upscale_target_size(
width,
height,
float(scale),
int(round_to),
float(max_megapixels),
)
model_used = bool(use_upscale_model and upscale_model is not None)
if model_used:
upscaled = _ttp_upscale_image_with_model(upscale_model, tile)
if _ttp_image_tensor_size(upscaled[0]) != (target_width, target_height):
upscaled = _ttp_resize_image_tensor(upscaled, target_width, target_height, str(resampling))
else:
upscaled = _ttp_resize_image_tensor(tile, target_width, target_height, str(resampling))
effective_scale_x = target_width / max(1, width)
effective_scale_y = target_height / max(1, height)
cap_text = " capped" if capped else ""
model_text = "model" if model_used else str(resampling)
info = (
f"{width}x{height} -> {target_width}x{target_height} "
f"requested_scale={float(scale):g} effective_scale={effective_scale_x:.4g}x{effective_scale_y:.4g} "
f"round_to={int(round_to)} max_megapixels={float(max_megapixels):g}{cap_text} method={model_text}"
)
return (upscaled, info)
class TTP_Smart_Tile_Assemble_Experimental:
+13
View File
@@ -226,6 +226,9 @@ assert_equal(
)
upscale_inputs = ttp.TTP_Smart_Tile_Image_Upscale_Prep_Experimental.INPUT_TYPES()
assert_equal(upscale_inputs["required"]["image"][0], "IMAGE", "upscale prep should accept one tile image")
assert_equal("max_megapixels" in upscale_inputs["required"], True, "upscale prep should expose a megapixel cap")
assert_equal("use_upscale_model" in upscale_inputs["required"], True, "upscale prep should allow disabling a connected upscale model")
assert_equal(upscale_inputs["optional"]["upscale_model"][0], "UPSCALE_MODEL", "upscale prep should accept ComfyUI upscale models")
assert_equal(
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Image_Upscale_Prep_Experimental"],
ttp.TTP_Smart_Tile_Image_Upscale_Prep_Experimental,
@@ -672,6 +675,16 @@ upscale_node = ttp.TTP_Smart_Tile_Image_Upscale_Prep_Experimental()
upscaled_tile, upscale_info = upscale_node.upscale_tile(loop_image, scale=1.5, round_to=16)
assert_equal(list(upscaled_tile.shape[1:3]), [400, 560], "upscale prep should scale and round tile dimensions")
assert_equal("368x264 -> 560x400" in upscale_info, True, "upscale prep should report size changes")
capped_tile, capped_info = upscale_node.upscale_tile(
ttp.pil2tensor(Image.new("RGB", (100, 100), "white")),
scale=4.0,
round_to=16,
max_megapixels=0.04,
use_upscale_model=False,
)
assert_equal(list(capped_tile.shape[1:3]), [192, 192], "upscale prep should round down under the megapixel cap")
assert_equal(capped_tile.shape[1] * capped_tile.shape[2] <= 40000, True, "upscale prep should stay under max megapixels after rounding")
assert_equal("max_megapixels=0.04 capped" in capped_info, True, "upscale prep should report megapixel capping")
aligned = ttp._ttp_align_pil_to_aspect(Image.new("RGB", (561, 401), "white"), 560, 400, "center_crop")
assert_equal(aligned.size, (560, 400), "assemble alignment should crop/resize drifted sampler output")
resized = ttp._ttp_align_pil_to_aspect(Image.new("RGB", (500, 500), "white"), 560, 400, "resize")