165 lines
7.9 KiB
Python
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),) |