Add noise blending type and operation used.
Add support for Brownian noise samplers
Sharpen added noise a wee-bit near the end for possibly better high frequency details
This commit is contained in:
Jordan Thompson
2023-09-12 20:40:06 -07:00
parent 01bfa8ab4e
commit 1162ab400f
4 changed files with 293 additions and 39 deletions
+4
View File
@@ -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
+47 -18
View File
@@ -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)),
+64
View File
@@ -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
+178 -21
View File
@@ -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