Files
Pirog17000-Pirogs-Nodes/node_modules/samplermultiseed.py
T
Pirog17000 7821135b0f feat(KSampler++): Implement seamless tiling and batch processing
- Refactors KSamplers into `node_modules` for better organization.
- Implements a robust tiling system in KSamplerMultiSeedPlus that uses gradient-mask inpainting to eliminate seams.
- Fixes critical bugs related to VAE dimension mismatches, incorrect tensor shapes, and uneven image splits.
- Corrects state pollution bugs, allowing for stable batch processing and chaining of multiple KSampler nodes.
- Updates README with details on new tiling functionality and refactoring.
2025-08-29 21:22:18 +02:00

645 lines
34 KiB
Python

"""
Enhanced KSampler nodes with tiling support
"""
import os
import sys
import torch
import importlib.util
import numpy as np
# Import our advanced noise generation library
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from noise_generator import create_spectral_diverse_noise, create_hierarchical_noise
# Add the ComfyUI root directory to Python path to access the main nodes
comfy_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
if comfy_dir not in sys.path:
sys.path.insert(0, comfy_dir)
# Import the common_ksampler function, MAX_RESOLUTION from the main nodes.py
try:
from nodes import common_ksampler, MAX_RESOLUTION
except ImportError:
# Fallback: try to import from nodes module in current directory
spec = importlib.util.spec_from_file_location("nodes", os.path.join(comfy_dir, "nodes.py"))
nodes_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(nodes_module)
common_ksampler = nodes_module.common_ksampler
MAX_RESOLUTION = nodes_module.MAX_RESOLUTION
# Import required ComfyUI modules
import comfy.samplers
import comfy.utils
import comfy.model_management
class KSamplerMultiSeed:
"""
KSampler Multi-Seed Node
This node is extracted from the main ComfyUI nodes.py and provides
multi-seed sampling functionality. It automatically uses the latest
common_ksampler function from ComfyUI, ensuring compatibility with updates.
Features optional noise injection for generating variations.
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL", {"tooltip": "The model used for denoising the input latent."}),
"positive": ("CONDITIONING", {
"tooltip": "The conditioning describing the attributes you want to include in the image."}),
"negative": ("CONDITIONING", {
"tooltip": "The conditioning describing the attributes you want to exclude from the image."}),
"latent_image": ("LATENT", {"tooltip": "The latent image to denoise."}),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01,
"tooltip": "The amount of denoising applied."}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000,
"tooltip": "The number of steps used in the denoising process."}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01,
"tooltip": "Classifier-Free Guidance scale."}),
"seed_count": ("INT", {"default": 1, "min": 1, "max": 1000,
"tooltip": "The number of seeds to generate images with."}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True,
"tooltip": "The starting random seed. It will be incremented for each image in the batch."}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"tooltip": "The algorithm used when sampling."}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,
{"tooltip": "The scheduler controls how noise is gradually removed."}),
"injected_noise": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "round": 0.01,
"tooltip": "Strength of noise injection for variation generation. 0.0=disabled, >0.0=blend base and variation noise."}),
}
}
RETURN_TYPES = ("LATENT",)
OUTPUT_TOOLTIPS = ("A batch of denoised latents, one for each seed.",)
FUNCTION = "sample"
CATEGORY = "pirog/sampling"
DESCRIPTION = "Generates multiple images by incrementing the seed for each generation for each latent in the input batch."
def slerp(self, val, low, high):
"""Spherical linear interpolation for noise blending"""
dims = low.shape
low = low.reshape(dims[0], -1)
high = high.reshape(dims[0], -1)
low_norm = low / torch.norm(low, dim=1, keepdim=True)
high_norm = high / torch.norm(high, dim=1, keepdim=True)
low_norm[low_norm != low_norm] = 0.0
high_norm[high_norm != high_norm] = 0.0
omega = torch.acos((low_norm * high_norm).sum(1))
so = torch.sin(omega)
res = (torch.sin((1.0 - val) * omega) / so).unsqueeze(1) * low + (torch.sin(val * omega) / so).unsqueeze(1) * high
return res.reshape(dims)
def sample(self, model, positive, negative, latent_image, denoise, steps, cfg, seed_count, seed, sampler_name,
scheduler, injected_noise=0.0):
output_latents = []
input_latents = latent_image["samples"]
total_generations = input_latents.shape[0] * seed_count
pbar = comfy.utils.ProgressBar(total_generations)
for latent_sample in input_latents:
for i in range(seed_count):
current_seed = seed + i
# Reshape the single sample to a batch of 1 for the sampler
work_latent = latent_sample.unsqueeze(0)
# Apply noise injection if enabled
if injected_noise > 0.0:
# Generate base noise from main seed
generator = torch.manual_seed(seed)
base_noise = torch.randn(work_latent.shape, generator=generator, device="cpu", dtype=work_latent.dtype)
# Generate variation noise from current seed (naturally varies per iteration)
generator = torch.manual_seed(current_seed + 12345) # Offset to ensure different noise
variation_noise = torch.randn(work_latent.shape, generator=generator, device="cpu", dtype=work_latent.dtype)
# Blend noises using SLERP
blended_noise = self.slerp(injected_noise, base_noise, variation_noise)
# Calculate sigma scaling factor
device = comfy.model_management.get_torch_device()
comfy.model_management.load_model_gpu(model)
sampler = comfy.samplers.KSampler(model, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=1.0, model_options=model.model_options)
sigmas = sampler.sigmas
start_at_step = round(steps - steps * denoise)
end_at_step = steps
sigma = sigmas[start_at_step] - sigmas[end_at_step]
sigma /= model.model.latent_format.scale_factor
sigma = sigma.detach().cpu().item()
# Apply scaled noise to latent
work_latent = work_latent + blended_noise.to(work_latent.device) * sigma
latent_for_sampler = {"samples": work_latent}
# Call the common ksampler function from the main ComfyUI nodes
# This ensures we always use the latest version
result_latent, = common_ksampler(model, current_seed, steps, cfg, sampler_name, scheduler, positive,
negative, latent_for_sampler, denoise=denoise)
output_latents.append(result_latent["samples"])
pbar.update(1)
# Combine all the generated latents into a single batch tensor
final_samples = torch.cat(output_latents, dim=0)
# Create the final output dictionary
final_latent = {"samples": final_samples}
return (final_latent,)
class KSamplerMultiSeedPlus:
"""
KSamplerMultiSeed+ Node - Integrated Sampling Pipeline
This node combines the KSamplerMultiSeed functionality with integrated VAE encode/decode
and empty latent image generation. It provides a complete pipeline in one node:
- For denoise=1.0: Creates empty latent → samples → decodes to image
- For denoise<1.0: Encodes input image → samples → decodes to image
Uses minimal approach by calling vanilla ComfyUI functions directly.
"""
def __init__(self):
self.device = comfy.model_management.intermediate_device()
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL", {"tooltip": "The model used for denoising."}),
"vae": ("VAE", {"tooltip": "The VAE model used for encoding/decoding."}),
"positive": ("CONDITIONING", {
"tooltip": "The conditioning describing the attributes you want to include in the image."}),
"negative": ("CONDITIONING", {
"tooltip": "The conditioning describing the attributes you want to exclude from the image."}),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01,
"tooltip": "The amount of denoising applied. 1.0=new image, <1.0=img2img."}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000,
"tooltip": "The number of steps used in the denoising process."}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01,
"tooltip": "Classifier-Free Guidance scale."}),
"seed_count": ("INT", {"default": 1, "min": 1, "max": 1000,
"tooltip": "The number of seeds to generate images with."}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True,
"tooltip": "The starting random seed. It will be incremented for each image in the batch."}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"tooltip": "The algorithm used when sampling."}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,
{"tooltip": "The scheduler controls how noise is gradually removed."}),
"width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8,
"tooltip": "The width of the generated image in pixels (used when denoise=1.0)."}),
"height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8,
"tooltip": "The height of the generated image in pixels (used when denoise=1.0)."}),
"noise_type": (["vanilla", "spectral-diverse", "hierarchical"], {
"default": "vanilla",
"tooltip": "🎲 Noise Generation Method:\n\n"
"• vanilla: Standard ComfyUI noise (torch.randn) - reliable baseline\n"
"• spectral-diverse: Frequency-controlled noise with pink/blue/hybrid patterns - enhanced diversity\n"
"• hierarchical: Multi-scale latent-aware noise with statistical modeling - maximum quality\n\n"
"Advanced methods produce more diverse and potentially higher quality results."
}),
"injected_noise": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "round": 0.01,
"tooltip": "Strength of noise injection for variation generation. 0.0=disabled, >0.0=blend base and variation noise."}),
"vertical_splits": ("INT", {"default": 1, "min": 1, "max": 8, "step": 1,
"tooltip": "Number of vertical splits (1 = no splitting, 2+ = process image in tiles)"}),
"horizontal_splits": ("INT", {"default": 1, "min": 1, "max": 8, "step": 1,
"tooltip": "Number of horizontal splits (1 = no splitting, 2+ = process image in tiles)"}),
"expansion_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 50.0, "step": 1.0,
"tooltip": "Expansion percentage for tile borders (0-50%). Helps reduce seam artifacts between tiles."}),
},
"optional": {
"input_image": ("IMAGE", {"tooltip": "Input image for img2img (used when denoise<1.0)."}),
}
}
RETURN_TYPES = ("IMAGE",)
OUTPUT_TOOLTIPS = ("Generated images, one for each seed.",)
FUNCTION = "sample_integrated"
CATEGORY = "pirog/sampling"
DESCRIPTION = "Integrated sampling pipeline: Creates or encodes latent → multi-seed sampling → decodes to images. For denoise=1.0 uses width/height to create empty latent. For denoise<1.0 uses input_image."
def slerp(self, val, low, high):
"""Spherical linear interpolation for noise blending"""
dims = low.shape
low = low.reshape(dims[0], -1)
high = high.reshape(dims[0], -1)
low_norm = low / torch.norm(low, dim=1, keepdim=True)
high_norm = high / torch.norm(high, dim=1, keepdim=True)
low_norm[low_norm != low_norm] = 0.0
high_norm[high_norm != high_norm] = 0.0
omega = torch.acos((low_norm * high_norm).sum(1))
so = torch.sin(omega)
res = (torch.sin((1.0 - val) * omega) / so).unsqueeze(1) * low + (torch.sin(val * omega) / so).unsqueeze(1) * high
return res.reshape(dims)
def split_image_into_tiles(self, image, vertical_splits, horizontal_splits, expansion_percent):
"""Split image into tiles with expansion borders and generate proper gradient masks for img2img"""
_, height, width, channels = image.shape
tiles = []
masks = []
bboxes = []
# Use linspace to create perfectly spaced coordinates, handling any image size
y_coords = np.linspace(0, height, vertical_splits + 1, dtype=int)
x_coords = np.linspace(0, width, horizontal_splits + 1, dtype=int)
def create_expansion_gradient_mask(tile_h, tile_w, orig_x_in_tile, orig_y_in_tile, orig_w, orig_h):
"""Create gradient mask spanning full expansion width - 1.0 at core edges, 0.0 at expansion boundaries"""
mask = torch.ones((tile_h, tile_w), dtype=torch.float32, device=image.device)
# Top expansion gradient
if orig_y_in_tile > 0:
expansion_top = orig_y_in_tile
alpha = torch.linspace(1 / (expansion_top + 1), 1.0, steps=expansion_top, device=image.device)
mask[:expansion_top, :] = alpha.view(-1, 1)
# Bottom expansion gradient
bottom_expansion_start = orig_y_in_tile + orig_h
if bottom_expansion_start < tile_h:
expansion_bottom = tile_h - bottom_expansion_start
alpha = torch.linspace(1.0, 1 / (expansion_bottom + 1), steps=expansion_bottom, device=image.device)
mask[bottom_expansion_start:, :] = alpha.view(-1, 1)
# Left expansion gradient
if orig_x_in_tile > 0:
expansion_left = orig_x_in_tile
alpha = torch.linspace(1 / (expansion_left + 1), 1.0, steps=expansion_left, device=image.device)
mask[:, :expansion_left] *= alpha.view(1, -1)
# Right expansion gradient
right_expansion_start = orig_x_in_tile + orig_w
if right_expansion_start < tile_w:
expansion_right = tile_w - right_expansion_start
alpha = torch.linspace(1.0, 1 / (expansion_right + 1), steps=expansion_right, device=image.device)
mask[:, right_expansion_start:] *= alpha.view(1, -1)
return mask
for i in range(vertical_splits):
for j in range(horizontal_splits):
y1, y2 = y_coords[i], y_coords[i+1]
x1, x2 = x_coords[j], x_coords[j+1]
core_h = y2 - y1
core_w = x2 - x1
# Expansion is now calculated per-tile, which is more accurate for uneven splits
expansion_h = int(core_h * expansion_percent / 100)
expansion_w = int(core_w * expansion_percent / 100)
y_start = max(0, y1 - expansion_h)
y_end = min(height, y2 + expansion_h)
x_start = max(0, x1 - expansion_w)
x_end = min(width, x2 + expansion_w)
# Skip tiles that have no area
if y_start >= y_end or x_start >= x_end:
continue
tile = image[:, y_start:y_end, x_start:x_end, :]
tiles.append(tile)
orig_x_in_tile = x1 - x_start
orig_y_in_tile = y1 - y_start
actual_tile_h = y_end - y_start
actual_tile_w = x_end - x_start
gradient_mask = create_expansion_gradient_mask(actual_tile_h, actual_tile_w, orig_x_in_tile, orig_y_in_tile, core_w, core_h)
masks.append(gradient_mask)
bbox_info = {
'orig_x': x1, 'orig_y': y1, 'orig_w': core_w, 'orig_h': core_h,
'exp_x_start': x_start, 'exp_y_start': y_start,
'exp_x_end': x_end, 'exp_y_end': y_end,
'tile_index': i * horizontal_splits + j
}
bboxes.append(bbox_info)
# Return lists of tensors, as they may have different sizes
return tiles, masks, bboxes
def reassemble_tiles(self, original_image, processed_tiles, tile_masks, bboxes, vertical_splits, horizontal_splits):
"""Reassemble tiles using a robust weighted accumulation method for seamless blending."""
_, height, width, channels = original_image.shape
device = original_image.device
dtype = original_image.dtype
# Initialize accumulation tensors
result_acc = torch.zeros_like(original_image)
weight_sum_acc = torch.zeros((1, height, width, 1), device=device, dtype=dtype)
for i, (tile, mask) in enumerate(zip(processed_tiles, tile_masks)):
bbox_info = bboxes[i]
exp_y_start, exp_x_start = bbox_info['exp_y_start'], bbox_info['exp_x_start']
tile_h, tile_w = tile.shape[1], tile.shape[2]
# VAE encode/decode can alter dimensions by a few pixels if not a multiple of 8.
# We must resize the mask to match the actual processed tile dimensions.
if mask.shape[0] != tile_h or mask.shape[1] != tile_w:
mask = torch.nn.functional.interpolate(
mask.unsqueeze(0).unsqueeze(0),
size=(tile_h, tile_w),
mode='bilinear'
).squeeze(0).squeeze(0)
# Expand mask to have a channel dimension for broadcasting
mask_expanded = mask.unsqueeze(-1)
# Accumulate weighted pixel data
result_acc[0, exp_y_start:exp_y_start+tile_h, exp_x_start:exp_x_start+tile_w, :] += tile[0] * mask_expanded
# Accumulate weights
weight_sum_acc[0, exp_y_start:exp_y_start+tile_h, exp_x_start:exp_x_start+tile_w, :] += mask_expanded
# Avoid division by zero by clamping weights
weight_sum_clamped = torch.clamp(weight_sum_acc, min=1e-6)
# Normalize the accumulated result
final_result = result_acc / weight_sum_clamped
# Where no tile contributed (weight is near zero), use the original image content
untouched_mask = weight_sum_acc <= 1e-6
final_result[untouched_mask.expand_as(final_result)] = original_image[untouched_mask.expand_as(original_image)]
return final_result
def sample_integrated(self, model, vae, positive, negative, denoise, steps, cfg, seed_count, seed,
sampler_name, scheduler, width, height, noise_type="vanilla", injected_noise=0.0,
vertical_splits=1, horizontal_splits=1, expansion_percent=0.0, input_image=None):
# Determine if we're doing txt2img (denoise=1.0) or img2img (denoise<1.0)
is_txt2img = abs(denoise - 1.0) < 0.001
# Check if we need tiling
use_tiling = vertical_splits > 1 or horizontal_splits > 1
if is_txt2img:
# Create latent image (txt2img pipeline)
if input_image is not None:
print("Warning: input_image provided but denoise=1.0, ignoring input_image and using width/height")
# Generate initial latent - detect required channel count from model
try:
# Try to get the required latent channels from the model
if hasattr(model, 'model') and hasattr(model.model, 'latent_format'):
latent_channels = model.model.latent_format.latent_channels
else:
# Fallback: use 4 channels for SD/SDXL models
latent_channels = 4
# Additional fallback based on model type detection
if latent_channels is None or latent_channels <= 0:
# Try to detect if it's a Flux model by checking model properties
try:
# Check if this might be a Flux model based on common indicators
model_config = getattr(model.model, 'model_config', {})
if hasattr(model.model, 'diffusion_model'):
# Check for Flux-specific architecture indicators
diff_model = model.model.diffusion_model
if (hasattr(diff_model, 'x_embedder') or
getattr(model_config, 'unet_config', {}).get('in_channels') == 16):
latent_channels = 16 # Flux models use 16 channels
else:
latent_channels = 4 # SD/SDXL models use 4 channels
else:
latent_channels = 4 # Default fallback
except:
latent_channels = 4 # Safe fallback
except:
# Final fallback
latent_channels = 4
latent_shape = [1, latent_channels, height // 8, width // 8]
latent = torch.zeros(latent_shape, device=self.device)
latent_image = {"samples": latent}
else:
# Encode input image (img2img pipeline)
if input_image is None:
raise ValueError("input_image is required when denoise < 1.0 (img2img mode)")
# Use vanilla VAE encode approach
encoded_tensor = vae.encode(input_image[:,:,:,:3])
latent_image = {"samples": encoded_tensor}
# Handle tiling or standard processing
if use_tiling and not is_txt2img:
# Tiled img2img processing
return self._process_tiled_img2img(model, vae, positive, negative, denoise, steps, cfg, seed_count, seed,
sampler_name, scheduler, noise_type, injected_noise, input_image,
vertical_splits, horizontal_splits, expansion_percent)
else:
# Standard processing (txt2img or non-tiled img2img)
return self._process_standard(model, vae, positive, negative, latent_image, denoise, steps, cfg, seed_count, seed,
sampler_name, scheduler, noise_type, injected_noise, is_txt2img)
def _process_standard(self, model, vae, positive, negative, latent_image, denoise, steps, cfg, seed_count, seed,
sampler_name, scheduler, noise_type, injected_noise, is_txt2img):
"""Standard processing without tiling"""
output_latents = []
input_latents = latent_image["samples"]
total_generations = input_latents.shape[0] * seed_count
pbar = comfy.utils.ProgressBar(total_generations)
for latent_sample in input_latents:
for i in range(seed_count):
current_seed = seed + i
# For advanced noise methods in txt2img mode, replace zeros with our noise
if is_txt2img and noise_type != "vanilla":
# Get the shape of the current latent sample
base_latent = latent_sample.unsqueeze(0)
try:
if noise_type == "spectral-diverse":
# Generate noise directly on the target device with exact shape
noise = create_spectral_diverse_noise(
base_latent.shape, current_seed, device=base_latent.device, noise_type="hybrid")
# Ensure exact compatibility
current_latent = noise.to(device=base_latent.device, dtype=base_latent.dtype)
elif noise_type == "hierarchical":
# Generate noise directly on the target device with exact shape
noise = create_hierarchical_noise(
base_latent.shape, current_seed, device=base_latent.device, diversity_strength=1.0)
# Ensure exact compatibility
current_latent = noise.to(device=base_latent.device, dtype=base_latent.dtype)
else:
current_latent = base_latent
except Exception as e:
# Fallback to vanilla noise if advanced noise generation fails
print(f"Warning: Advanced noise generation failed ({e}), falling back to vanilla noise")
current_latent = base_latent
work_latent = current_latent
else:
# Use the existing latent (vanilla mode or img2img)
work_latent = latent_sample.unsqueeze(0)
# Apply noise injection if enabled
if injected_noise > 0.0:
# Generate base noise from main seed
generator = torch.manual_seed(seed)
base_noise = torch.randn(work_latent.shape, generator=generator, device="cpu", dtype=work_latent.dtype)
# Generate variation noise from current seed (naturally varies per iteration)
generator = torch.manual_seed(current_seed + 12345) # Offset to ensure different noise
variation_noise = torch.randn(work_latent.shape, generator=generator, device="cpu", dtype=work_latent.dtype)
# Blend noises using SLERP
blended_noise = self.slerp(injected_noise, base_noise, variation_noise)
# Calculate sigma scaling factor
device = comfy.model_management.get_torch_device()
comfy.model_management.load_model_gpu(model)
sampler = comfy.samplers.KSampler(model, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=1.0, model_options=model.model_options)
sigmas = sampler.sigmas
start_at_step = round(steps - steps * denoise)
end_at_step = steps
sigma = sigmas[start_at_step] - sigmas[end_at_step]
sigma /= model.model.latent_format.scale_factor
sigma = sigma.detach().cpu().item()
# Apply scaled noise to latent
work_latent = work_latent + blended_noise.to(work_latent.device) * sigma
latent_for_sampler = {"samples": work_latent}
# Call the common ksampler function from the main ComfyUI nodes
# This ensures we always use the latest version
result_latent, = common_ksampler(model, current_seed, steps, cfg, sampler_name, scheduler, positive,
negative, latent_for_sampler, denoise=denoise)
output_latents.append(result_latent["samples"])
pbar.update(1)
# Combine all the generated latents into a single batch tensor
final_samples = torch.cat(output_latents, dim=0)
# Decode latents to images using vanilla VAE decode approach
images = vae.decode(final_samples)
if len(images.shape) == 5: # Combine batches
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
return (images,)
def _prepare_inpaint_conditioning(self, conditioning, concat_latent, latent_mask):
c = []
for t in conditioning:
# Create a deep enough copy to avoid modifying the original conditioning
n = [t[0], t[1].copy()]
n[1]["concat_latent_image"] = concat_latent
# The mask is inverted for ComfyUI's inpainting model
n[1]["concat_mask"] = 1.0 - latent_mask
c.append(n)
return c
def _process_tiled_img2img(self, model, vae, positive, negative, denoise, steps, cfg, seed_count, seed,
sampler_name, scheduler, noise_type, injected_noise, input_image,
vertical_splits, horizontal_splits, expansion_percent):
"""Tiled img2img processing with proper batch support."""
if input_image is None:
raise ValueError("input_image is required for tiled processing")
# This list will hold the final results for all images and all seeds
all_results = []
# The main loop to iterate over each image in the input batch
for i in range(input_image.shape[0]):
single_image = input_image[i:i+1] # Keep it as a batch of 1 [1, H, W, C]
# Now, run the multi-seed process on this single image
for seed_idx in range(seed_count):
current_seed = seed + seed_idx
# Split the single image into tiles
tiles, tile_masks, bboxes = self.split_image_into_tiles(single_image, vertical_splits, horizontal_splits, expansion_percent)
processed_tiles = []
# Process each tile for the current image and seed
for tile_idx, tile in enumerate(tiles):
# Get the corresponding mask for this tile
tile_mask = tile_masks[tile_idx]
# Encode the original tile content for inpainting conditioning
concat_latent = vae.encode(tile)
# Apply noise injection if enabled
if injected_noise > 0.0:
generator = torch.manual_seed(seed)
base_noise = torch.randn(concat_latent.shape, generator=generator, device="cpu", dtype=concat_latent.dtype)
generator = torch.manual_seed(current_seed + tile_idx + 12345)
variation_noise = torch.randn(concat_latent.shape, generator=generator, device="cpu", dtype=concat_latent.dtype)
blended_noise = self.slerp(injected_noise, base_noise, variation_noise)
device = comfy.model_management.get_torch_device()
comfy.model_management.load_model_gpu(model)
sampler = comfy.samplers.KSampler(model, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=1.0, model_options=model.model_options)
sigmas = sampler.sigmas
start_at_step = round(steps - steps * denoise)
end_at_step = steps
sigma = sigmas[start_at_step] - sigmas[end_at_step]
sigma /= model.model.latent_format.scale_factor
sigma = sigma.detach().cpu().item()
concat_latent = concat_latent + blended_noise.to(concat_latent.device) * sigma
# Downscale the pixel mask to latent dimensions
latent_h, latent_w = concat_latent.shape[2], concat_latent.shape[3]
latent_mask = torch.nn.functional.interpolate(
tile_mask.reshape((-1, 1, tile_mask.shape[-2], tile_mask.shape[-1])),
size=(latent_h, latent_w),
mode="bilinear"
)
# Set up the latent structure for the sampler
latent_for_sampler = {
"samples": concat_latent,
"noise_mask": latent_mask
}
# Set inpainting-specific conditioning info
positive_inp = self._prepare_inpaint_conditioning(positive, concat_latent, latent_mask)
negative_inp = self._prepare_inpaint_conditioning(negative, concat_latent, latent_mask)
# Sample tile
result_latent, = common_ksampler(
model, current_seed + tile_idx, steps, cfg, sampler_name, scheduler,
positive_inp, negative_inp, latent_for_sampler, denoise=denoise
)
# Decode tile back to image
tile_image = vae.decode(result_latent["samples"])
processed_tiles.append(tile_image)
# Reassemble the tiles for the current image and seed
result_image = self.reassemble_tiles(single_image, processed_tiles, tile_masks, bboxes, vertical_splits, horizontal_splits)
all_results.append(result_image)
# Stack all final results from all images and seeds into a single batch
return (torch.cat(all_results, dim=0),)