Files
rickyars-comfyui-llm-tile/node.py
T
rickyars c421531a32 version bump
i'm trying to improve the tiled diffusion - these changes in node and node advanced are to ensure that final output is always tile width * grid width by tile height * grid height. i'm also starting work on a new tiled diffusion upscaler.
2025-05-26 15:28:21 -05:00

280 lines
12 KiB
Python

import torch
import comfy.sample
import comfy.controlnet
from comfy.utils import ProgressBar
from .utils import gaussian_blend_tiles, parse_tile_prompts
from .utils import apply_controlnet_to_conditioning
class TiledImageGenerator:
"""
ComfyUI node that generates a tiled image composition with overlapping regions
that serve as seeds for neighboring tiles, using ControlNet Union SDXL for outpainting.
"""
@classmethod
def INPUT_TYPES(cls):
import comfy.samplers
return {
"required": {
"json_tile_prompts": ("STRING", {"multiline": True}),
"global_positive": ("STRING", {"multiline": True, "default": ""}),
"global_negative": ("STRING", {"multiline": True, "default": ""}),
"grid_width": ("INT", {"default": 4, "min": 1, "max": 16}),
"grid_height": ("INT", {"default": 6, "min": 1, "max": 16}),
"tile_width": ("INT", {"default": 1024, "min": 256, "max": 2048}),
"tile_height": ("INT", {"default": 1024, "min": 256, "max": 2048}),
"overlap_percent": ("FLOAT", {"default": 0.15, "min": 0.05, "max": 0.5, "step": 0.01}),
"blend_sigma": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 1.0, "step": 0.01}),
"model": ("MODEL",),
"clip": ("CLIP",),
"vae": ("VAE",),
"controlnet": ("CONTROL_NET",),
"controlnet_strength": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"steps": ("INT", {"default": 20, "min": 1, "max": 100}),
"cfg": ("FLOAT", {"default": 7.0, "min": 1.0, "max": 20.0, "step": 0.1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("composite_image", "individual_tiles")
FUNCTION = "generate_tiled_image"
CATEGORY = "image/generation"
def generate_tiled_image(self, json_tile_prompts, global_positive, global_negative,
grid_width, grid_height, tile_width, tile_height,
overlap_percent, blend_sigma, controlnet, controlnet_strength,
seed, model, clip, vae, sampler_name, scheduler, steps, cfg):
"""Generate a tiled image with uniform grown latents for all tiles."""
# Parse the JSON tile prompts
tile_prompts = parse_tile_prompts(json_tile_prompts, grid_width, grid_height)
# Calculate overlap in pixels
overlap_x = int(tile_width * overlap_percent)
overlap_y = int(tile_height * overlap_percent)
# SIMPLE FINAL SIZE CALCULATION - exact grid multiplication
final_width = tile_width * grid_width
final_height = tile_height * grid_height
print(f"Final output size: {final_width} x {final_height}")
print(f"Tile size: {tile_width} x {tile_height}")
print(f"Grid: {grid_width} x {grid_height}")
print(f"Overlap: {overlap_x} x {overlap_y} pixels ({overlap_percent * 100}%)")
# UNIFORM GENERATION CANVAS SIZE for all tiles
gen_width = ((tile_width + overlap_x + 7) // 8) * 8
gen_height = ((tile_height + overlap_y + 7) // 8) * 8
print(f"Uniform generation canvas: {gen_width} x {gen_height}")
# Create a blank canvas for the final composite
final_tensor = torch.ones((1, final_height, final_width, 3), dtype=torch.float32) * 0.5
# Storage for individual tiles and full grown tiles
individual_tensors = [] # For blending (cropped)
full_grown_tensors = [] # For debugging (full canvas)
positions = []
# Get device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")
# Calculate total number of tiles
total_tiles = grid_width * grid_height
pbar = ProgressBar(total_tiles)
# Generate tiles one by one
for y in range(grid_height):
for x in range(grid_width):
idx = y * grid_width + x
current_prompt = tile_prompts[idx]
current_seed = seed + idx
# Calculate position in the final image
final_pos_x = x * tile_width
final_pos_y = y * tile_height
# Create the combined prompt
combined_prompt = f"{current_prompt} {global_positive}" if global_positive else current_prompt
print(f"Generating tile ({x + 1},{y + 1}) with prompt: {current_prompt}")
# Encode the combined prompt
pos_tokens = clip.tokenize(combined_prompt)
pos_cond = clip.encode_from_tokens_scheduled(pos_tokens)
# Encode the negative prompt
neg_tokens = clip.tokenize(global_negative)
neg_cond = clip.encode_from_tokens_scheduled(neg_tokens)
# Create UNIFORM working canvas for ALL tiles
working_tensor = torch.zeros((1, gen_height, gen_width, 3), dtype=torch.float32)
outpaint_mask = torch.ones((1, gen_height, gen_width, 1), dtype=torch.float32)
# Check if we have neighbors to copy from
has_left_neighbor = x > 0
has_top_neighbor = y > 0
# COPY OVERLAP REGIONS from final_tensor (only if neighbors exist)
if has_left_neighbor:
# Copy left overlap region from previous tile
source_x = final_pos_x - overlap_x
if source_x >= 0 and source_x + overlap_x <= final_width:
source_start_y = final_pos_y
source_end_y = min(final_pos_y + tile_height, final_height)
source_height = source_end_y - source_start_y
# Copy to working tensor left edge (after top overlap region)
target_start_y = overlap_y
copy_height = min(source_height, gen_height - target_start_y)
if copy_height > 0:
working_tensor[0,
target_start_y:target_start_y + copy_height,
:overlap_x,
:] = final_tensor[0,
source_start_y:source_start_y + copy_height,
source_x:source_x + overlap_x,
:]
# Mark as "keep" in mask
outpaint_mask[0,
target_start_y:target_start_y + copy_height,
:overlap_x,
:] = 0
if has_top_neighbor:
# Copy top overlap region from previous tile
source_y = final_pos_y - overlap_y
if source_y >= 0 and source_y + overlap_y <= final_height:
source_start_x = final_pos_x
source_end_x = min(final_pos_x + tile_width, final_width)
source_width = source_end_x - source_start_x
# Copy to working tensor top edge (after left overlap region)
target_start_x = overlap_x
copy_width = min(source_width, gen_width - target_start_x)
if copy_width > 0:
working_tensor[0,
:overlap_y,
target_start_x:target_start_x + copy_width,
:] = final_tensor[0,
source_y:source_y + overlap_y,
source_start_x:source_start_x + copy_width,
:]
# Mark as "keep" in mask
outpaint_mask[0,
:overlap_y,
target_start_x:target_start_x + copy_width,
:] = 0
# Handle corner overlap if both neighbors exist
if has_left_neighbor and has_top_neighbor:
corner_source_x = final_pos_x - overlap_x
corner_source_y = final_pos_y - overlap_y
if (corner_source_x >= 0 and corner_source_y >= 0 and
corner_source_x + overlap_x <= final_width and
corner_source_y + overlap_y <= final_height):
working_tensor[0, :overlap_y, :overlap_x, :] = final_tensor[0,
corner_source_y:corner_source_y + overlap_y,
corner_source_x:corner_source_x + overlap_x,
:]
outpaint_mask[0, :overlap_y, :overlap_x, :] = 0
# Get latent shape for the uniform grown canvas
with torch.no_grad():
latent_shape = vae.encode(working_tensor).shape
# Create empty latent tensor with grown shape
latent_image = torch.zeros(latent_shape, device=device)
# Apply ControlNet to conditioning using the grown canvas
conditioning = apply_controlnet_to_conditioning(
positive=pos_cond,
negative=neg_cond,
control_net=controlnet,
image=working_tensor,
strength=controlnet_strength,
start_percent=0.0,
end_percent=1.0,
vae=vae
)
# Generate noise for grown canvas size
noise = comfy.sample.prepare_noise(latent_image, current_seed, None)
# Sample with the grown latent
samples = comfy.sample.sample(
model,
noise,
steps,
cfg,
sampler_name,
scheduler,
conditioning[0],
conditioning[1],
latent_image
)
# Decode the full grown canvas
full_grown_tensor = vae.decode(samples)
# EXTRACT the tile_width x tile_height region from consistent position
# Always extract from overlap_x, overlap_y for uniform behavior
extract_x = overlap_x
extract_y = overlap_y
extracted_tile = full_grown_tensor[0:1,
extract_y:extract_y + tile_height,
extract_x:extract_x + tile_width,
:]
# Store both versions
individual_tensors.append(extracted_tile.clone()) # For blending
full_grown_tensors.append(full_grown_tensor.clone()) # For debugging
positions.append((final_pos_x, final_pos_y))
# Place the tile in the final composite at exact grid position
final_tensor[0,
final_pos_y:final_pos_y + tile_height,
final_pos_x:final_pos_x + tile_width,
:] = extracted_tile[0, :, :, :]
# Update progress
pbar.update(1)
# Apply Gaussian blending for smooth transitions
if individual_tensors:
final_tensor = gaussian_blend_tiles(
individual_tensors, # Use cropped tiles for blending
positions,
tile_width,
tile_height,
overlap_x,
overlap_y,
final_width,
final_height,
sigma=blend_sigma
)
# Return full grown tiles for debugging
tile_batch = torch.cat(full_grown_tensors, dim=0)
else:
final_tensor = torch.zeros((1, final_height, final_width, 3), dtype=torch.float32)
tile_batch = torch.zeros((1, gen_height, gen_width, 3), dtype=torch.float32, device=device)
return final_tensor, tile_batch
# This part is needed for ComfyUI to recognize the nodes
NODE_CLASS_MAPPINGS = {
"TiledImageGenerator": TiledImageGenerator,
}
# Add descriptions for the web UI
NODE_DISPLAY_NAME_MAPPINGS = {
"TiledImageGenerator": "Tiled Image Generator",
}