Author SHA1 Message Date
blepping 3a1f24822d Update documentation for NoisyLatentLike changes + general improvements 2024-03-20 18:34:33 -06:00
blepping 52bab99ba8 Improve NoisyLatentLike to allow calculating strength with sigmas and noise injection 2024-03-20 13:00:18 -06:00
blepping 1ee8273771 Update README and changelog for SonarPowerNoise 2024-03-20 07:57:43 -06:00
elias-gaeros 52f929d54d SonarPowerNoise (#2)
* SonarPowerNoise: WIP

* don't allow highpass > lowpass

* fix filter unit gain

The filter was computed as an amplitude gain map. This change computes energy
gains instead. This makes easier to ensure unit-gain and makes the alpha values
more in line with literature where the power spectrum is obeying the power law.
new alpha = old alpha / 2

Also fixes the gain of common_mode.

* allow stretch < 1.0, paramter range fixes

* rename lowpass/highpass to min_freq/max_freq

* remove torch.no_grad wrappers

* add previews

* static seed for previews

* README: SonarPowerNoise documentation
2024-03-14 16:04:35 -06:00
blepping 090df2280d Make custom noise more extensible (internal change) 2024-03-02 04:48:54 -07:00
blepping a2fd7118b5 Noise refactor (#1)
* Refactor noise: stage 1

* Refactor noise: stage 2

* Refactor noise: stage 3

* Refactor noise: stage 4

* Update documentation and changelog
2024-02-27 02:42:19 -07:00
7 changed files with 549 additions and 46 deletions
+81 -9
View File
@@ -24,17 +24,83 @@ You can also just choose `sonar_euler`, `sonar_euler_ancestral` or `sonar_dpmpp_
## Nodes
* `SamplerSonarEuler` — Custom sampler node that combines Euler sampling and momentum and optionally guidance. A bit boring compared to the ancestral version but it has predictability going for it. You can possibly try setting init type to `RAND` and using different noise types, however this sampler seems _very_ sensitive to that init type. You may want to set direction to a very low value like `0.05` or `-0.15` when using the `RAND` init type. Setting `momentum=1` is the same as disabling momentum, so this sampler with `momentum=1` is basically the same as the basic `euler` sampler.
* `SamplerSonarEulerAncestral` — Ancestral version of the above. Same features, just with ancestral Euler.
* `SonarGuidanceConfig` — You can optionally plug this into the Sonar sampler nodes. See the [Guidance](#guidance) section below.
* `NoisyLatentLike` — If you give it a latent (or latent batch) it'll return a noisy latent of the same shape. Allows specifying all the custom noise types except `brownian` which has some special requirements. Provided just because the noise generation functions are conveniently available. You can also use this as a reference latent with `SonarGuidanceConfig` node and depending on the strength it can act like variation seed (you'd change the seed in the `NoisyLatentLike` node). *Note*: The seed stuff may or may not work correctly.
* `SamplerSonarDPMPPSDE` — This one is extra experimental but it is an attempt to add moment and guidance to the DPM++ SDE sampler. It may not work correctly but you can sample stuff with it and get interesting results. I actually really like this one, and you can get away with more extreme stuff like `green_test` noise and still produce reasonable results. You may want to use the `BlehDiscardPenultimateSigma` node from my [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) collection if you find the result seems a bit washed out and blurry.
* `SamplerConfigOverride` — can be used to override configuration settings for other samplers, including the noise type. For example, you could force `euler_ancestral` to use a different noise type. It's also possible to override other settings like `s_noise`, etc. *Note*: The wrapper inspects the sampling function's arguments to see what it supports, so you should connect the sampler directly to this rather than having other nodes (like a different sampler wrapper) in between.
* `SonarCustomNoise` — See the [Noise](#noise) section below.
### `SamplerSonarEuler`
*Note*: `NoisyLatentLike` and `SamplerConfigOverride` are candidates for moving to a different project. They're just here at the moment because the noise generation functions are readily available.
Custom sampler node that combines Euler sampling and momentum and optionally guidance. A bit boring compared to the ancestral version but it has predictability going for it. You can possibly try setting init type to `RAND` and using different noise types, however this sampler seems _very_ sensitive to that init type. You may want to set direction to a very low value like `0.05` or `-0.15` when using the `RAND` init type. Setting `momentum=1` is the same as disabling momentum, so this sampler with `momentum=1` is basically the same as the basic `euler` sampler.
## Parameters
### `SamplerSonarEulerAncestral`
Ancestral version of the above. Same features, just with ancestral Euler.
### `SamplerSonarDPMPPSDE`
Attempt to add momentum and guidance to the DPM++ SDE sampler. It may not work correctly but you can sample stuff with it and get interesting results. I actually really like this one, and you can get away with more extreme stuff like `green_test` noise and still produce reasonable results. You may want to use the `BlehDiscardPenultimateSigma` node from my [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) collection if you find the result seems a bit washed out and blurry.
### `SonarGuidanceConfig`
You can optionally plug this into the Sonar sampler nodes. See the [Guidance](#guidance) section below.
### `NoisyLatentLike`
This node takes a reference latent and generates noise of the same shape. The one required input is `latent`.
You can connect a `SonarCustomNoise` or `SonerPowerNoise` node to the `custom_noise_opt` input: if that is attached, the built in noise type selector is ignored. The generated noise will be multiplied by the `multiplier` value. Note that you cannot use `brownian` noise whether specified directly or via custom noise nodes.
The node has two main modes: simply generate and scale the noise by the multiplier and return or add it to the input latent. In this mode, you don't connect anything to the `mul_by_sigmas_opt` or `model_opt` inputs and you would use other nodes to calculate the correct strength.
In the second mode you must connect sigmas (for example from a `BasicScheduler` node) to the `mul_by_sigmas_opt` input and connect a model to the `model_opt` input. It will calculate the strength based on the first item in the list of sigmas (so you could use something like a `SplitSigmas` node to slice them as needed). Note that `multiplier` still applies: the calculated strength will be scaled by it. This second mode is generally this is the most convenient way to use the node since the two main uses cases are: making a latent with initial noise or adding noise to a latent (for img2img type stuff).
If you want to create noise for initial sampling, connect model and sigmas to the node, connect an empty latent (or one of the appropriate size) to it and that is basically all you need to do (aside from configuring the noise types). For img2img (upscaling, etc), either slice the sigmas at the appropriate or set a denoise in something like the `BasicScheduler` node. **Note**: You also need to turn on the `add_to_latent` toggle. Turning this on doesn't matter for initial noise since an empty latent is all zeros.
### `SamplerConfigOverride`
can be used to override configuration settings for other samplers, including the noise type. For example, you could force `euler_ancestral` to use a different noise type. It's also possible to override other settings like `s_noise`, etc. *Note*: The wrapper inspects the sampling function's arguments to see what it supports, so you should connect the sampler directly to this rather than having other nodes (like a different sampler wrapper) in between.
**Note**: If you are using this with Sonar samplers, make sure you set the noise type in the sampler to `gaussian` as the Sonar samplers only allow overriding noise types in that case.
### `SonarCustomNoise`
See the [Noise](#noise) section below for information on noise types.
### `SonarPowerNoise`
This node generates [fractional Brownian motion (fBm) noise](https://en.wikipedia.org/wiki/Fractional_Brownian_motion#Frequency-domain_interpretation). It offers versatility in producing various types of noise including gaussian, pink, 2D brownian noise, and all intermediates.
By default, the node generates normal gaussian noise.
<details>
<summary>Expand detailed explanation</summary>
Here's an overview of its parameters:
- `factor` and `rescale` operate similarly to `SonarCustomNoise`, enabling the addition of multiple sources of noises.
- `time_brownian` introduces correlation across sampler timesteps for SDE solvers.
- `alpha` is the main parameter. `alpha > 0` amplifies low frequencies; `alpha = 1` yields pink noise, and `alpha = 2` produces brownian noise. Conversely, for `alpha < 0`, it amplifies high frequencies.
- `min_freq` and `max_freq` determine the range of frequencies allowed through. Setting `max_freq = `$\sqrt{1/2} \simeq 0.7071$ enables the passage of the highest frequencies. In cases where `alpha < 0`, setting `max_freq = 0.5` is advisable to diminish the power of diagonally oriented frequencies.
- `stretch`, `rotate`, and `pnorm` alter the filter's shape by stretching, rotating, or cushioning the band-pass region.
- Lowering `mix` moderates the filter's effect by blending back unfiltered gaussian noise from the same sample.
- `common_mode` is an attempt to desaturate the latent by injecting the average across channels into every latent channel. However, this may result in a specific color due to the encoding of the unit vector by the latent space. Note that this is done _after_ the `mix`ing of unfiltered gaussian noise.
- Enabling `preview` provides a visual representation of the filter. `no_mix` sets `mix = 1` for the preview. The preview includes, from left to right:
- Fourier domain visualization: Low frequencies at the center, with black indicating filtered-out frequencies.
- Spatial visualization of the 2D kernel: The filtering can be interpreted as convolution with the displayed kernel.
- Sample: Gaussian sample with shaped frequency spectrum. A single latent channel will look like this.
**Frequency-domain Interpretation**: The Fourier transform decomposes a 2D latent into sinusoids covering all spatial orientations and frequencies. For an independent and identically distributed gaussian sample, energy is evenly distributed across all frequencies and orientations. Scaling the power spectrum by $1 / f^\alpha$, where $\alpha>0$, boosts low frequencies, introducing spatial correlations.
**Spatial Domain Interpretation**: A gaussian latent sample comprises independently sampled pixels, exhibiting no spatial correlations. Conversely, a requirement that each pixel value differs from its neighbors by a $\epsilon \sim \mathcal{N}(0, 1)$ results in 2D brownian noise ($\alpha=2$).
**Seed Considerations**: While the node defaults to outputting gaussian noise, a given seed produce a different sample than the one produced by other gaussian noise sources. This stems from sampling the noise directly in the frequency domain to avoid the cost of a FFT. When `time_brownian = true`, noise sampling occurs in the spatial domain, ensuring that default parameters yield output equivalent to `SonarCustomNoise` set to `brownian`.
</details>
From a usage perspective, using positive alpha will tend to create a colorful effect, using negative alpha will create line/streak like artifacts sort of like an oil painting canvas. Start with small values at first (`-0.1`, `0.1`) and adjust as necessary. `time_brownian` makes the effect of power noise (and alpha) stronger - also note that it can only be used when sampling and not for `NoisyLatentLike`. Setting `common_mode` also generally seems to intensify these effects. Different types of models (normal EPS models, v-prediction models, SDXL) generally react differently to these exotic noise types so my advice is to experiment! Lowering `mix` uses normal gaussian noise for part of the generated noise. For example, `mix=1.0` means 100% power noise, `mix=0.5` means 50/50 power noise and normal gaussian noise. This also is about the same as setting factor to `0.5` and plugging in a `SonarCustomNoise` node with factor at `0.5` also and the type set to `guassian`.
Noise from the `SonarCustomNoise` node and `SonarPowerNoise` can be freely mixed.
## Sonar Sampler Parameters
Very abbreviated section. The init type can make a big difference. If you use `RANDOM` you can get away with setting `direction` to high values (like up to `2.25` or so) and absurdly low values (like `-30.0`). It's also possible to set `momentum` and `momentum_hist` to negative values, although whether it's a good idea...
@@ -70,6 +136,8 @@ The sampler and `NoisyLatentLike` nodes now take an optional `SonarCustomNoise`
**Note**: If you connect the optional `SonarCustomNoise` node to a Sonar sampler, the `NoisyLatentLike` node or the `SamplerConfigOverride` node, it will override the noise type selected in the node.
## Related
I also have some other ComfyUI nodes here: https://github.com/blepping/ComfyUI-bleh/
@@ -82,8 +150,12 @@ My version basically just rips off this Sonar sampler implementation for Diffuse
Noise generation functions copied from https://github.com/Clybius/ComfyUI-Extra-Samplers with only minor modifications. I may have broken some of them in the process _or_ they may not have been suitable for use and I took them anyway. If they don't work it is not a reflection on the original source.
`SonarPowerNoise` contributed by [elias-gaeros](https://github.com/elias-gaeros/). Thanks!
## Examples
Unfortunately, right now these examples are somewhat incomplete and out of date. I hope to update them when I get the time.
### Guidance
<details>
+2 -1
View File
@@ -1,4 +1,4 @@
from .py import nodes, sonar
from .py import nodes, powernoise, sonar
sonar.add_samplers()
@@ -9,6 +9,7 @@ NODE_CLASS_MAPPINGS = {
"SamplerConfigOverride": nodes.SamplerNodeConfigOverride,
"NoisyLatentLike": nodes.NoisyLatentLikeNode,
"SonarCustomNoise": nodes.SonarCustomNoiseNode,
"SonarPowerNoise": powernoise.SonarPowerNoiseNode,
"SonarGuidanceConfig": nodes.GuidanceConfigNode,
}
+8
View File
@@ -2,6 +2,14 @@
Note, only relatively significant changes to user-visible functionality will be included here. Most recent changes at the top.
## 20240320
* `NoisyLatentLike` node improved to allow calculating strength with sigmas and injecting noise itself.
## 20240314
* `SonarPowerNoise` node added.
## 20240227
* Refactored noise generation functions (will break seeds).
+71 -16
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import abc
import inspect
from typing import Any, Callable
@@ -32,9 +33,13 @@ class NoisyLatentLikeNode:
),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
"latent": ("LATENT",),
"multiplier": ("FLOAT", {"default": 1.0}),
"add_to_latent": ("BOOLEAN", {"default": False}),
},
"optional": {
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
"mul_by_sigmas_opt": ("SIGMAS",),
"model_opt": ("MODEL",),
},
}
@@ -45,17 +50,41 @@ class NoisyLatentLikeNode:
def go(
self,
noise_type,
seed,
latent,
custom_noise_opt=None,
noise_type: str,
seed: None | int,
latent: dict,
multiplier: float = 1.0,
add_to_latent=False,
custom_noise_opt: object | None = None,
mul_by_sigmas_opt: None | torch.Tensor = None,
model_opt: object | None = None,
):
model, sigmas = model_opt, mul_by_sigmas_opt
if sigmas is not None and len(sigmas) > 0:
if model is None:
raise ValueError(
"NoisyLatentLike requires a model when sigmas are connected!",
)
while hasattr(model, "model"):
model = model.model
latent_scale_factor = model.latent_format.scale_factor
max_denoise = samplers.Sampler().max_denoise(
samplers.wrap_model(model),
sigmas,
)
multiplier *= (
float(
torch.sqrt(1.0 + sigmas[0] ** 2.0) if max_denoise else sigmas[0],
)
/ latent_scale_factor
)
latent_samples = latent["samples"]
if custom_noise_opt is not None:
ns = custom_noise_opt.make_noise_sampler(latent["samples"])
ns = custom_noise_opt.make_noise_sampler(latent_samples)
else:
ns = noise.get_noise_sampler(
noise.NoiseType[noise_type.upper()],
latent["samples"],
latent_samples,
None,
None,
seed=seed,
@@ -67,10 +96,18 @@ class NoisyLatentLikeNode:
result = ns(None, None)
finally:
torch.random.set_rng_state(randst)
if multiplier != 1.0:
result *= multiplier
if add_to_latent:
result += latent_samples.to(result.device)
return ({"samples": result},)
class SonarCustomNoiseNode:
class SonarCustomNoiseNodeBase(abc.ABC):
@abc.abstractmethod
def get_item_class(self):
raise NotImplementedError
@classmethod
def INPUT_TYPES(cls):
return {
@@ -95,13 +132,6 @@ class SonarCustomNoiseNode:
"round": False,
},
),
"noise_type": (
tuple(
t.name.lower()
for t in noise.NoiseType
if t is not noise.NoiseType.BROWNIAN
),
),
},
"optional": {
"sonar_custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
@@ -112,17 +142,42 @@ class SonarCustomNoiseNode:
CATEGORY = "advanced/noise"
FUNCTION = "go"
def go(self, factor, rescale, noise_type, sonar_custom_noise_opt=None):
def go(
self,
factor,
rescale,
sonar_custom_noise_opt=None,
**kwargs: dict[str, Any],
):
nis = (
sonar_custom_noise_opt.clone()
if sonar_custom_noise_opt
else noise.CustomNoiseChain()
)
if factor != 0:
nis.add(noise.CustomNoiseItem(factor, noise_type))
nis.add(self.get_item_class()(factor, **kwargs))
return (nis if rescale == 0 else nis.rescaled(rescale),)
class SonarCustomNoiseNode(SonarCustomNoiseNodeBase):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"noise_type": (
tuple(
t.name.lower()
for t in noise.NoiseType
if t is not noise.NoiseType.BROWNIAN
),
),
}
return result
def get_item_class(self):
return noise.CustomNoiseItem
class GuidanceConfigNode:
@classmethod
def INPUT_TYPES(cls):
+53 -20
View File
@@ -1,6 +1,7 @@
# Noise generation functions shamelessly yoinked from https://github.com/Clybius/ComfyUI-Extra-Samplers
from __future__ import annotations
import abc
import functools as fun
import math
import operator as op
@@ -16,7 +17,6 @@ from torch import FloatTensor, Generator, Tensor
def scale_noise(noise, factor=1.0):
mean, std = noise.mean(), noise.std()
# print(f"NOISE * {factor:.3}: std={std:.3}, mean={mean:.3}")
return (noise - mean).div_(std).mul_(factor)
@@ -42,10 +42,56 @@ class NoiseError(Exception):
pass
class CustomNoiseItem:
def __init__(self, factor, noise_type):
class CustomNoiseItemBase(abc.ABC):
def __init__(self, factor, **kwargs):
self.factor = factor
self.noise_type = noise_type
self.keys = set(kwargs.keys())
for k, v in kwargs.items():
setattr(self, k, v)
def clone(self):
return self.__class__(self.factor, **{k: getattr(self, k) for k in self.keys})
def set_factor(self, factor):
self.factor = factor
return self
@abc.abstractmethod
def make_noise_sampler(
self,
x: Tensor,
sigma_min=None,
sigma_max=None,
seed=None,
cpu=True,
):
raise NotImplementedError
class CustomNoiseItem(CustomNoiseItemBase):
def __init__(self, factor, **kwargs):
super().__init__(factor, **kwargs)
if getattr(self, "noise_type", None) is None:
raise ValueError("Noise type required!")
@torch.no_grad()
def make_noise_sampler(
self,
x: Tensor,
sigma_min=None,
sigma_max=None,
seed=None,
cpu=True,
):
return get_noise_sampler(
self.noise_type,
x,
sigma_min,
sigma_max,
seed=seed,
cpu=cpu,
factor=self.factor,
)
class CustomNoiseChain:
@@ -54,7 +100,7 @@ class CustomNoiseChain:
def clone(self):
return CustomNoiseChain(
[CustomNoiseItem(i.factor, i.noise_type) for i in self.items],
[i.clone() for i in self.items],
)
def add(self, item):
@@ -65,20 +111,9 @@ class CustomNoiseChain:
divisor = total / scale
divisor = divisor if divisor != 0 else 1.0
return CustomNoiseChain(
[CustomNoiseItem(i.factor / divisor, i.noise_type) for i in self.items],
[i.clone().set_factor(i.factor / divisor) for i in self.items],
)
def __call__(
self,
x: Tensor,
sigma_min=None,
sigma_max=None,
seed=None,
transform=lambda x: x,
cpu=True,
):
pass
@torch.no_grad()
def make_noise_sampler(
self,
@@ -89,14 +124,12 @@ class CustomNoiseChain:
cpu=True,
) -> Callable:
noise_samplers = tuple(
get_noise_sampler(
i.noise_type,
i.make_noise_sampler(
x,
sigma_min,
sigma_max,
seed=seed,
cpu=cpu,
factor=i.factor,
)
for i in self.items
)
+333
View File
@@ -0,0 +1,333 @@
from __future__ import annotations
import math
import os
import random
import folder_paths
import torch
from comfy.k_diffusion.sampling import BrownianTreeNoiseSampler
from PIL import Image
from torch import Tensor
from .nodes import SonarCustomNoiseNodeBase
from .noise import CustomNoiseItemBase
# ruff: noqa: ANN003, FBT001, FBT002
class PowerNoiseItem(CustomNoiseItemBase):
def __init__(self, factor, **kwargs):
super().__init__(factor, **kwargs)
self.max_freq = max(self.max_freq, self.min_freq)
def make_filter(self, shape, oversample=4, rel_bw=0.125):
"""Construct a band-pass * 1/f^alpha filter in rfft space."""
height, width = shape[-2:]
hfreq_bins = width // 2 + 1
# Flat unit gain frequency response
if self.mix < 1.0:
flat = torch.ones(1, 1, height, hfreq_bins)
if self.mix <= 0.0:
return flat
# Start with an over-sampled fftshift(rfft2freq()) grid. uses complex
# numbers for convenient 2d rotation (unrelated to the fft complex phase
# space)
fc = torch.complex(
# real-fftfreq
torch.linspace(0, 0.5, oversample * hfreq_bins),
# normal fftfreq
torch.linspace(
-(height // 2) / height,
((height - 1) // 2) / height,
oversample * height,
).unsqueeze(1),
)
# Rotate, stretch and p-norm
if abs(self.rotate) >= 1e-3:
fc *= torch.exp(1.0j * torch.deg2rad(torch.scalar_tensor(self.rotate)))
if self.stretch > 1.0:
fc.real *= self.stretch
else:
fc.imag *= 1.0 / self.stretch
if abs(self.pnorm - 2.0) < 1e-3:
d = fc.abs()
else:
d = (
torch.view_as_real(fc)
.abs()
.pow(self.pnorm)
.sum(-1)
.pow(1.0 / self.pnorm)
)
# filter gain function
op = torch.empty_like(d)
m_highpass = d >= self.min_freq
m_lowpass = d < self.max_freq
m_band = m_highpass & m_lowpass
# 1 / f^alpha for the band-pass region
op[m_band] = d[m_band].pow(-self.alpha)
# easing gaussian (TODO: try cosine windows)
m_lowpass = ~m_lowpass
op[m_lowpass] = math.pow(self.max_freq, -self.alpha) * torch.exp(
-(d[m_lowpass] - self.max_freq).square() / (rel_bw * self.max_freq) ** 2,
)
if self.min_freq > 0.0:
m_highpass = ~m_highpass
op[m_highpass] = math.pow(self.min_freq, -self.alpha) * torch.exp(
-(d[m_highpass] - self.min_freq).square()
/ (rel_bw * self.min_freq) ** 2,
)
op = torch.nn.functional.interpolate(
op[None, None, ...],
(height, hfreq_bins),
mode="bilinear",
align_corners=True,
)
op = op.roll(-(height // 2), -2) # ifftshift
if self.alpha > 0:
# In general, the mean offset should be kept as is, sampled from
# N(0, 1 / sqrt(H*W) ). However, gain goes to inf when alpha>0.
op[..., 0, 0] = 0
# Scale to unit power gain, then mix flat filter
mean_pow_gain = op.mean()
if mean_pow_gain <= 0.0:
# don't fail catastrophically when something broke
return flat
op *= 1.0 / mean_pow_gain
if self.mix < 1.0:
op = torch.lerp(flat, op, self.mix, out=op)
return op.sqrt_()
def make_noise_sampler(
self,
x: Tensor,
sigma_min: float | None,
sigma_max: float | None,
seed: int | None,
cpu: bool = True,
):
shape = x.shape
device = x.device
time_brownian = self.time_brownian
if self.time_brownian:
if sigma_min is None:
raise ValueError(
"time correlated brownian mode is valid only for stochastic samplers",
)
brownian_tree = BrownianTreeNoiseSampler(
x,
sigma_min,
sigma_max,
seed=seed,
cpu=cpu,
)
common_mode = self.common_mode
if common_mode > 0.0:
b, c, h, w = shape
torch.eye(c, c)
channel_mixer = torch.lerp(
torch.eye(c, c),
torch.ones(c, c) / c,
common_mode,
)
channel_mixer = channel_mixer.sqrt().to(device, non_blocking=True)
filter_rfft = self.make_filter(shape).to(device, non_blocking=True)
def sampler(sigma, sigma_next):
if time_brownian:
noise = brownian_tree(sigma, sigma_next).to(device)
noise_rfft = torch.fft.rfft2(noise, norm="ortho")
else:
noise_rfft = torch.randn(
(*shape[:-1], filter_rfft.shape[-1]),
dtype=torch.complex64,
device=device,
)
noise = torch.fft.irfft2(
noise_rfft.mul_(filter_rfft),
s=shape[-2:],
norm="ortho",
)
if common_mode > 0.0:
noise = channel_mixer @ noise.swapaxes(0, 1).reshape(c, -1)
noise = noise.reshape(c, b, h, w).swapaxes(1, 0)
return noise.mul_(self.factor)
return sampler
def preview(self, size=(128, 128)):
filter_rfft = self.make_filter(size, oversample=1)
filter_fft = rfft2_to_fft2(filter_rfft)
noise = torch.fft.irfft2(
filter_rfft
* torch.randn(
filter_rfft.shape,
dtype=torch.complex64,
generator=torch.Generator().manual_seed(0),
),
s=size,
norm="ortho",
)
kernel = torch.fft.irfft2(filter_rfft, s=size, norm="ortho")
kernel = kernel.roll((size[0] // 2, size[1] // 2), (-2, -1))
img = (
torch.cat(
[
filter_fft.mul_(1 / 3).tanh_().mul_(256.0),
kernel.mul_(1 / 3).tanh_().add_(1.0).mul_(128.0),
noise.mul_(1 / 3).tanh_().add_(1.0).mul_(128.0),
],
dim=-1,
)
.clamp(0, 255)
.to(torch.uint8)
)
return Image.fromarray(img[0, 0].numpy())
def rfft2_to_fft2(x):
"""Apply hermitian-summetry to reconstruct the second half of a fft.
Only for previews.
"""
height, width = x.shape[-2:]
x_r = x.roll(height // 2, -2) # torch.fft.fftshift(x, -2)
x_l = x_r[..., 1 : -1 if width & 1 else None]
x_l = torch.flip(x_l.conj(), dims=(-2, -1))
if height & 1 == 0:
x_l = x_l.roll(1, -2)
return torch.cat([x_l, x_r], dim=-1)
class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"time_brownian": ("BOOLEAN", {"default": False}),
"alpha": (
"FLOAT",
{
"default": 0.0,
"min": -5.0,
"max": 5.0,
"step": 0.001,
"round": False,
},
),
"max_freq": (
"FLOAT",
{
"default": 0.7071,
"min": 0.0,
"max": 0.7071,
"step": 0.001,
"round": False,
},
),
"min_freq": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 0.7071,
"step": 0.001,
"round": False,
},
),
"stretch": (
"FLOAT",
{
"default": 1.0,
"min": 0.01,
"max": 100,
"step": 0.1,
"round": False,
},
),
"rotate": (
"FLOAT",
{
"default": 0,
"min": -90,
"max": 90,
"step": 5,
"round": False,
},
),
"pnorm": (
"FLOAT",
{
"default": 2,
"min": 0.125,
"max": 100,
"step": 0.1,
"round": False,
},
),
"mix": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.001,
"round": False,
},
),
"common_mode": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.001,
"round": False,
},
),
"preview": (["none", "no_mix", "mix"],),
}
return result
def get_item_class(self):
return PowerNoiseItem
def go(
self,
preview="none",
**kwargs,
):
result = super().go(**kwargs)
if preview == "none":
return result
if preview == "no_mix":
kwargs["mix"] = 1.0
img = PowerNoiseItem(**kwargs).preview()
output_dir = folder_paths.get_temp_directory()
prefix_append = "sonar_temp_" + "".join(
random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5) # noqa: S311
)
full_output_folder, filename, counter, subfolder, _ = (
folder_paths.get_save_image_path(prefix_append, output_dir)
)
filename = f"{filename}_{counter:05}_.png"
file_path = os.path.join(full_output_folder, filename) # noqa: PTH118
img.save(file_path, compress_level=1)
return {
"ui": {
"images": [
{"filename": filename, "subfolder": subfolder, "type": "temp"},
],
},
"result": result,
}
+1
View File
@@ -22,6 +22,7 @@ ignore = [
"ERA001",
"F403",
"F405",
"FBT002",
"PLR0912",
"PLR0913",
"PLR0915",