Author SHA1 Message Date
blepping e36623a5f1 Add round and step to node FLOAT inputs that did not have it 2025-01-30 06:41:34 -07:00
blepping ca3ee58750 Internal cleanups and refactoring.
Some integration improvements.
Bump date in changelog
2025-01-30 06:10:36 -07:00
blepping a31eb6940b Better approach to integration with external nodes
Documentation updates
Other cleanups
2024-12-22 10:36:23 -07:00
blepping 3222b02318 Momentum sampler refactor/improvements (I hope) 2024-12-12 15:16:42 -07:00
blepping dcfea85e9c Add SonarResizedNoise node 2024-12-12 11:39:14 -07:00
blepping 3ba9f2e3d1 More distributions! 2024-12-11 17:57:30 -07:00
blepping b5be44720c Distro noise improvements, add SonarAdvancedDistroNoise node 2024-12-11 11:14:48 -07:00
blepping 30b37e98c2 Generalized distribution noise for most torch.distributions 2024-12-09 21:16:35 -07:00
blepping a951ad7392 Fix Brownian arg passing 2024-12-06 09:25:05 -07:00
blepping 27126d9f93 Add WaveletFilteredNoise node, other fixes 2024-12-05 15:41:15 -07:00
blepping 7365a9f30b Refactor noise generation
Try to make option passing and CPU/GPU noise selection work
Add advanced custom noise node that allows for parameter passing
Add wavelet noise type
2024-12-05 13:17:27 -07:00
blepping 6d15c0bbca Use ComfyUI union types for wildcard inputs when available 2024-12-05 03:57:16 -07:00
blepping f7cbbfcbda Merge pull request #11 from blepping/nov2024update
November 2024 mega update
2024-11-30 01:01:33 -07:00
blepping 6369627e99 November 2024 mega update, see changelog for details 2024-11-30 00:59:08 -07:00
blepping 4f1934345a Fix an issue with NoisyLatentLike where it didn't correctly pass a seed to connected custom noise nodes (probably only affected Brownian)
Add normalize and cpu_noise parameters to NoisyLatentLike
2024-10-10 16:09:54 -06:00
blepping d2dc34e9cf Merge pull request #10 from asagi4/main
Don't fail in momentum samplers when self.guidance is None
2024-09-02 10:20:25 -06:00
asagi4 12ff37d8e3 Don't fail when self.guidance is None 2024-09-02 19:08:31 +03:00
blepping 55d9346713 Fix pink noise and batch sizes over 1 2024-09-01 18:11:21 -06:00
blepping 28b9d9c9c2 Add SonarAdvancedPyramidNoise node 2024-08-23 09:26:00 -06:00
blepping 1fa6b44c47 Add tooltips and descriptions for most nodes.
Add repeat_batch parameter to NoisyLatentLike node.
Add a node to convert SONAR_CUSTOM_NOISE to ComfyUI NOISE.
Various code cleanups and lint squashing.
2024-08-23 07:28:10 -06:00
blepping 5eacd52bbf Workaround for Python 3.10 compatibility 2024-05-21 10:35:30 -06:00
blepping 408686b9b8 Fix FreeU Extreme example images/workflow 2024-05-21 07:34:10 -06:00
blepping 4844b7109e Mega update (#6)
Many new features, documentation reorganized.

* Add `SonarScheduledNoise`, `SonarCompositeNoise`, `SonarGuidedNoise`, `SonarRandomNoise` nodes.
* Add `SonarPowerFilterNoise`, `SonarPowerFilter`, `SonarPreviewFilter` nodes.
* Add `FreeUExtreme`, `FreeUExtremeConfig` nodes.
* 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.
* Refactoring, cleanups, reorganization.
2024-05-21 07:06:46 -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
90 changed files with 7737 additions and 1113 deletions
+95 -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,70 +60,51 @@ 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...
<details>
<summary>Click to expand advanced parameters info</summary>
There are some extra advanced parameters that may be passed by YAML/JSON using `SamplerConfigOVerride`'s `yaml_parameters`. Defaults:
```yaml
sonar_params:
# One of: classic, new, denoised
# classic: Should be the same as the way it works in the A1111 extension.
# new: Possibly improved version that doesn't blend in the history again.
# denoised: Instead of using the noise prediction, we do momentum on denoised instead.
momentum_mode: new
# The following two parameters may be used to control when
# momentum sampling is active. Steps are 0-based with 0 being the first step.
momentum_start_step: 0
momentum_end_step: 9999
# Controls whether history always gets updated, whether or not within the
# start/end step range or only in that range. Can be used to affect the initial
# history value.
always_update_history: true
# Only applies when the init type is RAND.
rand_init_noise_multiplier: 1.0
# If you have ComfyUI-bleh installed, you can use any blend mode it provides.
# Otherwise you can have your blend mode in any color you want as long as it's lerp.
blend_mode: lerp
# Defaultss to blend_mode if unset.
momentum_blend_mode: null
# Defaults to blend_mode if unset. Only applies to linear guidance mode.
guidance_blend_mode: null
```
Additionally, it's possible to override the normal Sonar parameters here as well. If they exist in the `sonar_params` block, they will overwrite the values in the node.
</details>
## Guidance
You can try the `SamplerSonarNaive` sampler which has an optional latent input. The guidance _probably_ isn't working correctly and the implementation definitely isn't exactly the same as the original A1111 version but it still might be fun to play with. The `linear` guidance type is a lot more sensitive to the `guidance_factor` than the `euler` type. For `euler`, reasonable values are around `0.01` to `0.1`, for `linear` reasonable values are more like `0.001` to `0.02`. It is also possible to set guidance factor to a negative value, I've found this results in high contrast and very vivid colors.
@@ -116,27 +117,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 +146,21 @@ 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.
* Noise spectral modulation modified from https://github.com/Clybius/ComfyUI-Extra-Samplers
* 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.
* 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.
* New 1/f (onef) and power law (white, grey, violet, velvet) noise types referenced from https://github.com/WASasquatch/PowerNoiseSuite
* Wavelet noise idea (and some of the default settings) from https://github.com/ClownsharkBatwing/RES4LYF
`SonarPowerNoise` contributed by [elias-gaeros](https://github.com/elias-gaeros/). Thanks!
## Errata
## Examples
* The noise types might not actually do what they claim. In that, I mean something I called "pink" noise might not be what is technically known as "pink noise". My implementations are best-effort. Bug reports and contributions to improve this repo are always welcome!
* Whether noise gets generated on GPU or CPU is probably inconsistent. This means changing GPU types may change seeds, also when this eventually gets fixed it will probably also change seeds.
## 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 +183,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: 459 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

+63
View File
@@ -2,6 +2,69 @@
Note, only relatively significant changes to user-visible functionality will be included here. Most recent changes at the top.
## 20250130
*Note*: May change seeds.
This set of changes includes some pretty major internal refactoring. Definitely possible that I broke something, so please create an issue if you run into problems.
* Noise generation should now respect whether generating on CPU vs GPU is selected. Previously it likely was defaulting to generating on GPU. This may change seeds.
* Refactored momentum samplers, this may change seeds especially if you were using weird parameters like negative direction.
* Added some new parameters for momentum samplers.
* Removed the `s_noise` and churn parameters from the normal Sonar Euler sampler. May break workflows. (Churn was the predecessor to ancestral samplers and is basically obsolete.)
* Added `wavelet` and `distro` noise types.
* Added `SonarCustomNoiseAdv` node that allows passing parameters via YAML.
* Added `SonarResizedNoise` node that allows you to generate noise at a fixed size and then crop/resize it to match the generation.
* Added `SonarAdvancedDistroNoise` node that allows generating noise with basically all the distributions PyTorch supports.
* Added `SonarWaveletFilteredNoise` node that lets you filter another noise generator using wavelets.
## 20241129
*Note*: Contains some potentially workflow-breaking changes.
* `pink` noise type renamed to `pink_old` - the implementation was incorrect.
* `power` noise type renamed to `power_old` - the implementation was incorrect.
* Added `onef_pinkish` (higher frequencye) and `onef_greenish` (lower frequency) noise types.
* Added `SonarAdvanced1fNoise` node and `onef_pinkish`, `onef_greenish`, `onef_pinkish_mix`, `onef_greenish_mix`, and `onef_pinkishgreenish` noise types.
* Added `SonarAdvancedPowerLawNoise` node and `grey`, `white`, `violet` and `velvet` noise types.
* The `SonarAdvancedPyramidNoise` node can now use upscale methods from my [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) node pack if it is available.
* Added the `SonarChannelNoise` and `SonarBlendedNoise` nodes.
* Added the `SonarBlehOpsNoise` node.
* Added advanced parameter input to the SampleConfigOverride node, you can now pass options directly to the wrapped sampler function.
* Custom noise inputs now are semi-wildcard and will accept `OCS_NOISE` or `SONAR_CUSTOM_NOISE` interchangeably.
## 20240823
* Added descriptions and tooltips for most nodes.
* Added `repeat_batch` parameter to `NoisyLatentLike` node.
* Added a `SONAR_CUSTOM_NOISE to NOISE` node to allow converting from Sonar's custom noise type to the built in ComfyUI `NOISE` (used by `SamplerCustomAdvanced` and possibly other nodes).
* Added a `SonarAdvancedPyramidNoise` node that allows setting parameters for the pyramid noise variants.
## 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.
+444
View File
@@ -0,0 +1,444 @@
# 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.
***
### `SonarCustomNoiseAdv`
Same as the `SonarCustomNoise` except it also includes a text widget for passing parameters by YAML or JSON (JSON is valid YAML).
Just for example, instead of using the absurdly large `SonarAdvancedDistroNoise` node, you could do something like:
```yaml
distro: wishart
quantile_norm: 0.5
wishart_cov_size: 4
wishart_df: 3.5
```
***
### `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.
You can enter YAML parameters in the text input, these arguments are passed directly to the sampler function without any error checking. If the same key exists in the node itself (i.e. `s_noise`) the one in the text input will take precedence. Note that these are based on the internal sampler function so the names of the arguments won't necessarily be the same as the sampler node (but they often are). You may need to check the source code for the sampler.
***
### `SONAR_CUSTOM_NOISE to NOISE`
This node can be used to convert Sonar custom noise to the `NOISE` type used by the builtin `SamplerCustomAdvanced` (and any other nodes that take a `NOISE` input).
***
### `SonarAdvancedPyramidNoise`
Allows setting some parameters for the pyramid noise variants (`pyramid`, `highres_pyramid` and `pyramid_old`). `discount` further from zero generally results in a more extreme colorful effect (can also be set to negative values). Higher `iterations` also tends to make the effect more extreme - zero iterations will just return normal Gaussian noise. You can also experiment with the `upscale_mode` for different effects.
### `SonarAdvanced1fNoise`
More extensive documentation TBD (hopefully). For now, a few recipes:
These differ differ only in alpha. For the other parameters, use `k=1, vf=1, hf=1, use_sqrt=true` to start.
* `blue`: `alpha=1`
* `green`: `alpha=0.75`
* `pink`: `alpha=0.5`
*
### `SonarAdvancedPowerLawNoise`
More extensive documentation TBD (hopefully). For now, a few recipes:
* `white`: `alpha=0, use_sign=true, div_max_dims=none`
* `grey`: `alpha=0, use_sign=false, div_max_dims=none`
* `velvet`: `alpha=1, use_sign=true, div_max_dims=all, use_div_max_abs=true`
* `violet`: `alpha=0.5, use_sign=true, div_max_dims=all, use_div_max_abs=true`
***
## `SonarWaveletFilteredNoise`
You will need [pytorch_wavelets](https://github.com/fbcotter/pytorch_wavelets) installed in your Python environment to use this one.
Allows filtering another noise source using wavelets. Parameters are specified using YAML (or JSON) in the text widget. The defaults are:
```yaml
use_dtcwt: false
mode: periodization
level: 3
wave: haar
# Only used in DTCWT mode.
qshift: qshift_a
# Only used in DTCWT mode.
biort: near_sym_a
# Additional parameters for the inverse wavelet operation
# are null by default and will use whatever the
# forward parameter is set to:
# inv_mode, inv_wave, inv_biort, inv_qshift
# Note: Using different parameters for the inverse wavelet
# operation may not work well (or just fail entirely).
# Scale for the lowpass filter.
yl_scale: 1.0
# Scales for the highpass filter. Can be a single value (null is basically 1.0).
yh_scales: null
```
***
### `SonarAdvancedDistroNoise`
See: https://pytorch.org/docs/stable/distributions.html
For the most part, we just pass parameters directly to PyTorch's distribution classes. Some of them have specific requirements so it is possible to set invalid parameters.
It may be more convenient to specify parameters using the `SonarCustomNoiseAdv` node than this gigantic monstrosity of a node. **Note**: In that case, pass the distribution name using `distro`, i.e. `distro: laplacian`.
Common parameters:
* `quantile_norm`: When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the distribution and how many of the values are extreme. Some actually work better with quantile normalization disabled.
* `quantile_norm_mode`: Controls what dimensions quantile normalization uses. By default, the noise is flattened first. You can try the nonflat versions but they may have a very strong row/column influence. Only applies when quantile_norm is active.
* `result_index`: When noise generation returns a batch of items, it will select the specified index. Negative indexes count from the end. Values outside the valid range will be automatically adjusted. You may enter a space-separated list of values for the case where there might be multiple added batch dimensions. Excess batch dimensions are removed from the end, indexe from result_index are used in order so you may want to enter the indexes in reverse order. Example: If your noise has shape `(1, 4, 3, 3)` and two 2-sized batch dims are added resulting in `(1, 4, 3, 3, 2, 2)` and you wanted index 0 from the first additional batch dimension and 1 from the second you would use result_index: `1 0`
Individual distributions have parameters beginning with their name, i.e. `laplacian_loc`. Parameters that are string inputs usually allow entering multiple space-separated items. This will usually result in the output noise being a batch, which can be selected with the `result_index` parameter.
Suggestions for fun distributions to try: Wishart and VonMises can produce some interesting results.
***
### `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.
### `SonarChannelNoise`
Allows using a different noise generator per channel. The custom noise items attached to this node are treated as a list where the furthest item from the node will correspond to channel 0. For example where CN is a custom noise node and SCN is the `SonarChannelNoise` node:
```plaintext
CN (channel 0) -> CN (channel 1) -> SCN
```
Don't enable `rescale` in the custom noise nodes attached to `SonarChannelNoise`. If you want a blend of noise types for a channel, you can use something like `SonarBlendedNoise`.
### `SonarBlendedNoise`
Allows blending two noise generators. If [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) is available, you will have access to many more blending modes.
### `SonarBlehOpsNoise`
Only provided if [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) is available. Allows transforming/manipulating noise with bleh blockops expressions. For instance, you can do something like:
```yaml
- ops:
- [multiply, -1]
- [roll, -2, 0.5]
```
to flip the sign on the noise and then roll dimension -2 (height) by 50%.
+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)
+189
View File
@@ -0,0 +1,189 @@
# 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).
## Documentation TBD
* `grey`
* `onef_greenish_mix` (50/50 mix of positive/negative noise.)
* `onef_greenish`
* `onef_pinkish_mix` (50/50 mix of positive/negative noise.)
* `onef_pinkish`
* `onef_pinkishgreenish` (50/50 mix of `onef_pinkish` and `onef_greenish`.)
* `velvet`
* `violet`
* `white`
## 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 Old
Previously known as `pink`. The implementation isn't correct, though in terms of results it's fine.
![Pink](../assets/example_images/noise_base_types/noise_pink.png)
***
## Power Old
Previously known as `power`. The implementation isn't correct, though in terms of results it's fine.
![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.
+125
View File
@@ -0,0 +1,125 @@
from __future__ import annotations
import contextlib
import importlib
import sys
from functools import partial
from typing import TYPE_CHECKING, Callable, NamedTuple
if TYPE_CHECKING:
from types import ModuleType
class Integrations:
class Integration(NamedTuple):
key: str
module_name: str
handler: Callable | None = None
def __init__(self):
self.initialized = False
self.modules = {}
self.init_handlers = []
self.handlers = []
def __getitem__(self, key):
return self.modules[key]
def __contains__(self, key):
return key in self.modules
def __getattr__(self, key):
return self.modules.get(key)
@staticmethod
def get_custom_node(name: str) -> ModuleType | None:
module_key = f"custom_nodes.{name}"
with contextlib.suppress(StopIteration):
spec = importlib.util.find_spec(module_key)
if spec is None:
return None
return next(
v
for v in sys.modules.copy().values()
if hasattr(v, "__spec__")
and v.__spec__ is not None
and v.__spec__.origin == spec.origin
)
return None
def register_init_handler(self, handler):
self.init_handlers.append(handler)
def register_integration(self, key: str, module_name: str, handler=None) -> None:
if self.initialized:
raise ValueError(
"Internal error: Cannot register integration after initialization",
)
if any(item[0] == key or item[1] == module_name for item in self.handlers):
errstr = (
f"Module {module_name} ({key}) already in integration handlers list!"
)
raise ValueError(errstr)
self.handlers.append(self.Integration(key, module_name, handler))
def initialize(self) -> None:
if self.initialized:
return
self.initialized = True
for ih in self.handlers:
module = self.get_custom_node(ih.module_name)
if module is None:
continue
if ih.handler is not None:
module = ih.handler(module)
if module is not None:
self.modules[ih.key] = module
for init_handler in self.init_handlers:
init_handler(self)
class SonarIntegrations(Integrations):
def __init__(self, *args: list, **kwargs: dict):
super().__init__(*args, **kwargs)
self.register_integration("bleh", "ComfyUI-bleh", self.bleh_integration)
self.register_integration(
"restart",
"ComfyUI_restart_sampling",
self.restart_integration,
)
@classmethod
def bleh_integration(cls, module: ModuleType) -> ModuleType | None:
bleh_version = getattr(module, "BLEH_VERSION", -1)
if bleh_version < 1:
return None
return module
@classmethod
def restart_integration(cls, module: ModuleType) -> ModuleType | None:
if hasattr(module, "restart_sampling") and hasattr(
module.restart_sampling,
"DEFAULT_SEGMENTS",
):
return module
return None
MODULES = SonarIntegrations()
class IntegratedNode(type):
@staticmethod
def wrap_INPUT_TYPES(orig_method: Callable, *args: list, **kwargs: dict) -> dict:
MODULES.initialize()
return orig_method(*args, **kwargs)
def __new__(cls: type, name: str, bases: tuple, attrs: dict) -> object:
obj = type.__new__(cls, name, bases, attrs)
if hasattr(obj, "INPUT_TYPES"):
obj.INPUT_TYPES = partial(cls.wrap_INPUT_TYPES, obj.INPUT_TYPES)
return obj
__all__ = ("MODULES",)
+429
View File
@@ -0,0 +1,429 @@
from __future__ import annotations
import torch
from . import utils
from .external import IntegratedNode
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)
class FreeUExtremeConfigNode(metaclass=IntegratedNode):
DESCRIPTION = "Allows setting configuration for FreeU Extreme."
RETURN_TYPES = ("FRUX_CONFIG",)
FUNCTION = "go"
CATEGORY = "model_patches"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"stage_1": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether this configuration applies to stage 1.",
},
),
"stage_2": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether this configuration applies to stage 2.",
},
),
"stage_3": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether this configuration applies to stage 3.",
},
),
"target": (
("backbone", "skip", "both"),
{
"tooltip": "Controls whether this filter applies to backbone or skip layers (or both).",
},
),
"start": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Start time as percentage of sampling this configuration applies to. Inclusive.",
},
),
"end": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "End time as percentage of sampling this configuration applies to. Inclusive.",
},
),
"slice": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Percentage of the layer the FreeU effect is applied to.",
},
),
"slice_offset": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Offset as a percentage the layer is applied to. For example if slice is 0.25 and slice_offset is 0.25 then the filter will apply to the range 25% through 50%.",
},
),
"filter_norm": (
"FLOAT",
{
"default": 0.0,
"min": -10.0,
"max": 10.0,
"step": 0.1,
"round": False,
"tooltip": "Normalization factor applied to the filter. 1.0 means 100% normalized.",
},
),
"scale": (
"FLOAT",
{
"default": 1,
"min": -100.0,
"max": 100.0,
"step": 0.1,
"round": False,
"tooltip": "Strength of the effects applied by this configuration.",
},
),
"blend": (
"FLOAT",
{
"default": 1.0,
"min": -10.0,
"max": 10.0,
"step": 0.1,
"round": False,
"tooltip": "Blends the filtered result based on the specified strength where 1.0 means 100% filtered.",
},
),
"blend_mode": (
tuple(utils.BLENDING_MODES.keys()),
{
"tooltip": "Mode used when blending. Generally only has an effect when blend is set to values other than 0 or 1",
},
),
"hidden_mean": (
"BOOLEAN",
{
"default": True,
"tooltip": "You can think of this as FreeU V2 mode.",
},
),
"final": (
"BOOLEAN",
{
"default": True,
"tooltip": "When enabled, other configurations won't be considered if this one matched. Otherwise, multiple configurations/filter effects can be stacked.",
},
),
},
"optional": {
"sonar_power_filter_opt": (
"SONAR_POWER_FILTER",
{
"tooltip": "Optionally attach a Power Filter here to set filtering parameters.",
},
),
"frux_config_opt": (
"FRUX_CONFIG",
{
"tooltip": "Optionally attach another configuration node here.",
},
),
},
}
@classmethod
def go(cls, **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
return not self.target not in {"skip" if is_skip else "backbone", "both"}
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 utils.BLENDING_MODES[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):
meh = {k: getattr(self, k) for k in self._keys}
return f"<FRUXConfig: {meh}>"
class FreeUExtremeNode(metaclass=IntegratedNode):
DESCRIPTION = "Main FreeU Extreme node. Allows patching a model with the FreeU (V2) effect with more control."
RETURN_TYPES = ("MODEL",)
FUNCTION = "go"
CATEGORY = "model_patches"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": (
"MODEL",
{
"tooltip": "Model to patch.",
},
),
"cpu_fft": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether to perform FFT calculations on the CPU. May be necessary for some GPUs that don't have native support for FFT operations at the cost of performance.",
},
),
},
"optional": {
"input_config": (
"FRUX_CONFIG",
{
"tooltip": "Allows specifying configuration for input blocks.",
},
),
"middle_config": (
"FRUX_CONFIG",
{
"tooltip": "Allows specifying configuration for middle blocks.",
},
),
"output_config": (
"FRUX_CONFIG",
{
"tooltip": "Allows specifying configuration for output blocks.",
},
),
},
}
@classmethod
def go(
cls,
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,)
+2112 -161
View File
File diff suppressed because it is too large Load Diff
+1404 -389
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+709 -96
View File
@@ -1,36 +1,199 @@
# 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 (
NOISE_INPUT_TYPES_HINT,
WILDCARD_NOISE,
SonarCustomNoiseNodeBase,
SonarNormalizeNoiseNodeMixin,
)
from .noise import CustomNoiseItemBase
from .utils 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).index_put_(
tuple(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: PowerFilter | None = 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 +255,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 +367,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,15 +457,100 @@ 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):
DESCRIPTION = "Custom noise type that applies a filter to generated noise."
@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}),
"time_brownian": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether brownian noise is used when mix isn't 1.0.",
},
),
"alpha": (
"FLOAT",
{
@@ -221,6 +559,7 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 5.0,
"step": 0.001,
"round": False,
"tooltip": "Values above 0 will amplify low frequencies, negative values will amplify high frequencies.",
},
),
"max_freq": (
@@ -231,6 +570,7 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 0.7071,
"step": 0.001,
"round": False,
"tooltip": "Maximum frequency to pass through the filter.",
},
),
"min_freq": (
@@ -241,6 +581,7 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 0.7071,
"step": 0.001,
"round": False,
"tooltip": "Minimum frequency to pass through the filter.",
},
),
"stretch": (
@@ -251,6 +592,7 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 100,
"step": 0.1,
"round": False,
"tooltip": "Stretches the filter's shape by the specified factor.",
},
),
"rotate": (
@@ -261,6 +603,7 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 90,
"step": 5,
"round": False,
"tooltip": "Rotates the filter.",
},
),
"pnorm": (
@@ -271,6 +614,7 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 100,
"step": 0.1,
"round": False,
"tooltip": "Factor used for cushioning the band-pass region.",
},
),
"mix": (
@@ -281,23 +625,40 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 1.0,
"step": 0.001,
"round": False,
"tooltip": "Controls the ratio of filtered noise. For example, 0.75 means 75% noise with the filter effects applied, 25% raw noise.",
},
),
"common_mode": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 1.0,
"min": -100.0,
"max": 100.0,
"step": 0.001,
"round": False,
"tooltip": "Attempts to desaturate thelatent by injecting the average across channels (controlled by channel_correction). Applied after mix.",
},
),
"channel_correlation": (
"STRING",
{
"default": "1, 1, 1, 1, 1, 1",
"multiline": False,
"dynamicPrompts": False,
"tooltip": "Comma-separated list of channel correlation strengths.",
},
),
"preview": (
("none", "no_mix", "mix"),
{
"tooltip": "When enabled, displays a preview of the filter shape and a sample of noise. Mix - previews noise after mix is applied. no_mix - only previews the filtered noise.",
},
),
"preview": (["none", "no_mix", "mix"],),
}
return result
def get_item_class(self):
@classmethod
def get_item_class(cls):
return PowerNoiseItem
def go(
@@ -310,24 +671,276 @@ 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):
DESCRIPTION = "Custom noise type that allows applying a Power Filter to another custom noise generator."
@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": (
WILDCARD_NOISE,
{
"tooltip": f"Custom noise type to filter.\n{NOISE_INPUT_TYPES_HINT}",
},
),
"sonar_power_filter": (
"SONAR_POWER_FILTER",
{
"tooltip": "Filter to use.",
},
),
"filter_norm_factor": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Normalization factor applied to the specified filter. 1.0 means 100% normalized.",
},
),
"normalize_result": (
("default", "forced", "disabled"),
{
"tooltip": "Controls whether the final result is normalized to 1.0 strength.",
},
),
"normalize_noise": (
("default", "forced", "disabled"),
{
"tooltip": "Controls whether the generated noise is normalized to 1.0 strength.",
},
),
}
result["required"]["preview"] = (
(*result["required"]["preview"][0], "custom"),
{
"tooltip": "When enabled, displays a preview of the filter shape and a sample of noise. Mix - previews noise after mix is applied. no_mix - only previews the filtered noise. custom - Like no_mix, but will use a latent previewer to display a color preview of the generated noise. Works best when previewer is set to TAESD.",
},
)
return result
@classmethod
def get_item_class(cls):
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):
include_keys = {"alpha", "max_freq", "min_freq", "stretch", "rotate", "pnorm"}
return {
"required": {
k: v
for k, v in SonarPowerNoiseNode.INPUT_TYPES()["required"].items()
if k in include_keys
}
| {
"oversample": (
"INT",
{
"default": 4,
"min": 1,
"max": 128,
"tooltip": "Oversampling factor used for the filter size.",
},
),
"blur": (
"FLOAT",
{
"default": 0.125,
"min": -10.0,
"max": 10.0,
"step": 0.01,
"round": False,
"tooltip": "Slightly blurs the filter to reduce artifacts.",
},
),
"scale": (
"FLOAT",
{
"default": 1,
"min": -100.0,
"max": 100.0,
"step": 0.1,
"round": False,
"tooltip": "Scales the filter to the specified strength. May be negative.",
},
),
"compose_mode": (
("max", "min", "add", "sub", "mul"),
{
"tooltip": "Controls composition of the option attached filter. For example, when set to MUL the result will be this filter multiplied by the attached filter. No effect if the optional filter input is not attached.",
},
),
},
"optional": {
"power_filter_opt": ("SONAR_POWER_FILTER",),
},
}
@classmethod
def go(
cls,
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:
DESCRIPTION = "Allows previewing a Power Filter."
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",
{
"tooltip": "Power Filter to preview.",
},
),
"filter_gain": (
"FLOAT",
{
"default": 1 / 3,
"min": 0.0,
"max": 1000000.0,
"step": 0.1,
"round": False,
"tooltip": "Gain factor applied to the filter part of the preview.",
},
),
"kernel_gain": (
"FLOAT",
{
"default": 1 / 3,
"min": 0.0,
"max": 1000000.0,
"step": 0.1,
"round": False,
"tooltip": "Gain factor applied to the kernel part of the preview.",
},
),
"norm_factor": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Normalization factor applied to the filter before previewing. 1.0 means 100% normalized.",
},
),
"preview_size": (
(
"128x128",
"256x256",
"384x256",
"256x384",
"768x512",
"512x768",
"768x768",
"128x127",
"127x128",
),
{
"tooltip": "Controls the size of the generated preview. Note: Sizes are in latent pixels. For most models, one latent pixel equals eight pixels",
},
),
},
}
@classmethod
def go(
cls,
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,),
)
+433 -230
View File
@@ -2,21 +2,26 @@
from __future__ import annotations
import importlib
from enum import Enum, auto
from functools import lru_cache
from sys import stderr
from typing import Any, Callable, NamedTuple
import torch
from comfy.k_diffusion import sampling
from comfy.k_diffusion.sampling import get_ancestral_step, to_d
from comfy.samplers import KSampler, k_diffusion_sampling
from torch import Tensor
from tqdm.auto import trange
from . import noise
from . import noise, utils
class HistoryType(Enum):
ZERO = auto()
RAND = auto()
SAMPLE = auto()
SAMPLE_NORM = auto()
class GuidanceType(Enum):
@@ -32,38 +37,114 @@ class GuidanceConfig(NamedTuple):
latent: Tensor | None = None
class MomentumMode(Enum):
CLASSIC = auto()
NEW = auto()
DENOISED = auto()
class SonarConfig(NamedTuple):
momentum: float = 0.95
momentum_hist: float = 0.75
direction: float = 1.0
momentum_start_step: int = 0
momentum_end_step: int = 9999
always_update_history: bool = True
momentum_mode: MomentumMode = MomentumMode.NEW
init: HistoryType = HistoryType.ZERO
noise_type: noise.NoiseType | None = None
custom_noise: noise.CustomNoise | None = None
rand_init_noise_type: noise.NoiseType | None = None
rand_init_noise_multiplier: float | int = 1.0
guidance: GuidanceConfig | None = None
blend_mode: str = "lerp"
momentum_blend_mode: str | None = None
history_blend_mode: str | None = None
guidance_blend_mode: str | None = None
def get_with_default(self, k: str, default: Any) -> Any: # noqa: ANN401
val = getattr(self, k)
return val if val is not None else default
class SonarBase:
DEFAULT_NOISE_TYPE = noise.NoiseType.GAUSSIAN
def __init__(self, cfg: SonarConfig) -> None:
self.history_d = None
self.cfg = cfg
self.noise_sampler = None
blend_mode = cfg.blend_mode
momentum_blend_mode = cfg.get_with_default("momentum_blend_mode", blend_mode)
history_blend_mode = cfg.get_with_default("history_blend_mode", blend_mode)
guidance_blend_mode = cfg.get_with_default("guidance_blend_mode", blend_mode)
bf = self.blend = utils.BLENDING_MODES[blend_mode]
self.momentum_blend = (
bf
if momentum_blend_mode == blend_mode
else utils.BLENDING_MODES[momentum_blend_mode]
)
self.history_blend = (
bf
if history_blend_mode == blend_mode
else utils.BLENDING_MODES[history_blend_mode]
)
self.guidance_blend = (
bf
if guidance_blend_mode == blend_mode
else utils.BLENDING_MODES[guidance_blend_mode]
)
_cfg_fixups = (
("momentum_mode", MomentumMode),
("init", HistoryType),
("noise_type", noise.NoiseType),
)
@classmethod
def get_config(
cls,
cfg: SonarConfig | None = None,
ext: dict | None = None,
) -> SonarConfig:
cfgdict = ext.copy() if ext is not None else {}
empty = object()
for k, enum_class in cls._cfg_fixups:
val = cfgdict.get(k, empty)
if val is empty:
continue
if isinstance(val, str):
val = getattr(enum_class, val.strip().upper(), empty)
if val is empty:
validstr = ", ".join(enum_class.__members__.keys())
errstr = f"Bad value for {k} of type enum {enum_class.__name__}, must be one of the following: {validstr}"
raise ValueError(errstr)
cfgdict[k] = val
continue
if not isinstance(val, enum_class):
errstr = f"Bad parameter type for {k}: Must be valid string or instance of {enum_class.__name__}"
raise TypeError(errstr)
if cfg is None:
return SonarConfig(**cfgdict)
cfgdict = cfg._asdict() | cfgdict
return SonarConfig(**cfgdict)
def set_noise_sampler(
self,
x: Tensor,
sigmas,
sigmas: Tensor,
noise_sampler: Callable | None,
seed: int | None = None,
):
) -> Callable:
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
if noise_sampler is not None and self.cfg.noise_type not in (
if noise_sampler is not None and self.cfg.noise_type not in {
None,
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",
self.DEFAULT_NOISE_TYPE,
}:
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,61 +153,171 @@ 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
def init_hist_d(self, x: Tensor) -> None:
if self.history_d is not None:
def init_hist_d(
self,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
*,
step: int,
) -> None:
if self.history_d is not None or not self.check_step(step, is_history=True):
return
cfg = self.cfg
init = cfg.init
# memorize delta momentum
if self.cfg.init == HistoryType.ZERO:
self.history_d = 0
elif self.cfg.init == HistoryType.SAMPLE:
self.history_d = x
elif self.cfg.init == HistoryType.RAND:
if init == HistoryType.ZERO:
self.history_d = None
elif init == HistoryType.SAMPLE:
self.history_d = (
x if cfg.momentum_mode != MomentumMode.DENOISED else denoised
)
elif init == HistoryType.SAMPLE_NORM:
self.history_d = (
x if cfg.momentum_mode != MomentumMode.DENOISED else denoised
) / sigma
elif init == HistoryType.RAND:
ns = noise.get_noise_sampler(
self.cfg.rand_init_noise_type,
cfg.rand_init_noise_type,
x,
None,
None,
seed=self.extra_args.get("seed"),
cpu=True,
normalized=True,
)
self.history_d = ns(None, None)
if cfg.rand_init_noise_multiplier != 1:
self.history_d *= cfg.rand_init_noise_multiplier
else:
raise ValueError("Sonar sampler: bad history type")
def update_hist(self, momentum_d):
q = 1.0 - self.cfg.momentum_hist
@property
@lru_cache(maxsize=1) # noqa: B019
def history_ratios(self):
direction = self.cfg.direction
momentum_hist = self.cfg.momentum_hist
return (
momentum_hist,
1.0 + abs(direction) * (1 - momentum_hist)
if direction < 0
else 2.0 - direction,
direction,
)
def check_step(self, step: int, *, is_history: bool = False):
cfg = self.cfg
if is_history and cfg.always_update_history:
return True
return cfg.momentum_start_step <= step <= cfg.momentum_end_step
def update_hist(self, momentum_d: torch.Tensor, step: int) -> None:
hd, cfg = self.history_d, self.cfg
if cfg.momentum_hist == 1 or not self.check_step(step, is_history=True):
return
hd_ratio, hd_scale, md_scale = self.history_ratios
self.history_d = (
momentum_d
if hd is None
else self.history_blend(momentum_d * md_scale, hd * hd_scale, hd_ratio)
)
def momentum_mix(
self,
history: Tensor | None,
item: Tensor,
sigma: Tensor,
*,
is_denoised: bool = False,
momentum=None,
) -> Tensor:
momentum = self.cfg.momentum if momentum is None else momentum
mode = self.cfg.momentum_mode
if (
momentum == 1 # noqa: PLR0916
or history is None
or (mode == MomentumMode.DENOISED and not is_denoised)
or (mode != MomentumMode.DENOISED and is_denoised)
):
return item
return self.momentum_blend(
history * sigma if is_denoised else history,
item,
momentum,
)
def get_momentum_denoised(
self,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
*,
step: int,
momentum: float | None = None,
update_history=True,
) -> Tensor:
hd = self.history_d
if isinstance(hd, int) and hd == 0:
self.history_d = momentum_d
else:
self.history_d = (1.0 - q) * hd + q * momentum_d
momentum_denoised = self.momentum_mix(
hd,
denoised,
sigma,
is_denoised=True,
momentum=momentum,
)
if update_history:
self.init_hist_d(x, denoised, sigma, step=step)
self.update_hist(denoised / sigma, step=step)
return momentum_denoised if self.check_step(step) else denoised
def momentum_step(self, x: Tensor, d: Tensor, dt: Tensor):
if self.cfg.momentum == 1.0:
return x + d * dt
def get_momentum_d(
self,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
*,
step: int,
momentum: float | None = None,
d: Tensor | None = None,
update_history=True,
) -> Tensor:
hd = self.history_d
# correct current `d` with momentum
p = (1.0 - self.cfg.momentum) * self.cfg.direction
momentum_d = (1.0 - p) * d + p * hd
cfg = self.cfg
momentum = cfg.momentum if momentum is None else momentum
mode = cfg.momentum_mode
d = to_d(x, sigma, denoised) if d is None else d
if momentum == 1 or mode == MomentumMode.DENOISED:
return d
momentum_d = self.momentum_mix(hd, d, sigma)
if update_history:
self.init_hist_d(x, denoised, sigma, step=step)
self.update_hist(d if mode == MomentumMode.NEW else momentum_d, step=step)
return momentum_d if self.check_step(step) else d
# Euler method with momentum
x = x + momentum_d * dt
self.update_hist(momentum_d)
return x
def momentum_step(
self,
step: int,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
sigma_down: Tensor,
) -> Tensor:
dt = sigma_down - sigma
denoised = self.get_momentum_denoised(x, denoised, sigma, step=step)
momentum_d = self.get_momentum_d(x, denoised, sigma, step=step)
return (momentum_d * dt).add_(x)
class SonarGuidanceMixin:
@@ -145,46 +336,67 @@ class SonarGuidanceMixin:
def prepare_ref_latent(latent: Tensor | None) -> Tensor:
if latent is None:
return None
avg_s = latent.mean(dim=[2, 3], keepdim=True)
std_s = latent.std(dim=[2, 3], keepdim=True)
return ((latent - avg_s) / std_s).to(latent.dtype)
avg_s = latent.mean(dim=(-2, -1), keepdim=True)
std_s = latent.std(dim=(-2, -1), keepdim=True)
return (latent - avg_s).div_(std_s).to(latent.dtype)
def guidance_step(self, step_index: int, x: Tensor, denoised: Tensor):
if (self.guidance is None or self.guidance.factor == 0.0) or not (
self.guidance.start_step <= (step_index + 1) <= self.guidance.end_step
def guidance_step(self, step_index: int, x: Tensor, denoised: Tensor) -> Tensor:
if (
self.guidance is None
or self.guidance.factor == 0.0
or not self.guidance.start_step <= step_index <= self.guidance.end_step
):
return x
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,
blend=self.guidance_blend,
)
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,
):
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_latent: Tensor,
factor: float = 0.2,
) -> Tensor:
avg_t = denoised.mean(dim=(-3, -2, -1), keepdim=True)
std_t = denoised.std(dim=(-3, -2, -1), keepdim=True)
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
return x + d * dt
d = to_d(x, sigma, ref_img_shift)
dt = (sigma_next - sigma) * factor
return (d * dt).add_(x)
@staticmethod
def guidance_linear(
self,
x: 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_latent: Tensor,
factor: float = 0.2,
*,
blend=torch.lerp,
) -> Tensor:
avg_t = x.mean(dim=(-3, -2, -1), keepdim=True)
std_t = x.std(dim=(-3, -2, -1), keepdim=True)
ref_img_shift = (ref_latent * std_t).add_(avg_t)
return blend(x, ref_img_shift, factor)
class SonarWithGuidance(SonarBase, SonarGuidanceMixin):
@@ -197,9 +409,9 @@ class SonarSampler(SonarWithGuidance):
def __init__(
self,
model,
sigmas,
s_in,
extra_args,
sigmas: Tensor,
s_in: Tensor,
extra_args: dict[str, Any],
*args: list[Any],
**kwargs: dict[str, Any],
):
@@ -209,62 +421,49 @@ class SonarSampler(SonarWithGuidance):
self.s_in = s_in
self.extra_args = extra_args
def call_model(
self,
x: Tensor,
sigma: Tensor,
*args: list[Any],
s_in=None,
extra_args=None,
) -> Tensor:
if s_in is None:
s_in = self.s_in
extra_args = (
self.extra_args if extra_args is None else self.extra_args | extra_args
)
return self.model(x, sigma * s_in, *args, **extra_args)
class SonarEuler(SonarSampler):
def __init__(
self,
s_churn: float = 0.0,
s_tmin: float = 0.0,
s_tmax: float = float("inf"),
s_noise: float = 1.0,
*args: list[Any],
**kwargs: dict[str, Any],
):
super().__init__(*args, **kwargs)
self.s_churn = s_churn
self.s_tmin = s_tmin
self.s_tmax = s_tmax
self.s_noise = s_noise
def step(
self,
step_index: int,
sample: torch.FloatTensor,
):
self.init_hist_d(sample)
def step(self, step_index: int, sample: torch.FloatTensor):
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma, sigma_to = self.sigmas[step_index], self.sigmas[step_index + 1]
gamma = (
min(self.s_churn / (len(self.sigmas) - 1), 2**0.5 - 1)
if self.s_tmin <= sigma <= self.s_tmax
else 0.0
denoised = self.call_model(sample, sigma)
result_sample = self.momentum_step(
step_index,
sample,
denoised,
sigma,
sigma_next,
)
sigma_hat = sigma * (gamma + 1)
if gamma > 0:
noise = (
self.noise_sampler(sigma, sigma_to)
if self.noise_sampler
else torch.randn_like(sample)
)
eps = noise * self.s_noise
sample = sample + eps * (sigma_hat**2 - sigma**2) ** 0.5
denoised = self.model(sample, sigma_hat * self.s_in, **self.extra_args)
derivative = sampling.to_d(sample, sigma, denoised)
dt = self.sigmas[step_index + 1] - sigma_hat
result_sample = self.momentum_step(sample, derivative, dt)
if self.sigmas[step_index + 1] > 0:
if sigma_next > 0:
result_sample = self.guidance_step(step_index, result_sample, denoised)
return (
result_sample,
sigma,
sigma_hat,
sigma,
denoised,
)
@@ -273,26 +472,18 @@ class SonarEuler(SonarSampler):
def sampler(
cls,
model,
x,
sigmas,
extra_args=None,
x: Tensor,
sigmas: Tensor,
extra_args: dict | None = None,
callback=None,
disable=None,
disable: bool | None = None, # noqa: FBT001
noise_sampler: Callable | None = None,
sonar_config=None,
s_churn=0.0,
s_tmin=0.0,
s_tmax=float("inf"),
s_noise=1.0,
):
if sonar_config is None:
sonar_config = SonarConfig()
s_in = x.new_ones([x.shape[0]])
sonar_config: SonarConfig | None = None,
sonar_params: dict | None = None,
) -> Tensor:
sonar_config = cls.get_config(sonar_config, sonar_params)
s_in = x.new_ones((x.shape[0],))
sonar = cls(
s_churn,
s_tmin,
s_tmax,
s_noise,
model,
sigmas,
s_in,
@@ -316,7 +507,7 @@ class SonarEuler(SonarSampler):
{
"x": x,
"i": i,
"sigma": sigmas[i],
"sigma": sigma,
"sigma_hat": sigma_hat,
"denoised": denoised,
},
@@ -341,31 +532,32 @@ class SonarEulerAncestral(SonarSampler):
step_index: int,
sample: torch.FloatTensor,
):
self.init_hist_d(sample)
sigma_from, sigma_to = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma_down, sigma_up = sampling.get_ancestral_step(
sigma_from,
sigma_to,
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma_down, sigma_up = get_ancestral_step(
sigma,
sigma_next,
eta=self.eta,
)
denoised = self.model(sample, sigma_from * self.s_in, **self.extra_args)
derivative = sampling.to_d(sample, sigma_from, denoised)
dt = sigma_down - sigma_from
result_sample = self.momentum_step(sample, derivative, dt)
if sigma_to > 0:
denoised = self.call_model(sample, sigma)
result_sample = self.momentum_step(
step_index,
sample,
denoised,
sigma,
sigma_down,
)
if sigma_next > 0:
result_sample = self.guidance_step(step_index, result_sample, denoised)
result_sample = (
result_sample = ( # noqa: PLR6104
result_sample
+ self.noise_sampler(sigma_from, sigma_to) * self.s_noise * sigma_up
+ self.noise_sampler(sigma, sigma_next) * (self.s_noise * sigma_up)
)
return (
result_sample,
sigma_from,
sigma_from,
sigma,
sigma,
denoised,
)
@@ -379,22 +571,14 @@ class SonarEulerAncestral(SonarSampler):
extra_args=None,
callback=None,
disable=None,
sonar_config=None,
sonar_config: SonarConfig | None = None,
sonar_params: dict | None = None,
eta=1.0,
s_noise=1.0,
noise_sampler: Callable | None = None,
):
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_config = cls.get_config(sonar_config, sonar_params)
s_in = x.new_ones((x.shape[0],))
sonar = cls(
eta,
s_noise,
@@ -412,7 +596,7 @@ class SonarEulerAncestral(SonarSampler):
)
for i in trange(len(sigmas) - 1, disable=disable):
x, sigma, sigma_hat, denoised = sonar.step(
x, _sigma, sigma_hat, denoised = sonar.step(
i,
x,
)
@@ -430,6 +614,8 @@ class SonarEulerAncestral(SonarSampler):
class SonarDPMPPSDE(SonarSampler):
DEFAULT_NOISE_TYPE = noise.NoiseType.BROWNIAN
def __init__(
self,
eta: float = 1.0,
@@ -442,104 +628,134 @@ class SonarDPMPPSDE(SonarSampler):
self.s_noise = s_noise
@staticmethod
def sigma_fn(t) -> float:
def sigma_fn(t: Tensor) -> float:
return t.neg().exp()
@staticmethod
def t_fn(sigma) -> float:
return sigma.log.neg()
def t_fn(sigma: Tensor) -> float:
return sigma.log().neg()
# DPM++ solver algorithm copied from ComfyUI source.
def momentum_step(
def momentum_step( # noqa: PLR0914
self,
step_index,
step_index: int,
x: Tensor,
denoised: Tensor,
sigma_from,
sigma_to,
sigma_down,
):
if sigma_to == 0:
derivative = sampling.to_d(x, sigma_from, denoised)
dt = sigma_down - sigma_from
return super().momentum_step(x, derivative, dt)
sigma: Tensor,
sigma_next: Tensor,
sigma_down: Tensor,
) -> Tensor:
if sigma_next == 0:
return super().momentum_step(step_index, x, denoised, sigma, sigma_down)
def sigma_fn(t):
return t.neg().exp()
def t_fn(sigma):
return sigma.log().neg()
hd = self.history_d
p = (1.0 - self.cfg.momentum) * self.cfg.direction
cfg = self.cfg
# Halve the momentum proportion if there's history since we will use it twice.
adjusted_momentum = (
cfg.momentum + (1 - cfg.momentum) / 2
if self.history_d is not None
else cfg.momentum
)
r = 1 / 2
# DPM-Solver++
t, t_next = t_fn(sigma_from), t_fn(sigma_to)
t, t_next = self.t_fn(sigma), self.t_fn(sigma_next)
h = t_next - t
s = t + h * r
fac = 1 / (2 * r)
# Step 1
sd, su = sampling.get_ancestral_step(sigma_fn(t), sigma_fn(s), self.eta)
s_ = t_fn(sd)
diff_2 = (t - s_).expm1() * denoised
momentum_d = (1.0 - p) * diff_2 + p * hd
self.update_hist(momentum_d)
hd = self.history_d
x_2 = (sigma_fn(s_) / sigma_fn(t)) * x - momentum_d
x_2 = x_2 + self.noise_sampler(sigma_fn(t), sigma_fn(s)) * self.s_noise * su
denoised_2 = self.model(x_2, sigma_fn(s) * self.s_in, **self.extra_args)
# Step 2
sd, su = sampling.get_ancestral_step(
sigma_fn(t),
sigma_fn(t_next),
s_t, s_s = self.sigma_fn(t), self.sigma_fn(s)
sd, su = get_ancestral_step(
s_t,
s_s,
self.eta,
)
t_next_ = t_fn(sd)
denoised_d = (1 - fac) * denoised + fac * denoised_2
diff_1 = (t - t_next_).expm1() * denoised_d
momentum_d = (1.0 - p) * diff_1 + p * hd
self.update_hist(momentum_d)
x = (sigma_fn(t_next_) / sigma_fn(t)) * x - momentum_d
s_ = self.t_fn(sd)
momentum_denoised = self.get_momentum_denoised(
x,
denoised,
sigma,
step=step_index,
)
diff_2 = (t - s_).expm1() * momentum_denoised
momentum_d = self.get_momentum_d(
x,
momentum_denoised,
sigma,
step=step_index,
momentum=adjusted_momentum,
d=diff_2,
)
x_2 = ((self.sigma_fn(s_) / s_t) * x).sub_(momentum_d)
x_2 += self.noise_sampler(s_t, s_s).mul_(
self.s_noise * su,
)
sigma_2 = s_s
denoised_2 = self.call_model(x_2, sigma_2)
momentum_denoised_2 = self.get_momentum_denoised(
x,
denoised_2,
sigma_2,
step=step_index,
)
# Step 2
s_t_next = self.sigma_fn(t_next)
sd, su = get_ancestral_step(
s_t,
s_t_next,
self.eta,
)
t_down = self.t_fn(sd)
denoised_d = (1 - fac) * momentum_denoised + fac * momentum_denoised_2
diff_1 = (t - t_down).expm1() * denoised_d
momentum_d = self.get_momentum_d(
x,
momentum_denoised_2,
sigma_2,
step=step_index,
momentum=adjusted_momentum,
d=diff_1,
)
x = ((self.sigma_fn(t_down) / s_t) * x).sub_(momentum_d)
x = self.guidance_step(step_index, x, denoised_d)
return x + self.noise_sampler(sigma_fn(t), sigma_fn(t_next)) * self.s_noise * su
x += self.noise_sampler(s_t, s_t_next).mul_(
self.s_noise * su,
)
return x
def step(
self,
step_index: int,
sample: torch.FloatTensor,
):
) -> Tensor:
def sigma_fn(t):
return t.neg().exp()
def t_fn(sigma):
return sigma.log().neg()
self.init_hist_d(sample)
sigma_from, sigma_to = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma_down, sigma_up = sampling.get_ancestral_step(
sigma_from,
sigma_to,
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma_down, _sigma_up = get_ancestral_step(
sigma,
sigma_next,
eta=self.eta,
)
denoised = self.model(sample, sigma_from * self.s_in, **self.extra_args)
denoised = self.call_model(sample, sigma)
result_sample = self.momentum_step(
step_index,
sample,
denoised,
sigma_from,
sigma_to,
sigma,
sigma_next,
sigma_down,
)
return (
result_sample,
sigma_from,
sigma_from,
sigma,
sigma,
denoised,
)
@@ -548,28 +764,19 @@ class SonarDPMPPSDE(SonarSampler):
def sampler(
cls,
model,
x,
sigmas,
extra_args=None,
x: Tensor,
sigmas: Tensor,
extra_args: dict | None = None,
callback=None,
disable=None,
sonar_config=None,
disable: bool | None = None, # noqa: FBT001
sonar_config: SonarConfig | None = None,
sonar_params: dict | None = None,
eta=1.0,
s_noise=1.0,
noise_sampler=None,
):
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]])
) -> Tensor:
sonar_config = cls.get_config(sonar_config, sonar_params)
s_in = x.new_ones((x.shape[0],))
sonar = cls(
eta,
s_noise,
@@ -587,7 +794,7 @@ class SonarDPMPPSDE(SonarSampler):
)
for i in trange(len(sigmas) - 1, disable=disable):
x, sigma, sigma_hat, denoised = sonar.step(
x, _sigma, sigma_hat, denoised = sonar.step(
i,
x,
)
@@ -604,11 +811,7 @@ class SonarDPMPPSDE(SonarSampler):
return x
def add_samplers():
import importlib
from comfy.samplers import KSampler, k_diffusion_sampling
def add_samplers() -> None:
extra_samplers = {
"sonar_euler": SonarEuler.sampler,
"sonar_euler_ancestral": SonarEulerAncestral.sampler,
+196
View File
@@ -0,0 +1,196 @@
from __future__ import annotations
import math
import torch
from comfy.model_management import device_supports_non_blocking
from comfy.utils import common_upscale
from .external import MODULES as EXT
BLENDING_MODES = {"lerp": torch.lerp}
UPSCALE_METHODS = (
"bilinear",
"nearest-exact",
"nearest",
"area",
"bicubic",
"bislerp",
)
def scale_samples(
samples: torch.Tensor,
width: int,
height: int,
*,
mode: str = "bicubic",
) -> torch.Tensor:
return common_upscale(samples, width, height, mode, None)
def init_integrations(integrations) -> None:
global scale_samples, BLENDING_MODES, UPSCALE_METHODS # noqa: PLW0603
bleh = integrations.bleh
if bleh is None:
return
bleh_latentutils = bleh.py.latent_utils
BLENDING_MODES = bleh_latentutils.BLENDING_MODES
UPSCALE_METHODS = bleh_latentutils.UPSCALE_METHODS
scale_samples = bleh_latentutils.scale_samples
EXT.register_init_handler(init_integrations)
def scale_noise(
noise: torch.Tensor,
factor: float = 1.0,
*,
normalized: bool = True,
threshold_std_devs: float = 2.5,
normalize_dims: tuple | None = None,
) -> torch.Tensor:
numel = noise.numel()
if not normalized or numel == 0:
return noise.mul_(factor) if factor != 1 else noise
if normalize_dims is not None:
std = noise.std(dim=normalize_dims, keepdim=True)
noise = noise / std # noqa: PLR6104
return noise.sub_(noise.mean(dim=normalize_dims, keepdim=True)).mul_(factor)
mean, std = noise.mean().item(), noise.std().item()
threshold = threshold_std_devs / math.sqrt(numel)
if abs(mean) > threshold:
noise -= mean
if abs(1.0 - std) > threshold:
noise /= std
return noise.mul_(factor) if factor != 1 else noise
CAN_NONBLOCK = {}
def tensor_to(
tensor: torch.Tensor,
dest: torch.Tensor | torch.Device | str,
) -> torch.Tensor:
device = dest.device if isinstance(dest, torch.Tensor) else dest
non_blocking = CAN_NONBLOCK.get(device)
if non_blocking is None:
non_blocking = device_supports_non_blocking(device)
CAN_NONBLOCK[device] = non_blocking
return tensor.to(dest, non_blocking=non_blocking)
def quantile_normalize(
noise: torch.Tensor,
*,
quantile: float = 0.75,
dim: int | None = 1,
flatten: bool = True,
nq_fac: float = 1.0,
pow_fac: float = 0.5,
) -> torch.Tensor:
if quantile is None or quantile <= 0 or quantile >= 1:
return noise
orig_shape = noise.shape
if isinstance(quantile, (tuple, list)):
quantile = torch.tensor(
quantile,
device=noise.device,
dtype=noise.dtype,
)
qdim = dim
if noise.ndim > 1 and flatten:
if qdim is not None and qdim >= noise.ndim:
qdim = 1 if noise.ndim > 2 else None
if qdim is None:
flatdim = 0
elif qdim in {0, 1}:
flatdim = qdim + 1
elif qdim in {2, 3}:
noise = noise.movedim(qdim, 1)
tempshape = noise.shape
flatdim = 2
else:
raise ValueError(
"Cannot handling quantile normalization flattening dims > 3",
)
else:
flatdim = None
nq = torch.quantile(
(noise if flatdim is None else noise.flatten(start_dim=flatdim)).abs(),
quantile,
dim=-1,
)
nq_shape = tuple(nq.shape) + (1,) * (noise.ndim - nq.ndim)
nq = nq.mul_(nq_fac).reshape(*nq_shape)
noise = noise.clamp(-nq, nq)
noise = torch.copysign(
torch.pow(torch.abs(noise), pow_fac),
noise,
)
if flatdim is not None and qdim in {2, 3}:
return (
noise.reshape(tempshape).movedim(1, qdim).reshape(orig_shape).contiguous()
)
return noise
def adjust_slice(s: slice, size: int, offset: int) -> slice:
if offset == 0:
return s
# Input slice must have positive start/stop and be in bounds for the object that will be sliced here.
start = s.start if s.start is not None else 0
stop = s.stop if s.stop is not None else size
if offset < 0:
adj = min(start, abs(offset))
return slice(start - adj, stop - adj)
adj = min(size - stop, offset)
return slice(start + adj, stop + adj)
def crop_samples(
tensor: torch.Tensor,
width: int,
height: int,
*,
mode="center",
offset_width: int = 0,
offset_height: int = 0,
):
if tensor.ndim < 3:
raise ValueError("Can only handle >= 3 dimensional tensors")
th, tw = tensor.shape[-2:]
if (tw, th) == (width, height):
return tensor
if tw < width or th < height:
raise ValueError("Can't crop sample smaller than requested width or height")
if mode == "center":
hmode = wmode = "center"
else:
hmode, wmode, *splitextra = mode.split("_")
if splitextra:
raise ValueError("Bad composite mode")
if hmode == "top":
hslice = slice(0, height)
elif hmode == "center":
hoffs = (th - height) // 2
hslice = slice(hoffs, hoffs + height)
elif hmode == "bottom":
hslice = slice(th - height, th)
else:
raise ValueError("Bad height mode in composite mode")
if wmode == "left":
wslice = slice(0, width)
elif wmode == "center":
woffs = (tw - width) // 2
wslice = slice(woffs, woffs + width)
elif wmode == "right":
wslice = slice(tw - width, tw)
else:
raise ValueError("Bad width mode in composite mode")
wslice = adjust_slice(wslice, tw, offset_width)
hslice = adjust_slice(hslice, th, offset_height)
return tensor[..., hslice, wslice]
+5
View File
@@ -8,11 +8,15 @@ ignore = [
"ANN204",
"ANN206",
"C901",
"CPY001",
"DOC201",
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
"D211",
"D213",
@@ -26,6 +30,7 @@ ignore = [
"PLR0912",
"PLR0913",
"PLR0915",
"PLR0917",
"PLR2004",
"T201",
"TRY003",