Files
KarmaSwint-ComfyUI-KarmaNodes/KarmaKSamplerCycle.py
KarmaSwint 802bd560f0 Initial commit: ComfyUI-KarmaNodes with advanced sampling and post-processing
- Add Karma KSampler Cycle node with multi-cycle sampling and progressive upscaling
- Add Karma Film Grain node for authentic analog film texture effects
- Add Karma Kolors node for professional color grading and correction
- Include comprehensive documentation with usage examples and troubleshooting
- Add support section with donation link for future development
- Include all necessary project files: LICENSE, pyproject.toml, requirements.txt
- Set up proper Python package structure with __init__.py
2025-06-29 16:28:58 +00:00

867 lines
47 KiB
Python

"""
ComfyUI node for multi-cycle sampling with progressive upscaling.
This module provides a specialized KSampler node that performs multiple sampling cycles
with progressive upscaling between cycles. It supports both basic and model-based upscaling,
additive conditioning, and dynamic parameter adjustments between cycles.
The main component is the Karma_KSampler_Cycle class which handles the entire workflow
of sampling, upscaling, and re-sampling for high-quality image generation.
"""
import torch
from PIL import Image
import numpy as np
import folder_paths
import nodes
import comfy.samplers
import comfy.model_management
import comfy.utils
from skimage.filters import unsharp_mask
# Helper functions
def tensor2pil(image: torch.Tensor) -> Image.Image:
"""
Convert a PyTorch tensor to a PIL Image.
Args:
image: Input tensor with shape [C, H, W] or [1, C, H, W]
Returns:
PIL Image converted from tensor
"""
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def pil2tensor(image: Image.Image) -> torch.Tensor:
"""
Convert a PIL Image to a PyTorch tensor.
Args:
image: Input PIL Image
Returns:
PyTorch tensor with shape [1, H, W, C]
"""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class Karma_Image_Rescale:
"""
Image rescaling utility class for high-quality image resizing operations.
This class provides methods to rescale images with optional supersampling
for improved quality. It supports both factor-based rescaling and
resizing to specific dimensions.
"""
def __init__(self):
"""Initialize the image rescaler."""
pass
def image_rescale(self, image: list[torch.Tensor], mode: str = "rescale",
supersample: str = 'true', resampling: str = "lanczos",
rescale_factor: float = 2, resize_width: int = 1024,
resize_height: int = 1024) -> tuple[torch.Tensor]:
"""
Rescale a batch of image tensors.
Args:
image: List of image tensors to rescale
mode: Rescaling mode, either "rescale" (by factor) or "resize" (to dimensions)
supersample: Whether to use supersampling ('true' or 'false')
resampling: Resampling method ('nearest', 'bilinear', 'bicubic', or 'lanczos')
rescale_factor: Factor to scale by when mode is "rescale"
resize_width: Target width when mode is "resize"
resize_height: Target height when mode is "resize"
Returns:
Tuple containing the batch of rescaled image tensors
"""
# Process each image tensor in the batch
resized_image_tensors = []
for input_tensor in image:
# Convert tensor to PIL for resizing
input_pil = tensor2pil(input_tensor)
# Apply resize operation
resized_pil = self.apply_resize_image(
input_pil,
mode,
supersample,
rescale_factor,
resize_width,
resize_height,
resampling
)
# Convert back to tensor and add to results
resized_image_tensors.append(pil2tensor(resized_pil))
# Combine all tensors into a batch
batch_result = torch.cat(resized_image_tensors, dim=0)
return (batch_result, )
def apply_resize_image(self, image: Image.Image, mode: str = 'scale',
supersample: str = 'true', factor: float = 2,
width: int = 1024, height: int = 1024,
resample: str = 'bicubic') -> Image.Image:
"""
Apply resize operation to a single PIL image.
Args:
image: PIL Image to resize
mode: 'rescale' to use factor, or 'scale'/'resize' to use dimensions
supersample: Whether to use supersampling for higher quality
factor: Scaling factor when mode is 'rescale'
width: Target width when mode is not 'rescale'
height: Target height when mode is not 'rescale'
resample: Resampling method ('nearest', 'bilinear', 'bicubic', or 'lanczos')
Returns:
Resized PIL Image
"""
current_width, current_height = image.size
if mode == 'rescale':
new_width, new_height = int(current_width * factor), int(current_height * factor)
else:
# Ensure dimensions are divisible by 8 for VAE compatibility
new_width = width if width % 8 == 0 else width + (8 - width % 8)
new_height = height if height % 8 == 0 else height + (8 - height % 8)
resample_filters = {
'nearest': 0,
'bilinear': 2,
'bicubic': 3,
'lanczos': 1
}
if supersample == 'true':
# Supersample by first upscaling to 8x the target size then downscaling
image = image.resize((new_width * 8, new_height * 8),
resample=Image.Resampling(resample_filters[resample]))
resized_image = image.resize((new_width, new_height),
resample=Image.Resampling(resample_filters[resample]))
return resized_image
class Karma_KSampler_Cycle:
"""
Advanced KSampler node that performs multiple sampling cycles with progressive upscaling.
This node extends the standard KSampler functionality by implementing a multi-cycle
approach where the image is progressively upscaled between sampling cycles. This allows
for high-quality, high-resolution image generation with better detail preservation.
Features:
- Multiple sampling cycles with configurable parameters
- Progressive upscaling between cycles (basic or model-based)
- Primary and optional secondary model switching at specified cycle
- Additive conditioning with strength scaling
- Dynamic denoise strength adjustment
- Optional sharpening between cycles
- Automatic device management for optimal performance
"""
@classmethod
def INPUT_TYPES(cls):
"""
Define the input parameters for the node.
Returns:
Dictionary containing required and optional input parameters
"""
required = {
# Core Models
"primary_model": ("MODEL",),
"vae": ("VAE",),
"latent_image": ("LATENT", ),
# Conditioning Inputs
"positive": ("CONDITIONING", {"tooltip": "The main positive conditioning."}),
"negative": ("CONDITIONING", {"tooltip": "The main negative conditioning."}),
# Sampling Parameters
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
"starting_denoise": ("FLOAT", {"default":1.0, "min":0.0, "max":1.0, "step":0.01}),
"cycle_denoise": ("FLOAT", {"default":0.5, "min":0.0, "max":1.0, "step":0.01}),
# Cycle Parameters
"total_cycles": ("INT", {"default":2, "min":2, "max":12, "step":1}),
"enable_denoise_scaling": ("BOOLEAN", {"default": True, "tooltip": "Enable automatic denoise strength scaling between cycles"}),
"denoise_min_threshold": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 1.0, "step": 0.01, "tooltip": "Minimum denoise threshold to prevent going too low"}),
"enable_steps_scaling": ("BOOLEAN", {"default": False, "tooltip": "Enable automatic steps adjustment between cycles"}),
"steps_scaling_value": ("INT", {"default": 5, "min": 1, "max": 50, "step": 1, "tooltip": "Amount to adjust steps by each cycle"}),
"steps_adjustment_mode": (["decrement", "increment"], {"default": "decrement", "tooltip": "Whether to decrease or increase steps each cycle"}),
"steps_threshold_mode": (["auto", "manual"], {"default": "auto", "tooltip": "Auto calculates threshold based on initial steps, manual uses fixed value"}),
"steps_manual_threshold": ("INT", {"default": 5, "min": 1, "max": 200, "step": 1, "tooltip": "Manual threshold value (only used when threshold mode is manual)"}),
"secondary_model_start_cycle": ("INT", {"default": 2, "min": 1, "max": 12, "step": 1, "tooltip": "Cycle at which to switch to secondary model (if provided)"}),
# Upscaling Parameters
"upscale_factor": ("FLOAT", {"default":2.0, "min":0.1, "max":8.0, "step":0.1}),
"upscale_method": (["basic", "model"], {"default": "basic"}),
"scale_sampling": (["bilinear", "bicubic", "nearest", "lanczos"], {"tooltip": "Resampling method for image upscaling"}),
"latent_upscale_method": (["bilinear", "bicubic", "nearest", "lanczos"], {"default": "lanczos", "tooltip": "Resampling method for latent space upscaling"}),
"enable_gradual_upscaling": ("BOOLEAN", {"default": False, "tooltip": "Enable gradual upscaling with multiple intermediate steps"}),
"gradual_upscale_steps": ("INT", {"default": 3, "min": 1, "max": 10, "step": 1, "tooltip": "Number of gradual upscaling steps (only used when gradual upscaling is enabled)"}),
# Additive Prompt Parameters
"pos_add_strength": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 1.0, "step": 0.01}),
"enable_pos_strength_scaling": ("BOOLEAN", {"default": False, "tooltip": "Enable positive strength scaling between cycles"}),
"pos_add_strength_cutoff": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 10.0, "step": 0.01}),
"neg_add_strength": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 1.0, "step": 0.01}),
"enable_neg_strength_scaling": ("BOOLEAN", {"default": False, "tooltip": "Enable negative strength scaling between cycles"}),
"neg_add_strength_cutoff": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 10.0, "step": 0.01}),
# Post-processing Parameters
"sharpen_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"sharpen_radius": ("INT", {"default": 2, "min": 1, "max": 12, "step": 1}),
# VAE Parameters
"use_tiled_vae": ("BOOLEAN", {"default": False, "tooltip": "Use tiled VAE processing for large images"}),
}
optional = {
"secondary_model": ("MODEL", {"tooltip": "Optional secondary model to switch to at specified cycle"}),
"upscale_model": ("UPSCALE_MODEL",),
"pos_additive": ("CONDITIONING", {"tooltip": "Additional positive conditioning to blend in."}),
"neg_additive": ("CONDITIONING", {"tooltip": "Additional negative conditioning to blend in."}),
}
return {"required": required, "optional": optional}
RETURN_TYPES = ("LATENT", "VAE")
RETURN_NAMES = ("latent(s)", "vae")
FUNCTION = "sample"
CATEGORY = "KarmaNodes/Sampling"
def sample(self, primary_model, vae, positive, negative,
steps: int, seed: int, cfg: float, sampler_name: str, scheduler: str,
latent_image: dict, use_tiled_vae: bool, upscale_factor: float,
total_cycles: int, starting_denoise: float, cycle_denoise: float,
enable_denoise_scaling: bool, denoise_min_threshold: float,
scale_sampling: str, pos_add_strength: float, enable_pos_strength_scaling: bool,
pos_add_strength_cutoff: float, neg_add_strength: float, enable_neg_strength_scaling: bool,
neg_add_strength_cutoff: float, sharpen_strength: float, sharpen_radius: int,
enable_steps_scaling: bool, steps_scaling_value: int, steps_adjustment_mode: str,
steps_threshold_mode: str, steps_manual_threshold: int, upscale_method: str,
latent_upscale_method: str, enable_gradual_upscaling: bool, gradual_upscale_steps: int,
secondary_model_start_cycle: int, secondary_model=None, upscale_model=None,
pos_additive=None, neg_additive=None) -> tuple:
"""
Execute the multi-cycle sampling process with progressive upscaling.
This method performs the following steps in each cycle:
1. Combine conditioning inputs (with optional additive conditioning)
2. Run the sampler with current parameters
3. Decode the latent to an image
4. Upscale the image
5. Re-encode to latent for the next cycle
Args:
primary_model: Primary diffusion model
vae: VAE model for encoding/decoding
positive: Main positive conditioning
negative: Main negative conditioning
steps: Number of sampling steps
seed: Random seed for sampling
cfg: Classifier-free guidance scale
sampler_name: Name of the sampler to use
scheduler: Name of the scheduler to use
latent_image: Initial latent image
use_tiled_vae: Whether to use tiled VAE processing
upscale_factor: Total upscale factor to achieve
total_cycles: Number of sampling cycles
starting_denoise: Initial denoise strength
cycle_denoise: Denoise strength for subsequent cycles
enable_denoise_scaling: Whether to enable denoise scaling
denoise_min_threshold: Minimum denoise threshold value
scale_sampling: Resampling method for scaling
pos_add_strength: Strength of positive additive conditioning
enable_pos_strength_scaling: Whether to scale positive strength
pos_add_strength_cutoff: Maximum positive additive strength
neg_add_strength: Strength of negative additive conditioning
enable_neg_strength_scaling: Whether to scale negative strength
neg_add_strength_cutoff: Maximum negative additive strength
sharpen_strength: Strength of sharpening filter
sharpen_radius: Radius of sharpening filter
enable_steps_scaling: Whether to adjust steps between cycles
steps_scaling_value: Amount to adjust steps by
steps_adjustment_mode: Direction of steps adjustment ("decrement" or "increment")
steps_threshold_mode: How to calculate steps threshold ("auto" or "manual")
steps_manual_threshold: Manual threshold value when steps_threshold_mode is "manual"
upscale_method: Method for upscaling ("basic" or "model")
latent_upscale_method: Resampling method for latent upscaling
enable_gradual_upscaling: Whether to use gradual upscaling
gradual_upscale_steps: Number of gradual upscaling steps
secondary_model_start_cycle: Cycle number at which to switch to secondary model
secondary_model: Optional secondary diffusion model to switch to
upscale_model: Upscale model to use (required if upscale_method is "model")
pos_additive: Additional positive conditioning to blend in (optional)
neg_additive: Additional negative conditioning to blend in (optional)
Returns:
Tuple containing the final latent image and the VAE
"""
# Use provided conditioning inputs directly
# --- Setup ---
num_sampling_cycles = total_cycles if steps >= total_cycles else steps
model_switched = False
# Calculate steps threshold based on mode
if steps_threshold_mode == "manual":
steps_min_threshold = steps_manual_threshold
else: # auto mode
if steps_adjustment_mode == 'decrement':
steps_min_threshold = max(1, steps // 4) # Minimum threshold for decrement mode
else: # increment mode
steps_min_threshold = steps * 4 # Maximum threshold for increment mode
# Validate upscale method and model
if upscale_method == "model" and upscale_model is None:
print("WARNING: Upscale method set to 'model' but no upscale model provided. Falling back to basic method.")
upscale_method = "basic"
# Calculate upscale factors - use total_cycles for upscaling distribution
upscale_cycles_needed = total_cycles - 1 # Number of upscale operations (cycles - 1)
# Calculate the absolute final target dimensions based on the initial latent
initial_latent_height, initial_latent_width = latent_image['samples'].shape[2], latent_image['samples'].shape[3]
initial_image_height, initial_image_width = initial_latent_height * 8, initial_latent_width * 8
final_target_width = int(initial_image_width * upscale_factor)
final_target_height = int(initial_image_height * upscale_factor)
if upscale_method == "model" and upscale_model is not None:
# For model upscaling, we'll use the model's native scale then adjust to target
model_scale = upscale_model.scale
if upscale_cycles_needed > 0:
# Calculate the target scale per cycle to reach total upscale_factor
target_scale_per_cycle = upscale_factor ** (1 / upscale_cycles_needed)
print(f"INFO: Target scale per cycle: {target_scale_per_cycle:.2f}x, Model native scale: {model_scale}x")
# We'll use the model's native scale and then adjust with PIL if needed
current_upscale_factor = target_scale_per_cycle # This is what we want to achieve
model_native_scale = model_scale # This is what the model provides
else:
current_upscale_factor = 1.0
model_native_scale = 1.0
else:
# For basic upscaling, calculate factor per cycle to reach total target
current_upscale_factor = upscale_factor ** (1 / upscale_cycles_needed) if upscale_cycles_needed > 0 else 1.0
latent_image_result = None # Initialize to prevent potential UnboundLocalError if num_sampling_cycles is 0
print(f"--- Initial Setup ---")
print(f" Upscale Cycles: {total_cycles}, Total Steps: {steps}")
print(f" Number of Sampling Cycles: {num_sampling_cycles}")
print(f" Target Total Upscale Factor: {upscale_factor}")
print(f" Initial Image Size: {initial_image_width}x{initial_image_height}")
print(f" Final Target Size: {final_target_width}x{final_target_height}")
print(f" Upscale Method: {upscale_method}")
if upscale_method == "model" and upscale_model is not None:
print(f" Upscale Model Native Scale: {upscale_model.scale}x")
if upscale_cycles_needed > 0:
print(f" Target Scale Per Cycle: {current_upscale_factor:.4f}x")
actual_total = current_upscale_factor ** upscale_cycles_needed
print(f" Actual Total Upscale: {actual_total:.2f}x")
else:
print(f" Factor Per Cycle: {current_upscale_factor:.4f}")
print(f" Upscale Operations: {upscale_cycles_needed} (between {total_cycles} cycles)")
print(f" Tiled VAE: {use_tiled_vae}")
print(f" Denoise Scaling Enabled: {enable_denoise_scaling}")
if enable_denoise_scaling:
print(f" Denoise Min Threshold: {denoise_min_threshold}")
print(f" Steps Scaling Enabled: {enable_steps_scaling}")
if enable_steps_scaling:
print(f" Steps Scaling Mode: {steps_adjustment_mode}")
print(f" Steps Scaling Value: {steps_scaling_value}")
print(f" Steps Threshold Mode: {steps_threshold_mode}")
print(f" Steps Min Threshold: {steps_min_threshold}")
print(f" Gradual Upscaling Enabled: {enable_gradual_upscaling}")
if enable_gradual_upscaling:
print(f" Gradual Upscale Steps: {gradual_upscale_steps}")
print(f" Scale Sampling Method: {scale_sampling}")
print(f" Latent Upscale Method: {latent_upscale_method}")
if secondary_model is not None:
print(f" Secondary Model: Enabled (switches at cycle {secondary_model_start_cycle})")
else:
print(f" Secondary Model: Disabled")
print(f"---------------------")
# --- Main Sampling Loop ---
for i in range(num_sampling_cycles):
# Print cycle info
print(f"==== Main Cycle Pass {i+1}/{num_sampling_cycles} ====")
# --- Model Selection Logic ---
# Determine which model to use for this cycle
current_cycle = i + 1 # Convert to 1-based indexing for user clarity
if secondary_model is not None and current_cycle >= secondary_model_start_cycle:
active_model = secondary_model
if not model_switched:
print(f" Switching to secondary model at cycle {current_cycle}")
model_switched = True
else:
active_model = primary_model
# --- Get Target Device ---
# Assume the model's device is the target device
target_device = active_model.load_device
print(f" Target device for tensors: {target_device}")
print(f" Using {'secondary' if active_model == secondary_model else 'primary'} model")
# Update latent_image for passes > 0
if i > 0:
print(f" Updating latent_image for pass {i+1} from previous latent_image_result")
latent_image = latent_image_result
# Debug print for the input latent to the sampler
if isinstance(latent_image, dict) and 'samples' in latent_image:
latent_samples_tensor = latent_image['samples']
print(f" Input latent type for ksampler: {type(latent_image)}, shape: {latent_samples_tensor.shape}, device: {latent_samples_tensor.device}")
# --- Check for NaNs/Infs before sampling ---
if torch.isnan(latent_samples_tensor).any(): print(" FATAL WARNING: Input latent (pre-sample) contains NaNs!")
if torch.isinf(latent_samples_tensor).any(): print(" FATAL WARNING: Input latent (pre-sample) contains Infs!")
# --- Ensure latent is on the correct device ---
if latent_samples_tensor.device != target_device:
print(f" Moving latent samples from {latent_samples_tensor.device} to {target_device}...")
latent_image['samples'] = latent_samples_tensor.to(target_device)
print(f" Latent samples now on device: {latent_image['samples'].device}")
else:
print(f" WARNING: Input latent for ksampler is unexpected format: {type(latent_image)}")
else:
# Debug print for the initial input latent
if isinstance(latent_image, dict) and 'samples' in latent_image:
latent_samples_tensor = latent_image['samples']
print(f" Initial input latent type: {type(latent_image)}, shape: {latent_samples_tensor.shape}, device: {latent_samples_tensor.device}")
# --- Ensure initial latent is on the correct device ---
if latent_samples_tensor.device != target_device:
print(f" Moving initial latent samples from {latent_samples_tensor.device} to {target_device}...")
latent_image['samples'] = latent_samples_tensor.to(target_device)
print(f" Initial latent samples now on device: {latent_image['samples'].device}")
else:
print(f" WARNING: Initial input latent is unexpected format: {type(latent_image)}")
if enable_denoise_scaling:
denoise = (
( round(cycle_denoise * (2 ** (-(i-1))), 2) if i > 0 else cycle_denoise )
if i > 0 else round(starting_denoise, 2)
)
else:
denoise = round((cycle_denoise if i > 0 else starting_denoise), 2)
if denoise < denoise_min_threshold and enable_denoise_scaling:
denoise = denoise_min_threshold
# No secondary_model/secondary_start_cycle in this version
if enable_steps_scaling and i > 0:
original_steps = steps
# Adjust steps based on the adjustment mode
if steps_adjustment_mode == 'increment':
steps = steps + steps_scaling_value
# For increment mode, cap at maximum threshold
if steps > steps_min_threshold:
steps = steps_min_threshold
print(f" Steps scaling: {original_steps} + {steps_scaling_value} = {original_steps + steps_scaling_value} (capped to {steps})")
else:
print(f" Steps scaling: {original_steps} + {steps_scaling_value} = {steps}")
else: # decrement mode
steps = steps - steps_scaling_value
# For decrement mode, ensure we don't go below minimum threshold
if steps < steps_min_threshold:
steps = steps_min_threshold
print(f" Steps scaling: {original_steps} - {steps_scaling_value} = {original_steps - steps_scaling_value} (capped to {steps})")
else:
print(f" Steps scaling: {original_steps} - {steps_scaling_value} = {steps}")
print("Steps:", steps)
print("Denoise:", denoise)
if pos_additive:
pos_strength = 0.0 if i <= 0 else pos_add_strength
pos_strength = (
( round(pos_add_strength * (2 ** (i-1)), 2)
if i > 0
else pos_add_strength )
if enable_pos_strength_scaling
else pos_add_strength
)
pos_strength = (
pos_add_strength_cutoff
if pos_strength > pos_add_strength_cutoff
else pos_strength
)
comb = nodes.ConditioningAverage()
positive = comb.addWeighted(pos_additive, positive, pos_strength)[0]
print("Positive Additive Strength:", pos_strength)
if neg_additive:
neg_strength = 0.0 if i <= 0 else neg_add_strength
neg_strength = (
( round(neg_add_strength * (2 ** (i-1)), 2)
if i > 0
else neg_add_strength )
if enable_neg_strength_scaling
else neg_add_strength
)
neg_strength = (
neg_add_strength_cutoff
if neg_strength > neg_add_strength_cutoff
else neg_strength
)
comb = nodes.ConditioningAverage()
negative = comb.addWeighted(neg_additive, negative, neg_strength)[0]
print("Negative Additive Strength:", neg_strength)
# Run KSampler
print(f" Running common_ksampler (Pass {i+1})...")
samples = nodes.common_ksampler(
active_model,
seed + i, # Increment seed per main cycle
steps,
cfg,
sampler_name,
scheduler,
positive,
negative,
latent_image, # Input latent (already checked for device)
denoise=denoise,
)
# Debug print for the sampler output
if isinstance(samples, tuple) and len(samples) > 0 and isinstance(samples[0], dict) and 'samples' in samples[0]:
sampled_latent_tensor = samples[0]['samples']
print(f" Sampler output type: {type(samples[0])}, shape: {sampled_latent_tensor.shape}, device: {sampled_latent_tensor.device}")
# Check for NaNs/Infs after sampling, just in case
if torch.isnan(sampled_latent_tensor).any(): print(" FATAL WARNING: Sampled latent contains NaNs!")
if torch.isinf(sampled_latent_tensor).any(): print(" FATAL WARNING: Sampled latent contains Infs!")
else:
print(f" WARNING: Sampled output is unexpected format: {type(samples)}")
# Upscale between cycles (only during total_cycles, not all sampling cycles)
if i < total_cycles - 1:
print(f" Performing Image Upscale for next cycle (Pass {i+1}/{total_cycles})...")
# 1. Decode Latent to Image
print(" 1. Decoding latent to image...")
try:
# Ensure sampled latent is on the correct device before decode
sampled_latent_tensor = samples[0]['samples'].to(target_device)
if use_tiled_vae:
decoded_image_tensor = vae.decode_tiled(sampled_latent_tensor)
else:
decoded_image_tensor = vae.decode(sampled_latent_tensor)
print(f" Decoded image tensor shape: {decoded_image_tensor.shape}, device: {decoded_image_tensor.device}")
except Exception as e:
print(f"\\n!!! ERROR: Failed during VAE Decode for upscaling: {e}")
print("!!! ERROR: Skipping upscale, passing latent directly. This might cause issues!\\")
latent_image_result = samples[0] # Pass original latent if decode fails
continue # Skip to next iteration
# 2. Perform Gradual Upscaling
if enable_gradual_upscaling:
# Gradual upscaling with multiple intermediate steps
num_upscale_steps = gradual_upscale_steps
step_upscale_factor = current_upscale_factor ** (1 / num_upscale_steps)
print(f" 2. Performing gradual upscaling: {num_upscale_steps} steps, {step_upscale_factor:.4f}x per step")
# Calculate the target for this upscale operation based on progression toward absolute final target
# We're in upscale operation (i+1) out of upscale_cycles_needed total operations
upscale_progress = (i + 1) / upscale_cycles_needed # Progress through upscale operations (0 to 1)
cycle_target_width = int(initial_image_width + (final_target_width - initial_image_width) * upscale_progress)
cycle_target_height = int(initial_image_height + (final_target_height - initial_image_height) * upscale_progress)
initial_pil = tensor2pil(decoded_image_tensor)
initial_width, initial_height = initial_pil.size
print(f" Initial: {initial_width}x{initial_height}, Cycle target: {cycle_target_width}x{cycle_target_height}")
print(f" Upscale progress: {upscale_progress:.2f} toward final {final_target_width}x{final_target_height}")
current_image_tensor = decoded_image_tensor
for step in range(num_upscale_steps):
if step == num_upscale_steps - 1:
# For the final step, use exact cycle target dimensions
current_pil = tensor2pil(current_image_tensor)
current_width, current_height = current_pil.size
step_factor = cycle_target_width / current_width # Calculate exact factor needed
print(f" Upscale step {step+1}/{num_upscale_steps} (final step, exact factor: {step_factor:.4f}x)")
print(f" Final step: {current_width}x{current_height} -> {cycle_target_width}x{cycle_target_height}")
else:
step_factor = step_upscale_factor
print(f" Upscale step {step+1}/{num_upscale_steps} (factor: {step_factor:.4f}x)")
current_image_tensor = self.perform_single_upscale_step(
current_image_tensor, step_factor, upscale_method, upscale_model, scale_sampling,
is_final_step=(step == num_upscale_steps - 1),
final_target_width=cycle_target_width if step == num_upscale_steps - 1 else None,
final_target_height=cycle_target_height if step == num_upscale_steps - 1 else None
)
upscaled_image_tensor = current_image_tensor
else:
# Single upscaling step - calculate target based on upscale operation progress
upscale_progress = (i + 1) / upscale_cycles_needed # Progress through upscale operations (0 to 1)
cycle_target_width = int(initial_image_width + (final_target_width - initial_image_width) * upscale_progress)
cycle_target_height = int(initial_image_height + (final_target_height - initial_image_height) * upscale_progress)
initial_pil = tensor2pil(decoded_image_tensor)
initial_width, initial_height = initial_pil.size
actual_factor = cycle_target_width / initial_width
print(f" 2. Performing single upscaling step (target factor: {actual_factor:.4f}x)")
print(f" {initial_width}x{initial_height} -> {cycle_target_width}x{cycle_target_height}")
upscaled_image_tensor = self.perform_single_upscale_step(
decoded_image_tensor, actual_factor, upscale_method, upscale_model, scale_sampling,
is_final_step=True, final_target_width=cycle_target_width, final_target_height=cycle_target_height
)
# 3. Apply Sharpening if enabled
if sharpen_strength > 0.0:
print(f" 3. Applying sharpening (Strength: {sharpen_strength}, Radius: {sharpen_radius})...")
# Sharpening helper works on PIL images
upscaled_pil_for_sharpen = tensor2pil(upscaled_image_tensor)
sharpened_pil = self.unsharp_filter(upscaled_pil_for_sharpen, radius=sharpen_radius, amount=sharpen_strength)
upscaled_image_tensor = pil2tensor(sharpened_pil) # Convert back
print(f" Sharpened image tensor shape: {upscaled_image_tensor.shape}")
# 4. Encode Upscaled Image back to Latent
print(" 4. Encoding upscaled image back to latent...")
try:
# Ensure upscaled image tensor is on the target device for VAE encode
upscaled_image_tensor = upscaled_image_tensor.to(target_device)
# Crop pixels if needed before encode
cropped_pixels = self.vae_encode_crop_pixels(upscaled_image_tensor)
if use_tiled_vae:
latent_image_result = {"samples": vae.encode_tiled(cropped_pixels[:,:,:,:3])}
else:
latent_image_result = {"samples": vae.encode(cropped_pixels[:,:,:,:3])}
print(f" Re-encoded latent shape: {latent_image_result['samples'].shape}, device: {latent_image_result['samples'].device}")
# Final check for NaNs/Infs after the full upscale process
if torch.isnan(latent_image_result['samples']).any(): print(" FATAL WARNING: Re-encoded latent contains NaNs!")
if torch.isinf(latent_image_result['samples']).any(): print(" FATAL WARNING: Re-encoded latent contains Infs!")
except Exception as e:
print(f"\\n!!! ERROR: Failed during VAE Encode after upscaling: {e}")
print("!!! ERROR: Skipping upscale, passing latent directly. This might cause issues!\\")
latent_image_result = samples[0] # Pass original latent if encode fails
continue # Skip to next iteration
else: # Last iteration
latent_image_result = samples[0]
print(f" Last iteration, latent_image_result assigned from sampler output.")
# --- Final Device Check ---
final_latent = latent_image_result # Get the result after main loop
if isinstance(final_latent, dict) and 'samples' in final_latent:
final_samples_tensor = final_latent['samples']
# Use model device as target
try:
target_device = active_model.load_device
print(f"Final check: Output latent device is {final_samples_tensor.device}, target is {target_device}")
if final_samples_tensor.device != target_device:
print(f" Moving final output latent samples from {final_samples_tensor.device} to {target_device}...")
final_latent['samples'] = final_samples_tensor.to(target_device)
print(f" Final latent samples now on device: {final_latent['samples'].device}")
except AttributeError:
print("Warning: Could not determine model device for final check.") # Handle cases where model might not have load_device
else:
print("WARNING: Final latent output is not in expected dict format for device check.")
# Handle case where loop didn't run (num_sampling_cycles=0?)
if final_latent is None:
print("WARNING: Main loop did not run or produce a result. Returning input latent.")
final_latent = latent_image
return (final_latent, vae)
def perform_single_upscale_step(self, image_tensor: torch.Tensor, upscale_factor: float,
upscale_method: str, upscale_model, scale_sampling: str,
is_final_step: bool = False, final_target_width: int = None,
final_target_height: int = None) -> torch.Tensor:
"""
Perform a single upscaling step on an image tensor.
This method handles both model-based and basic upscaling methods.
For model-based upscaling, it first uses the model's native scale
and then adjusts to the exact target dimensions if needed.
Args:
image_tensor: Input image tensor to upscale
upscale_factor: Factor to upscale by
upscale_method: Method to use ("model" or "basic")
upscale_model: Upscale model to use (required if method is "model")
scale_sampling: Resampling method for basic upscaling
is_final_step: Whether this is the final upscaling step
final_target_width: Exact target width for final step
final_target_height: Exact target height for final step
Returns:
Upscaled image tensor
"""
try:
if upscale_method == "model" and upscale_model is not None:
# Calculate the target size
input_pil = tensor2pil(image_tensor)
input_width, input_height = input_pil.size
if is_final_step and final_target_width is not None and final_target_height is not None:
# Use exact final target dimensions
target_width = final_target_width
target_height = final_target_height
else:
# Calculate based on upscale factor
target_width = int(input_width * upscale_factor)
target_height = int(input_height * upscale_factor)
print(f" Input size: {input_width}x{input_height}, Target size: {target_width}x{target_height}")
# First, upscale with the model at its native scale
model_upscaled_tensor = self.upscale_with_model(upscale_model, image_tensor)
# Always adjust to the exact target size using PIL
model_upscaled_pil = tensor2pil(model_upscaled_tensor)
model_width, model_height = model_upscaled_pil.size
print(f" Model output size: {model_width}x{model_height}")
# Resize to exact target dimensions
if model_width != target_width or model_height != target_height:
print(f" Adjusting from {model_width}x{model_height} to {target_width}x{target_height}")
adjusted_pil = model_upscaled_pil.resize((target_width, target_height), resample=Image.Resampling.LANCZOS)
return pil2tensor(adjusted_pil)
else:
print(f" Model output matches target size exactly")
return model_upscaled_tensor
else:
# Use Karma_Image_Rescale helper class from this file
rescaler = Karma_Image_Rescale()
# Need to convert tensor to PIL temporarily for the helper
pil_image = tensor2pil(image_tensor)
upscaled_pil = rescaler.apply_resize_image(pil_image, mode='rescale', supersample='true', factor=upscale_factor, resample=scale_sampling)
# Convert back to tensor
return pil2tensor(upscaled_pil)
except Exception as e:
print(f"ERROR in perform_single_upscale_step: {e}")
return image_tensor # Return original if upscaling fails
@staticmethod
def vae_encode_crop_pixels(pixels: torch.Tensor) -> torch.Tensor:
"""
Crop pixel tensor to dimensions divisible by 8 for VAE compatibility.
Args:
pixels: Input pixel tensor with shape [B, H, W, C]
Returns:
Cropped pixel tensor with dimensions divisible by 8
"""
target_height = (pixels.shape[1] // 8) * 8
target_width = (pixels.shape[2] // 8) * 8
if pixels.shape[1] != target_height or pixels.shape[2] != target_width:
height_offset = (pixels.shape[1] % 8) // 2
width_offset = (pixels.shape[2] % 8) // 2
pixels = pixels[:, height_offset:target_height + height_offset, width_offset:target_width + width_offset, :]
return pixels
@staticmethod
def unsharp_filter(image: Image.Image, radius: int = 2, amount: float = 1.0) -> Image.Image:
"""
Apply unsharp mask filter to enhance image details.
Args:
image: Input PIL image
radius: Blur radius for the unsharp mask
amount: Strength of the sharpening effect
Returns:
Sharpened PIL image
"""
# Convert PIL image to normalized numpy array (0-1 range)
image_array = np.array(image)
normalized_array = image_array / 255.0
# Apply unsharp mask filter
sharpened_array = unsharp_mask(normalized_array, radius=radius, amount=amount, channel_axis=2)
# Convert back to uint8 range (0-255)
sharpened_uint8 = (sharpened_array * 255.0).astype(np.uint8)
# Convert back to PIL image
sharpened_image = Image.fromarray(sharpened_uint8)
return sharpened_image
@staticmethod
def upscale_with_model(upscale_model, image_tensor: torch.Tensor) -> torch.Tensor:
"""
Upscale image tensor using an upscale model with tiled processing.
This method handles memory management and uses tiled processing to
avoid out-of-memory errors with large images.
Args:
upscale_model: The upscale model to use
image_tensor: Input image tensor to upscale
Returns:
Upscaled image tensor
Raises:
OOM_EXCEPTION: If tiling cannot prevent out-of-memory error
"""
device = comfy.model_management.get_torch_device()
# Calculate memory requirements and free memory if needed
memory_required = comfy.model_management.module_size(upscale_model.model)
memory_required += (512 * 512 * 3) * image_tensor.element_size() * max(upscale_model.scale, 1.0) * 384.0
memory_required += image_tensor.nelement() * image_tensor.element_size()
comfy.model_management.free_memory(memory_required, device)
# Move model and input to the target device
upscale_model.to(device)
input_tensor_rearranged = image_tensor.movedim(-1,-3).to(device)
# Start with large tiles and reduce if OOM occurs
tile_size = 512
tile_overlap = 32
out_of_memory = True
while out_of_memory:
try:
total_steps = input_tensor_rearranged.shape[0] * comfy.utils.get_tiled_scale_steps(
input_tensor_rearranged.shape[3],
input_tensor_rearranged.shape[2],
tile_x=tile_size,
tile_y=tile_size,
overlap=tile_overlap
)
progress_bar = comfy.utils.ProgressBar(total_steps)
upscaled_tensor = comfy.utils.tiled_scale(
input_tensor_rearranged,
lambda a: upscale_model(a),
tile_x=tile_size,
tile_y=tile_size,
overlap=tile_overlap,
upscale_amount=upscale_model.scale,
pbar=progress_bar
)
out_of_memory = False
except comfy.model_management.OOM_EXCEPTION as e:
# Reduce tile size and try again
tile_size //= 2
if tile_size < 128:
raise e # If tiles are too small, propagate the error
# Move model back to CPU to free GPU memory
upscale_model.to("cpu")
result_tensor = torch.clamp(upscaled_tensor.movedim(-3,-1), min=0, max=1.0)
return result_tensor
# Register the node for ComfyUI
NODE_CLASS_MAPPINGS = {
"Karma-KSampler-Cycle": Karma_KSampler_Cycle,
}
# Define display names for the ComfyUI interface
NODE_DISPLAY_NAME_MAPPINGS = {
"Karma-KSampler-Cycle": "Karma KSampler Cycle",
}