5 Commits
7 changed files with 420 additions and 175 deletions
+28 -4
View File
@@ -1,18 +1,42 @@
# SimpleTiles
## TileSplit
## DynamicTileSplit / DynamicTileMerge
![](dynamic.png)
Splits image into tiles. Overlap value decides how much overlap there is between tiles on y axis, x axis is calculated to have the same ratio to image height as y axis.
## TileMerge
Automatically splits image into tiles based on image size and tile size. Tiles can be different ratio than images.
DynamicTileSplit outputs a `tile_calc` object. The object contains info about size and overlap and should be passed to DynamicTileMerge.
**Overlap** value decides how much overlap there is between tiles on y axis, x axis is calculated to have the same ratio to image height as y axis. Should be set to same value as used in TileSplit.
**Blend** value decides how many pixels the blending is done over. Should be less than overlap value. Blending is done linearly from 0 to 1 over the blend distance.
## Legacy
DynamicTileSplit and DynamicTileMerge are the new versions of TileSplit and TileMerge. They are more flexible and easier to use.
Legacy nodes don't work well if image ratio and tile ratio is different.
Use TileCalc to calculate the final image size, pipe the final size to TileMerge and ImageScale.
### TileSplit (Legacy)
Splits image into tiles. Overlap value decides how much overlap there is between tiles on y axis, x axis is calculated to have the same ratio to image height as y axis.
### TileMerge (Legacy)
Merge tiles into image.
**Overlap** value decides how much overlap there is between tiles on y axis, x axis is calculated to have the same ratio to image height as y axis. Should be set to same value as used in TileSplit.
**Blend** value decides how many pixels the blending is done over. Should be less than overlap value. Blending is done linearly from 0 to 1 over the blend distance.
## TileCalc
### TileCalc (Legacy)
Util to calculate final image size based on tile sizes and overlaps.
## Example Ipadapter
![](ipadapter.png)
+1 -1
View File
@@ -1,3 +1,3 @@
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+203
View File
@@ -0,0 +1,203 @@
import sys
import os
import torch
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
def order_by_center_last(tiles, image_width, image_height, tile_width, tile_height):
# for 3x3: custom_order = [0, 2, 6, 8, 1, 3, 5, 7, 4] # First 4 corners, then the sides, then the center
# order the tiles so they are add based on absolute distance from the center of the tile to the center of the image
# this is done so that the center of the image is the last tile to be added, so that the center of the image is the most refined
# get the center of the image
center_x = image_width // 2
center_y = image_height // 2
# sort the tiles by distance from the center
tiles = sorted(
tiles,
key=lambda tile: abs(tile[0] + tile_width // 2 - center_x)
+ abs(tile[1] + tile_height // 2 - center_y),
)
# reverse the order so that the center is last
tiles = tiles[::-1]
return tiles
def generate_tiles(
image_width, image_height, tile_width, tile_height, overlap, offset=0
):
tiles = []
y = 0
while y < image_height:
if y == 0:
next_y = y + tile_height - overlap + offset
else:
next_y = y + tile_height - overlap
if y + tile_height >= image_height:
y = max(image_height - tile_height, 0)
next_y = image_height
x = 0
while x < image_width:
if x == 0:
next_x = x + tile_width - overlap + offset
else:
next_x = x + tile_width - overlap
if x + tile_width >= image_width:
x = max(image_width - tile_width, 0)
next_x = image_width
tiles.append((x, y))
if next_x > image_width:
break
x = next_x
if next_y > image_height:
break
y = next_y
return order_by_center_last(tiles, image_width, image_height, tile_width, tile_height)
class DynamicTileSplit:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"tile_width": ("INT", {"default": 512, "min": 1, "max": 10000}),
"tile_height": ("INT", {"default": 512, "min": 1, "max": 10000}),
"overlap": ("INT", {"default": 128, "min": 1, "max": 10000}),
"offset": ("INT", {"default": 0, "min": 0, "max": 10000}),
}
}
RETURN_TYPES = ("IMAGE", "TILE_CALC")
FUNCTION = "process"
CATEGORY = "ipadapter"
def process(self, image, tile_width, tile_height, overlap, offset):
image_height = image.shape[1]
image_width = image.shape[2]
tile_coordinates = generate_tiles(
image_width, image_height, tile_width, tile_height, overlap, offset
)
print("Tile coordinates: {}".format(tile_coordinates))
iteration = 1
image_tiles = []
for tile_coordinate in tile_coordinates:
print("Processing tile {} of {}".format(iteration, len(tile_coordinates)))
print("Tile coordinate: {}".format(tile_coordinate))
iteration += 1
image_tile = image[
:,
tile_coordinate[1] : tile_coordinate[1] + tile_height,
tile_coordinate[0] : tile_coordinate[0] + tile_width,
:,
]
image_tiles.append(image_tile)
tiles_tensor = torch.stack(image_tiles).squeeze(1)
tile_calc = (overlap, image_height, image_width, offset)
return (tiles_tensor, tile_calc)
class DynamicTileMerge:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"blend": ("INT", {"default": 64, "min": 0, "max": 4096}),
"tile_calc": ("TILE_CALC",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process"
CATEGORY = "utils"
def process(self, images, blend, tile_calc):
overlap, final_height, final_width, offset = tile_calc
tile_height = images.shape[1]
tile_width = images.shape[2]
print("Tile height: {}".format(tile_height))
print("Tile width: {}".format(tile_width))
print("Final height: {}".format(final_height))
print("Final width: {}".format(final_width))
print("Overlap: {}".format(overlap))
tile_coordinates = generate_tiles(
final_width, final_height, tile_width, tile_height, overlap, offset
)
print("Tile coordinates: {}".format(tile_coordinates))
original_shape = (1, final_height, final_width, 3)
count = torch.zeros(original_shape, dtype=images.dtype)
output = torch.zeros(original_shape, dtype=images.dtype)
index = 0
iteration = 1
for tile_coordinate in tile_coordinates:
image_tile = images[index]
x = tile_coordinate[0]
y = tile_coordinate[1]
print("Processing tile {} of {}".format(iteration, len(tile_coordinates)))
print("Tile coordinate: {}".format(tile_coordinate))
iteration += 1
channels = images.shape[3]
weight_matrix = torch.ones((tile_height, tile_width, channels))
# blend border
for i in range(blend):
weight = float(i) / blend
weight_matrix[i, :, :] *= weight # Top rows
weight_matrix[-(i + 1), :, :] *= weight # Bottom rows
weight_matrix[:, i, :] *= weight # Left columns
weight_matrix[:, -(i + 1), :] *= weight # Right columns
# We only want to blend with already processed pixels, so we keep
# track if it has been processed.
old_tile = output[:, y : y + tile_height, x : x + tile_width, :]
old_tile_count = count[:, y : y + tile_height, x : x + tile_width, :]
weight_matrix = (
weight_matrix * (old_tile_count != 0).float()
+ (old_tile_count == 0).float()
)
image_tile = image_tile * weight_matrix + old_tile * (1 - weight_matrix)
output[:, y : y + tile_height, x : x + tile_width, :] = image_tile
count[:, y : y + tile_height, x : x + tile_width, :] = 1
index += 1
return [output]
NODE_CLASS_MAPPINGS = {
"DynamicTileSplit": DynamicTileSplit,
"DynamicTileMerge": DynamicTileMerge,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DynamicTileSplit": "DynamicTileSplit",
"DynamicTileMerge": "DynamicTileMerge",
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+9 -170
View File
@@ -1,179 +1,18 @@
import sys
import os
import torch
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
IMAGE_SIZE = 1472
TILE_SIZE = 4096
OVERLAP = 64
# Splits an image in four tiles and returns them as a list
class TileSplit:
@classmethod
def INPUT_TYPES(s):
return {"required":{
"image": ("IMAGE", ),
"tile_height": ("INT", {"default": 64, "min": 64, "max": 4096}),
"tile_width": ("INT", {"default": 64, "min": 64, "max": 4096}),
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "split"
CATEGORY = "utils"
def split(self, image, tile_height, tile_width, overlap):
height, width = image.shape[1], image.shape[2]
overlap_x = overlap
overlap_y = int(overlap * (tile_height / tile_width))
tiles = []
for y in range(0, height-tile_height+1, tile_height-overlap_y):
for x in range(0, width-tile_width+1, tile_width-overlap_x):
tile = image[:, y:y+tile_height, x:x+tile_width, :]
tiles.append(tile)
# Convert tiles list to a tensor if needed
tiles_tensor = torch.stack(tiles).squeeze(1)
return [tiles_tensor]
class TileMerge:
@classmethod
def INPUT_TYPES(s):
return {"required":{
"images": ("IMAGE", ),
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096}),
"blend": ("INT", {"default": 64, "min": 0, "max": 4096}),
"final_height": ("INT", {"default": 2048, "min": 0, "max": 9*4096}),
"final_width": ("INT", {"default": 2048, "min": 0, "max": 9*4096}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "blend_tiles"
CATEGORY = "utils"
def blend_tiles(self, images, overlap, blend, final_height, final_width):
tiles = images
tile_height, tile_width = images.shape[1], images.shape[2]
original_shape = (1, final_height, final_width, 3)
overlap_x = overlap
overlap_y = int(overlap * (tile_height / tile_width))
batch, height, width, channels = original_shape
output = torch.zeros(original_shape, dtype=tiles.dtype)
count = torch.zeros(original_shape, dtype=tiles.dtype)
idx = 0
# for 3x3: custom_order = [0, 2, 6, 8, 1, 3, 5, 7, 4] # First 4 corners, then the sides, then the center
# Calculate grid dimensions
# rows = (height - tile_height) // (tile_height - overlap_y) + 1
# cols = (width - tile_width) // (tile_width - overlap_x) + 1
# Calculate the center of the grid
# center_row, center_col = rows // 2, cols // 2
# print("Rows: {}, Cols: {}".format(rows, cols))
# print("Center row: {}, Center col: {}".format(center_row, center_col))
# # Calculate the order in which to blend the tiles
# # Order based on distance from center
# distances = []
# for i in range(rows):
# for j in range(cols):
# distance = abs(i - center_row) + abs(j - center_col)
# distances.append(distance)
# Sort the tiles based on distance from center
# reverse_custom_order = sorted(range(len(distances)), key=lambda k: distances[k])
custom_order = [0, 1, 2, 3, 4, 5, 6, 7, 8]
print("Custom order: {}".format(custom_order))
ys = [y for y in range(0, height-tile_height+1, tile_height-overlap_y)]
xs = [x for x in range(0, width-tile_width+1, tile_width-overlap_x)]
for idx in custom_order:
y = ys[idx // len(ys)]
x = xs[idx % len(xs)]
tile = tiles[idx]
weight_matrix = torch.ones((tile_height, tile_width, channels))
# if not center tile
for i in range(blend):
weight = float(i) / blend
weight_matrix[i, :, :] *= weight # Top rows
weight_matrix[-(i + 1), :, :] *= weight # Bottom rows
weight_matrix[:, i, :] *= weight # Left columns
weight_matrix[:, -(i + 1), :] *= weight # Right columns
old_tile = output[:, y:y+tile_height, x:x+tile_width, :]
old_tile_count = count[:, y:y+tile_height, x:x+tile_width, :]
weight_matrix = weight_matrix * (old_tile_count != 0).float() + (old_tile_count == 0).float()
# Blend the old tile with the new tile
tile = tile * weight_matrix + old_tile * (1 - weight_matrix)
output[:, y:y+tile_height, x:x+tile_width, :] = tile
count[:, y:y+tile_height, x:x+tile_width, :] = 1
# Normalize the output and return
#output /= count
return [output]
class TileCalc:
@classmethod
def INPUT_TYPES(s):
return {"required":{
"tile_height": ("INT", {"default": 64, "min": 64, "max": 4096}),
"tile_width": ("INT", {"default": 64, "min": 64, "max": 4096}),
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096}),
"tile_width_n": ("INT", {"default": 3, "min": 1, "max": 9}),
"tile_height_n": ("INT", {"default": 3, "min": 1, "max": 9}),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("final_height", "final_width")
FUNCTION = "calc"
CATEGORY = "utils"
def calc(self, tile_height, tile_width, overlap, tile_width_n, tile_height_n):
overlap_x = overlap
overlap_y = int(overlap * (tile_height / tile_width))
final_height = tile_height * tile_height_n - overlap_y * (tile_height_n - 1)
final_width = tile_width * tile_width_n - overlap_x * (tile_width_n - 1)
print("Final height: {}, Final width: {}".format(final_height, final_width))
return [final_height, final_width]
from ComfyUI_SimpleTiles.standard import TileSplit, TileMerge, TileCalc
from ComfyUI_SimpleTiles.dynamic import DynamicTileSplit, DynamicTileMerge
NODE_CLASS_MAPPINGS = {
"TileSplit": TileSplit,
"TileMerge": TileMerge,
"TileCalc": TileCalc,
"DynamicTileSplit": DynamicTileSplit,
"DynamicTileMerge": DynamicTileMerge,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TileSplit": "TileSplit",
"TileMerge": "TileMerge",
"TileCalc": "TileCalc",
"TileSplit": "TileSplit (Legacy)",
"TileMerge": "TileMerge (Legacy)",
"TileCalc": "TileCalc (Legacy)",
"DynamicTileSplit": "TileSplit (Dynamic)",
"DynamicTileMerge": "TileMerge (Dynamic)",
}
+179
View File
@@ -0,0 +1,179 @@
import sys
import os
import torch
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
IMAGE_SIZE = 1472
TILE_SIZE = 4096
OVERLAP = 64
# Splits an image in four tiles and returns them as a list
class TileSplit:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"tile_height": ("INT", {"default": 64, "min": 64, "max": 4096}),
"tile_width": ("INT", {"default": 64, "min": 64, "max": 4096}),
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "split"
CATEGORY = "utils"
def split(self, image, tile_height, tile_width, overlap):
height, width = image.shape[1], image.shape[2]
overlap_x = overlap
overlap_y = int(overlap * (tile_height / tile_width))
tiles = []
for y in range(0, height - tile_height + 1, tile_height - overlap_y):
for x in range(0, width - tile_width + 1, tile_width - overlap_x):
tile = image[:, y : y + tile_height, x : x + tile_width, :]
tiles.append(tile)
# Convert tiles list to a tensor if needed
tiles_tensor = torch.stack(tiles).squeeze(1)
return [tiles_tensor]
class TileMerge:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096}),
"blend": ("INT", {"default": 64, "min": 0, "max": 4096}),
"final_height": ("INT", {"default": 2048, "min": 0, "max": 9 * 4096}),
"final_width": ("INT", {"default": 2048, "min": 0, "max": 9 * 4096}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "blend_tiles"
CATEGORY = "utils"
def blend_tiles(self, images, overlap, blend, final_height, final_width):
tiles = images
tile_height, tile_width = images.shape[1], images.shape[2]
original_shape = (1, final_height, final_width, 3)
overlap_x = overlap
overlap_y = int(overlap * (tile_height / tile_width))
batch, height, width, channels = original_shape
output = torch.zeros(original_shape, dtype=tiles.dtype)
count = torch.zeros(original_shape, dtype=tiles.dtype)
idx = 0
# for 3x3: custom_order = [0, 2, 6, 8, 1, 3, 5, 7, 4] # First 4 corners, then the sides, then the center
# Calculate grid dimensions
rows = (height - tile_height) // (tile_height - overlap_y) + 1
cols = (width - tile_width) // (tile_width - overlap_x) + 1
# Calculate the center of the grid
center_row, center_col = rows // 2, cols // 2
print("Rows: {}, Cols: {}".format(rows, cols))
print("Center row: {}, Center col: {}".format(center_row, center_col))
# Calculate the order in which to blend the tiles
# Order based on distance from center
distances = []
for i in range(rows):
for j in range(cols):
distance = abs(i - center_row) + abs(j - center_col)
distances.append(distance)
# Sort the tiles based on distance from center
reverse_custom_order = sorted(range(len(distances)), key=lambda k: distances[k])
custom_order = reverse_custom_order[::-1]
print("Custom order: {}".format(custom_order))
ys = [y for y in range(0, height - tile_height + 1, tile_height - overlap_y)]
xs = [x for x in range(0, width - tile_width + 1, tile_width - overlap_x)]
for idx in custom_order:
y = ys[idx // len(ys)]
x = xs[idx % len(xs)]
tile = tiles[idx]
weight_matrix = torch.ones((tile_height, tile_width, channels))
# if not center tile
for i in range(blend):
weight = float(i) / blend
weight_matrix[i, :, :] *= weight # Top rows
weight_matrix[-(i + 1), :, :] *= weight # Bottom rows
weight_matrix[:, i, :] *= weight # Left columns
weight_matrix[:, -(i + 1), :] *= weight # Right columns
old_tile = output[:, y : y + tile_height, x : x + tile_width, :]
old_tile_count = count[:, y : y + tile_height, x : x + tile_width, :]
weight_matrix = (
weight_matrix * (old_tile_count != 0).float()
+ (old_tile_count == 0).float()
)
# Blend the old tile with the new tile
tile = tile * weight_matrix + old_tile * (1 - weight_matrix)
output[:, y : y + tile_height, x : x + tile_width, :] = tile
count[:, y : y + tile_height, x : x + tile_width, :] = 1
# Normalize the output and return
# output /= count
return [output]
class TileCalc:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"tile_height": ("INT", {"default": 64, "min": 64, "max": 4096}),
"tile_width": ("INT", {"default": 64, "min": 64, "max": 4096}),
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096}),
"tile_width_n": ("INT", {"default": 3, "min": 1, "max": 9}),
"tile_height_n": ("INT", {"default": 3, "min": 1, "max": 9}),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("final_height", "final_width")
FUNCTION = "calc"
CATEGORY = "utils"
def calc(self, tile_height, tile_width, overlap, tile_width_n, tile_height_n):
overlap_x = overlap
overlap_y = int(overlap * (tile_height / tile_width))
final_height = tile_height * tile_height_n - overlap_y * (tile_height_n - 1)
final_width = tile_width * tile_width_n - overlap_x * (tile_width_n - 1)
print("Final height: {}, Final width: {}".format(final_height, final_width))
return [final_height, final_width]
NODE_CLASS_MAPPINGS = {
"TileSplit": TileSplit,
"TileMerge": TileMerge,
"TileCalc": TileCalc,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TileSplit": "TileSplit",
"TileMerge": "TileMerge",
"TileCalc": "TileCalc",
}