diff --git a/README.md b/README.md index 3d96ec0..894dd5e 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,10 @@ The `PPFNKSamplerAdvanced` class is part of the Power Noise Suite and provides a - `noise_type`: ["white", "grey", "pink", "green", "blue", "mix"]: - The type of [power-law noise](https://en.wikipedia.org/wiki/Colors_of_noise#Technical_definitions) to generate. +- `noise_blending`: ["bislerp", "cosine interp", "cuberp", "hslerp", "lerp", "add", "inject"] + - The noise blending method used during sampling +- `noise_mode`: ["additive", "subtractive"] + - The noise operation used on the Power noise added to base noise. - `frequency` (FLOAT): The frequency of the noise range. - Default: 3.141592653589793 - Range: 0.001 to 1024.0 diff --git a/modules/latent_filters.py b/modules/latent_filters.py index 97f452e..c9cbd7f 100644 --- a/modules/latent_filters.py +++ b/modules/latent_filters.py @@ -1,5 +1,6 @@ import torch import torch.nn.functional as F +import math from .latent_util import normalize @@ -14,9 +15,12 @@ def sharpen_latents(latent, alpha=1.5): Returns: torch.Tensor: The sharpened latent tensor. """ + # Ensure that all operations are done on the input 'latent' device + device = latent.device + sharpen_kernel = torch.tensor([[-1, -1, -1], [-1, 9, -1], - [-1, -1, -1]], dtype=torch.float32) + [-1, -1, -1]], dtype=torch.float32, device=device) sharpen_kernel = sharpen_kernel.view(1, 1, 3, 3) sharpen_kernel /= sharpen_kernel.sum() @@ -56,8 +60,6 @@ def high_pass_latents(latent, radius=3, strength=1.0): gaussian_kernel = torch.exp(-(x ** 2) / (2 * sigma ** 2)) gaussian_kernel = gaussian_kernel / gaussian_kernel.sum() - padding_size = int((kernel_size - 1) // 2) - high_pass_overlays = [] for channel in range(latent.size(1)): @@ -69,20 +71,20 @@ def high_pass_latents(latent, radius=3, strength=1.0): input_blur_h = F.conv2d( channel_tensor, weight_h, - padding=(0, padding_size), + padding=0, ) input_blur_v = F.conv2d( input_blur_h, weight_v, - padding=(padding_size, 0), + padding=0, ) - input_blur_h = F.interpolate(input_blur_h, size=(channel_tensor.size(2), channel_tensor.size(3)), mode='nearest') - input_blur_v = F.interpolate(input_blur_v, size=(channel_tensor.size(2), channel_tensor.size(3)), mode='nearest') + + input_blur_h = F.interpolate(input_blur_h, size=channel_tensor.shape[-2:], mode='bilinear', align_corners=False) + input_blur_v = F.interpolate(input_blur_v, size=channel_tensor.shape[-2:], mode='bilinear', align_corners=False) high_pass_component = channel_tensor - input_blur_v high_pass_channel = channel_tensor + strength * high_pass_component - high_pass_channel = high_pass_channel[:, :, padding_size:-padding_size, padding_size:-padding_size] high_pass_channel = torch.clamp(high_pass_channel, 0, 1) high_pass_overlays.append(high_pass_channel) @@ -91,6 +93,42 @@ def high_pass_latents(latent, radius=3, strength=1.0): return high_pass_overlay +def hslerp(a, b, t): + """ + Perform Hybrid Spherical Linear Interpolation (HSLERP) between two tensors. + + This function combines two input tensors `a` and `b` using HSLERP, which is a specialized + interpolation method for smooth transitions between orientations or colors. + + Args: + a (tensor): The first input tensor. + b (tensor): The second input tensor. + t (float): The blending factor, a value between 0 and 1 that controls the interpolation. + + Returns: + tensor: The result of HSLERP interpolation between `a` and `b`. + + Note: + HSLERP provides smooth transitions between orientations or colors, particularly useful + in applications like image processing and 3D graphics. + """ + if a.shape != b.shape: + raise ValueError("Input tensors a and b must have the same shape.") + + num_channels = a.size(1) + + interpolation_tensor = torch.zeros(1, num_channels, 1, 1, device=a.device, dtype=a.dtype) + interpolation_tensor[0, 0, 0, 0] = 1.0 + + result = (1 - t) * a + t * b + + if t < 0.5: + result += (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor + else: + result -= (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor + + return result + blending_modes = { # Args: @@ -121,16 +159,7 @@ blending_modes = { # Interpolates between tensors a and b using normalized linear interpolation, # with a twist when t is greater than or equal to 0.5. - 'hslerp': lambda a, b, t: ( - (1 - t) * a + t * b + - ((torch.norm(b - a, dim=1, keepdim=True) / 6) * torch.tensor([1.0, 0.0, 0.0], device=a.device, dtype=a.dtype) - .unsqueeze(0).unsqueeze(2).unsqueeze(3).expand_as(a)) - if t < 0.5 - else - (1 - t) * a + t * b - - ((torch.norm(b - a, dim=1, keepdim=True) / 6) * torch.tensor([1.0, 0.0, 0.0], device=a.device, dtype=a.dtype) - .unsqueeze(0).unsqueeze(2).unsqueeze(3).expand_as(a)) - ), + 'hslerp': hslerp, # Simulates a glowing effect by applying a formula based on the input tensors a and b, scaled by t. 'glow': lambda a, b, t: torch.where(a <= 1, a ** 2 / (1 - b + 1e-6), b * (a - 1) / (a + 1e-6)), diff --git a/modules/latent_util.py b/modules/latent_util.py index c7caa15..f160aaf 100644 --- a/modules/latent_util.py +++ b/modules/latent_util.py @@ -54,3 +54,67 @@ def latents_to_images(latents, l2rgb=False): tensors = latents.permute(0, 2, 3, 1) return tensors + +def noise_sigma_scale(noise, sigma_min, sigma_max): + """ + Scales the input noise values to a specified sigma range. + + Args: + noise (Tensor): The input noise tensor. + sigma_min (float): The lower bound of the sigma range. + sigma_max (float): The upper bound of the sigma range. + + Returns: + Tensor: The scaled noise tensor within the specified sigma range. + """ + normalized_noise = normalize(noise) + scaled_noise = sigma_min + (sigma_max - sigma_min) * normalized_noise + + return scaled_noise + +def within_percentage_range(num, total, percentage, tolerance_factor): + """ + Check if a number is within a specified percentage of a total accounting for a tolerance. + + Args: + numb (float): The number to check. + total (float): The total value against which to compare. + percentage (float): The desired percentage (between 0 and 1, e.g., 0.5 for 50%). + tolerance_factor (float): The tolerance factor as a percentage (between 0 and 1, e.g., 0.1 for 10%). + + Returns: + bool: True if the number is within the specified range, False otherwise. + """ + lower_bound = total * (percentage - tolerance_factor) + upper_bound = total * (percentage + tolerance_factor) + return lower_bound <= num <= upper_bound + + +def scale_from_perentage(number, total, percentage, tolerance_factor): + """ + Calculate a scaling factor within a percentage tolerance range. + + This function calculates a scaling factor that is 1.0 at the exact percentage value + and scales down towards 0.0 within the tolerance_factor range. + + Args: + number (float): The number to check against the percentage. + total (float): The total value against which the percentage is calculated. + percentage (float): The target percentage value (between 0.0 and 1.0). + tolerance_factor (float): The tolerance factor for the percentage range. + + Returns: + float: A scaling factor that indicates how close the number is to the target percentage + within the specified tolerance range. A value of 1.0 indicates an exact match, + while values closer to 0.0 indicate deviation from the target within the tolerance range. + """ + + lower_bound = percentage - tolerance_factor + upper_bound = percentage + tolerance_factor + + if lower_bound <= (number / total) <= upper_bound: + return 1.0 + if lower_bound <= percentage and (number / total) >= lower_bound: + return (number / (total / 2)) * 0.25 + elif upper_bound >= percentage and (number / total) <= upper_bound: + return (number / (total / 2)) * 0.25 \ No newline at end of file diff --git a/nodes.py b/nodes.py index f842966..af274ea 100644 --- a/nodes.py +++ b/nodes.py @@ -1,13 +1,20 @@ import torch import torch.nn.functional as F +import torchsde +import math import nodes import comfy.samplers import comfy.k_diffusion.sampling +import comfy.model_management + from .modules.latent_util import ( normalize, - latents_to_images + latents_to_images, + noise_sigma_scale, + scale_from_perentage, + within_percentage_range ) from .modules.latent_filters import ( sharpen_latents, @@ -54,6 +61,7 @@ class PPFNoiseNode: }, "optional": { "optional_vae": ("VAE",), + "ppf_settings": ("PPF_SETTINGS",), } } @@ -61,9 +69,24 @@ class PPFNoiseNode: RETURN_NAMES = ("latents","previews") FUNCTION = "power_fractal_latent" - CATEGORY = "Power Noise Suite/Latent/Noise" + CATEGORY = "Power Noise Suite/Noise" - def power_fractal_latent(self, batch_size, width, height, resampling, X, Y, Z, evolution, frame, scale, octaves, persistence, lacunarity, exponent, brightness, contrast, clamp_min, clamp_max, seed, device, optional_vae=None): + def power_fractal_latent(self, batch_size, width, height, resampling, X, Y, Z, evolution, frame, scale, octaves, persistence, lacunarity, exponent, brightness, contrast, clamp_min, clamp_max, seed, device, optional_vae=None, ppf_settings=None): + + if ppf_settings: + ppf = ppf_settings + X = ppf['X'] + Y = ppf['Y'] + Z = ppf['Z'] + evolution = ppf['evolution'] + frame = ppf['frame'] + scale = ppf['scale'] + octaves = ppf['octaves'] + persistence = ppf['persistence'] + lacunarity = ppf['lacunarity'] + exponent = ppf['exponent'] + brightness = ppf['brightness'] + contrast = ppf['contrast'] color_intensity = 1 masking_intensity = 1 @@ -145,7 +168,7 @@ class PPFNPowerLawNoise: RETURN_NAMES = ("latents", "previews") FUNCTION = "power_noise" - CATEGORY = "Power Noise Suite/Latent/Noise" + CATEGORY = "Power Noise Suite/Noise" def power_noise(self, batch_size, width, height, resampling, noise_type, frequency, attenuation, seed, device, optional_vae=None): @@ -201,6 +224,7 @@ class PPFNCrossHatchNode: }, "optional": { "optional_vae": ("VAE",), + "ch_settings": ("CH_SETTINGS",), } } @@ -208,9 +232,21 @@ class PPFNCrossHatchNode: RETURN_NAMES = ("latents","previews") FUNCTION = "cross_hatch" - CATEGORY = "Power Noise Suite/Latent/Noise" + CATEGORY = "Power Noise Suite/Noise" - def cross_hatch(self, batch_size, width, height, resampling, frequency, octaves, persistence, color_tolerance, num_colors, angle_degrees, brightness, contrast, blur, clamp_min, clamp_max, seed, device, optional_vae=None): + def cross_hatch(self, batch_size, width, height, resampling, frequency, octaves, persistence, color_tolerance, num_colors, angle_degrees, brightness, contrast, blur, clamp_min, clamp_max, seed, device, optional_vae=None, ch_settings=None): + + if ch_settings: + ch = ch_settings + frequency = ch['frequency'] + octaves = ch['octaves'] + persistence = ch['persistence'] + color_tolerance = ch['color_tolerance'] + num_colors = ch['num_colors'] + angle_degrees = ch['angle_degrees'] + brightness = ch['brightness'] + contrast = ch['contrast'] + blur = ch['blur'] cross_hatch = CrossHatchPowerFractal(width=width, height=height, frequency=frequency, octaves=octaves, persistence=persistence, num_colors=num_colors, color_tolerance=color_tolerance, angle_degrees=angle_degrees, blur=blur, clamp_min=clamp_min, clamp_max=clamp_max) tensors = cross_hatch(batch_size, device, seed).to(device="cpu") @@ -268,7 +304,7 @@ class PPFNLinearCrossHatchNode: RETURN_NAMES = ("latents","previews") FUNCTION = "cross_hatch" - CATEGORY = "Power Noise Suite/Latent/Noise" + CATEGORY = "Power Noise Suite/Noise" def cross_hatch(self, batch_size, width, height, resampling, frequency, gain, octaves, persistence, add_noise, linear_range, linear_tolerance, angle_degrees, brightness, contrast, seed, device, optional_vae=None): @@ -507,7 +543,7 @@ class PPFNKSamplerAdvanced: pln = PowerLawNoise('cpu') noise_types = pln.get_noise_types() noise_types.append('vanilla_comfy') - samplers = [x for x in comfy.samplers.KSampler.SAMPLERS if x in ['euler_ancestral', 'dpm_2_ancestral', 'dpmpp_2s_ancestral', 'dpm_fast', 'dpm_adaptive']] + samplers = ['dpmpp_sde', 'dpmpp_sde_gpu', 'dpmpp_2m', 'dpmpp_2m_sde', 'dpmpp_2m_sde_gpu', 'dpmpp_3m_sde', 'dpmpp_3m_sde_gpu', 'euler_ancestral', 'dpm_2_ancestral', 'dpmpp_2s_ancestral', 'dpm_fast', 'dpm_adaptive'] return { "required": { "model": ("MODEL",), @@ -526,9 +562,12 @@ class PPFNKSamplerAdvanced: }, "optional": { "noise_type": (noise_types,), - "frequency": ("FLOAT", {"default": 3.141592653589793, "max": 1024.0, "min": 0.001, "step": 0.001}), + "noise_blending": (["bislerp", "cosine interp", "cuberp", "hslerp", "lerp", "add", "inject"],), + "noise_mode": (["additive", "subtractive"],), + "frequency": ("FLOAT", {"default": 64, "max": 1024.0, "min": 0.001, "step": 0.001}), "attenuation": ("FLOAT", {"default": 0.75, "max": 1024.0, "min": 0.001, "step": 0.001}), "sigma_tolerance": ("FLOAT", {"default": 0.5, "max": 1.0, "min": 0.0, "step": 0.001}), + "boost_leading_sigma": (["false", "true"],), "ppf_settings": ("PPF_SETTINGS",), "ch_settings": ("CH_SETTINGS",), } @@ -539,12 +578,13 @@ class PPFNKSamplerAdvanced: CATEGORY = "Power Noise Suite/Sampling" - def sample(self, model, add_noise, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise=1.0, noise_type='grey', frequency=11.6, attenuation=0.75, sigma_tolerance=1.0, ppf_settings=None, ch_settings=None): - - def ppfn_noise_sampler(x): + def sample(self, model, add_noise, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise=1.0, noise_type='grey', noise_blending="bislerp", noise_mode="additive", frequency=11.6, attenuation=0.75, sigma_tolerance=1.0, boost_leading_sigma="false", ppf_settings=None, ch_settings=None): + + # WHITE-NOISE SAMPLER HIJACK + + def pns_noise_sampler(x): seed_base = seed noise_idx = [0] - sigma_base = [None] height = int(x.shape[2] * 8) width = int(x.shape[3] * 8) method = noise_type @@ -553,8 +593,12 @@ class PPFNKSamplerAdvanced: sigma_tol = sigma_tolerance ppfs = ppf_settings chs = ch_settings + total_steps = steps + blending_mode = noise_blending + blend_type = noise_mode + boost_sigma = (boost_leading_sigma == "true") - def ppfn_return_noise(seed, x, sigma, sigma_tol, method, freq, atten, blending_modes, ppfs, chs): + def pns_return_noise(seed, x, sigma, sigma_tol, boost_sigma, total_steps, method, freq, atten, blending_modes, blending_mode, ppfs, chs): seed = seed_base + noise_idx[0] rand_noise = torch.randn_like(x) @@ -563,7 +607,17 @@ class PPFNKSamplerAdvanced: sigma_min = 0 sigma_max = 14.614643096923828 - scaled_sigma = ((sigma.item() - sigma_min) / (sigma_max - sigma_min)) * sigma_tol + if isinstance(sigma, torch.Tensor) and sigma.numel() == 1: + sigma = sigma.item() + elif isinstance(sigma, float): + sigma = sigma + else: + sigma = 0 + + scaled_sigma = ((sigma - sigma_min) / (sigma_max - sigma_min)) * sigma_tol + + if boost_sigma and noise_idx[0] < (total_steps // 4): + scaled_sigma = scaled_sigma * 1.25 if scaled_sigma * 1.25 <= 1.0 else 1.0 if not ppfs and not chs: power_law = PowerLawNoise(device=rand_noise.device) @@ -583,30 +637,133 @@ class PPFNKSamplerAdvanced: alpha = torch.ones((1, x.shape[2], x.shape[3], 1), dtype=x.dtype, device=x.device).permute(0, 3, 1, 2) noise = torch.cat((noise, alpha), dim=1) - blended_noise = rand_noise + 0.25 * (blending_modes["bislerp"](rand_noise.to(device=rand_noise.device), noise.to(device=rand_noise.device), scaled_sigma) - rand_noise) - + if blend_type == "additive": + blended_noise = rand_noise + 0.25 * (blending_modes[blending_mode](rand_noise.to(device=rand_noise.device), noise.to(device=rand_noise.device), scaled_sigma) - rand_noise) + else: + blended_noise = rand_noise - 0.25 * (blending_modes[blending_mode](rand_noise.to(device=rand_noise.device), noise.to(device=rand_noise.device), scaled_sigma) - rand_noise) + noise_idx[0] += 1 return blended_noise - return lambda sigma, sigma_next, **kwargs: ppfn_return_noise(seed_base + noise_idx[0], x, sigma, sigma_tol, method, freq, atten, blending_modes, ppfs, chs) + return lambda sigma, sigma_next, **kwargs: pns_return_noise(seed_base + noise_idx[0], x, sigma, sigma_tol, boost_sigma, total_steps, method, freq, atten, blending_modes, blending_mode, ppfs, chs) + # BROWNIAN NOISE SAMPLER HIJACK + + class PNSNoiseSampler: + + seed_base = seed + noise_idx = [0] + method = noise_type + freq = frequency + atten = attenuation + sigma_tol = sigma_tolerance + ppfs = ppf_settings + chs = ch_settings + total_steps = steps + blending_mode = noise_blending + blend_type = noise_mode + boost_sigma = (boost_leading_sigma == "true") + + def __init__(self, x, sigma_min, sigma_max, seed=None, transform=lambda x: x, cpu=False): + self.noise_idx = [0] + self.x = x + self.height = int(x.shape[2] * 8) + self.width = int(x.shape[3] * 8) + self.sigma_min = sigma_min + self.sigma_max = sigma_max + self.transform = transform + t0, t1 = self.transform(torch.as_tensor(sigma_min)), self.transform(torch.as_tensor(sigma_max)) + self.tree = comfy.k_diffusion.sampling.BatchedBrownianTree(x, t0, t1, seed, cpu=cpu) + + def __call__(self, sigma, sigma_next): + noise = self.sample_noise(self.x, sigma, sigma_next) + return noise + + def sample_noise(self, x, sigma, sigma_next): + t0, t1 = self.transform(torch.as_tensor(sigma)), self.transform(torch.as_tensor(sigma_next)) + tree = self.tree(t0, t1) / (t1 - t0).abs().sqrt() + + seed = self.seed_base + self.noise_idx[0] + rand_noise = torch.randn_like(x) + + if self.sigma_tol == 0.0: + return tree + + sigma_min = self.sigma_min + sigma_max = self.sigma_max / 2 # 14.614643096923828 + if isinstance(sigma, torch.Tensor) and sigma.numel() == 1: + sigma = sigma.item() + elif isinstance(sigma, float): + sigma = sigma + else: + sigma = 0 + + scaled_sigma = (((sigma - sigma_min) / (sigma_max - sigma_min)) * self.sigma_tol) / 2 + + if self.boost_sigma and self.noise_idx[0] < (self.total_steps // 4): + scaled_sigma = scaled_sigma * 1.25 if scaled_sigma * 1.25 <= 1.0 else 1.0 + + ppfs = self.ppfs + chs = self.chs + + if not ppfs and not chs: + power_law = PowerLawNoise(device=tree.device) + noise = power_law(1, self.width, self.height, noise_type=self.method, frequency=self.freq, attenuation=self.atten, seed=seed) + elif ppfs: + power_fractal = PPFNoiseNode() + noise = power_fractal.power_fractal_latent(1, self.width, self.height, 'nearest', ppfs['X'], ppfs['Y'], ppfs['Z'], ppfs['evolution'], ppfs['frame'], ppfs['scale'], ppfs['octaves'], ppfs['persistence'], ppfs['lacunarity'], ppfs['exponent'], ppfs['brightness'], ppfs['contrast'], 0.0, 1.0, seed, device=('cuda' if torch.cuda.is_available() else 'cpu'), optional_vae=None)[0]['samples'].to(device=tree.device) + elif chs: + ch_fractal = PPFNCrossHatchNode() + noise = ch_fractal.cross_hatch(1, self.width, self.height, 'nearest', chs['frequency'], chs['octaves'], chs['persistence'], chs['color_tolerance'], chs['num_colors'], chs['angle_degrees'], chs['brightness'], chs['contrast'], chs['blur'], 0.0, 1.0, seed, device=('cuda' if torch.cuda.is_available() else 'cpu'), optional_vae=None)[0]['samples'].to(device=tree.device) + + noise = noise_sigma_scale(noise, self.sigma_min, self.sigma_max) + + noise = noise.permute(0, 3, 1, 2) + noise = F.interpolate(noise, size=(x.shape[2], x.shape[3]), mode='nearest') + noise = noise[:, :tree.shape[1], :, :] + + if not ppfs and not chs: + alpha = torch.ones((1, x.shape[2], x.shape[3], 1), dtype=x.dtype, device=x.device).permute(0, 3, 1, 2) + noise = torch.cat((noise, alpha), dim=1) + + if self.blend_type == "additive": + blended_noise = tree + 0.025 * (blending_modes[self.blending_mode](tree.to(device=tree.device), sharpen_latents(noise.to(device=tree.device), 1.5), scaled_sigma) - tree) + else: + blended_noise = tree - 0.025 * (blending_modes[self.blending_mode](tree.to(device=tree.device), sharpen_latents(noise.to(device=tree.device), 1.5), scaled_sigma) - tree) + + self.noise_idx[0] += 1 + + return blended_noise force_full_denoise = True if return_with_leftover_noise == "enable": force_full_denoise = False disable_noise = False + if add_noise == "disable": disable_noise = True else: if noise_type != "vanilla_comfy": + print("Running with PNS Noise Samplers") dns = comfy.k_diffusion.sampling.default_noise_sampler - comfy.k_diffusion.sampling.default_noise_sampler = ppfn_noise_sampler + btns = comfy.k_diffusion.sampling.BrownianTreeNoiseSampler + comfy.k_diffusion.sampling.default_noise_sampler = pns_noise_sampler + comfy.k_diffusion.sampling.BrownianTreeNoiseSampler = PNSNoiseSampler - result = nodes.common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise) - + try: + result = nodes.common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise) + except comfy.model_management.InterruptProcessingException as e: + if noise_type != "vanilla_comfy": + print("Restoring ComfyUI Noise Samplers.") + comfy.k_diffusion.sampling.default_noise_sampler = dns + comfy.k_diffusion.sampling.BrownianTreeNoiseSampler = btns + raise e + if noise_type != "vanilla_comfy": + print("\nRestoring ComfyUI Noise Samplers") comfy.k_diffusion.sampling.default_noise_sampler = dns + comfy.k_diffusion.sampling.BrownianTreeNoiseSampler = btns return result