Files

165 lines
7.9 KiB
Python

import torch
import torch.nn.functional as F
class ImageTiler:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"rows": ("INT", {"default": 2, "min": 1, "max": 256}),
"cols": ("INT", {"default": 2, "min": 1, "max": 256}),
"overlap": ("INT", {"default": 64, "min": 0, "max": 1024, "step": 2}),
}
}
RETURN_TYPES = ("IMAGE", "INT", "INT")
RETURN_NAMES = ("tiles", "original_width", "original_height")
FUNCTION = "execute"
CATEGORY = "image/tiling"
def execute(self, image, rows, cols, overlap):
b, h, w, c = image.shape
original_h, original_w = h, w
# Ensure overlap is an even number for symmetrical padding.
overlap = (overlap // 2) * 2
padding = overlap // 2
# 1. Pad canvas to be perfectly divisible by rows/cols.
h_padded = (h + rows - 1) // rows * rows if rows > 0 else h
w_padded = (w + cols - 1) // cols * cols if cols > 0 else w
if h_padded != h or w_padded != w:
pad_h, pad_w = h_padded - h, w_padded - w
image = F.pad(image.permute(0, 3, 1, 2), (0, pad_w, 0, pad_h), mode='constant', value=0).permute(0, 2, 3, 1)
# 2. Calculate tile dimensions including overlap.
base_tile_h = h_padded // rows
base_tile_w = w_padded // cols
# Prevent overlap from being larger than the tile itself.
final_padding_y = min(padding, base_tile_h // 2) if rows > 1 else 0
final_padding_x = min(padding, base_tile_w // 2) if cols > 1 else 0
output_tile_h = base_tile_h + (final_padding_y * 2)
output_tile_w = base_tile_w + (final_padding_x * 2)
# 3. Slice image into tiles.
tiles = []
with torch.no_grad():
for img in image:
for i in range(rows):
for j in range(cols):
y1_src = max(0, i * base_tile_h - final_padding_y)
y2_src = min(h_padded, (i + 1) * base_tile_h + final_padding_y)
x1_src = max(0, j * base_tile_w - final_padding_x)
x2_src = min(w_padded, (j + 1) * base_tile_w + final_padding_x)
source_slice = img[y1_src:y2_src, x1_src:x2_src, :]
# Pad edge tiles to match the full tile size, ensuring a consistent batch.
# 'replicate' mode is used to avoid black borders on edge tiles.
pad_top = 0 if i > 0 else final_padding_y - (i * base_tile_h - y1_src)
pad_bottom = 0 if i < rows - 1 else final_padding_y - (y2_src - (i + 1) * base_tile_h)
pad_left = 0 if j > 0 else final_padding_x - (j * base_tile_w - x1_src)
pad_right = 0 if j < cols - 1 else final_padding_x - (x2_src - (j + 1) * base_tile_w)
# Correction for single row/col cases
if rows == 1: pad_top = pad_bottom = 0
if cols == 1: pad_left = pad_right = 0
padded_slice = F.pad(source_slice.permute(2, 0, 1).unsqueeze(0),
(pad_left, pad_right, pad_top, pad_bottom), mode='replicate')
tiles.append(padded_slice.squeeze(0).permute(1, 2, 0).unsqueeze(0))
return (torch.cat(tiles, dim=0), original_w, original_h)
class ImageUntiler:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"tiles": ("IMAGE",),
"rows": ("INT", {"default": 2, "min": 1, "max": 256}),
"cols": ("INT", {"default": 2, "min": 1, "max": 256}),
"overlap": ("INT", {"default": 64, "min": 0, "max": 1024, "step": 2}),
},
"optional": {
"original_width": ("INT", {"default": 0, "min": 0, "step": 1, "display": "number"}),
"original_height": ("INT", {"default": 0, "min": 0, "step": 1, "display": "number"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "execute"
CATEGORY = "image/tiling"
def execute(self, tiles, rows, cols, overlap, original_width=0, original_height=0):
group_size = rows * cols
if tiles.shape[0] % group_size != 0:
raise ValueError(f"Number of tiles ({tiles.shape[0]}) is not divisible by rows*cols ({group_size}).")
overlap = (overlap // 2) * 2
padding = overlap // 2
num_groups = tiles.shape[0] // group_size
output_images = []
with torch.no_grad():
for g in range(num_groups):
group_tiles = tiles[g * group_size:(g + 1) * group_size]
_, tile_h_full, tile_w_full, c = group_tiles.shape
base_tile_h = tile_h_full - overlap
base_tile_w = tile_w_full - overlap
# 1. Prepare canvases for image data and blending weights.
final_h, final_w = rows * base_tile_h, cols * base_tile_w
canvas_h, canvas_w = final_h + overlap, final_w + overlap
image_canvas = torch.zeros((1, canvas_h, canvas_w, c), device=tiles.device, dtype=tiles.dtype)
weights_canvas = torch.zeros_like(image_canvas)
# 2. Generate blending masks and assemble tiles.
for i in range(rows):
for j in range(cols):
# Create a linear fade mask (window) for blending.
fade = torch.linspace(0, 1, padding, device=tiles.device, dtype=tiles.dtype)
window_y = torch.ones(tile_h_full, device=tiles.device, dtype=tiles.dtype)
if padding > 0:
window_y[:padding], window_y[-padding:] = fade, fade.flip(0)
window_x = torch.ones(tile_w_full, device=tiles.device, dtype=tiles.dtype)
if padding > 0:
window_x[:padding], window_x[-padding:] = fade, fade.flip(0)
# At the final image edges, the mask should be solid, not faded.
if i == 0 and rows > 1: window_y[:padding] = 1.0
if i == rows - 1 and rows > 1: window_y[-padding:] = 1.0
if j == 0 and cols > 1: window_x[:padding] = 1.0
if j == cols - 1 and cols > 1: window_x[-padding:] = 1.0
mask = (window_y.unsqueeze(1) * window_x.unsqueeze(0)).unsqueeze(0).unsqueeze(-1)
# Add weighted tile to canvas.
idx = i * cols + j
tile = group_tiles[idx:idx+1]
y1, x1 = i * base_tile_h, j * base_tile_w
image_canvas[:, y1:y1+tile_h_full, x1:x1+tile_w_full, :] += tile * mask
weights_canvas[:, y1:y1+tile_h_full, x1:x1+tile_w_full, :] += mask
# 3. Normalize canvas and crop to final dimensions.
# Use clamp to prevent division by zero in areas with no tile data.
final_canvas = image_canvas / weights_canvas.clamp(min=1e-6)
assembled_image = final_canvas[:, padding : padding + final_h, padding : padding + final_w, :]
# Crop back to original dimensions if provided.
if original_width > 0 and original_height > 0:
final_image = assembled_image[:, :original_height, :original_width, :]
else:
final_image = assembled_image
output_images.append(final_image)
return (torch.cat(output_images, dim=0),)