Author SHA1 Message Date
blepping 742e071364 Update changelog 2024-05-21 07:03:00 -06:00
blepping 0f748bd1f8 Update documentation and examples 2024-05-21 06:47:08 -06:00
blepping a7da42c305 Adjust FrUX input names 2024-05-21 03:10:56 -06:00
blepping e64606c672 Refactoring, cleanups 2024-05-19 03:41:46 -06:00
blepping fb667e08c8 Add more filter preview sizes, use configured oversample 2024-05-18 15:53:04 -06:00
blepping 4a01af10e7 Allow setting size and gain in filter preview node 2024-05-18 09:25:02 -06:00
blepping ec7c9a4632 Add py/external.py - derp!
Accelerate FRUX by caching the filters when possible

Allow doing FFT on CPU in FRUX for GPUs that won't work otherwise

Allow disabling normalization in SamplerConfigOverride node

Fix base power and pink noise types.

Other cleanups
2024-05-18 07:33:42 -06:00
blepping c235bc18b2 Allow using brownian noise in NoisyLatentLike node when sigmas are attached 2024-05-16 18:46:13 -06:00
blepping b47f3f3291 Add FreeUExtreme node and associated config node 2024-05-16 16:24:07 -06:00
blepping 9dc16c5402 Add SonarPreviewFilter node
Better filter normalization (maybe)

Add a scale parameter to filters
2024-05-16 16:22:50 -06:00
blepping 40726e5d84 GuidedNoise fixes 2024-05-14 18:00:30 -06:00
blepping 9cad01df09 Add SonarPowerFilter node, improve RepeatedNoise, other stuff 2024-05-14 16:41:39 -06:00
blepping ab2f08268f Allow showing custom noise preview in SonarPowerFilterNoise node 2024-05-13 10:06:49 -06:00
blepping 9a6ee9ac33 Improve channel filter (not written by me obviously, thanks Gaeros!) 2024-05-12 18:35:13 -06:00
blepping 144c7ba43a Fix channel correlation construction in PowerNoise 2024-05-12 12:47:16 -06:00
blepping 0ef1bd5bbd Add the ability to set channel correlations in SonarPowerNoise and SonarPowerFilterNoise 2024-05-12 12:35:49 -06:00
blepping e4f53e9594 Add SonarPowerFilterNoise, SonarRandomNoise and SonarBlendFilterNoise nodes 2024-05-12 07:59:47 -06:00
blepping 202a371337 Documentation updates 2024-05-11 11:01:46 -06:00
blepping c455599e9e More cleanups and fixes 2024-05-11 08:08:35 -06:00
blepping b950e1b051 Cleanups and fixes 2024-05-11 07:32:11 -06:00
blepping 4e87817908 Refactor, add scheduled, guided and composite noise types 2024-05-09 23:05:25 -06:00
blepping 78e8451324 Feat modulated repeated noise (#5)
* Add modulated and repeated noise nodes
2024-05-06 16:51:23 -06:00
blepping b78cbe0b2a Fix NoisyLatentLike to work with recent ComfyUI changes 2024-04-05 12:57:02 -06:00
blepping 24e1536cb7 Various noise improvements (#4)
* Fixed issue when using Sonar samplers in normal sampling nodes/via stuff like `KSamplerSelect`.
* Add `pyramid` (non-high-res) noise type.
* Allow selecting `brownian` noise in custom noise nodes (but it won't work with `NoisyLatentLike`).
* Use `brownian` as the default noise type for `SamplerSonarDPMPP`.
* Make overriding the selected noise type in Sonar samplers a warning instead of a hard error.
* Improve noise scaling (may change seeds).
* Add `KRestartSamplerCustomNoise` if the user has a recent enough version of ComfyUI_restart_sampling installed.
2024-04-01 11:59:36 -06:00
blepping ecadbfcd19 Improvements to NoisyLatentLike node (#3)
* Improve NoisyLatentLike to allow calculating strength with sigmas and noise injection

* Update documentation for NoisyLatentLike changes + general improvements
2024-03-20 18:37:51 -06:00
88 changed files with 3610 additions and 790 deletions
+48 -226
View File
@@ -1,13 +1,33 @@
# ComfyUI-sonar
A janky implementation of Sonar sampling (momentum-based sampling) for [ComfyUI](https://github.com/comfyanonymous/ComfyUI). It may or may not be working _properly_ but it does produce pretty reasonable results. I am using it personally. At this point, I would say it's suitable for general use with the caveat that it's very likely stuff like implementation and inputs to nodes will still be changing fairly frequently. In other words, don't depend on reproduceable generations with this unless you're willing to keep track of the git revision something was generated with.
A janky implementation of Sonar sampling (momentum-based sampling) for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) as well as an assortment of advanced noise tools.
Currently supports Euler, Euler Ancestral, and DPM++ SDE sampling.
Disclaimer: It's very likely stuff like implementation and inputs to nodes will still be changing fairly frequently. In other words, don't depend on reproduceable generations with this unless you're willing to keep track of the git revision something was generated with.
Momentum based sampling currently supports Euler, Euler Ancestral, and DPM++ SDE sampling.
See the [ChangeLog](changelog.md) for recent user-visible changes.
## Description
This started out as an implementation of Sonar sampling and has evolved into something more like a noise toybox.
Please note that while a lot of the nodes in here have a `Sonar` prefix, that doesn't indicate a relation with
the original Sonar sampling implementation. Why is there random noise stuff in this repo? Mainly because it gets
very awkward having node collections depending on other node collections.
Keep reading below this section for information on Sonar sampling and associated nodes.
For information on the advanced noise tools which include many different noise types, nodes to schedule,
composite and otherwise manipulate noise see:
* [Base Noise Types](docs/base_noise_types.md) - examples and descriptions of the base noise types.
* [Advanced Power Noise](docs/advanced_power_noise.md) - examples and descriptions of the advanced power noise node.
* [Advanced Noise Nodes](docs/advanced_noise_nodes.md) - examples and descriptions of advanced noise nodes (schedule, composite, etc).
* [FreeU Extreme](docs/frux.md) - a build your own FreeU kit that allows advanced filtering, blending, scheduling of effects as well as targetting input and middle blocks.
## Sonar Description
See https://github.com/Kahsolt/stable-diffusion-webui-sonar for a more in-depth explanation.
The `direction` parameter should (unless I screwed it up) work like setting sign to positive or negative: `1.0` is positive, `-1.0` is negative. You can also potentially play with fractional values.
@@ -40,66 +60,6 @@ Attempt to add momentum and guidance to the DPM++ SDE sampler. It may not work c
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...
@@ -116,27 +76,26 @@ Without guidance it should basically work the same as the ancestral Euler versio
## Noise
I basically just copied a bunch of noise functions without really knowing what they do. The main thing I can say is they produce a semi-reasonable result and it's different from the other noise samplers. See [Credits](#credits) below.
See [Base Noise Types](docs/base_noise_types.md) for examples.
1. `gaussian`: This is the default noise type.
2. `uniform`: Might enhance background details?
3. `brownian`: This is the noise type SDE samplers use.
4. `perlin`
5. `studentt`: There's a comment that says it may enhance subject details. It seemed to produce a fairly dark result.
6. `pink`
7. `highres_pyramid`: Not extensively tested, but it is slower than the other noise types. I would guess it does something like enhance details.
8. `laplacian`
9. `power`
10. `rainbow_mild` and `rainbow_intense`: A combination of green (-ish, the implementation may be broken) noise plus perlin noise. Very colorful results.
11. `green_test`: Even more rainbow-y than the rainbow noise types. It _probably_ isn't working correctly, but the results are very interesting and colorful. Depending on the model, it may not work well for an initial generation but may be worth trying with img2img type workflows.
You can scroll down to the the [Examples](#examples) section near the bottom to see some example generations with different noise types.
The sampler and `NoisyLatentLike` nodes now take an optional `SonarCustomNoise` input. You can chain `SonarCustomNoise` nodes together to mix different types of noise, similar to how some of the built in ones. It shouldn't matter what order the noise types are chained. If `rescale` is set to `0.0` no rescaling will occur. `factor` is the proportion of that type of noise you want. If you want to use `rescale` it should be on the node that you are plugging into a sampler. Just for example if you had two `SonarCustomNoise` nodes both with `factor=0.7` and `rescale=1.0` on the last one, it would be effectively the same as if you'd used `factor=0.5` and `rescale=1.0` doesn't actually do anything. You can also rescale to values above `1.0` — the result is more noise, similar to increasing `s_noise` above `1.0` on a sampler. The simple explanation is `rescale` means you don't have to make sure the `factor`s add up to the scale you want (which normally would be `1.0`).
The sampler and `NoisyLatentLike` nodes now take an optional `SonarCustomNoise` input.
**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.
## Integrations
You'll get some bonus features if you have some other node collections installed:
### `KRestartSamplerCustomNoise`
If you have a recent enough version of [ComfyUI_restart_sampling](https://github.com/ssitu/ComfyUI_restart_sampling/)
installed, you'll also get the `KRestartSamplerCustomNoise` node which is exactly the same as `KRestartSamplerCustom`
except for adding an optional custom noise input.
See the restart sampling repo for more information: https://github.com/ssitu/ComfyUI_restart_sampling
### `RestartSamplerCustomNoise`
As above, except this is the custom sampler version.
## Related
@@ -146,13 +105,15 @@ I also have some other ComfyUI nodes here: https://github.com/blepping/ComfyUI-b
Original Sonar Sampler implementation (for A1111): https://github.com/Kahsolt/stable-diffusion-webui-sonar
My version basically just rips off this Sonar sampler implementation for Diffusers: https://github.com/alexblattner/modified-euler-samplers-for-sonar-diffusers/
My version was initially based on this Sonar sampler implementation for Diffusers: https://github.com/alexblattner/modified-euler-samplers-for-sonar-diffusers/
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.
Many 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!
New pyramid noise based on implementation in [Jonathan Whitaker](https://wandb.ai/johnowhitaker/multires_noise/reports/Multi-Resolution-Noise-for-Diffusion-Model-Training--VmlldzozNjYyOTU2)'s article on multi-resolution noise.
## Examples
Original `SonarPowerNoise` contributed by [elias-gaeros](https://github.com/elias-gaeros/). Additionally, he provided a lot of guidance with refactoring it to allow separate filtering and other enhancements and answered a multitude of dumb questions. To say those changes are only co-authored is probably giving myself too much credit. Thank you! Your patience and help is very much appreciated.
## Sonar Examples
Unfortunately, right now these examples are somewhat incomplete and out of date. I hope to update them when I get the time.
@@ -175,149 +136,10 @@ Using the `linear` guidance type and `guidance_factor=-0.015`. The reference ima
</details>
### Noise Types
### Noise Types (img2img)
See:
These were generated with `s_noise=1.05` to make the noise effect more pronounced, 30 steps at `0.66` denoise, sonar settings increased slightly to enhance the effect (`momentum=0.9, momentum_hist=0.85, direction=1.0, momentum_init=ZERO`). It is probably easier to compare using these as the image _mostly_ stays the same as the sonar sampler settings change.
<details>
<summary>Expand renoise example images</summary>
#### Base
Base image - no Sonar Sampler steps.
![Base](assets/example_images/noise/renoise_base.png)
#### Euler A
Normal (non-sonar) Eular A. Not really a comparison with noise (think it would use gaussian) but with the difference in effect from momentum.
![Euler A](assets/example_images/noise/renoise_eulera.png)
#### Gaussian
![Gaussian](assets/example_images/noise/renoise_gaussian.png)
#### Brownian
![Brownian](assets/example_images/noise/renoise_brownian.png)
#### Perlin
![Perlin](assets/example_images/noise/renoise_perlin.png)
#### Uniform
![Uniform](assets/example_images/noise/renoise_uniform.png)
#### Highres Pyramid
![Highres_pyramid](assets/example_images/noise/renoise_highres_pyramid.png)
#### Pink
![Pink](assets/example_images/noise/renoise_pink.png)
#### StudentT
**outdated**
![StudentT](assets/example_images/noise/renoise_studentt.png)
#### StudentT_test
**outdated**
![StudentT_test](assets/example_images/noise/renoise_studentt_test.png)
#### Laplacian
![Laplacian](assets/example_images/noise/renoise_laplacian.png)
#### Power
![Power](assets/example_images/noise/renoise_power.png)
#### Rainbow Mild
![Rainbow Mild](assets/example_images/noise/renoise_rainbow_mild.png)
#### Rainbow Intense
![Rainbow Intense](assets/example_images/noise/renoise_rainbow_intense.png)
#### Green_test
![Green_test](assets/example_images/noise/renoise_green_test.png)
</details>
### Noise Types (Initial Generations)
These were generated with `s_noise=1.1` to make the noise effect more pronounced, default sonar settings (`momentum=0.95, momentum_hist=0.75, direction=1.0, momentum_init=ZERO`). It may be harder to see the noise effects since the composition can change a lot in initial generations.
<details>
<summary>Expand initial generation example images</summary>
#### Gaussian
![Gaussian](assets/example_images/noise/noise_gaussian.png)
#### Brownian
![Brownian](assets/example_images/noise/noise_brownian.png)
#### Perlin
![Perlin](assets/example_images/noise/noise_perlin.png)
#### Uniform
![Uniform](assets/example_images/noise/noise_uniform.png)
#### Highres Pyramid
![Highres_pyramid](assets/example_images/noise/noise_highres_pyramid.png)
#### Pink
![Pink](assets/example_images/noise/noise_pink.png)
#### StudentT
**outdated**
![StudentT](assets/example_images/noise/noise_studentt.png)
#### StudentT_test
**outdated**
![StudentT_test](assets/example_images/noise/noise_studentt_test.png)
#### Laplacian
![Laplacian](assets/example_images/noise/noise_laplacian.png)
#### Power
![Power](assets/example_images/noise/noise_power.png)
#### Rainbow Mild
![Rainbow Mild](assets/example_images/noise/noise_rainbow_mild.png)
#### Rainbow Intense
![Rainbow Intense](assets/example_images/noise/noise_rainbow_intense.png)
#### Green_test
This might seem too crazy for actual use, but you can actually get decent results using the DPMPP Sonar sampler and a relatively high step count.
![Green_test](assets/example_images/noise/noise_green_test.png)
</details>
* [Base Noise Types](docs/base_noise_types.md)
* [Advanced Power Noise](docs/advanced_power_noise.md)
* [Advanced Noise Nodes](docs/advanced_noise_nodes.md)
+8 -11
View File
@@ -1,18 +1,15 @@
from .py import nodes, powernoise, sonar
from .py import freeu_extreme, nodes, powernoise, sonar
sonar.add_samplers()
NODE_CLASS_MAPPINGS = {
"SamplerSonarEuler": nodes.SamplerNodeSonarEuler,
"SamplerSonarEulerA": nodes.SamplerNodeSonarEulerAncestral,
"SamplerSonarDPMPPSDE": nodes.SamplerNodeSonarDPMPPSDE,
"SamplerConfigOverride": nodes.SamplerNodeConfigOverride,
"NoisyLatentLike": nodes.NoisyLatentLikeNode,
"SonarCustomNoise": nodes.SonarCustomNoiseNode,
NODE_CLASS_MAPPINGS = nodes.NODE_CLASS_MAPPINGS | {
"SonarPowerNoise": powernoise.SonarPowerNoiseNode,
"SonarGuidanceConfig": nodes.GuidanceConfigNode,
"SonarPowerFilterNoise": powernoise.SonarPowerFilterNoiseNode,
"SonarPowerFilter": powernoise.SonarPowerFilterNode,
"SonarPreviewFilter": powernoise.SonarPreviewFilterNode,
"FreeUExtremeConfig": freeu_extreme.FreeUExtremeConfigNode,
"FreeUExtreme": freeu_extreme.FreeUExtremeNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = nodes.NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

+25
View File
@@ -2,6 +2,31 @@
Note, only relatively significant changes to user-visible functionality will be included here. Most recent changes at the top.
## 20240521
Mega update! Many new features, documentation reorganized.
* Add `SonarScheduledNoise`, `SonarCompositeNoise`, `SonarGuidedNoise`, `SonarRandomNoise` nodes. See [Advanced Noise Nodes](docs/advanced_noise_nodes.md).
* Add `SonarPowerFilterNoise`, `SonarPowerFilter`, `SonarPreviewFilter` nodes. See [Advanced Power Noise](docs/advanced_power_noise.md).
* Add `FreeUExtreme`, `FreeUExtremeConfig` nodes. See [FreeU Extreme](docs/frux.md).
* Replace `pyramid` noise type with a (hopefully) more correct implementation. You can use `pyramid_old` for the previous behavior.
* Add more noise types and variations.
* The `NoisyLatentLike` node now allows using brownian noise if you connect a model and sigmas.
## 20240506
* Add `SonarModulatedNoise` and `SonarRepeatedNoise` nodes.
## 20240327
* Fixed issue when using Sonar samplers in normal sampling nodes/via stuff like `KSamplerSelect`.
* Add `pyramid` (non-high-res) noise type.
* Allow selecting `brownian` noise in custom noise nodes (but it won't work with `NoisyLatentLike`).
* Use `brownian` as the default noise type for `SamplerSonarDPMPP`.
* Make overriding the selected noise type in Sonar samplers a warning instead of a hard error.
* Improve noise scaling (may change seeds).
* Add `KRestartSamplerCustomNoise` if the user has a recent enough version of ComfyUI_restart_sampling installed.
## 20240320
* `NoisyLatentLike` node improved to allow calculating strength with sigmas and injecting noise itself.
+316
View File
@@ -0,0 +1,316 @@
# Advanced Nodes
## Normalization
Normalization essentially rebalances the noise (or mixture of noise) to 1.0 strength and then scales based
on the factor of the node. Most nodes will allow you to set three values:
* `default`: By default, noise will be normalized only just before it's used. So you could consider this setting to be false except for where it is connected to an actual noise consumer (i.e. a `SamplerConfigOverride` node).
* `forced`: Will always normalize.
* `disabled`: Will never normalize.
## `SONAR_CUSTOM_NOISE`
This node output type actually constitutes a chain of noise items. For most nodes, when you use it as input,
they will add an item to the chain. There are some exceptions that treat the `SONAR_CUSTOM_NOISE` input as a list:
* `SonarRepeatedNoise`
* `SonarRandomNoise`
There are also some exceptions that will consume the list rather than adding an item to it:
* `SonarModulatedNoise`
* `SonarCompositeNoise`
* `SonarScheduledNoise`
* `SonarGuidedNoise`
The distinction is mainly only important when setting `rescale`. Visual example:
![Chain example](../assets/example_images/noise_adv/noise_chain_example.png)
It may be counter intuitive that there are actually two separate chains here.
## Examples
Note on the examples included for some of these nodes:
The example images included for some of these nodes all have metadata and can be loaded in ComfyUI.
Generated using `dpmpp_2s_ancestral`, Karras scheduler and starting out with gaussian noise then switching
to the custom noise type at the 35% mark.
***
### `SonarCustomNoise`
You can chain `SonarCustomNoise` nodes together to mix different types of noise. The order of `SonarCustomNoise` nodes is not important.
Parameters:
- `factor` controls the strength of the noise.
- `rescale` controls rebalancing `factor` for nodes in the chain. When `rescale` is set to `0.0`, no rebalancing will occur. Otherwise the current node as well as the nodes connect to it will have their `factor` adjusted to add up to the rescale value. For example, if you have three nodes with `factor` 1.0 and the last with `rescale` 1.0, then the `factor` value will be adjusted to `1/3 = 0.3333...`. *Note*: Rescaling uses the `factor` absolute value.
- `noise_type` allows you to select the built-in noise type.
***
### `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**: If you select `brownian` noise (either through the dropdown or by connecting custom noise nodes) you must connect a model and sigmas.
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*: For img2img, 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.
**Note**: This node does not currently respect the latent noise mask.
***
### `SamplerConfigOverride`
This node 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.
***
### `SonarModulatedNoise`
Experimental noise modulation based on code stolen from
[ComfyUI-Extra-Samplers](https://github.com/Clybius/ComfyUI-Extra-Samplers). `intensity` and `frequency` modulation
types _probably_ do not work correctly for normal sampling — I expect the modulation will be based on the tensor
where the noise sampler was created rather than each step. However it may be useful for something like restart sampling
noise (see `KRestartSamplerCustomNoise` below). You can also pass it a reference latent to modulate based on
instead (only used for `intensity` and `frequency` modulation types).
*Note*: It's likely this node will be changed in the future.
<details>
<summary>⭐ Expand Example Images ⭐</summary>
<br/>
These examples all use the `spectral_signum` modulation type as it doesn't depend on a reference.
#### Positive Strength
Dims 3:
![Dims 3](../assets/example_images/noise_adv/noise_modulated_ss_dims3.png)
Dims 3 (with studentt noise):
![Dims 3](../assets/example_images/noise_adv/noise_modulated_ss_dims3_studentt.png)
Dims 2:
![Dims 2](../assets/example_images/noise_adv/noise_modulated_ss_dims2.png)
Dims 1:
![Dims 1](../assets/example_images/noise_adv/noise_modulated_ss_dims1.png)
#### Negative Strength
Dims 3:
![Dims 3 Negative](../assets/example_images/noise_adv/noise_modulated_ss_neg_dims3.png)
Dims 3 (with studentt noise):
![Dims 3](../assets/example_images/noise_adv/noise_modulated_ss_neg_dims3_studentt.png)
Dims 2:
![Dims 2 Negative](../assets/example_images/noise_adv/noise_modulated_ss_neg_dims2.png)
Dims 1:
![Dims 1 Negative](../assets/example_images/noise_adv/noise_modulated_ss_neg_dims1.png)
</details>
***
### `SonarRepeatedNoise`
Experimental node to cache noise sampler results. Why would you want to do this? Some noise samplers are
relatively slow (`pyramid` for example) or it may be slow to generate noise if you are mixing many types
of noise. When `permute` is enabled, a random effect like flipping the noise or rolling it in some dimension
will be chosen each time the noise sampler is called. I recommend leaving `permute` on. Note that repeated
noise (especially with `permute` disabled) can be stronger than normal noise, so you may need to rescale to
a value lower than `1.0` or decrease `s_noise` for the sampler. You may also set the maximum number of
times noise is reused by setting `max_recycle`.
<details>
<summary>⭐ Expand Example Images ⭐</summary>
<br/>
Repeated noise is very strong (especially when permute is disabled). You generally won't get good
results using 1.0 strength:
![Normal](../assets/example_images/noise_adv/noise_repeated_normal.png)
I recommend considerably decreasing the strength (example here is using 0.75 which is still a bit too much):
![Adjusted](../assets/example_images/noise_adv/noise_repeated_adjusted.png)
</details>
***
### `SonarCompositeNoise`
Allows compositing noise types based on a mask. Noise is mixed based on the strength of the mask at a location.
For example, where the mask is 1.0 (max strength) you will get 100% `noise_src` and 0% `noise_dst`. Where the
mask is 0.75 you will get 75% `noise_src` and 25% `noise_dst`.
<details>
<summary>⭐ Expand Example Images ⭐</summary>
<br/>
These examples use a base noise type of gaussian and composite in an area with a different type
near middle. The custom noise is also set to a higher strength than normal to highlight the effect.
**No Composite (for comparison)**
![No Composite](../assets/example_images/noise_base_types/noise_gaussian.png)
**Brownian**
![Brownian](../assets/example_images/noise_adv/noise_composite_brownian.png)
**Pyramid**
![Pyramid](../assets/example_images/noise_adv/noise_composite_pyramid.png)
**Pyramid negative factor**
![Pyramid negative](../assets/example_images/noise_adv/noise_composite_pyramid_neg.png)
</details>
***
### `SonarScheduledNoise`
Allows switching between noise types based on percentage of sampling (note: not percentage of steps).
**Note**: You don't have to connect the fallback noise type but the default is to generate _no_ noise, which
is most likely not what you want. The majority of the time, it is recommend to connect something like gaussian
noise at 1.0 strength.
All the example images here use the `SonarScheduledNoise` node so you can pick any one of them to see it
in action!
***
### `SonarGuidedNoise`
Works similarly as described in the [Guidance](../README.md#guidance) section of the main README, however the guidance is applied
to the raw noise. You can use `SonarScheduledNoise` to only apply guidance at certain times. Using `euler`
mode seems considerably stronger than `linear`. The default value should be reasonable for `euler`, may need to be
increased somewhat for `linear`.
<details>
<summary>⭐ Expand Example Images ⭐</summary>
<br/>
#### Pattern
These examples use a half circle pattern as the reference: ![pattern](../assets/example_images/noise_adv/noise_guided_ref_pattern.png)
##### Euler
Positive strength:
![Positive](../assets/example_images/noise_adv/noise_guided_pattern_euler.png)
Negative strength:
![Negative](../assets/example_images/noise_adv/noise_guided_pattern_euler_neg.png)
***
##### Linear
Normal positive strength:
![Positive](../assets/example_images/noise_adv/noise_guided_pattern_linear.png)
Normal negative strength:
![Negative](../assets/example_images/noise_adv/noise_guided_pattern_linear_neg.png)
Strong positive strength:
![Strong Positive](../assets/example_images/noise_adv/noise_guided_pattern_linear_strong.png)
Strong negative strength:
![Strong Negative](../assets/example_images/noise_adv/noise_guided_pattern_linear_strong_neg.png)
***
#### Gradient
These examples use a vertical gradient as the reference: ![pattern](../assets/example_images/noise_adv/noise_guided_ref_gradient.png)
That is dark to light. Light to dark examples just flip the gradient vertically.
##### Euler
Dark to light:
![Dark to light](../assets/example_images/noise_adv/noise_guided_dtol_euler.png)
Light to dark:
![Light to dark](../assets/example_images/noise_adv/noise_guided_ltod_euler.png)
Dark to light (negative strength):
![Dark to light](../assets/example_images/noise_adv/noise_guided_dtol_euler_neg.png)
Light to dark (negative strength):
![Light to dark](../assets/example_images/noise_adv/noise_guided_ltod_euler_neg.png)
***
##### Linear
Dark to light:
![Dark to light](../assets/example_images/noise_adv/noise_guided_dtol_linear.png)
Light to dark:
![Light to dark](../assets/example_images/noise_adv/noise_guided_ltod_linear.png)
Dark to light (negative strength):
![Dark to light](../assets/example_images/noise_adv/noise_guided_dtol_linear_neg.png)
Light to dark (negative strength):
![Light to dark](../assets/example_images/noise_adv/noise_guided_ltod_linear_neg.png)
</details>
***
### `SonarRandomNoise`
Randomly chooses between the noise types in the chain connected to it each time the noise sampler is called.
You generally do not want to use `rescale` here. You can also set `mix_count` to choose and combine multiple
types.
+127
View File
@@ -0,0 +1,127 @@
# Advanced Power Noise
## `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 advanced parameter 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. **FIXME: it's not all channels anymore** 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.
- `channel_correlation` **FIXME**: TBD
- 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>
<br/>
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.
## `SonarPowerFilterNoise`
This node lets you connect a filter (see below) and a custom noise chain. It basically lets you run any type of noise through the power noise filter.
New parameters:
* `filter_norm_factor` controls how much normalization is applied to the filter. `1.0` means fully normalized, `0.0` means no normalization.
* You may set the preview type to `custom` to see a color preview of the filtered noise. Note that this uses whatever preview type you have configured in ComfyUI (for example, TAESD). The preview is based on SD 1.5's interpretation of the noise.
## `SonarPowerFilter`
Most of the parameters here are similar to the `SonarPowerNoise` node. New parameters:
* `scale` allows you to scale the filter (you could consider this to be set to `1.0` in the `SonarPowerNoise` node).
* `compose_mode` allows you to compose multiple filters. Note that composition occurs like `current_filter OPERATION connected_filter`. So if you set `compose_mode` to `sub`, you will get `current_filter - connected_filter`. Scaling occurs before composition.
## `SonarPreviewFilter`
Allows you to preview a filter. It does not modify the input filter.
***
## Examples
The example images are all workflow-included. Generated using `dpmpp_2s_ancestral`, Karras scheduler and
starting out with gaussian noise then switching to power noise at the 35% mark. `filter_norm_factor` is set to
1.0 in these examples.
### Node Defaults
This should be the same as normal gaussian noise.
![PowernoiseDefault](../assets/example_images/noise_base_types/noise_powernoise_default.png)
### Positive Alpha
Positive alpha generally produces a colorful effect. Start with relatively low values and increase
until you achieve the desired result. Note that these examples use _relatively_ extreme settings.
With alpha 0.25:
![PowernoiseAlpha_0.25](../assets/example_images/noise_base_types/noise_powernoise_alpha_0.25.png)
With alpha 0.25, common mode 0.25:
![PowernoiseAlpha_0.25_common_0.25](../assets/example_images/noise_base_types/noise_powernoise_alpha_0.25_common_0.25.png)
With alpha 0.35:
![PowernoiseAlpha_0.35](../assets/example_images/noise_base_types/noise_powernoise_alpha_0.35.png)
With alpha 0.35, common mode 0.35:
![PowernoiseAlpha_0.35_common_0.35](../assets/example_images/noise_base_types/noise_powernoise_alpha_0.35_common_0.35.png)
With alpha 0.5:
![PowernoiseAlpha_0.5](../assets/example_images/noise_base_types/noise_powernoise_alpha_0.5.png)
With alpha 0.5, common mode 0.5:
![PowernoiseAlpha_0.5_common_0.5](../assets/example_images/noise_base_types/noise_powernoise_alpha_0.5_common_0.5.png)
### Negative Alpha
With alpha -0.5:
![PowernoiseAlpha-0.5](../assets/example_images/noise_base_types/noise_powernoise_alpha_-0.5.png)
With alpha -1.5:
![PowernoiseAlpha-1.5](../assets/example_images/noise_base_types/noise_powernoise_alpha_-1.5.png)
### Time Brownian Mode
![PowernoiseTb](../assets/example_images/noise_base_types/noise_powernoise_tb.png)
With alpha 0.5:
![PowernoiseTbAlpha_0.5](../assets/example_images/noise_base_types/noise_powernoise_tb_alpha_0.5.png)
With alpha -0.5:
![PowernoiseTbAlpha-0.5](../assets/example_images/noise_base_types/noise_powernoise_tb_alpha_-0.5.png)
+173
View File
@@ -0,0 +1,173 @@
# Base Noise Examples
The example images are all workflow-included. Generated using `dpmpp_2s_ancestral`, Karras scheduler and
starting out with gaussian noise then switching to the custom noise type at the 35% mark.
Some of these noise types are too extreme to be used for initial generations or even with pure
noise of that type. However you can either schedule the noise type to kick in at a certain percentage
(as in these examples) and/or mix it with something a bit more run of the mill. See
[advanced_noise_nodes](advanced_noise_nodes.md).
## Brownian
This is the default noise type for SDE samplers.
![Brownian](../assets/example_images/noise_base_types/noise_brownian.png)
***
## Gaussian
This is the default noise type for non-SDE samplers.
![Gaussian](../assets/example_images/noise_base_types/noise_gaussian.png)
***
## Green Test
This is _probably_ not actually green noise. It produces a very colorful effect, however
it's very strong and not really suitable for initial generation.
![GreenTest](../assets/example_images/noise_base_types/noise_green_test.png)
You can also use a negative multiplier to achieve a different effect:
![GreenTestNeg](../assets/example_images/noise_base_types/noise_green_test_neg.png)
***
## Highres Pyramid
![HighresPyramid](../assets/example_images/noise_base_types/noise_highres_pyramid.png)
Variation using area scaling:
![HighresPyramidArea](../assets/example_images/noise_base_types/noise_highres_pyramid_area.png)
Variation using bislerp scaling:
![HighresPyramidBislerp](../assets/example_images/noise_base_types/noise_highres_pyramid_bislerp.png)
***
## Laplacian
![Laplacian](../assets/example_images/noise_base_types/noise_laplacian.png)
***
## Perlin
![Perlin](../assets/example_images/noise_base_types/noise_perlin.png)
***
## Pink
![Pink](../assets/example_images/noise_base_types/noise_pink.png)
***
## Power Builtin
![PowerBuiltin](../assets/example_images/noise_base_types/noise_power_builtin.png)
Also see the [Advanced Power Noise](advanced_power_noise.md) examples.
***
## Pyramid
![Pyramid](../assets/example_images/noise_base_types/noise_pyramid.png)
You can also use a negative multiplier to achieve a different effect:
![PyramidNeg](../assets/example_images/noise_base_types/noise_pyramid_neg.png)
Variation using area scaling:
![PyramidArea](../assets/example_images/noise_base_types/noise_pyramid_area.png)
Variation using bislerp scaling:
![PyramidBislerp](../assets/example_images/noise_base_types/noise_pyramid_bislerp.png)
***
## Pyramid Discount5
Pyramid noise, generated with a discount of 0.5. (Generally less extreme effect.)
![PyramidDiscount5](../assets/example_images/noise_base_types/noise_pyramid_discount5.png)
***
## Pyramid Mix
Pyramid mix is a combination of positive and negative pyramid noise. The effect on
the generation is mild compared to raw pyramid noise.
![PyramidMix](../assets/example_images/noise_base_types/noise_pyramid_mix.png)
You can also use a negative multiplier to achieve a different effect:
![PyramidMixNeg](../assets/example_images/noise_base_types/noise_pyramid_mix_neg.png)
Variation using area scaling:
![PyramidMixArea](../assets/example_images/noise_base_types/noise_pyramid_mix_area.png)
You can also use a negative multiplier to achieve a different effect:
![PyramidMixAreaNeg](../assets/example_images/noise_base_types/noise_pyramid_mix_area_neg.png)
Variation using bislerp scaling:
![PyramidMixBislerp](../assets/example_images/noise_base_types/noise_pyramid_mix_bislerp.png)
You can also use a negative multiplier to achieve a different effect:
![PyramidMixBislerpNeg](../assets/example_images/noise_base_types/noise_pyramid_mix_bislerp_neg.png)
***
## Pyramid Old
This may not actually be pyramid noise at all. Also note that it is quite slow to generate as it
effectively generates noise ~60x the latent size.
![PyramidOld](../assets/example_images/noise_base_types/noise_pyramid_old.png)
Variation using area scaling:
![PyramidOldArea](../assets/example_images/noise_base_types/noise_pyramid_old_area.png)
Variation using bislerp scaling:
![PyramidOldBislerp](../assets/example_images/noise_base_types/noise_pyramid_old_bislerp.png)
***
## Rainbow
Rainbow is a mix of Perlin and Green noise types.
The "mild" variation uses a relatively low proportion of green noise:
![RainbowMild](../assets/example_images/noise_base_types/noise_rainbow_mild.png)
The "intense" variation uses a higher proportion of green noise for a more extreme effect.
![RainbowIntense](../assets/example_images/noise_base_types/noise_rainbow_intense.png)
***
## Studentt
![Studentt](../assets/example_images/noise_base_types/noise_studentt.png)
***
## Uniform
![Uniform](../assets/example_images/noise_base_types/noise_uniform.png)
+40
View File
@@ -0,0 +1,40 @@
# FreeU Extreme
I admit it's a really dumb name. This is basically a build-your-own FreeU kit.
## Example Workflow
Workflow image is also workflow-embedded.
![FRUX Workflow](../assets/example_images/frux/fruxworkflow.png)
## Nodes
### `FreeUExtreme`
Allows you to apply a FreeU (v1 or v2) effect to input, output or middle blocks.
**Note**: ComfyUI by default does not allow patching the middle in the required way. You will need to have
[FreeU Advanced](https://github.com/WASasquatch/FreeU_Advanced) installed and enabled, otherwise connecting
configs to the `middle` input will have no effect.
Also note that input and middle do not have a `skip` target so configs targetting that will never match.
### `FreeUExtremeConfig`
Better documentation coming soon hopefully. For now, see the workflow example above to get started.
Also see documentation on filters [here](./advanced_power_noise.md#sonarpowerfilter).
## Examples
ComfyUI built-in FreeU V2 for reference:
![Builtin FreeUV2](../assets/example_images/frux/freeuv2_builtin.png)
FreeU Extreme example:
![FreeU Extreme](../assets/example_images/frux/frux.png)
Note that this is just for example purposes - no attempt was made to get a pretty generation. You
may get better results enabling `hidden_mean` even for the skip connections.
+22
View File
@@ -0,0 +1,22 @@
import contextlib
import importlib
MODULES = {}
with contextlib.suppress(ImportError, NotImplementedError):
bleh = importlib.import_module("custom_nodes.ComfyUI-bleh")
bleh_version = getattr(bleh, "BLEH_VERSION", -1)
if bleh_version < 1:
raise NotImplementedError
MODULES["bleh"] = bleh
with contextlib.suppress(ImportError, NotImplementedError):
import custom_nodes.ComfyUI_restart_sampling as rs
if not hasattr(rs.restart_sampling, "DEFAULT_SEGMENTS"):
# Dumb test but this should only exist in restart sampling versions that
# support plugging in custom noise.
raise NotImplementedError
MODULES["restart"] = rs
__all__ = ("MODULES",)
+350
View File
@@ -0,0 +1,350 @@
from __future__ import annotations
import torch
from .external import MODULES as EXTERNAL_MODULES
from .powernoise import PowerFilter
def ffilter(x, pfilter, normalization_factor=1.0, cfg_idx=None, filter_cache=None):
cache_key = None
if filter_cache is not None and cfg_idx is not None:
cache_key = (cfg_idx, x.shape[-2:])
filter_rfft = filter_cache.get(cache_key)
if filter_rfft is None:
filter_rfft = PowerFilter.normalize(
pfilter.build(x.shape),
x.shape,
normalization_factor=normalization_factor,
).to(x.device, non_blocking=True)
if cache_key:
filter_cache[cache_key] = filter_rfft
x_rfft = torch.fft.rfft2(x.to(torch.float32), norm="ortho")
x_filt = torch.fft.irfft2(
x_rfft.mul_(filter_rfft),
s=x.shape[-2:],
norm="ortho",
)
return x_filt.to(x.dtype, non_blocking=True)
BLEND_OPS = (
{"lerp": torch.lerp}
if "bleh" not in EXTERNAL_MODULES
else EXTERNAL_MODULES["bleh"].py.latent_utils.BLENDING_MODES
)
class FreeUExtremeConfigNode:
RETURN_TYPES = ("FRUX_CONFIG",)
FUNCTION = "go"
CATEGORY = "model_patches"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"stage_1": ("BOOLEAN", {"default": True}),
"stage_2": ("BOOLEAN", {"default": False}),
"stage_3": ("BOOLEAN", {"default": False}),
"target": (("backbone", "skip", "both"),),
"start": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
},
),
"end": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
},
),
"slice": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
},
),
"slice_offset": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
},
),
"filter_norm": (
"FLOAT",
{
"default": 0.0,
"min": -10.0,
"max": 10.0,
"step": 0.1,
"round": False,
},
),
"scale": (
"FLOAT",
{
"default": 1,
"min": -100.0,
"max": 100.0,
"step": 0.1,
"round": False,
},
),
"blend": (
"FLOAT",
{
"default": 1.0,
"min": -10.0,
"max": 10.0,
"step": 0.1,
"round": False,
},
),
"blend_mode": (tuple(BLEND_OPS.keys()),),
"hidden_mean": ("BOOLEAN", {"default": True}),
"final": ("BOOLEAN", {"default": True}),
},
"optional": {
"sonar_power_filter_opt": ("SONAR_POWER_FILTER",),
"frux_config_opt": ("FRUX_CONFIG",),
},
}
def go(self, **kwargs: dict):
return (FreeUExtremeConfig(**kwargs),)
class FreeUExtremeConfig:
_keys = (
"target",
"stage_1",
"stage_2",
"stage_3",
"start",
"end",
"slice",
"slice_offset",
"filter_norm",
"scale",
"blend",
"blend_mode",
"hidden_mean",
"final",
"sonar_power_filter",
"frux_config",
)
def __init__(
self,
*,
target,
stage_1=False,
stage_2=False,
stage_3=False,
start=0.0,
end=1.0,
slice=1.0, # noqa: A002
slice_offset=0.0,
filter_norm=1.0,
scale=1.0,
blend=1.0,
blend_mode=None,
hidden_mean=True,
final=True,
sonar_power_filter_opt=None,
frux_config_opt=None,
):
self.target = target
self.stage_1 = stage_1
self.stage_2 = stage_2
self.stage_3 = stage_3
self.start = start
self.end = end
self.slice = slice
self.slice_offset = slice_offset
self.filter_norm = filter_norm
self.scale = scale
self.blend = blend
self.blend_mode = blend_mode
self.hidden_mean = hidden_mean
self.final = final
self.sonar_power_filter = sonar_power_filter_opt
self.frux_config = frux_config_opt
def get_config_list(self):
result = [self]
curr = self
while cfg := curr.frux_config:
curr = cfg
if (
cfg.start >= 1
or cfg.end <= 0
or cfg.blend == 0
or not (cfg.stage_1 or cfg.stage_2 or cfg.stage_3)
):
continue
result.append(cfg)
result.reverse()
return result
# Hidden mean function modified from https://github.com/WASasquatch/FreeU_Advanced
def get_scale(self, h: torch.Tensor) -> torch.Tensor:
if not self.hidden_mean:
return self.scale
hmean = h.mean(1).unsqueeze(1)
hmax, hmin = (
op(hmean.view(hmean.shape[0], -1), dim=-1, keepdim=True)[0]
for op in (torch.max, torch.min)
)
hmean -= hmin.unsqueeze(2).unsqueeze(3)
hmean /= (hmax - hmin).unsqueeze(2).unsqueeze(3)
return 1.0 + (self.scale - 1.0) * hmean
def check_match(self, pct, stage, is_skip=False):
if pct < self.start or pct > self.end:
return False
if not getattr(self, f"stage_{stage}"):
return False
if self.target not in ("skip" if is_skip else "backbone", "both"):
return False
return True
def apply(self, idx, x, filter_cache, cpu_fft=False):
batch, features, height, width = x.shape
scale = self.get_scale(x)
slice_size = int(features * self.slice)
slice_offs = int(features * self.slice_offset)
xslice = (
self.apply_filter(
idx,
x[:, slice_offs : slice_offs + slice_size],
filter_cache,
cpu_fft=cpu_fft,
)
* scale
)
x[:, slice_offs : slice_offs + slice_size] = (
xslice
if self.blend == 1.0
else BLEND_OPS[self.blend_mode](
x[:, slice_offs : slice_offs + slice_size],
xslice,
self.blend,
)
)
return x
def apply_filter(self, idx, xslice, filter_cache, cpu_fft=False):
filt = self.sonar_power_filter
if filt is None:
return xslice
device = xslice.device
if cpu_fft:
xslice = xslice.to("cpu")
xslice = ffilter(
xslice,
filt,
normalization_factor=self.filter_norm,
cfg_idx=idx,
filter_cache=filter_cache,
)
if cpu_fft:
xslice = xslice.to(device)
return xslice
def clone(self):
return self.__class__(**{k: getattr(self, k) for k in self._keys})
def __repr__(self): # noqa: D105
meh = {k: getattr(self, k) for k in self._keys}
return f"<FRUXConfig: {meh}>"
class FreeUExtremeNode:
RETURN_TYPES = ("MODEL",)
FUNCTION = "go"
CATEGORY = "model_patches"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"cpu_fft": ("BOOLEAN", {"default": False}),
},
"optional": {
"input_config": ("FRUX_CONFIG",),
"middle_config": ("FRUX_CONFIG",),
"output_config": ("FRUX_CONFIG",),
},
}
def go(
self,
model,
cpu_fft,
input_config=None,
middle_config=None,
output_config=None,
):
model_channels = model.model.model_config.unet_config["model_channels"]
stages = {model_channels * 4: 1, model_channels * 2: 2, model_channels: 3}
icfg, mcfg, ocfg = (
() if cfg is None else cfg.get_config_list()
for cfg in (input_config, middle_config, output_config)
)
m = model.clone()
ms = m.get_model_object("model_sampling")
filter_cache = {}
def handler(_typ, h_shape, cfg, x, toptions, is_skip=False):
stage = stages.get(h_shape[1])
if stage is None:
return x
sigma = toptions["sigmas"].max().detach().cpu()
pct = 1.0 - (ms.timestep(sigma) / 999.0)
for idx, ci in enumerate(cfg):
if not ci.check_match(pct, stage, is_skip):
continue
x = ci.apply(idx, x, filter_cache, cpu_fft=cpu_fft)
if ci.final:
break
return x
def in_patch(h, toptions):
return handler("input", h.shape, icfg, h, toptions)
def mid_patch(h, toptions):
return handler("middle", h.shape, mcfg, h, toptions)
def out_patch(h, hsp, toptions):
h = handler("output", h.shape, ocfg, h, toptions)
hsp = handler("output", h.shape, ocfg, hsp, toptions, is_skip=True)
return h, hsp
if icfg:
m.set_model_input_block_patch(in_patch)
if mcfg:
m.set_model_patch(mid_patch, "middle_block_patch")
if ocfg:
m.set_model_output_block_patch(out_patch)
return (m,)
+565 -52
View File
@@ -2,12 +2,15 @@ from __future__ import annotations
import abc
import inspect
from types import SimpleNamespace
from typing import Any, Callable
import torch
from comfy import samplers
from . import noise
from . import external, noise
from .noise import NoiseType
from .noise_generation import scale_noise
from .sonar import (
GuidanceConfig,
GuidanceType,
@@ -24,13 +27,7 @@ class NoisyLatentLikeNode:
def INPUT_TYPES(cls):
return {
"required": {
"noise_type": (
tuple(
t.name.lower()
for t in noise.NoiseType
if t is not noise.NoiseType.BROWNIAN
),
),
"noise_type": (tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
"latent": ("LATENT",),
"multiplier": ("FLOAT", {"default": 1.0}),
@@ -69,7 +66,7 @@ class NoisyLatentLikeNode:
model = model.model
latent_scale_factor = model.latent_format.scale_factor
max_denoise = samplers.Sampler().max_denoise(
samplers.wrap_model(model),
SimpleNamespace(inner_model=model),
sigmas,
)
multiplier *= (
@@ -78,39 +75,51 @@ class NoisyLatentLikeNode:
)
/ latent_scale_factor
)
if sigmas is not None and sigmas.numel() > 1:
sigma_min, sigma_max = sigmas[0], sigmas[-1]
sigma, sigma_next = sigmas[0], sigmas[1]
else:
sigma_min, sigma_max, sigma, sigma_next = (None,) * 4
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,
sigma_min=sigma_min,
sigma_max=sigma_max,
)
else:
ns = noise.get_noise_sampler(
noise.NoiseType[noise_type.upper()],
NoiseType[noise_type.upper()],
latent_samples,
None,
None,
sigma_min,
sigma_max,
seed=seed,
cpu=True,
)
randst = torch.random.get_rng_state()
try:
torch.random.manual_seed(seed)
result = ns(None, None)
result = ns(sigma, sigma_next)
finally:
torch.random.set_rng_state(randst)
if multiplier != 1.0:
result *= multiplier
result = scale_noise(result, multiplier, normalized=True)
if add_to_latent:
result += latent_samples.to(result.device)
result += latent_samples.to(result)
return ({"samples": result},)
class SonarCustomNoiseNodeBase(abc.ABC):
RETURN_TYPES = ("SONAR_CUSTOM_NOISE",)
CATEGORY = "advanced/noise"
FUNCTION = "go"
@abc.abstractmethod
def get_item_class(self):
raise NotImplementedError
@classmethod
def INPUT_TYPES(cls):
return {
def INPUT_TYPES(cls, *, include_rescale=True, include_chain=True):
result = {
"required": {
"factor": (
"FLOAT",
@@ -122,6 +131,11 @@ class SonarCustomNoiseNodeBase(abc.ABC):
"round": False,
},
),
},
"optional": {},
}
if include_rescale:
result["required"] |= {
"rescale": (
"FLOAT",
{
@@ -132,20 +146,17 @@ class SonarCustomNoiseNodeBase(abc.ABC):
"round": False,
},
),
},
"optional": {
}
if include_chain:
result["optional"] |= {
"sonar_custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
},
}
RETURN_TYPES = ("SONAR_CUSTOM_NOISE",)
CATEGORY = "advanced/noise"
FUNCTION = "go"
}
return result
def go(
self,
factor,
rescale,
factor=1.0,
rescale=0.0,
sonar_custom_noise_opt=None,
**kwargs: dict[str, Any],
):
@@ -164,13 +175,7 @@ class SonarCustomNoiseNode(SonarCustomNoiseNodeBase):
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
),
),
"noise_type": (tuple(NoiseType.get_names()),),
}
return result
@@ -178,6 +183,268 @@ class SonarCustomNoiseNode(SonarCustomNoiseNodeBase):
return noise.CustomNoiseItem
class SonarNormalizeNoiseNodeMixin:
@staticmethod
def get_normalize(val: str) -> None | bool:
return None if val == "default" else val == "forced"
class SonarModulatedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
result["required"] |= {
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
"modulation_type": (
(
"intensity",
"frequency",
"spectral_signum",
"none",
),
),
"dims": ("INT", {"default": 3, "min": 1, "max": 3}),
"strength": ("FLOAT", {"default": 2.0, "min": -100.0, "max": 100.0}),
"normalize_result": (("default", "forced", "disabled"),),
"normalize_noise": (("default", "forced", "disabled"),),
"normalize_ref": (
"BOOLEAN",
{"default": True},
),
}
result["optional"] |= {"ref_latent_opt": ("LATENT",)}
return result
def get_item_class(self):
return noise.ModulatedNoise
def go(
self,
factor,
sonar_custom_noise,
modulation_type,
dims,
strength,
normalize_result,
normalize_noise,
normalize_ref,
ref_latent_opt=None,
):
if ref_latent_opt is not None:
ref_latent_opt = ref_latent_opt["samples"].clone()
return super().go(
factor,
noise=sonar_custom_noise,
modulation_type=modulation_type,
modulation_dims=dims,
modulation_strength=strength,
normalize_result=self.get_normalize(normalize_result),
normalize_noise=self.get_normalize(normalize_noise),
normalize_ref=self.get_normalize(normalize_ref),
ref_latent_opt=ref_latent_opt,
)
class SonarRepeatedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
result["required"] |= {
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
"repeat_length": ("INT", {"default": 8, "min": 1, "max": 100}),
"max_recycle": ("INT", {"default": 1000, "min": 1, "max": 1000}),
"normalize": (("default", "forced", "disabled"),),
"permute": (("enabled", "disabled", "always"),),
}
return result
def get_item_class(self):
return noise.RepeatedNoise
def go(
self,
factor,
sonar_custom_noise,
repeat_length,
max_recycle,
normalize,
permute=True,
):
return super().go(
factor,
noise=sonar_custom_noise,
repeat_length=repeat_length,
max_recycle=max_recycle,
normalize=self.get_normalize(normalize),
permute=permute,
)
class SonarScheduledNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
result["required"] |= {
"model": ("MODEL",),
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0}),
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0}),
"normalize": (("default", "forced", "disabled"),),
}
result["optional"] |= {"fallback_sonar_custom_noise": ("SONAR_CUSTOM_NOISE",)}
return result
def get_item_class(self):
return noise.ScheduledNoise
def go(
self,
model,
factor,
sonar_custom_noise,
start_percent,
end_percent,
normalize,
fallback_sonar_custom_noise=None,
):
ms = model.get_model_object("model_sampling")
start_sigma = ms.percent_to_sigma(start_percent)
end_sigma = ms.percent_to_sigma(end_percent)
return super().go(
factor,
noise=sonar_custom_noise,
start_sigma=start_sigma,
end_sigma=end_sigma,
normalize=self.get_normalize(normalize),
fallback_noise=fallback_sonar_custom_noise,
)
class SonarCompositeNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
result["required"] |= {
"sonar_custom_noise_dst": ("SONAR_CUSTOM_NOISE",),
"sonar_custom_noise_src": ("SONAR_CUSTOM_NOISE",),
"normalize_dst": (("default", "forced", "disabled"),),
"normalize_src": (("default", "forced", "disabled"),),
"normalize_result": (("default", "forced", "disabled"),),
"mask": ("MASK",),
}
return result
def get_item_class(self):
return noise.CompositeNoise
def go(
self,
factor,
sonar_custom_noise_dst,
sonar_custom_noise_src,
normalize_src,
normalize_dst,
normalize_result,
mask,
):
return super().go(
factor,
dst_noise=sonar_custom_noise_dst,
src_noise=sonar_custom_noise_src,
normalize_dst=self.get_normalize(normalize_src),
normalize_src=self.get_normalize(normalize_dst),
normalize_result=self.get_normalize(normalize_result),
mask=mask,
)
class SonarGuidedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
result["required"] |= {
"latent": ("LATENT",),
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
"method": (("euler", "linear"),),
"guidance_factor": (
"FLOAT",
{
"default": 0.0125,
"min": -100.0,
"max": 100.0,
"step": 0.001,
"round": False,
},
),
"normalize_noise": (("default", "forced", "disabled"),),
"normalize_result": (("default", "forced", "disabled"),),
"normalize_ref": (
"BOOLEAN",
{"default": True},
),
}
return result
def get_item_class(self):
return noise.GuidedNoise
def go(
self,
factor,
latent,
sonar_custom_noise,
normalize_noise,
normalize_result,
normalize_ref=True,
method="euler",
guidance_factor=0.5,
):
from .sonar import SonarGuidanceMixin
return super().go(
factor,
ref_latent=scale_noise(
SonarGuidanceMixin.prepare_ref_latent(latent["samples"].clone()),
normalized=normalize_ref,
),
guidance_factor=guidance_factor,
noise=sonar_custom_noise.clone(),
method=method,
normalize_noise=self.get_normalize(normalize_noise),
normalize_result=self.get_normalize(normalize_result),
)
class SonarRandomNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
result["required"] |= {
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
"mix_count": ("INT", {"default": 1, "min": 1, "max": 100}),
"normalize": (("default", "forced", "disabled"),),
}
return result
def get_item_class(self):
return noise.RandomNoise
def go(
self,
factor,
sonar_custom_noise,
mix_count,
normalize,
):
return super().go(
factor,
noise=sonar_custom_noise,
mix_count=mix_count,
normalize=self.get_normalize(normalize),
)
class GuidanceConfigNode:
@classmethod
def INPUT_TYPES(cls):
@@ -261,11 +528,7 @@ class SamplerNodeSonarBase:
},
),
"rand_init_noise_type": (
tuple(
t.name.lower()
for t in noise.NoiseType
if t is not noise.NoiseType.BROWNIAN
),
tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),
),
},
"optional": {
@@ -317,7 +580,7 @@ class SamplerNodeSonarEuler(SamplerNodeSonarBase):
init=HistoryType[momentum_init.upper()],
momentum_hist=momentum_hist,
direction=direction,
rand_init_noise_type=noise.NoiseType[rand_init_noise_type.upper()],
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
guidance=guidance_cfg_opt,
)
return (
@@ -347,7 +610,7 @@ class SamplerNodeSonarEulerAncestral(SamplerNodeSonarEuler):
"round": False,
},
),
"noise_type": (tuple(t.name.lower() for t in noise.NoiseType),),
"noise_type": (tuple(NoiseType.get_names()),),
},
)
result["optional"].update(
@@ -375,8 +638,8 @@ class SamplerNodeSonarEulerAncestral(SamplerNodeSonarEuler):
init=HistoryType[momentum_init.upper()],
momentum_hist=momentum_hist,
direction=direction,
rand_init_noise_type=noise.NoiseType[rand_init_noise_type.upper()],
noise_type=noise.NoiseType[noise_type.upper()],
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
noise_type=NoiseType[noise_type.upper()],
custom_noise=custom_noise_opt.clone() if custom_noise_opt else None,
guidance=guidance_cfg_opt,
)
@@ -408,7 +671,7 @@ class SamplerNodeSonarDPMPPSDE(SamplerNodeSonarEuler):
"round": False,
},
),
"noise_type": (tuple(t.name.lower() for t in noise.NoiseType),),
"noise_type": (tuple(NoiseType.get_names(default=NoiseType.BROWNIAN)),),
},
)
result["optional"].update(
@@ -436,8 +699,8 @@ class SamplerNodeSonarDPMPPSDE(SamplerNodeSonarEuler):
init=HistoryType[momentum_init.upper()],
momentum_hist=momentum_hist,
direction=direction,
rand_init_noise_type=noise.NoiseType[rand_init_noise_type.upper()],
noise_type=noise.NoiseType[noise_type.upper()],
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
noise_type=NoiseType[noise_type.upper()],
custom_noise=custom_noise_opt.clone() if custom_noise_opt else None,
guidance=guidance_cfg_opt,
)
@@ -495,9 +758,11 @@ class SamplerNodeConfigOverride:
},
),
"sde_solver": (("midpoint", "heun"),),
"cpu_noise": ("BOOLEAN", {"default": True}),
"normalize": ("BOOLEAN", {"default": True}),
},
"optional": {
"noise_type": (tuple(t.name.lower() for t in noise.NoiseType),),
"noise_type": (tuple(NoiseType.get_names()),),
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
},
}
@@ -515,8 +780,10 @@ class SamplerNodeConfigOverride:
s_churn,
r,
sde_solver,
cpu_noise=True,
noise_type=None,
custom_noise_opt=None,
normalize=True,
):
return (
samplers.KSAMPLER(
@@ -525,7 +792,7 @@ class SamplerNodeConfigOverride:
| {
"override_sampler_cfg": {
"sampler": sampler,
"noise_type": noise.NoiseType[noise_type.upper()]
"noise_type": NoiseType[noise_type.upper()]
if noise_type is not None
else None,
"custom_noise": custom_noise_opt,
@@ -534,6 +801,8 @@ class SamplerNodeConfigOverride:
"s_churn": s_churn,
"r": r,
"solver_type": sde_solver,
"cpu_noise": cpu_noise,
"normalize": normalize,
},
},
inpaint_options=sampler.inpaint_options | {},
@@ -558,10 +827,12 @@ class SamplerNodeConfigOverride:
if extra_args is None:
extra_args = {}
cfg = override_sampler_cfg
sampler, noise_type, custom_noise = (
sampler, noise_type, custom_noise, cpu, normalize = (
cfg["sampler"],
cfg.get("noise_type"),
cfg.get("custom_noise"),
cfg.get("cpu_noise", True),
cfg.get("normalize", True),
)
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
seed = extra_args.get("seed")
@@ -571,6 +842,8 @@ class SamplerNodeConfigOverride:
sigma_min,
sigma_max,
seed=seed,
cpu=cpu,
normalized=normalize,
)
elif noise_type is not None:
noise_sampler = noise.get_noise_sampler(
@@ -579,7 +852,8 @@ class SamplerNodeConfigOverride:
sigma_min,
sigma_max,
seed=seed,
cpu=True,
cpu=cpu,
normalized=normalize,
)
sig = inspect.signature(sampler.sampler_function)
params = sig.parameters
@@ -598,3 +872,242 @@ class SamplerNodeConfigOverride:
extra_args=extra_args,
**kwargs,
)
NODE_CLASS_MAPPINGS = {
"SamplerSonarEuler": SamplerNodeSonarEuler,
"SamplerSonarEulerA": SamplerNodeSonarEulerAncestral,
"SamplerSonarDPMPPSDE": SamplerNodeSonarDPMPPSDE,
"SonarGuidanceConfig": GuidanceConfigNode,
"SamplerConfigOverride": SamplerNodeConfigOverride,
"NoisyLatentLike": NoisyLatentLikeNode,
"SonarCustomNoise": SonarCustomNoiseNode,
"SonarCompositeNoise": SonarCompositeNoiseNode,
"SonarModulatedNoise": SonarModulatedNoiseNode,
"SonarRepeatedNoise": SonarRepeatedNoiseNode,
"SonarScheduledNoise": SonarScheduledNoiseNode,
"SonarGuidedNoise": SonarGuidedNoiseNode,
"SonarRandomNoise": SonarRandomNoiseNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {}
if "bleh" in external.MODULES:
bleh = external.MODULES["bleh"]
bleh_latentutils = bleh.py.latent_utils
class SonarBlendFilterNoiseNode(
SonarCustomNoiseNodeBase,
SonarNormalizeNoiseNodeMixin,
):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
result["required"] |= {
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
"blend_mode": (
("simple_add", *bleh_latentutils.BLENDING_MODES.keys()),
),
"ffilter": (tuple(bleh_latentutils.FILTER_PRESETS.keys()),),
"ffilter_custom": ("STRING", {"default": ""}),
"ffilter_scale": (
"FLOAT",
{"default": 1.0, "min": -100.0, "max": 100.0},
),
"ffilter_strength": (
"FLOAT",
{"default": 0.0, "min": -100.0, "max": 100.0},
),
"ffilter_threshold": (
"INT",
{"default": 1, "min": 1, "max": 32},
),
"enhance_mode": (("none", *bleh_latentutils.ENHANCE_METHODS),),
"enhance_strength": (
"FLOAT",
{"default": 0.0, "min": -100.0, "max": 100.0},
),
"affect": (("result", "noise", "both"),),
"normalize_result": (("default", "forced", "disabled"),),
"normalize_noise": (("default", "forced", "disabled"),),
}
return result
def get_item_class(self):
return noise.BlendFilterNoise
def go(
self,
factor,
sonar_custom_noise,
blend_mode,
ffilter,
ffilter_custom,
ffilter_scale,
ffilter_strength,
ffilter_threshold,
enhance_mode,
enhance_strength,
affect,
normalize_result,
normalize_noise,
):
import ast
ffilter_custom = ffilter_custom.strip()
normalize_result = (
None if normalize_result == "default" else normalize_result == "forced"
)
normalize_noise = (
None if normalize_noise == "default" else normalize_noise == "forced"
)
if ffilter_custom:
ffilter = ast.literal_eval(f"[{ffilter_custom}]")
else:
ffilter = bleh_latentutils.FILTER_PRESETS[ffilter]
return super().go(
factor,
noise=sonar_custom_noise.clone(),
blend_mode=blend_mode,
ffilter=ffilter,
ffilter_scale=ffilter_scale,
ffilter_strength=ffilter_strength,
ffilter_threshold=ffilter_threshold,
enhance_mode=enhance_mode,
enhance_strength=enhance_strength,
affect=affect,
normalize_noise=self.get_normalize(normalize_noise),
normalize_result=self.get_normalize(normalize_result),
)
NODE_CLASS_MAPPINGS["SonarBlendFilterNoise"] = SonarBlendFilterNoiseNode
if "restart" in external.MODULES:
rs = external.MODULES["restart"]
class KRestartSamplerCustomNoise:
@classmethod
def INPUT_TYPES(cls):
get_normal_schedulers = getattr(
rs.nodes,
"get_supported_normal_schedulers",
rs.nodes.get_supported_restart_schedulers,
)
return {
"required": {
"model": ("MODEL",),
"add_noise": (["enable", "disable"],),
"noise_seed": (
"INT",
{"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF},
),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler": ("SAMPLER",),
"scheduler": (get_normal_schedulers(),),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"latent_image": ("LATENT",),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"return_with_leftover_noise": (["disable", "enable"],),
"segments": (
"STRING",
{
"default": rs.restart_sampling.DEFAULT_SEGMENTS,
"multiline": False,
},
),
"restart_scheduler": (rs.nodes.get_supported_restart_schedulers(),),
"chunked_mode": ("BOOLEAN", {"default": True}),
},
"optional": {
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
},
}
RETURN_TYPES = ("LATENT", "LATENT")
RETURN_NAMES = ("output", "denoised_output")
FUNCTION = "sample"
CATEGORY = "sampling"
def sample(
self,
model,
add_noise,
noise_seed,
steps,
cfg,
sampler,
scheduler,
positive,
negative,
latent_image,
start_at_step,
end_at_step,
return_with_leftover_noise,
segments,
restart_scheduler,
chunked_mode=False,
custom_noise_opt=None,
):
return rs.restart_sampling.restart_sampling(
model,
noise_seed,
steps,
cfg,
sampler,
scheduler,
positive,
negative,
latent_image,
segments,
restart_scheduler,
disable_noise=add_noise == "disable",
step_range=(start_at_step, end_at_step),
force_full_denoise=return_with_leftover_noise != "enable",
output_only=False,
chunked_mode=chunked_mode,
custom_noise=custom_noise_opt.make_noise_sampler
if custom_noise_opt
else None,
)
NODE_CLASS_MAPPINGS["KRestartSamplerCustomNoise"] = KRestartSamplerCustomNoise
if hasattr(rs.restart_sampling, "RestartSampler"):
class RestartSamplerCustomNoise:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sampler": ("SAMPLER",),
"chunked_mode": ("BOOLEAN", {"default": True}),
},
"optional": {
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
},
}
RETURN_TYPES = ("SAMPLER",)
FUNCTION = "go"
CATEGORY = "sampling/custom_sampling/samplers"
def go(self, sampler, chunked_mode, custom_noise_opt=None):
restart_options = {
"restart_chunked": chunked_mode,
"restart_wrapped_sampler": sampler,
"restart_custom_noise": None
if custom_noise_opt is None
else custom_noise_opt.make_noise_sampler,
}
restart_sampler = samplers.KSAMPLER(
rs.restart_sampling.RestartSampler.sampler_function,
extra_options=sampler.extra_options | restart_options,
inpaint_options=sampler.inpaint_options,
)
return (restart_sampler,)
NODE_CLASS_MAPPINGS["RestartSamplerCustomNoise"] = RestartSamplerCustomNoise
+802 -370
View File
File diff suppressed because it is too large Load Diff
+426
View File
@@ -0,0 +1,426 @@
# Noise generation functions shamelessly yoinked from https://github.com/Clybius/ComfyUI-Extra-Samplers
from __future__ import annotations
import math
from enum import Enum, auto
from typing import Callable
import torch
from comfy.utils import common_upscale
from torch import FloatTensor, Generator, Tensor
from torch.distributions import Laplace, StudentT
# ruff: noqa: D412, D413, D417, D212, D407, ANN002, ANN003, FBT001, FBT002, S311
class NoiseType(Enum):
GAUSSIAN = auto()
UNIFORM = auto()
BROWNIAN = auto()
PERLIN = auto()
STUDENTT = auto()
HIGHRES_PYRAMID = auto()
PYRAMID = auto()
PYRAMID_MIX = auto()
PINK = auto()
LAPLACIAN = auto()
POWER = auto()
RAINBOW_MILD = auto()
RAINBOW_INTENSE = auto()
GREEN_TEST = auto()
PYRAMID_OLD = auto()
PYRAMID_BISLERP = auto()
HIGHRES_PYRAMID_BISLERP = auto()
PYRAMID_OLD_BISLERP = auto()
PYRAMID_OLD_AREA = auto()
PYRAMID_AREA = auto()
HIGHRES_PYRAMID_AREA = auto()
PYRAMID_DISCOUNT5 = auto()
PYRAMID_MIX_BISLERP = auto()
PYRAMID_MIX_AREA = auto()
@classmethod
def get_names(cls, default=None, skip=None):
if default is not None:
yield default.name.lower()
for nt in cls:
if nt == default or (skip and nt in skip):
continue
yield nt.name.lower()
class NoiseError(Exception):
pass
def scale_noise(noise, factor=1.0, *, normalized=True, threshold_std_devs=2.5):
if not normalized or noise.numel() == 0:
return noise.mul_(factor) if factor != 1 else noise
mean, std = noise.mean().item(), noise.std().item()
threshold = threshold_std_devs / math.sqrt(noise.numel())
if abs(mean) > threshold:
noise -= mean
if abs(1.0 - std) > threshold:
noise /= std
return noise.mul_(factor) if factor != 1 else noise
def get_positions(block_shape: tuple[int, int]) -> Tensor:
"""
Generate position tensor.
Arguments:
block_shape -- (height, width) of position tensor
Returns:
position vector shaped (1, height, width, 1, 1, 2)
"""
bh, bw = block_shape
return torch.stack(
torch.meshgrid(
[(torch.arange(b) + 0.5) / b for b in (bw, bh)],
indexing="xy",
),
-1,
).view(1, bh, bw, 1, 1, 2)
def unfold_grid(vectors: Tensor) -> Tensor:
"""
Unfold vector grid to batched vectors.
Arguments:
vectors -- grid vectors
Returns:
batched grid vectors
"""
batch_size, _, gpy, gpx = vectors.shape
return (
torch.nn.functional.unfold(vectors, (2, 2))
.view(batch_size, 2, 4, -1)
.permute(0, 2, 3, 1)
.view(batch_size, 4, gpy - 1, gpx - 1, 2)
)
def smooth_step(t: Tensor) -> Tensor:
"""
Smooth step function [0, 1] -> [0, 1].
Arguments:
t -- input values (any shape)
Returns:
output values (same shape as input values)
"""
return t * t * (3.0 - 2.0 * t)
def perlin_noise_tensor(
vectors: Tensor,
positions: Tensor,
step: Callable | None = None,
) -> Tensor:
"""
Generate perlin noise from batched vectors and positions.
Arguments:
vectors -- batched grid vectors shaped (batch_size, 4, grid_height, grid_width, 2)
positions -- batched grid positions shaped (batch_size or 1, block_height, block_width, grid_height or 1, grid_width or 1, 2)
Keyword Arguments:
step -- smooth step function [0, 1] -> [0, 1] (default: `smooth_step`)
Raises:
Exception: if position and vector shapes do not match
Returns:
(batch_size, block_height * grid_height, block_width * grid_width)
"""
if step is None:
step = smooth_step
batch_size = vectors.shape[0]
# grid height, grid width
gh, gw = vectors.shape[2:4]
# block height, block width
bh, bw = positions.shape[1:3]
for i in range(2):
if positions.shape[i + 3] not in (1, vectors.shape[i + 2]):
msg = f"Blocks shapes do not match: vectors ({vectors.shape[1]}, {vectors.shape[2]}), positions {gh}, {gw})"
raise NoiseError(msg)
if positions.shape[0] not in (1, batch_size):
msg = f"Batch sizes do not match: vectors ({vectors.shape[0]}), positions ({positions.shape[0]})"
raise NoiseError(msg)
vectors = vectors.view(batch_size, 4, 1, gh * gw, 2)
positions = positions.view(positions.shape[0], bh * bw, -1, 2)
step_x = step(positions[..., 0])
step_y = step(positions[..., 1])
row0 = torch.lerp(
(vectors[:, 0] * positions).sum(dim=-1),
(vectors[:, 1] * (positions - positions.new_tensor((1, 0)))).sum(dim=-1),
step_x,
)
row1 = torch.lerp(
(vectors[:, 2] * (positions - positions.new_tensor((0, 1)))).sum(dim=-1),
(vectors[:, 3] * (positions - positions.new_tensor((1, 1)))).sum(dim=-1),
step_x,
)
noise = torch.lerp(row0, row1, step_y)
return (
noise.view(
batch_size,
bh,
bw,
gh,
gw,
)
.permute(0, 3, 1, 4, 2)
.reshape(batch_size, gh * bh, gw * bw)
)
def perlin_noise(
grid_shape: tuple[int, int],
out_shape: tuple[int, int],
batch_size: int = 1,
generator: Generator | None = None,
*args,
**kwargs,
) -> Tensor:
"""
Generate perlin noise with given shape. `*args` and `**kwargs` are forwarded to `Tensor` creation.
Arguments:
grid_shape -- Shape of grid (height, width).
out_shape -- Shape of output noise image (height, width).
Keyword Arguments:
batch_size -- (default: {1})
generator -- random generator used for grid vectors (default: {None})
Raises:
Exception: if grid and out shapes do not match
Returns:
Noise image shaped (batch_size, height, width)
"""
# grid height and width
gh, gw = grid_shape
# output height and width
oh, ow = out_shape
# block height and width
bh, bw = oh // gh, ow // gw
if oh != bh * gh:
msg = f"Output height {oh} must be divisible by grid height {gh}"
raise NoiseError(msg)
if ow != bw * gw != 0:
msg = f"Output width {ow} must be divisible by grid width {gw}"
raise NoiseError(msg)
angle = torch.empty(
[batch_size] + [s + 1 for s in grid_shape],
*args,
**kwargs,
).uniform_(to=2.0 * math.pi, generator=generator)
# random vectors on grid points
vectors = unfold_grid(torch.stack((torch.cos(angle), torch.sin(angle)), dim=1))
# positions inside grid cells [0, 1)
positions = get_positions((bh, bw)).to(vectors)
return perlin_noise_tensor(vectors, positions).squeeze(0)
def rand_perlin_like(x):
noise = torch.randn_like(x) / 2.0
noise_height = noise.size(dim=2)
noise_width = noise.size(dim=3)
for _ in range(2):
noise += perlin_noise(
(noise_height, noise_width),
(noise_height, noise_width),
batch_size=x.shape[1], # This should be the number of channels.
).to(x.device)
return scale_noise(noise)
def uniform_noise_like(x):
return (torch.rand_like(x) - 0.5) * 3.46
def highres_pyramid_noise_like(x, discount=0.7, upscale_mode="bilinear"):
(
b,
c,
h,
w,
) = x.shape # EDIT: w and h get over-written, rename for a different variant!
orig_w, orig_h = w, h
noise = uniform_noise_like(x)
rs = torch.rand(4, dtype=torch.float32) * 2 + 2
for i in range(4):
r = rs[i]
h, w = min(orig_h * 15, int(h * (r**i))), min(orig_w * 15, int(w * (r**i)))
noise += common_upscale(
torch.randn(b, c, h, w).to(x),
orig_w,
orig_h,
upscale_mode,
None,
).mul_(discount**i)
if h >= orig_h * 15 or w >= orig_w * 15:
break # Lowest resolution is 1x1
return scale_noise(noise)
def pyramid_old_noise_like(
x,
generator=None,
device="cpu",
discount=0.8,
upscale_mode="nearest-exact",
):
size = x.size()
b, c, h, w = size
orig_h, orig_w = h, w
noise = torch.zeros(size=size, dtype=x.dtype, layout=x.layout, device=device)
r = 1
for i in range(5):
r *= 2
noise += common_upscale(
torch.normal(
mean=0,
std=0.5**i,
size=(b, c, h * r, w * r),
dtype=x.dtype,
layout=x.layout,
generator=generator,
device=device,
),
orig_w,
orig_h,
upscale_mode,
None,
).mul_(discount**i)
return noise.to(device=x.device)
# Copied from https://wandb.ai/johnowhitaker/multires_noise/reports/Multi-Resolution-Noise-for-Diffusion-Model-Training--VmlldzozNjYyOTU2
def pyramid_noise_like(x, discount=0.7, upscale_mode="bilinear"):
b, c, w, h = (
x.shape
) # NOTE: w and h get over-written, rename for a different variant!
orig_w, orig_h = w, h
noise = torch.randn_like(x)
for i in range(10):
r = torch.rand(1, device="cpu").item() * 2 + 2 # Rather than always going 2x,
w, h = max(1, int(w / (r**i))), max(1, int(h / (r**i)))
noise += common_upscale(
torch.randn(b, c, w, h).to(x),
orig_h,
orig_w,
upscale_mode,
None,
).mul_(
discount**i,
)
if w == 1 or h == 1:
break # Lowest resolution is 1x1
return scale_noise(noise)
def studentt_noise_like(x):
noise = StudentT(loc=0, scale=0.2, df=1).rsample(x.size())
s: FloatTensor = torch.quantile(noise.flatten(start_dim=1).abs(), 0.75, dim=-1)
s = s.reshape(*s.shape, 1, 1, 1)
noise = noise.clamp(-s, s)
return torch.copysign(torch.pow(torch.abs(noise), 0.5), noise)
def green_noise_like(x):
# The comments said this didn't work and I had to learn the hard way. Turns out it's true!
width, height = x.size(dim=2), x.size(dim=3)
noise = torch.randn_like(x)
scale = 1.0 / (width * height)
fy = torch.fft.fftfreq(width, device=x.device)[:, None] ** 2
fx = torch.fft.fftfreq(height, device=x.device) ** 2
f = fy + fx
power = torch.sqrt(f)
power[0, 0] = 1
noise = torch.fft.ifft2(torch.fft.fft2(noise) / torch.sqrt(power))
noise *= scale / noise.std()
noise = torch.real(noise).to(x.device)
return scale_noise(noise)
def generate_1f_noise(tensor, alpha, k, generator=None):
"""Generate 1/f noise for a given tensor.
Args:
tensor: The tensor to add noise to.
alpha: The parameter that determines the slope of the spectrum.
k: A constant.
Returns:
A tensor with the same shape as `tensor` containing 1/f noise.
"""
fft = torch.fft.fft2(tensor)
freq = torch.arange(1, len(fft) + 1, dtype=torch.float)
spectral_density = k / freq**alpha
return torch.randn(tensor.shape, generator=generator) * spectral_density
def pink_noise_like(x):
return scale_noise(generate_1f_noise(x, 2.0, 1.0)).to(x.device)
def laplacian_noise_like(x):
noise = torch.randn_like(x).div_(4.0)
noise += Laplace(loc=0, scale=1.0).rsample(x.size()).to(noise.device)
return scale_noise(noise)
def power_noise_like(tensor, alpha=2, k=1): # This doesn't work properly right now
"""Generate 1/f noise for a given tensor.
Args:
tensor: The tensor to add noise to.
alpha: The parameter that determines the slope of the spectrum.
k: A constant.
Returns:
A tensor with the same shape as `tensor` containing 1/f noise.
"""
tensor = torch.randn_like(tensor)
fft = torch.fft.fft2(tensor)
freq = torch.arange(1, len(fft) + 1, dtype=torch.float).reshape(
(len(fft),) + (1,) * (tensor.dim() - 1),
)
spectral_density = k / freq**alpha
noise = torch.rand(tensor.shape).mul_(spectral_density)
mean = torch.mean(noise, dim=(-2, -1), keepdim=True).to(tensor.device)
std = torch.std(noise, dim=(-2, -1), keepdim=True).to(tensor.device)
return noise.to(tensor.device).sub_(mean).div_(std)
__all__ = (
"NoiseType",
"NoiseError",
"scale_noise",
"green_noise_like",
"highres_pyramid_noise_like",
"laplacian_noise_like",
"pink_noise_like",
"power_noise_like",
"pyramid_noise_like",
"pyramid_old_noise_like",
"rand_perlin_like",
"studentt_noise_like",
"uniform_noise_like",
)
+673 -94
View File
@@ -1,36 +1,192 @@
# Initial implementation by https://github.com/elias-gaeros/
# He also provided a lot of help with refactoring and other improvements. Thanks!
# (But if anything is broken in here, I'm almost certainly the one to blame.)
from __future__ import annotations
import math
import os
import random
import comfy
import folder_paths
import latent_preview
import torch
from comfy.k_diffusion.sampling import BrownianTreeNoiseSampler
from PIL import Image
from torch import Tensor
from .nodes import SonarCustomNoiseNodeBase
from .nodes import SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin
from .noise import CustomNoiseItemBase
from .noise_generation import scale_noise
# ruff: noqa: ANN003, FBT001, FBT002
PREVIEW_FORMAT = comfy.latent_formats.SD15()
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."""
def make_preview_result(img, result, prefix="sonar_temp"):
output_dir = folder_paths.get_temp_directory()
prefix_append = f"{prefix}_" + "".join(
random.choice("abcdefghijklmnopqrstupvxyz") # noqa: S311
for x in range(5)
)
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,
}
class ChannelMixer:
def __init__(self, channel_count, common_mode, channel_correlation):
self.channel_count = channel_count
self.common_mode = common_mode
self.channel_correlation = channel_correlation
self.mixer = self.build() if common_mode is not None else None
def build(self):
c = self.channel_count
common_mode = self.common_mode
correlation_count = c * (c - 1) // 2
channel_correlation = self.channel_correlation[:correlation_count]
channel_correlation = torch.cat(
(
channel_correlation * common_mode,
torch.full(
(correlation_count - channel_correlation.numel(),),
common_mode,
),
),
)
channel_mixer = torch.eye(c)
channel_mixer[*torch.tril_indices(c, c, offset=-1)] = channel_correlation
channel_mixer += channel_mixer.tril(-1).mT
channel_mixer = torch.linalg.ldl_factor(channel_mixer).LD
dc = torch.diagonal_copy(channel_mixer)
torch.diagonal(channel_mixer)[:] = 1.0
channel_mixer *= dc.clamp_min(0).sqrt().unsqueeze(0)
channel_mixer /= channel_mixer.norm(dim=1, keepdim=True)
return channel_mixer
def to(self, *args: list, **kwargs: dict):
if self.mixer is not None:
self.mixer = self.mixer.to(*args, **kwargs)
return self
def apply(self, noise, shape, copy=False):
if self.mixer is None:
return noise if not copy else noise.clone()
b, c, h, w = shape
if c != self.channel_count:
raise ValueError("Channel count mismatch")
noise = self.mixer @ noise.swapaxes(0, 1).reshape(c, -1)
return noise.reshape(c, b, h, w).swapaxes(1, 0)
def __call__(self, *args: list, **kwargs: dict):
return self.apply(*args, **kwargs)
class PowerFilter:
def __init__(
self,
*,
min_freq=0.0,
max_freq=0.7071,
stretch=1.0,
rotate=0.0,
pnorm=2.0,
alpha=0.0,
scale=1.0,
rel_bw=0.125,
oversample=4,
compose_with: None | PowerFilter = None,
compose_mode="max",
):
self.min_freq = min_freq
self.max_freq = max(max_freq, min_freq)
self.stretch = stretch
self.rotate = rotate
self.pnorm = pnorm
self.alpha = alpha
self.scale = scale
self.rel_bw = rel_bw
self.oversample = oversample
self.compose_with = compose_with
self.compose_mode = compose_mode
def clone(self):
fargs = {
k: getattr(self, k)
for k in (
"min_freq",
"max_freq",
"stretch",
"rotate",
"pnorm",
"alpha",
"scale",
"rel_bw",
"oversample",
"compose_mode",
)
}
fargs["compose_with"] = (
self.compose_with.clone() if self.compose_with is not None else None
)
return self.__class__(**fargs)
@classmethod
def compose(cls, a, b, compose_mode="max"):
if a.shape != b.shape:
raise ValueError("Filter compose size mismatch!")
cf = {
"max": torch.max,
"min": torch.min,
"add": torch.add,
"sub": torch.sub,
"mul": torch.mul,
}.get(compose_mode, torch.max)
return cf(a, b).clamp_(min=0.0)
@classmethod
def normalize(cls, op, shape, mix=1.0, normalization_factor=1.0):
height, width = shape[-2:]
hfreq_bins = width // 2 + 1
# Flat unit gain frequency response
if self.mix < 1.0:
if mix < 1.0:
flat = torch.ones(1, 1, height, hfreq_bins)
if self.mix <= 0.0:
return flat
if mix <= 0.0:
return flat
if normalization_factor != 0:
op *= torch.lerp(
torch.scalar_tensor(1.0),
1.0 / op.square().mean().sqrt(),
normalization_factor,
)
if mix < 1.0:
op = torch.lerp(flat, op, mix, out=op)
return op
def build(self, shape, override_oversample=None, composed=True):
"""Construct a band-pass * 1/f^alpha filter in rfft space."""
oversample = (
override_oversample if override_oversample is not None else self.oversample
)
rel_bw = self.rel_bw
height, width = shape[-2:]
hfreq_bins = width // 2 + 1
# Start with an over-sampled fftshift(rfft2freq()) grid. uses complex
# numbers for convenient 2d rotation (unrelated to the fft complex phase
@@ -92,16 +248,110 @@ class PowerNoiseItem(CustomNoiseItemBase):
# 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
if self.scale != 1.0:
op *= self.scale
if composed and self.compose_with is not None:
return self.compose(
op,
self.compose_with.build(shape, override_oversample=override_oversample),
self.compose_mode,
)
return op
# 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 preview(
self,
size=(128, 128),
mix=1.0,
normalization_factor=1.0,
raw=False,
kernel_gain=1 / 3,
filter_gain=1 / 3,
):
shape = (1, 4, *size)
filter_rfft = self.__class__.normalize(
self.build(size),
shape,
mix=mix,
normalization_factor=normalization_factor,
)
filter_fft = rfft2_to_fft2(filter_rfft)
kernel = torch.fft.irfft2(filter_rfft, s=size, norm="ortho")
kernel = kernel.roll((size[0] // 2, size[1] // 2), (-2, -1))
img = (
filter_fft.mul_(filter_gain).tanh_().mul_(256.0),
kernel.mul_(kernel_gain).tanh_().add_(1.0).mul_(128.0),
)
if raw:
return img
img = torch.cat(img, dim=-1).clamp(0, 255).to(torch.uint8)
return Image.fromarray(img[0, 0].numpy())
class PowerNoiseItem(CustomNoiseItemBase):
def __init__(self, factor, *, channel_correlation, power_filter=None, **kwargs):
if isinstance(channel_correlation, str):
channel_correlation = torch.tensor(
tuple(
float(val)
for val in (val.strip() for val in channel_correlation.split(","))
if val
),
device="cpu",
dtype=torch.float,
)
if power_filter is None:
fargs = {
k: kwargs.pop(k)
for k in ("min_freq", "max_freq", "stretch", "rotate", "pnorm", "alpha")
if k in kwargs
}
power_filter = PowerFilter(**fargs)
super().__init__(
factor,
power_filter=power_filter,
channel_correlation=channel_correlation,
**kwargs,
)
def make_filter(self, shape, oversample=None):
return PowerFilter.normalize(
self.power_filter.build(shape, override_oversample=oversample),
shape,
mix=self.mix,
normalization_factor=getattr(self, "filter_norm_factor", 1.0),
)
def make_noise_sampler_internal(
self,
x: Tensor,
noise_sampler,
filter_rfft,
normalized=True,
):
shape = x.shape
device = x.device
time_brownian = self.time_brownian
channel_mixer = ChannelMixer(
shape[1],
self.common_mode,
self.channel_correlation,
).to(device, non_blocking=True)
def sampler(sigma, sigma_next):
noise = noise_sampler(sigma, sigma_next).to(device)
noise_rfft = (
torch.fft.rfft2(noise, norm="ortho") if time_brownian else noise
)
noise = torch.fft.irfft2(
noise_rfft.mul_(filter_rfft),
s=shape[-2:],
norm="ortho",
)
noise = channel_mixer(noise, shape)
return scale_noise(noise, self.factor, normalized=normalized)
return sampler
def make_noise_sampler(
self,
@@ -110,81 +360,77 @@ class PowerNoiseItem(CustomNoiseItemBase):
sigma_max: float | None,
seed: int | None,
cpu: bool = True,
normalized=True,
):
shape = x.shape
device = x.device
time_brownian = self.time_brownian
shape, device = x.shape, x.device
filter_rfft = self.make_filter(shape).to(device, non_blocking=True)
if self.time_brownian:
if sigma_min is None:
raise ValueError(
"time correlated brownian mode is valid only for stochastic samplers",
)
brownian_tree = BrownianTreeNoiseSampler(
noise_sampler = BrownianTreeNoiseSampler(
x,
sigma_min,
sigma_max,
seed=seed,
cpu=cpu,
)
else:
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(
def noise_sampler(_s, _sn):
return torch.randn(
(*shape[:-1], filter_rfft.shape[-1]),
dtype=torch.complex64,
device=device,
)
return self.make_noise_sampler_internal(
x,
noise_sampler,
filter_rfft,
normalized=normalized,
)
def preview(
self,
size=(128, 128),
noise=None,
kernel_gain=1 / 3,
filter_gain=1 / 3,
):
filter_rfft = self.make_filter(size, oversample=1)
if noise is None:
noise = torch.fft.irfft2(
noise_rfft.mul_(filter_rfft),
s=shape[-2:],
filter_rfft
* torch.randn(
filter_rfft.shape,
dtype=torch.complex64,
generator=torch.Generator().manual_seed(0),
),
s=size,
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",
else:
noise_rfft = torch.fft.rfft2(noise, norm="ortho")
noise = torch.fft.irfft2(
noise_rfft.mul_(filter_rfft),
s=noise.shape[-2:],
norm="ortho",
)
filter_preview = self.power_filter.preview(
size=size,
normalization_factor=getattr(self, "filter_norm_factor", 1.0),
filter_gain=filter_gain,
kernel_gain=kernel_gain,
raw=True,
)
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),
(
*filter_preview,
noise.mul_(1 / 3).tanh_().add_(1.0).mul_(128.0),
],
),
dim=-1,
)
.clamp(0, 255)
@@ -204,13 +450,90 @@ def rfft2_to_fft2(x):
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)
return torch.cat((x_l, x_r), dim=-1)
class PowerFilterNoiseItem(PowerNoiseItem):
def __init__(self, factor, *, noise, normalize_noise, normalize_result, **kwargs):
super().__init__(
factor,
noise=noise.clone(),
normalize_noise=normalize_noise,
normalize_result=normalize_result,
**kwargs,
)
def clone_key(self, k):
if k == "noise":
return self.noise.clone()
return super().clone_key(k)
def make_noise_sampler(
self,
x: Tensor,
sigma_min: float | None,
sigma_max: float | None,
seed: int | None,
cpu: bool = True,
normalized=True,
):
shape, device = x.shape, x.device
normalize_noise = self.get_normalize("normalize_noise", False) # noqa: FBT003
normalize_result = self.get_normalize("normalize_result", normalized)
filter_rfft = self.make_filter(shape).to(device, non_blocking=True)
noise_sampler = self.noise.make_noise_sampler(
x,
sigma_min,
sigma_max,
seed,
cpu,
normalized=normalize_noise,
)
return self.make_noise_sampler_internal(
x,
noise_sampler,
filter_rfft,
normalized=normalize_result,
)
def preview(self, size=(128, 128)):
if getattr(self, "preview_type", None) != "custom":
return super().preview(size=size)
torch.manual_seed(0)
x = torch.randn((1, 4, *size), dtype=torch.float, device="cpu")
ns = self.noise.make_noise_sampler(
x,
torch.scalar_tensor(0.0),
torch.scalar_tensor(14.0),
0,
True, # noqa: FBT003
normalized=self.normalize_noise is True,
)
filtered_ns = self.make_noise_sampler_internal(
x,
ns,
self.make_filter(x.shape),
self.normalize_result in (True, None),
)
filtered_noise = filtered_ns(
torch.scalar_tensor(14.0),
torch.scalar_tensor(10.0),
)
previewer = latent_preview.get_previewer(None, PREVIEW_FORMAT)
default_preview = super().preview(size=size).convert("RGB")
preview = previewer.decode_latent_to_preview(filtered_noise.cpu())
default_preview.paste(
preview.resize((size[-1], size[-2])),
box=(size[-1] * 2, 0),
)
return default_preview
class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
def INPUT_TYPES(cls, *args: list, **kwargs: dict):
result = super().INPUT_TYPES(*args, **kwargs)
result["required"] |= {
"time_brownian": ("BOOLEAN", {"default": False}),
"alpha": (
@@ -287,13 +610,21 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 1.0,
"min": -100.0,
"max": 100.0,
"step": 0.001,
"round": False,
},
),
"preview": (["none", "no_mix", "mix"],),
"channel_correlation": (
"STRING",
{
"default": "1, 1, 1, 1, 1, 1",
"multiline": False,
"dynamicPrompts": False,
},
),
"preview": (("none", "no_mix", "mix"),),
}
return result
@@ -310,24 +641,272 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
return result
if preview == "no_mix":
kwargs["mix"] = 1.0
img = PowerNoiseItem(**kwargs).preview()
img = self.get_item_class()(preview_type=preview, **kwargs).preview()
return make_preview_result(img, result)
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,
class SonarPowerFilterNoiseNode(SonarPowerNoiseNode, SonarNormalizeNoiseNodeMixin):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
for k in (
"min_freq",
"max_freq",
"stretch",
"rotate",
"pnorm",
"alpha",
"time_brownian",
):
del result["required"][k]
result["required"] |= {
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
"sonar_power_filter": ("SONAR_POWER_FILTER",),
"filter_norm_factor": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
},
),
"normalize_result": (("default", "forced", "disabled"),),
"normalize_noise": (("default", "forced", "disabled"),),
}
result["required"]["preview"] = ((*result["required"]["preview"][0], "custom"),)
return result
def get_item_class(self):
return PowerFilterNoiseItem
def go(
self,
factor,
sonar_custom_noise,
sonar_power_filter,
filter_norm_factor,
normalize_noise,
normalize_result,
preview="none",
**kwargs: dict,
):
return super().go(
factor=factor,
noise=sonar_custom_noise,
normalize_noise=self.get_normalize(normalize_noise),
normalize_result=self.get_normalize(normalize_result),
preview=preview,
time_brownian=True,
power_filter=sonar_power_filter,
filter_norm_factor=filter_norm_factor,
**kwargs,
)
class SonarPowerFilterNode:
RETURN_TYPES = ("SONAR_POWER_FILTER",)
CATEGORY = "advanced/noise"
FUNCTION = "go"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"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,
},
),
"oversample": ("INT", {"default": 4, "min": 1, "max": 128}),
"blur": (
"FLOAT",
{
"default": 0.125,
"min": -10.0,
"max": 10.0,
"step": 0.01,
"round": False,
},
),
"scale": (
"FLOAT",
{
"default": 1,
"min": -100.0,
"max": 100.0,
"step": 0.1,
"round": False,
},
),
"compose_mode": (("max", "min", "add", "sub", "mul"),),
},
"optional": {
"power_filter_opt": ("SONAR_POWER_FILTER",),
},
}
def go(
self,
min_freq=0.0,
max_freq=0.7071,
stretch=1.0,
rotate=0.0,
pnorm=2.0,
alpha=0.0,
blur=0.125,
oversample=4,
scale=1.0,
compose_mode="max",
power_filter_opt=None,
):
return (
PowerFilter(
min_freq=min_freq,
max_freq=max_freq,
stretch=stretch,
rotate=rotate,
pnorm=pnorm,
alpha=alpha,
scale=scale,
rel_bw=blur,
oversample=oversample,
compose_mode=compose_mode,
compose_with=power_filter_opt,
),
)
class SonarPreviewFilterNode:
RETURN_TYPES = ("SONAR_POWER_FILTER",)
CATEGORY = "advanced/noise"
FUNCTION = "go"
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sonar_power_filter": ("SONAR_POWER_FILTER",),
"filter_gain": (
"FLOAT",
{
"default": 1 / 3,
"min": 0.0,
"max": 1000000.0,
"step": 0.1,
"round": False,
},
),
"kernel_gain": (
"FLOAT",
{
"default": 1 / 3,
"min": 0.0,
"max": 1000000.0,
"step": 0.1,
"round": False,
},
),
"norm_factor": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
},
),
"preview_size": (
(
"128x128",
"256x256",
"384x256",
"256x384",
"768x512",
"512x768",
"768x768",
"128x127",
"127x128",
),
),
},
}
def go(
self,
sonar_power_filter,
filter_gain=1 / 3,
kernel_gain=1 / 3,
norm_factor=1.0,
preview_size="256x256",
):
filt = sonar_power_filter.clone()
filt.preview_type = "custom"
preview_size = tuple(int(val) for val in preview_size.split("x", 1))
return make_preview_result(
filt.preview(
size=(preview_size[1], preview_size[0]),
filter_gain=filter_gain,
kernel_gain=kernel_gain,
normalization_factor=norm_factor,
),
(filt,),
)
+35 -37
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
from enum import Enum, auto
from sys import stderr
from typing import Any, Callable, NamedTuple
import torch
@@ -44,6 +45,8 @@ class SonarConfig(NamedTuple):
class SonarBase:
DEFAULT_NOISE_TYPE = noise.NoiseType.GAUSSIAN
def __init__(self, cfg: SonarConfig) -> None:
self.history_d = None
self.cfg = cfg
@@ -59,11 +62,11 @@ class SonarBase:
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
if noise_sampler is not None and self.cfg.noise_type not in (
None,
noise.NoiseType.GAUSSIAN,
self.DEFAULT_NOISE_TYPE,
):
# Possibly we should just use the supplied already-created noise sampler here.
raise ValueError(
"Unexpected noise_sampler presence with non-default noise type requested",
print(
"Sonar: Warning: Noise sampler supplied, overriding noise type from settings",
file=stderr,
)
if self.cfg.custom_noise:
noise_sampler = self.cfg.custom_noise.make_noise_sampler(
@@ -72,14 +75,15 @@ class SonarBase:
sigma_max,
seed=seed,
)
elif noise_sampler is None and self.cfg.noise_type:
elif noise_sampler is None:
noise_sampler = noise.get_noise_sampler(
self.cfg.noise_type,
self.cfg.noise_type or self.DEFAULT_NOISE_TYPE,
x,
sigma_min,
sigma_max,
seed=seed,
cpu=True,
normalized=True,
)
self.noise_sampler = noise_sampler
return noise_sampler
@@ -100,6 +104,7 @@ class SonarBase:
None,
seed=self.extra_args.get("seed"),
cpu=True,
normalized=True,
)
self.history_d = ns(None, None)
else:
@@ -157,34 +162,42 @@ class SonarGuidanceMixin:
if self.ref_latent.device != x.device:
self.ref_latent = self.ref_latent.to(device=x.device)
if self.guidance.guidance_type == GuidanceType.LINEAR:
return self.guidance_linear(x)
return self.guidance_linear(x, self.ref_latent, self.guidance.factor)
if self.guidance.guidance_type == GuidanceType.EULER:
return self.guidance_euler(step_index, x, denoised)
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
return self.guidance_euler(
sigma,
sigma_next,
x,
denoised,
self.ref_latent,
self.guidance.factor,
)
raise ValueError("Sonar: Guidance: Unknown guidance type")
@staticmethod
def guidance_euler(
self,
step_index: int,
sigma: Tensor,
sigma_next: Tensor,
x: Tensor,
denoised: Tensor,
):
ref_latent: Tensor,
factor: float = 0.2,
) -> Tensor:
avg_t = denoised.mean(dim=[1, 2, 3], keepdim=True)
std_t = denoised.std(dim=[1, 2, 3], keepdim=True)
ref_img_shift = self.ref_latent * std_t + avg_t
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
ref_img_shift = ref_latent * std_t + avg_t
d = sampling.to_d(x, sigma, ref_img_shift)
dt = (sigma_next - sigma) * self.guidance.factor
dt = (sigma_next - sigma) * factor
return x + d * dt
def guidance_linear(
self,
x: Tensor,
):
@staticmethod
def guidance_linear(x: Tensor, ref_latent: Tensor, factor: float = 0.2) -> Tensor:
avg_t = x.mean(dim=[1, 2, 3], keepdim=True)
std_t = x.std(dim=[1, 2, 3], keepdim=True)
ref_img_shift = self.ref_latent * std_t + avg_t
return (1.0 - self.guidance.factor) * x + self.guidance.factor * ref_img_shift
ref_img_shift = ref_latent * std_t + avg_t
return (1.0 - factor) * x + factor * ref_img_shift
class SonarWithGuidance(SonarBase, SonarGuidanceMixin):
@@ -386,14 +399,6 @@ class SonarEulerAncestral(SonarSampler):
):
if sonar_config is None:
sonar_config = SonarConfig()
if (
noise_sampler is not None
and sonar_config.noise_type != noise.NoiseType.GAUSSIAN
):
# Possibly we should just use the supplied already-created noise sampler here.
raise ValueError(
"Unexpected noise_sampler presence with non-default noise type requested",
)
s_in = x.new_ones([x.shape[0]])
sonar = cls(
eta,
@@ -430,6 +435,8 @@ class SonarEulerAncestral(SonarSampler):
class SonarDPMPPSDE(SonarSampler):
DEFAULT_NOISE_TYPE = noise.NoiseType.BROWNIAN
def __init__(
self,
eta: float = 1.0,
@@ -560,15 +567,6 @@ class SonarDPMPPSDE(SonarSampler):
):
if sonar_config is None:
sonar_config = SonarConfig()
if (
noise_sampler is not None
and sonar_config.noise_type != noise.NoiseType.GAUSSIAN
):
# Possibly we should just use the supplied already-created noise sampler here.
raise ValueError(
"Unexpected noise_sampler presence with non-default noise type requested",
)
s_in = x.new_ones([x.shape[0]])
sonar = cls(
eta,