Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
742e071364 | ||
|
|
0f748bd1f8 | ||
|
|
a7da42c305 | ||
|
|
e64606c672 | ||
|
|
fb667e08c8 | ||
|
|
4a01af10e7 | ||
|
|
ec7c9a4632 | ||
|
|
c235bc18b2 | ||
|
|
b47f3f3291 | ||
|
|
9dc16c5402 | ||
|
|
40726e5d84 | ||
|
|
9cad01df09 | ||
|
|
ab2f08268f | ||
|
|
9a6ee9ac33 | ||
|
|
144c7ba43a | ||
|
|
0ef1bd5bbd | ||
|
|
e4f53e9594 | ||
|
|
202a371337 | ||
|
|
c455599e9e | ||
|
|
b950e1b051 | ||
|
|
4e87817908 | ||
|
|
78e8451324 | ||
|
|
b78cbe0b2a | ||
|
|
24e1536cb7 | ||
|
|
ecadbfcd19 | ||
|
|
1ee8273771 | ||
|
|
52f929d54d | ||
|
|
090df2280d | ||
|
|
a2fd7118b5 |
@@ -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.
|
||||
@@ -24,17 +44,23 @@ You can also just choose `sonar_euler`, `sonar_euler_ancestral` or `sonar_dpmpp_
|
||||
|
||||
## Nodes
|
||||
|
||||
* `SamplerSonarEuler` — Custom sampler node that combines Euler sampling and momentum and optionally guidance. A bit boring compared to the ancestral version but it has predictability going for it. You can possibly try setting init type to `RAND` and using different noise types, however this sampler seems _very_ sensitive to that init type. You may want to set direction to a very low value like `0.05` or `-0.15` when using the `RAND` init type. Setting `momentum=1` is the same as disabling momentum, so this sampler with `momentum=1` is basically the same as the basic `euler` sampler.
|
||||
* `SamplerSonarEulerAncestral` — Ancestral version of the above. Same features, just with ancestral Euler.
|
||||
* `SonarGuidanceConfig` — You can optionally plug this into the Sonar sampler nodes. See the [Guidance](#guidance) section below.
|
||||
* `NoisyLatentLike` — If you give it a latent (or latent batch) it'll return a noisy latent of the same shape. Allows specifying all the custom noise types except `brownian` which has some special requirements. Provided just because the noise generation functions are conveniently available. You can also use this as a reference latent with `SonarGuidanceConfig` node and depending on the strength it can act like variation seed (you'd change the seed in the `NoisyLatentLike` node). *Note*: The seed stuff may or may not work correctly.
|
||||
* `SamplerSonarDPMPPSDE` — This one is extra experimental but it is an attempt to add moment and guidance to the DPM++ SDE sampler. It may not work correctly but you can sample stuff with it and get interesting results. I actually really like this one, and you can get away with more extreme stuff like `green_test` noise and still produce reasonable results. You may want to use the `BlehDiscardPenultimateSigma` node from my [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) collection if you find the result seems a bit washed out and blurry.
|
||||
* `SamplerConfigOverride` — can be used to override configuration settings for other samplers, including the noise type. For example, you could force `euler_ancestral` to use a different noise type. It's also possible to override other settings like `s_noise`, etc. *Note*: The wrapper inspects the sampling function's arguments to see what it supports, so you should connect the sampler directly to this rather than having other nodes (like a different sampler wrapper) in between.
|
||||
* `SonarCustomNoise` — See the [Noise](#noise) section below.
|
||||
### `SamplerSonarEuler`
|
||||
|
||||
*Note*: `NoisyLatentLike` and `SamplerConfigOverride` are candidates for moving to a different project. They're just here at the moment because the noise generation functions are readily available.
|
||||
Custom sampler node that combines Euler sampling and momentum and optionally guidance. A bit boring compared to the ancestral version but it has predictability going for it. You can possibly try setting init type to `RAND` and using different noise types, however this sampler seems _very_ sensitive to that init type. You may want to set direction to a very low value like `0.05` or `-0.15` when using the `RAND` init type. Setting `momentum=1` is the same as disabling momentum, so this sampler with `momentum=1` is basically the same as the basic `euler` sampler.
|
||||
|
||||
## Parameters
|
||||
### `SamplerSonarEulerAncestral`
|
||||
|
||||
Ancestral version of the above. Same features, just with ancestral Euler.
|
||||
|
||||
### `SamplerSonarDPMPPSDE`
|
||||
|
||||
Attempt to add momentum and guidance to the DPM++ SDE sampler. It may not work correctly but you can sample stuff with it and get interesting results. I actually really like this one, and you can get away with more extreme stuff like `green_test` noise and still produce reasonable results. You may want to use the `BlehDiscardPenultimateSigma` node from my [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) collection if you find the result seems a bit washed out and blurry.
|
||||
|
||||
### `SonarGuidanceConfig`
|
||||
|
||||
You can optionally plug this into the Sonar sampler nodes. See the [Guidance](#guidance) section below.
|
||||
|
||||
## 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...
|
||||
|
||||
@@ -50,26 +76,27 @@ 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
|
||||
|
||||
I also have some other ComfyUI nodes here: https://github.com/blepping/ComfyUI-bleh/
|
||||
@@ -78,11 +105,17 @@ 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.
|
||||
|
||||
## Examples
|
||||
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.
|
||||
|
||||
## Sonar Examples
|
||||
|
||||
Unfortunately, right now these examples are somewhat incomplete and out of date. I hope to update them when I get the time.
|
||||
|
||||
### Guidance
|
||||
|
||||
@@ -103,149 +136,10 @@ Using the `linear` guidance type and `guidance_factor=-0.015`. The reference ima
|
||||
|
||||
</details>
|
||||
|
||||
### Noise Types
|
||||
|
||||
### Noise Types (img2img)
|
||||
See:
|
||||
|
||||
These were generated with `s_noise=1.05` to make the noise effect more pronounced, 30 steps at `0.66` denoise, sonar settings increased slightly to enhance the effect (`momentum=0.9, momentum_hist=0.85, direction=1.0, momentum_init=ZERO`). It is probably easier to compare using these as the image _mostly_ stays the same as the sonar sampler settings change.
|
||||
|
||||
<details>
|
||||
<summary>Expand renoise example images</summary>
|
||||
|
||||
#### Base
|
||||
|
||||
Base image - no Sonar Sampler steps.
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||

|
||||
|
||||
|
||||
#### Gaussian
|
||||
|
||||

|
||||
|
||||
#### Brownian
|
||||
|
||||

|
||||
|
||||
#### Perlin
|
||||
|
||||

|
||||
|
||||
#### Uniform
|
||||
|
||||

|
||||
|
||||
#### Highres Pyramid
|
||||
|
||||

|
||||
|
||||
#### Pink
|
||||
|
||||

|
||||
|
||||
#### StudentT
|
||||
|
||||
**outdated**
|
||||
|
||||

|
||||
|
||||
|
||||
#### StudentT_test
|
||||
|
||||
**outdated**
|
||||
|
||||

|
||||
|
||||
#### Laplacian
|
||||
|
||||

|
||||
|
||||
#### Power
|
||||
|
||||

|
||||
|
||||
#### Rainbow Mild
|
||||
|
||||

|
||||
|
||||
#### Rainbow Intense
|
||||
|
||||

|
||||
|
||||
#### Green_test
|
||||
|
||||

|
||||
|
||||
</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
|
||||
|
||||

|
||||
|
||||
#### Brownian
|
||||
|
||||

|
||||
|
||||
#### Perlin
|
||||
|
||||

|
||||
|
||||
#### Uniform
|
||||
|
||||

|
||||
|
||||
#### Highres Pyramid
|
||||
|
||||

|
||||
|
||||
#### Pink
|
||||
|
||||

|
||||
|
||||
#### StudentT
|
||||
|
||||
**outdated**
|
||||
|
||||

|
||||
|
||||
#### StudentT_test
|
||||
|
||||
**outdated**
|
||||
|
||||

|
||||
|
||||
#### Laplacian
|
||||
|
||||

|
||||
|
||||
#### Power
|
||||
|
||||

|
||||
|
||||
#### Rainbow Mild
|
||||
|
||||

|
||||
|
||||
#### Rainbow Intense
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||

|
||||
|
||||
</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)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
from .py import nodes, 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,
|
||||
"SonarGuidanceConfig": nodes.GuidanceConfigNode,
|
||||
NODE_CLASS_MAPPINGS = nodes.NODE_CLASS_MAPPINGS | {
|
||||
"SonarPowerNoise": powernoise.SonarPowerNoiseNode,
|
||||
"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"]
|
||||
|
||||
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 435 KiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 434 KiB |
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 392 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 483 KiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 550 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 479 KiB |
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 474 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 476 KiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 466 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 451 KiB |
@@ -2,6 +2,39 @@
|
||||
|
||||
Note, only relatively significant changes to user-visible functionality will be included here. Most recent changes at the top.
|
||||
|
||||
## 20240521
|
||||
|
||||
Mega update! Many new features, documentation reorganized.
|
||||
|
||||
* Add `SonarScheduledNoise`, `SonarCompositeNoise`, `SonarGuidedNoise`, `SonarRandomNoise` nodes. See [Advanced Noise Nodes](docs/advanced_noise_nodes.md).
|
||||
* Add `SonarPowerFilterNoise`, `SonarPowerFilter`, `SonarPreviewFilter` nodes. See [Advanced Power Noise](docs/advanced_power_noise.md).
|
||||
* Add `FreeUExtreme`, `FreeUExtremeConfig` nodes. See [FreeU Extreme](docs/frux.md).
|
||||
* Replace `pyramid` noise type with a (hopefully) more correct implementation. You can use `pyramid_old` for the previous behavior.
|
||||
* Add more noise types and variations.
|
||||
* The `NoisyLatentLike` node now allows using brownian noise if you connect a model and sigmas.
|
||||
|
||||
## 20240506
|
||||
|
||||
* Add `SonarModulatedNoise` and `SonarRepeatedNoise` nodes.
|
||||
|
||||
## 20240327
|
||||
|
||||
* Fixed issue when using Sonar samplers in normal sampling nodes/via stuff like `KSamplerSelect`.
|
||||
* Add `pyramid` (non-high-res) noise type.
|
||||
* Allow selecting `brownian` noise in custom noise nodes (but it won't work with `NoisyLatentLike`).
|
||||
* Use `brownian` as the default noise type for `SamplerSonarDPMPP`.
|
||||
* Make overriding the selected noise type in Sonar samplers a warning instead of a hard error.
|
||||
* Improve noise scaling (may change seeds).
|
||||
* Add `KRestartSamplerCustomNoise` if the user has a recent enough version of ComfyUI_restart_sampling installed.
|
||||
|
||||
## 20240320
|
||||
|
||||
* `NoisyLatentLike` node improved to allow calculating strength with sigmas and injecting noise itself.
|
||||
|
||||
## 20240314
|
||||
|
||||
* `SonarPowerNoise` node added.
|
||||
|
||||
## 20240227
|
||||
|
||||
* Refactored noise generation functions (will break seeds).
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
# Advanced Nodes
|
||||
|
||||
## Normalization
|
||||
|
||||
Normalization essentially rebalances the noise (or mixture of noise) to 1.0 strength and then scales based
|
||||
on the factor of the node. Most nodes will allow you to set three values:
|
||||
|
||||
* `default`: By default, noise will be normalized only just before it's used. So you could consider this setting to be false except for where it is connected to an actual noise consumer (i.e. a `SamplerConfigOverride` node).
|
||||
* `forced`: Will always normalize.
|
||||
* `disabled`: Will never normalize.
|
||||
|
||||
## `SONAR_CUSTOM_NOISE`
|
||||
|
||||
This node output type actually constitutes a chain of noise items. For most nodes, when you use it as input,
|
||||
they will add an item to the chain. There are some exceptions that treat the `SONAR_CUSTOM_NOISE` input as a list:
|
||||
|
||||
* `SonarRepeatedNoise`
|
||||
* `SonarRandomNoise`
|
||||
|
||||
There are also some exceptions that will consume the list rather than adding an item to it:
|
||||
|
||||
* `SonarModulatedNoise`
|
||||
* `SonarCompositeNoise`
|
||||
* `SonarScheduledNoise`
|
||||
* `SonarGuidedNoise`
|
||||
|
||||
The distinction is mainly only important when setting `rescale`. Visual example:
|
||||
|
||||

|
||||
|
||||
It may be counter intuitive that there are actually two separate chains here.
|
||||
|
||||
## Examples
|
||||
|
||||
Note on the examples included for some of these nodes:
|
||||
|
||||
The example images included for some of these nodes all have metadata and can be loaded in ComfyUI.
|
||||
Generated using `dpmpp_2s_ancestral`, Karras scheduler and starting out with gaussian noise then switching
|
||||
to the custom noise type at the 35% mark.
|
||||
|
||||
***
|
||||
|
||||
### `SonarCustomNoise`
|
||||
|
||||
You can chain `SonarCustomNoise` nodes together to mix different types of noise. The order of `SonarCustomNoise` nodes is not important.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `factor` controls the strength of the noise.
|
||||
- `rescale` controls rebalancing `factor` for nodes in the chain. When `rescale` is set to `0.0`, no rebalancing will occur. Otherwise the current node as well as the nodes connect to it will have their `factor` adjusted to add up to the rescale value. For example, if you have three nodes with `factor` 1.0 and the last with `rescale` 1.0, then the `factor` value will be adjusted to `1/3 = 0.3333...`. *Note*: Rescaling uses the `factor` absolute value.
|
||||
- `noise_type` allows you to select the built-in noise type.
|
||||
|
||||
***
|
||||
|
||||
### `NoisyLatentLike`
|
||||
|
||||
This node takes a reference latent and generates noise of the same shape. The one required input is `latent`.
|
||||
|
||||
You can connect a `SonarCustomNoise` or `SonerPowerNoise` node to the `custom_noise_opt` input: if that is attached, the built in noise type selector is ignored. The generated noise will be multiplied by the `multiplier` value. **Note**: If you select `brownian` noise (either through the dropdown or by connecting custom noise nodes) you must connect a model and sigmas.
|
||||
|
||||
The node has two main modes: simply generate and scale the noise by the multiplier and return or add it to the input latent. In this mode, you don't connect anything to the `mul_by_sigmas_opt` or `model_opt` inputs and you would use other nodes to calculate the correct strength.
|
||||
|
||||
In the second mode you must connect sigmas (for example from a `BasicScheduler` node) to the `mul_by_sigmas_opt` input and connect a model to the `model_opt` input. It will calculate the strength based on the first item in the list of sigmas (so you could use something like a `SplitSigmas` node to slice them as needed). Note that `multiplier` still applies: the calculated strength will be scaled by it. This second mode is generally this is the most convenient way to use the node since the two main uses cases are: making a latent with initial noise or adding noise to a latent (for img2img type stuff).
|
||||
|
||||
If you want to create noise for initial sampling, connect model and sigmas to the node, connect an empty latent (or one of the appropriate size) to it and that is basically all you need to do (aside from configuring the noise types). For img2img (upscaling, etc), either slice the sigmas at the appropriate or set a denoise in something like the `BasicScheduler` node. *Note*: For img2img, you also need to turn on the `add_to_latent` toggle. Turning this on doesn't matter for initial noise since an empty latent is all zeros.
|
||||
|
||||
**Note**: This node does not currently respect the latent noise mask.
|
||||
|
||||
***
|
||||
|
||||
### `SamplerConfigOverride`
|
||||
|
||||
This node can be used to override configuration settings for other samplers, including the noise type. For example, you could force `euler_ancestral` to use a different noise type. It's also possible to override other settings like `s_noise`, etc. *Note*: The wrapper inspects the sampling function's arguments to see what it supports, so you should connect the sampler directly to this rather than having other nodes (like a different sampler wrapper) in between.
|
||||
|
||||
***
|
||||
|
||||
### `SonarModulatedNoise`
|
||||
|
||||
Experimental noise modulation based on code stolen from
|
||||
[ComfyUI-Extra-Samplers](https://github.com/Clybius/ComfyUI-Extra-Samplers). `intensity` and `frequency` modulation
|
||||
types _probably_ do not work correctly for normal sampling — I expect the modulation will be based on the tensor
|
||||
where the noise sampler was created rather than each step. However it may be useful for something like restart sampling
|
||||
noise (see `KRestartSamplerCustomNoise` below). You can also pass it a reference latent to modulate based on
|
||||
instead (only used for `intensity` and `frequency` modulation types).
|
||||
|
||||
*Note*: It's likely this node will be changed in the future.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>⭐ Expand Example Images ⭐</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
These examples all use the `spectral_signum` modulation type as it doesn't depend on a reference.
|
||||
|
||||
#### Positive Strength
|
||||
|
||||
Dims 3:
|
||||
|
||||

|
||||
|
||||
Dims 3 (with studentt noise):
|
||||
|
||||

|
||||
|
||||
Dims 2:
|
||||
|
||||

|
||||
|
||||
Dims 1:
|
||||
|
||||

|
||||
|
||||
#### Negative Strength
|
||||
|
||||
Dims 3:
|
||||
|
||||

|
||||
|
||||
Dims 3 (with studentt noise):
|
||||
|
||||

|
||||
|
||||
Dims 2:
|
||||
|
||||

|
||||
|
||||
Dims 1:
|
||||
|
||||

|
||||
|
||||
</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:
|
||||
|
||||

|
||||
|
||||
I recommend considerably decreasing the strength (example here is using 0.75 which is still a bit too much):
|
||||
|
||||

|
||||
|
||||
</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)**
|
||||
|
||||

|
||||
|
||||
**Brownian**
|
||||
|
||||

|
||||
|
||||
**Pyramid**
|
||||
|
||||

|
||||
|
||||
**Pyramid negative factor**
|
||||
|
||||

|
||||
|
||||
</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: 
|
||||
|
||||
|
||||
##### Euler
|
||||
|
||||
Positive strength:
|
||||
|
||||

|
||||
|
||||
Negative strength:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
##### Linear
|
||||
|
||||
Normal positive strength:
|
||||
|
||||

|
||||
|
||||
Normal negative strength:
|
||||
|
||||

|
||||
|
||||
Strong positive strength:
|
||||
|
||||

|
||||
|
||||
Strong negative strength:
|
||||
|
||||

|
||||
|
||||
|
||||
***
|
||||
|
||||
#### Gradient
|
||||
|
||||
These examples use a vertical gradient as the reference: 
|
||||
|
||||
That is dark to light. Light to dark examples just flip the gradient vertically.
|
||||
|
||||
##### Euler
|
||||
|
||||
Dark to light:
|
||||
|
||||

|
||||
|
||||
Light to dark:
|
||||
|
||||

|
||||
|
||||
Dark to light (negative strength):
|
||||
|
||||

|
||||
|
||||
Light to dark (negative strength):
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
##### Linear
|
||||
|
||||
Dark to light:
|
||||
|
||||

|
||||
|
||||
Light to dark:
|
||||
|
||||

|
||||
|
||||
Dark to light (negative strength):
|
||||
|
||||

|
||||
|
||||
Light to dark (negative strength):
|
||||
|
||||

|
||||
|
||||
</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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
With alpha 0.25, common mode 0.25:
|
||||
|
||||

|
||||
|
||||
With alpha 0.35:
|
||||
|
||||

|
||||
|
||||
With alpha 0.35, common mode 0.35:
|
||||
|
||||

|
||||
|
||||
With alpha 0.5:
|
||||
|
||||

|
||||
|
||||
With alpha 0.5, common mode 0.5:
|
||||
|
||||

|
||||
|
||||
### Negative Alpha
|
||||
|
||||
With alpha -0.5:
|
||||
|
||||

|
||||
|
||||
With alpha -1.5:
|
||||
|
||||

|
||||
|
||||
### Time Brownian Mode
|
||||
|
||||

|
||||
|
||||
With alpha 0.5:
|
||||
|
||||

|
||||
|
||||
With alpha -0.5:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# Base Noise Examples
|
||||
|
||||
The example images are all workflow-included. Generated using `dpmpp_2s_ancestral`, Karras scheduler and
|
||||
starting out with gaussian noise then switching to the custom noise type at the 35% mark.
|
||||
|
||||
Some of these noise types are too extreme to be used for initial generations or even with pure
|
||||
noise of that type. However you can either schedule the noise type to kick in at a certain percentage
|
||||
(as in these examples) and/or mix it with something a bit more run of the mill. See
|
||||
[advanced_noise_nodes](advanced_noise_nodes.md).
|
||||
|
||||
## Brownian
|
||||
|
||||
This is the default noise type for SDE samplers.
|
||||
|
||||

|
||||
|
||||
***
|
||||
## Gaussian
|
||||
|
||||
This is the default noise type for non-SDE samplers.
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Highres Pyramid
|
||||
|
||||

|
||||
|
||||
Variation using area scaling:
|
||||
|
||||

|
||||
|
||||
Variation using bislerp scaling:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Laplacian
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Perlin
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Pink
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Power Builtin
|
||||
|
||||

|
||||
|
||||
Also see the [Advanced Power Noise](advanced_power_noise.md) examples.
|
||||
|
||||
***
|
||||
|
||||
## Pyramid
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
Variation using area scaling:
|
||||
|
||||

|
||||
|
||||
Variation using bislerp scaling:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Pyramid Discount5
|
||||
|
||||
Pyramid noise, generated with a discount of 0.5. (Generally less extreme effect.)
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
Variation using area scaling:
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
Variation using bislerp scaling:
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
Variation using area scaling:
|
||||
|
||||

|
||||
|
||||
Variation using bislerp scaling:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Rainbow
|
||||
|
||||
Rainbow is a mix of Perlin and Green noise types.
|
||||
|
||||
The "mild" variation uses a relatively low proportion of green noise:
|
||||
|
||||

|
||||
|
||||
The "intense" variation uses a higher proportion of green noise for a more extreme effect.
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Studentt
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Uniform
|
||||
|
||||

|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
FreeU Extreme example:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -0,0 +1,22 @@
|
||||
import contextlib
|
||||
import importlib
|
||||
|
||||
MODULES = {}
|
||||
|
||||
with contextlib.suppress(ImportError, NotImplementedError):
|
||||
bleh = importlib.import_module("custom_nodes.ComfyUI-bleh")
|
||||
bleh_version = getattr(bleh, "BLEH_VERSION", -1)
|
||||
if bleh_version < 1:
|
||||
raise NotImplementedError
|
||||
MODULES["bleh"] = bleh
|
||||
|
||||
with contextlib.suppress(ImportError, NotImplementedError):
|
||||
import custom_nodes.ComfyUI_restart_sampling as rs
|
||||
|
||||
if not hasattr(rs.restart_sampling, "DEFAULT_SEGMENTS"):
|
||||
# Dumb test but this should only exist in restart sampling versions that
|
||||
# support plugging in custom noise.
|
||||
raise NotImplementedError
|
||||
MODULES["restart"] = rs
|
||||
|
||||
__all__ = ("MODULES",)
|
||||
@@ -0,0 +1,350 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
|
||||
from .external import MODULES as EXTERNAL_MODULES
|
||||
from .powernoise import PowerFilter
|
||||
|
||||
|
||||
def ffilter(x, pfilter, normalization_factor=1.0, cfg_idx=None, filter_cache=None):
|
||||
cache_key = None
|
||||
if filter_cache is not None and cfg_idx is not None:
|
||||
cache_key = (cfg_idx, x.shape[-2:])
|
||||
filter_rfft = filter_cache.get(cache_key)
|
||||
if filter_rfft is None:
|
||||
filter_rfft = PowerFilter.normalize(
|
||||
pfilter.build(x.shape),
|
||||
x.shape,
|
||||
normalization_factor=normalization_factor,
|
||||
).to(x.device, non_blocking=True)
|
||||
if cache_key:
|
||||
filter_cache[cache_key] = filter_rfft
|
||||
x_rfft = torch.fft.rfft2(x.to(torch.float32), norm="ortho")
|
||||
x_filt = torch.fft.irfft2(
|
||||
x_rfft.mul_(filter_rfft),
|
||||
s=x.shape[-2:],
|
||||
norm="ortho",
|
||||
)
|
||||
return x_filt.to(x.dtype, non_blocking=True)
|
||||
|
||||
|
||||
BLEND_OPS = (
|
||||
{"lerp": torch.lerp}
|
||||
if "bleh" not in EXTERNAL_MODULES
|
||||
else EXTERNAL_MODULES["bleh"].py.latent_utils.BLENDING_MODES
|
||||
)
|
||||
|
||||
|
||||
class FreeUExtremeConfigNode:
|
||||
RETURN_TYPES = ("FRUX_CONFIG",)
|
||||
FUNCTION = "go"
|
||||
CATEGORY = "model_patches"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"stage_1": ("BOOLEAN", {"default": True}),
|
||||
"stage_2": ("BOOLEAN", {"default": False}),
|
||||
"stage_3": ("BOOLEAN", {"default": False}),
|
||||
"target": (("backbone", "skip", "both"),),
|
||||
"start": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"end": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"slice": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"slice_offset": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"filter_norm": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": -10.0,
|
||||
"max": 10.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"scale": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": -100.0,
|
||||
"max": 100.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"blend": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": -10.0,
|
||||
"max": 10.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"blend_mode": (tuple(BLEND_OPS.keys()),),
|
||||
"hidden_mean": ("BOOLEAN", {"default": True}),
|
||||
"final": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"sonar_power_filter_opt": ("SONAR_POWER_FILTER",),
|
||||
"frux_config_opt": ("FRUX_CONFIG",),
|
||||
},
|
||||
}
|
||||
|
||||
def go(self, **kwargs: dict):
|
||||
return (FreeUExtremeConfig(**kwargs),)
|
||||
|
||||
|
||||
class FreeUExtremeConfig:
|
||||
_keys = (
|
||||
"target",
|
||||
"stage_1",
|
||||
"stage_2",
|
||||
"stage_3",
|
||||
"start",
|
||||
"end",
|
||||
"slice",
|
||||
"slice_offset",
|
||||
"filter_norm",
|
||||
"scale",
|
||||
"blend",
|
||||
"blend_mode",
|
||||
"hidden_mean",
|
||||
"final",
|
||||
"sonar_power_filter",
|
||||
"frux_config",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
target,
|
||||
stage_1=False,
|
||||
stage_2=False,
|
||||
stage_3=False,
|
||||
start=0.0,
|
||||
end=1.0,
|
||||
slice=1.0, # noqa: A002
|
||||
slice_offset=0.0,
|
||||
filter_norm=1.0,
|
||||
scale=1.0,
|
||||
blend=1.0,
|
||||
blend_mode=None,
|
||||
hidden_mean=True,
|
||||
final=True,
|
||||
sonar_power_filter_opt=None,
|
||||
frux_config_opt=None,
|
||||
):
|
||||
self.target = target
|
||||
self.stage_1 = stage_1
|
||||
self.stage_2 = stage_2
|
||||
self.stage_3 = stage_3
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.slice = slice
|
||||
self.slice_offset = slice_offset
|
||||
self.filter_norm = filter_norm
|
||||
self.scale = scale
|
||||
self.blend = blend
|
||||
self.blend_mode = blend_mode
|
||||
self.hidden_mean = hidden_mean
|
||||
self.final = final
|
||||
self.sonar_power_filter = sonar_power_filter_opt
|
||||
self.frux_config = frux_config_opt
|
||||
|
||||
def get_config_list(self):
|
||||
result = [self]
|
||||
curr = self
|
||||
while cfg := curr.frux_config:
|
||||
curr = cfg
|
||||
if (
|
||||
cfg.start >= 1
|
||||
or cfg.end <= 0
|
||||
or cfg.blend == 0
|
||||
or not (cfg.stage_1 or cfg.stage_2 or cfg.stage_3)
|
||||
):
|
||||
continue
|
||||
result.append(cfg)
|
||||
result.reverse()
|
||||
return result
|
||||
|
||||
# Hidden mean function modified from https://github.com/WASasquatch/FreeU_Advanced
|
||||
def get_scale(self, h: torch.Tensor) -> torch.Tensor:
|
||||
if not self.hidden_mean:
|
||||
return self.scale
|
||||
hmean = h.mean(1).unsqueeze(1)
|
||||
hmax, hmin = (
|
||||
op(hmean.view(hmean.shape[0], -1), dim=-1, keepdim=True)[0]
|
||||
for op in (torch.max, torch.min)
|
||||
)
|
||||
hmean -= hmin.unsqueeze(2).unsqueeze(3)
|
||||
hmean /= (hmax - hmin).unsqueeze(2).unsqueeze(3)
|
||||
return 1.0 + (self.scale - 1.0) * hmean
|
||||
|
||||
def check_match(self, pct, stage, is_skip=False):
|
||||
if pct < self.start or pct > self.end:
|
||||
return False
|
||||
if not getattr(self, f"stage_{stage}"):
|
||||
return False
|
||||
if self.target not in ("skip" if is_skip else "backbone", "both"):
|
||||
return False
|
||||
return True
|
||||
|
||||
def apply(self, idx, x, filter_cache, cpu_fft=False):
|
||||
batch, features, height, width = x.shape
|
||||
scale = self.get_scale(x)
|
||||
slice_size = int(features * self.slice)
|
||||
slice_offs = int(features * self.slice_offset)
|
||||
|
||||
xslice = (
|
||||
self.apply_filter(
|
||||
idx,
|
||||
x[:, slice_offs : slice_offs + slice_size],
|
||||
filter_cache,
|
||||
cpu_fft=cpu_fft,
|
||||
)
|
||||
* scale
|
||||
)
|
||||
x[:, slice_offs : slice_offs + slice_size] = (
|
||||
xslice
|
||||
if self.blend == 1.0
|
||||
else BLEND_OPS[self.blend_mode](
|
||||
x[:, slice_offs : slice_offs + slice_size],
|
||||
xslice,
|
||||
self.blend,
|
||||
)
|
||||
)
|
||||
return x
|
||||
|
||||
def apply_filter(self, idx, xslice, filter_cache, cpu_fft=False):
|
||||
filt = self.sonar_power_filter
|
||||
if filt is None:
|
||||
return xslice
|
||||
device = xslice.device
|
||||
if cpu_fft:
|
||||
xslice = xslice.to("cpu")
|
||||
xslice = ffilter(
|
||||
xslice,
|
||||
filt,
|
||||
normalization_factor=self.filter_norm,
|
||||
cfg_idx=idx,
|
||||
filter_cache=filter_cache,
|
||||
)
|
||||
if cpu_fft:
|
||||
xslice = xslice.to(device)
|
||||
return xslice
|
||||
|
||||
def clone(self):
|
||||
return self.__class__(**{k: getattr(self, k) for k in self._keys})
|
||||
|
||||
def __repr__(self): # noqa: D105
|
||||
meh = {k: getattr(self, k) for k in self._keys}
|
||||
return f"<FRUXConfig: {meh}>"
|
||||
|
||||
|
||||
class FreeUExtremeNode:
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
FUNCTION = "go"
|
||||
CATEGORY = "model_patches"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"cpu_fft": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"input_config": ("FRUX_CONFIG",),
|
||||
"middle_config": ("FRUX_CONFIG",),
|
||||
"output_config": ("FRUX_CONFIG",),
|
||||
},
|
||||
}
|
||||
|
||||
def go(
|
||||
self,
|
||||
model,
|
||||
cpu_fft,
|
||||
input_config=None,
|
||||
middle_config=None,
|
||||
output_config=None,
|
||||
):
|
||||
model_channels = model.model.model_config.unet_config["model_channels"]
|
||||
stages = {model_channels * 4: 1, model_channels * 2: 2, model_channels: 3}
|
||||
icfg, mcfg, ocfg = (
|
||||
() if cfg is None else cfg.get_config_list()
|
||||
for cfg in (input_config, middle_config, output_config)
|
||||
)
|
||||
m = model.clone()
|
||||
ms = m.get_model_object("model_sampling")
|
||||
filter_cache = {}
|
||||
|
||||
def handler(_typ, h_shape, cfg, x, toptions, is_skip=False):
|
||||
stage = stages.get(h_shape[1])
|
||||
if stage is None:
|
||||
return x
|
||||
sigma = toptions["sigmas"].max().detach().cpu()
|
||||
pct = 1.0 - (ms.timestep(sigma) / 999.0)
|
||||
for idx, ci in enumerate(cfg):
|
||||
if not ci.check_match(pct, stage, is_skip):
|
||||
continue
|
||||
x = ci.apply(idx, x, filter_cache, cpu_fft=cpu_fft)
|
||||
if ci.final:
|
||||
break
|
||||
return x
|
||||
|
||||
def in_patch(h, toptions):
|
||||
return handler("input", h.shape, icfg, h, toptions)
|
||||
|
||||
def mid_patch(h, toptions):
|
||||
return handler("middle", h.shape, mcfg, h, toptions)
|
||||
|
||||
def out_patch(h, hsp, toptions):
|
||||
h = handler("output", h.shape, ocfg, h, toptions)
|
||||
hsp = handler("output", h.shape, ocfg, hsp, toptions, is_skip=True)
|
||||
return h, hsp
|
||||
|
||||
if icfg:
|
||||
m.set_model_input_block_patch(in_patch)
|
||||
if mcfg:
|
||||
m.set_model_patch(mid_patch, "middle_block_patch")
|
||||
if ocfg:
|
||||
m.set_model_output_block_patch(out_patch)
|
||||
return (m,)
|
||||
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import inspect
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Callable
|
||||
|
||||
import torch
|
||||
from comfy import samplers
|
||||
|
||||
from . import noise
|
||||
from . import external, noise
|
||||
from .noise import NoiseType
|
||||
from .noise_generation import scale_noise
|
||||
from .sonar import (
|
||||
GuidanceConfig,
|
||||
GuidanceType,
|
||||
@@ -23,18 +27,16 @@ class NoisyLatentLikeNode:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"noise_type": (
|
||||
tuple(
|
||||
t.name.lower()
|
||||
for t in noise.NoiseType
|
||||
if t is not noise.NoiseType.BROWNIAN
|
||||
),
|
||||
),
|
||||
"noise_type": (tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
||||
"latent": ("LATENT",),
|
||||
"multiplier": ("FLOAT", {"default": 1.0}),
|
||||
"add_to_latent": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
"mul_by_sigmas_opt": ("SIGMAS",),
|
||||
"model_opt": ("MODEL",),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -45,35 +47,79 @@ class NoisyLatentLikeNode:
|
||||
|
||||
def go(
|
||||
self,
|
||||
noise_type,
|
||||
seed,
|
||||
latent,
|
||||
custom_noise_opt=None,
|
||||
noise_type: str,
|
||||
seed: None | int,
|
||||
latent: dict,
|
||||
multiplier: float = 1.0,
|
||||
add_to_latent=False,
|
||||
custom_noise_opt: object | None = None,
|
||||
mul_by_sigmas_opt: None | torch.Tensor = None,
|
||||
model_opt: object | None = None,
|
||||
):
|
||||
model, sigmas = model_opt, mul_by_sigmas_opt
|
||||
if sigmas is not None and len(sigmas) > 0:
|
||||
if model is None:
|
||||
raise ValueError(
|
||||
"NoisyLatentLike requires a model when sigmas are connected!",
|
||||
)
|
||||
while hasattr(model, "model"):
|
||||
model = model.model
|
||||
latent_scale_factor = model.latent_format.scale_factor
|
||||
max_denoise = samplers.Sampler().max_denoise(
|
||||
SimpleNamespace(inner_model=model),
|
||||
sigmas,
|
||||
)
|
||||
multiplier *= (
|
||||
float(
|
||||
torch.sqrt(1.0 + sigmas[0] ** 2.0) if max_denoise else sigmas[0],
|
||||
)
|
||||
/ latent_scale_factor
|
||||
)
|
||||
if sigmas is not None and sigmas.numel() > 1:
|
||||
sigma_min, sigma_max = sigmas[0], sigmas[-1]
|
||||
sigma, sigma_next = sigmas[0], sigmas[1]
|
||||
else:
|
||||
sigma_min, sigma_max, sigma, sigma_next = (None,) * 4
|
||||
latent_samples = latent["samples"]
|
||||
if custom_noise_opt is not None:
|
||||
ns = custom_noise_opt.make_noise_sampler(latent["samples"])
|
||||
ns = custom_noise_opt.make_noise_sampler(
|
||||
latent_samples,
|
||||
sigma_min=sigma_min,
|
||||
sigma_max=sigma_max,
|
||||
)
|
||||
else:
|
||||
ns = noise.get_noise_sampler(
|
||||
noise.NoiseType[noise_type.upper()],
|
||||
latent["samples"],
|
||||
None,
|
||||
None,
|
||||
NoiseType[noise_type.upper()],
|
||||
latent_samples,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=True,
|
||||
)
|
||||
randst = torch.random.get_rng_state()
|
||||
try:
|
||||
torch.random.manual_seed(seed)
|
||||
result = ns(None, None)
|
||||
result = ns(sigma, sigma_next)
|
||||
finally:
|
||||
torch.random.set_rng_state(randst)
|
||||
result = scale_noise(result, multiplier, normalized=True)
|
||||
if add_to_latent:
|
||||
result += latent_samples.to(result)
|
||||
return ({"samples": result},)
|
||||
|
||||
|
||||
class SonarCustomNoiseNode:
|
||||
class SonarCustomNoiseNodeBase(abc.ABC):
|
||||
RETURN_TYPES = ("SONAR_CUSTOM_NOISE",)
|
||||
CATEGORY = "advanced/noise"
|
||||
FUNCTION = "go"
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_item_class(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
def INPUT_TYPES(cls, *, include_rescale=True, include_chain=True):
|
||||
result = {
|
||||
"required": {
|
||||
"factor": (
|
||||
"FLOAT",
|
||||
@@ -85,6 +131,11 @@ class SonarCustomNoiseNode:
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
},
|
||||
"optional": {},
|
||||
}
|
||||
if include_rescale:
|
||||
result["required"] |= {
|
||||
"rescale": (
|
||||
"FLOAT",
|
||||
{
|
||||
@@ -95,34 +146,305 @@ class SonarCustomNoiseNode:
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"noise_type": (
|
||||
tuple(
|
||||
t.name.lower()
|
||||
for t in noise.NoiseType
|
||||
if t is not noise.NoiseType.BROWNIAN
|
||||
),
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
}
|
||||
if include_chain:
|
||||
result["optional"] |= {
|
||||
"sonar_custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
RETURN_TYPES = ("SONAR_CUSTOM_NOISE",)
|
||||
CATEGORY = "advanced/noise"
|
||||
FUNCTION = "go"
|
||||
|
||||
def go(self, factor, rescale, noise_type, sonar_custom_noise_opt=None):
|
||||
def go(
|
||||
self,
|
||||
factor=1.0,
|
||||
rescale=0.0,
|
||||
sonar_custom_noise_opt=None,
|
||||
**kwargs: dict[str, Any],
|
||||
):
|
||||
nis = (
|
||||
sonar_custom_noise_opt.clone()
|
||||
if sonar_custom_noise_opt
|
||||
else noise.CustomNoiseChain()
|
||||
)
|
||||
if factor != 0:
|
||||
nis.add(noise.CustomNoiseItem(factor, noise_type))
|
||||
nis.add(self.get_item_class()(factor, **kwargs))
|
||||
return (nis if rescale == 0 else nis.rescaled(rescale),)
|
||||
|
||||
|
||||
class SonarCustomNoiseNode(SonarCustomNoiseNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES()
|
||||
result["required"] |= {
|
||||
"noise_type": (tuple(NoiseType.get_names()),),
|
||||
}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.CustomNoiseItem
|
||||
|
||||
|
||||
class SonarNormalizeNoiseNodeMixin:
|
||||
@staticmethod
|
||||
def get_normalize(val: str) -> None | bool:
|
||||
return None if val == "default" else val == "forced"
|
||||
|
||||
|
||||
class SonarModulatedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
|
||||
result["required"] |= {
|
||||
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
|
||||
"modulation_type": (
|
||||
(
|
||||
"intensity",
|
||||
"frequency",
|
||||
"spectral_signum",
|
||||
"none",
|
||||
),
|
||||
),
|
||||
"dims": ("INT", {"default": 3, "min": 1, "max": 3}),
|
||||
"strength": ("FLOAT", {"default": 2.0, "min": -100.0, "max": 100.0}),
|
||||
"normalize_result": (("default", "forced", "disabled"),),
|
||||
"normalize_noise": (("default", "forced", "disabled"),),
|
||||
"normalize_ref": (
|
||||
"BOOLEAN",
|
||||
{"default": True},
|
||||
),
|
||||
}
|
||||
result["optional"] |= {"ref_latent_opt": ("LATENT",)}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.ModulatedNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
modulation_type,
|
||||
dims,
|
||||
strength,
|
||||
normalize_result,
|
||||
normalize_noise,
|
||||
normalize_ref,
|
||||
ref_latent_opt=None,
|
||||
):
|
||||
if ref_latent_opt is not None:
|
||||
ref_latent_opt = ref_latent_opt["samples"].clone()
|
||||
return super().go(
|
||||
factor,
|
||||
noise=sonar_custom_noise,
|
||||
modulation_type=modulation_type,
|
||||
modulation_dims=dims,
|
||||
modulation_strength=strength,
|
||||
normalize_result=self.get_normalize(normalize_result),
|
||||
normalize_noise=self.get_normalize(normalize_noise),
|
||||
normalize_ref=self.get_normalize(normalize_ref),
|
||||
ref_latent_opt=ref_latent_opt,
|
||||
)
|
||||
|
||||
|
||||
class SonarRepeatedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
|
||||
result["required"] |= {
|
||||
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
|
||||
"repeat_length": ("INT", {"default": 8, "min": 1, "max": 100}),
|
||||
"max_recycle": ("INT", {"default": 1000, "min": 1, "max": 1000}),
|
||||
"normalize": (("default", "forced", "disabled"),),
|
||||
"permute": (("enabled", "disabled", "always"),),
|
||||
}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.RepeatedNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
repeat_length,
|
||||
max_recycle,
|
||||
normalize,
|
||||
permute=True,
|
||||
):
|
||||
return super().go(
|
||||
factor,
|
||||
noise=sonar_custom_noise,
|
||||
repeat_length=repeat_length,
|
||||
max_recycle=max_recycle,
|
||||
normalize=self.get_normalize(normalize),
|
||||
permute=permute,
|
||||
)
|
||||
|
||||
|
||||
class SonarScheduledNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
|
||||
result["required"] |= {
|
||||
"model": ("MODEL",),
|
||||
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
|
||||
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0}),
|
||||
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0}),
|
||||
"normalize": (("default", "forced", "disabled"),),
|
||||
}
|
||||
result["optional"] |= {"fallback_sonar_custom_noise": ("SONAR_CUSTOM_NOISE",)}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.ScheduledNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
model,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
start_percent,
|
||||
end_percent,
|
||||
normalize,
|
||||
fallback_sonar_custom_noise=None,
|
||||
):
|
||||
ms = model.get_model_object("model_sampling")
|
||||
start_sigma = ms.percent_to_sigma(start_percent)
|
||||
end_sigma = ms.percent_to_sigma(end_percent)
|
||||
return super().go(
|
||||
factor,
|
||||
noise=sonar_custom_noise,
|
||||
start_sigma=start_sigma,
|
||||
end_sigma=end_sigma,
|
||||
normalize=self.get_normalize(normalize),
|
||||
fallback_noise=fallback_sonar_custom_noise,
|
||||
)
|
||||
|
||||
|
||||
class SonarCompositeNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
|
||||
result["required"] |= {
|
||||
"sonar_custom_noise_dst": ("SONAR_CUSTOM_NOISE",),
|
||||
"sonar_custom_noise_src": ("SONAR_CUSTOM_NOISE",),
|
||||
"normalize_dst": (("default", "forced", "disabled"),),
|
||||
"normalize_src": (("default", "forced", "disabled"),),
|
||||
"normalize_result": (("default", "forced", "disabled"),),
|
||||
"mask": ("MASK",),
|
||||
}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.CompositeNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
sonar_custom_noise_dst,
|
||||
sonar_custom_noise_src,
|
||||
normalize_src,
|
||||
normalize_dst,
|
||||
normalize_result,
|
||||
mask,
|
||||
):
|
||||
return super().go(
|
||||
factor,
|
||||
dst_noise=sonar_custom_noise_dst,
|
||||
src_noise=sonar_custom_noise_src,
|
||||
normalize_dst=self.get_normalize(normalize_src),
|
||||
normalize_src=self.get_normalize(normalize_dst),
|
||||
normalize_result=self.get_normalize(normalize_result),
|
||||
mask=mask,
|
||||
)
|
||||
|
||||
|
||||
class SonarGuidedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
|
||||
result["required"] |= {
|
||||
"latent": ("LATENT",),
|
||||
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
|
||||
"method": (("euler", "linear"),),
|
||||
"guidance_factor": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0125,
|
||||
"min": -100.0,
|
||||
"max": 100.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"normalize_noise": (("default", "forced", "disabled"),),
|
||||
"normalize_result": (("default", "forced", "disabled"),),
|
||||
"normalize_ref": (
|
||||
"BOOLEAN",
|
||||
{"default": True},
|
||||
),
|
||||
}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.GuidedNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
latent,
|
||||
sonar_custom_noise,
|
||||
normalize_noise,
|
||||
normalize_result,
|
||||
normalize_ref=True,
|
||||
method="euler",
|
||||
guidance_factor=0.5,
|
||||
):
|
||||
from .sonar import SonarGuidanceMixin
|
||||
|
||||
return super().go(
|
||||
factor,
|
||||
ref_latent=scale_noise(
|
||||
SonarGuidanceMixin.prepare_ref_latent(latent["samples"].clone()),
|
||||
normalized=normalize_ref,
|
||||
),
|
||||
guidance_factor=guidance_factor,
|
||||
noise=sonar_custom_noise.clone(),
|
||||
method=method,
|
||||
normalize_noise=self.get_normalize(normalize_noise),
|
||||
normalize_result=self.get_normalize(normalize_result),
|
||||
)
|
||||
|
||||
|
||||
class SonarRandomNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
|
||||
result["required"] |= {
|
||||
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
|
||||
"mix_count": ("INT", {"default": 1, "min": 1, "max": 100}),
|
||||
"normalize": (("default", "forced", "disabled"),),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.RandomNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
mix_count,
|
||||
normalize,
|
||||
):
|
||||
return super().go(
|
||||
factor,
|
||||
noise=sonar_custom_noise,
|
||||
mix_count=mix_count,
|
||||
normalize=self.get_normalize(normalize),
|
||||
)
|
||||
|
||||
|
||||
class GuidanceConfigNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -206,11 +528,7 @@ class SamplerNodeSonarBase:
|
||||
},
|
||||
),
|
||||
"rand_init_noise_type": (
|
||||
tuple(
|
||||
t.name.lower()
|
||||
for t in noise.NoiseType
|
||||
if t is not noise.NoiseType.BROWNIAN
|
||||
),
|
||||
tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
@@ -262,7 +580,7 @@ class SamplerNodeSonarEuler(SamplerNodeSonarBase):
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=noise.NoiseType[rand_init_noise_type.upper()],
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
return (
|
||||
@@ -292,7 +610,7 @@ class SamplerNodeSonarEulerAncestral(SamplerNodeSonarEuler):
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"noise_type": (tuple(t.name.lower() for t in noise.NoiseType),),
|
||||
"noise_type": (tuple(NoiseType.get_names()),),
|
||||
},
|
||||
)
|
||||
result["optional"].update(
|
||||
@@ -320,8 +638,8 @@ class SamplerNodeSonarEulerAncestral(SamplerNodeSonarEuler):
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=noise.NoiseType[rand_init_noise_type.upper()],
|
||||
noise_type=noise.NoiseType[noise_type.upper()],
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
noise_type=NoiseType[noise_type.upper()],
|
||||
custom_noise=custom_noise_opt.clone() if custom_noise_opt else None,
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
@@ -353,7 +671,7 @@ class SamplerNodeSonarDPMPPSDE(SamplerNodeSonarEuler):
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"noise_type": (tuple(t.name.lower() for t in noise.NoiseType),),
|
||||
"noise_type": (tuple(NoiseType.get_names(default=NoiseType.BROWNIAN)),),
|
||||
},
|
||||
)
|
||||
result["optional"].update(
|
||||
@@ -381,8 +699,8 @@ class SamplerNodeSonarDPMPPSDE(SamplerNodeSonarEuler):
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=noise.NoiseType[rand_init_noise_type.upper()],
|
||||
noise_type=noise.NoiseType[noise_type.upper()],
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
noise_type=NoiseType[noise_type.upper()],
|
||||
custom_noise=custom_noise_opt.clone() if custom_noise_opt else None,
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
@@ -440,9 +758,11 @@ class SamplerNodeConfigOverride:
|
||||
},
|
||||
),
|
||||
"sde_solver": (("midpoint", "heun"),),
|
||||
"cpu_noise": ("BOOLEAN", {"default": True}),
|
||||
"normalize": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"noise_type": (tuple(t.name.lower() for t in noise.NoiseType),),
|
||||
"noise_type": (tuple(NoiseType.get_names()),),
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
}
|
||||
@@ -460,8 +780,10 @@ class SamplerNodeConfigOverride:
|
||||
s_churn,
|
||||
r,
|
||||
sde_solver,
|
||||
cpu_noise=True,
|
||||
noise_type=None,
|
||||
custom_noise_opt=None,
|
||||
normalize=True,
|
||||
):
|
||||
return (
|
||||
samplers.KSAMPLER(
|
||||
@@ -470,7 +792,7 @@ class SamplerNodeConfigOverride:
|
||||
| {
|
||||
"override_sampler_cfg": {
|
||||
"sampler": sampler,
|
||||
"noise_type": noise.NoiseType[noise_type.upper()]
|
||||
"noise_type": NoiseType[noise_type.upper()]
|
||||
if noise_type is not None
|
||||
else None,
|
||||
"custom_noise": custom_noise_opt,
|
||||
@@ -479,6 +801,8 @@ class SamplerNodeConfigOverride:
|
||||
"s_churn": s_churn,
|
||||
"r": r,
|
||||
"solver_type": sde_solver,
|
||||
"cpu_noise": cpu_noise,
|
||||
"normalize": normalize,
|
||||
},
|
||||
},
|
||||
inpaint_options=sampler.inpaint_options | {},
|
||||
@@ -503,10 +827,12 @@ class SamplerNodeConfigOverride:
|
||||
if extra_args is None:
|
||||
extra_args = {}
|
||||
cfg = override_sampler_cfg
|
||||
sampler, noise_type, custom_noise = (
|
||||
sampler, noise_type, custom_noise, cpu, normalize = (
|
||||
cfg["sampler"],
|
||||
cfg.get("noise_type"),
|
||||
cfg.get("custom_noise"),
|
||||
cfg.get("cpu_noise", True),
|
||||
cfg.get("normalize", True),
|
||||
)
|
||||
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
|
||||
seed = extra_args.get("seed")
|
||||
@@ -516,6 +842,8 @@ class SamplerNodeConfigOverride:
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu,
|
||||
normalized=normalize,
|
||||
)
|
||||
elif noise_type is not None:
|
||||
noise_sampler = noise.get_noise_sampler(
|
||||
@@ -524,7 +852,8 @@ class SamplerNodeConfigOverride:
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=True,
|
||||
cpu=cpu,
|
||||
normalized=normalize,
|
||||
)
|
||||
sig = inspect.signature(sampler.sampler_function)
|
||||
params = sig.parameters
|
||||
@@ -543,3 +872,242 @@ class SamplerNodeConfigOverride:
|
||||
extra_args=extra_args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SamplerSonarEuler": SamplerNodeSonarEuler,
|
||||
"SamplerSonarEulerA": SamplerNodeSonarEulerAncestral,
|
||||
"SamplerSonarDPMPPSDE": SamplerNodeSonarDPMPPSDE,
|
||||
"SonarGuidanceConfig": GuidanceConfigNode,
|
||||
"SamplerConfigOverride": SamplerNodeConfigOverride,
|
||||
"NoisyLatentLike": NoisyLatentLikeNode,
|
||||
"SonarCustomNoise": SonarCustomNoiseNode,
|
||||
"SonarCompositeNoise": SonarCompositeNoiseNode,
|
||||
"SonarModulatedNoise": SonarModulatedNoiseNode,
|
||||
"SonarRepeatedNoise": SonarRepeatedNoiseNode,
|
||||
"SonarScheduledNoise": SonarScheduledNoiseNode,
|
||||
"SonarGuidedNoise": SonarGuidedNoiseNode,
|
||||
"SonarRandomNoise": SonarRandomNoiseNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
|
||||
if "bleh" in external.MODULES:
|
||||
bleh = external.MODULES["bleh"]
|
||||
bleh_latentutils = bleh.py.latent_utils
|
||||
|
||||
class SonarBlendFilterNoiseNode(
|
||||
SonarCustomNoiseNodeBase,
|
||||
SonarNormalizeNoiseNodeMixin,
|
||||
):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
|
||||
result["required"] |= {
|
||||
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
|
||||
"blend_mode": (
|
||||
("simple_add", *bleh_latentutils.BLENDING_MODES.keys()),
|
||||
),
|
||||
"ffilter": (tuple(bleh_latentutils.FILTER_PRESETS.keys()),),
|
||||
"ffilter_custom": ("STRING", {"default": ""}),
|
||||
"ffilter_scale": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": -100.0, "max": 100.0},
|
||||
),
|
||||
"ffilter_strength": (
|
||||
"FLOAT",
|
||||
{"default": 0.0, "min": -100.0, "max": 100.0},
|
||||
),
|
||||
"ffilter_threshold": (
|
||||
"INT",
|
||||
{"default": 1, "min": 1, "max": 32},
|
||||
),
|
||||
"enhance_mode": (("none", *bleh_latentutils.ENHANCE_METHODS),),
|
||||
"enhance_strength": (
|
||||
"FLOAT",
|
||||
{"default": 0.0, "min": -100.0, "max": 100.0},
|
||||
),
|
||||
"affect": (("result", "noise", "both"),),
|
||||
"normalize_result": (("default", "forced", "disabled"),),
|
||||
"normalize_noise": (("default", "forced", "disabled"),),
|
||||
}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.BlendFilterNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
blend_mode,
|
||||
ffilter,
|
||||
ffilter_custom,
|
||||
ffilter_scale,
|
||||
ffilter_strength,
|
||||
ffilter_threshold,
|
||||
enhance_mode,
|
||||
enhance_strength,
|
||||
affect,
|
||||
normalize_result,
|
||||
normalize_noise,
|
||||
):
|
||||
import ast
|
||||
|
||||
ffilter_custom = ffilter_custom.strip()
|
||||
normalize_result = (
|
||||
None if normalize_result == "default" else normalize_result == "forced"
|
||||
)
|
||||
normalize_noise = (
|
||||
None if normalize_noise == "default" else normalize_noise == "forced"
|
||||
)
|
||||
if ffilter_custom:
|
||||
ffilter = ast.literal_eval(f"[{ffilter_custom}]")
|
||||
else:
|
||||
ffilter = bleh_latentutils.FILTER_PRESETS[ffilter]
|
||||
return super().go(
|
||||
factor,
|
||||
noise=sonar_custom_noise.clone(),
|
||||
blend_mode=blend_mode,
|
||||
ffilter=ffilter,
|
||||
ffilter_scale=ffilter_scale,
|
||||
ffilter_strength=ffilter_strength,
|
||||
ffilter_threshold=ffilter_threshold,
|
||||
enhance_mode=enhance_mode,
|
||||
enhance_strength=enhance_strength,
|
||||
affect=affect,
|
||||
normalize_noise=self.get_normalize(normalize_noise),
|
||||
normalize_result=self.get_normalize(normalize_result),
|
||||
)
|
||||
|
||||
NODE_CLASS_MAPPINGS["SonarBlendFilterNoise"] = SonarBlendFilterNoiseNode
|
||||
|
||||
if "restart" in external.MODULES:
|
||||
rs = external.MODULES["restart"]
|
||||
|
||||
class KRestartSamplerCustomNoise:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
get_normal_schedulers = getattr(
|
||||
rs.nodes,
|
||||
"get_supported_normal_schedulers",
|
||||
rs.nodes.get_supported_restart_schedulers,
|
||||
)
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"add_noise": (["enable", "disable"],),
|
||||
"noise_seed": (
|
||||
"INT",
|
||||
{"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF},
|
||||
),
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
|
||||
"sampler": ("SAMPLER",),
|
||||
"scheduler": (get_normal_schedulers(),),
|
||||
"positive": ("CONDITIONING",),
|
||||
"negative": ("CONDITIONING",),
|
||||
"latent_image": ("LATENT",),
|
||||
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
||||
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
|
||||
"return_with_leftover_noise": (["disable", "enable"],),
|
||||
"segments": (
|
||||
"STRING",
|
||||
{
|
||||
"default": rs.restart_sampling.DEFAULT_SEGMENTS,
|
||||
"multiline": False,
|
||||
},
|
||||
),
|
||||
"restart_scheduler": (rs.nodes.get_supported_restart_schedulers(),),
|
||||
"chunked_mode": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LATENT", "LATENT")
|
||||
RETURN_NAMES = ("output", "denoised_output")
|
||||
FUNCTION = "sample"
|
||||
CATEGORY = "sampling"
|
||||
|
||||
def sample(
|
||||
self,
|
||||
model,
|
||||
add_noise,
|
||||
noise_seed,
|
||||
steps,
|
||||
cfg,
|
||||
sampler,
|
||||
scheduler,
|
||||
positive,
|
||||
negative,
|
||||
latent_image,
|
||||
start_at_step,
|
||||
end_at_step,
|
||||
return_with_leftover_noise,
|
||||
segments,
|
||||
restart_scheduler,
|
||||
chunked_mode=False,
|
||||
custom_noise_opt=None,
|
||||
):
|
||||
return rs.restart_sampling.restart_sampling(
|
||||
model,
|
||||
noise_seed,
|
||||
steps,
|
||||
cfg,
|
||||
sampler,
|
||||
scheduler,
|
||||
positive,
|
||||
negative,
|
||||
latent_image,
|
||||
segments,
|
||||
restart_scheduler,
|
||||
disable_noise=add_noise == "disable",
|
||||
step_range=(start_at_step, end_at_step),
|
||||
force_full_denoise=return_with_leftover_noise != "enable",
|
||||
output_only=False,
|
||||
chunked_mode=chunked_mode,
|
||||
custom_noise=custom_noise_opt.make_noise_sampler
|
||||
if custom_noise_opt
|
||||
else None,
|
||||
)
|
||||
|
||||
NODE_CLASS_MAPPINGS["KRestartSamplerCustomNoise"] = KRestartSamplerCustomNoise
|
||||
|
||||
if hasattr(rs.restart_sampling, "RestartSampler"):
|
||||
|
||||
class RestartSamplerCustomNoise:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"sampler": ("SAMPLER",),
|
||||
"chunked_mode": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SAMPLER",)
|
||||
FUNCTION = "go"
|
||||
CATEGORY = "sampling/custom_sampling/samplers"
|
||||
|
||||
def go(self, sampler, chunked_mode, custom_noise_opt=None):
|
||||
restart_options = {
|
||||
"restart_chunked": chunked_mode,
|
||||
"restart_wrapped_sampler": sampler,
|
||||
"restart_custom_noise": None
|
||||
if custom_noise_opt is None
|
||||
else custom_noise_opt.make_noise_sampler,
|
||||
}
|
||||
restart_sampler = samplers.KSAMPLER(
|
||||
rs.restart_sampling.RestartSampler.sampler_function,
|
||||
extra_options=sampler.extra_options | restart_options,
|
||||
inpaint_options=sampler.inpaint_options,
|
||||
)
|
||||
return (restart_sampler,)
|
||||
|
||||
NODE_CLASS_MAPPINGS["RestartSamplerCustomNoise"] = RestartSamplerCustomNoise
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
# Noise generation functions shamelessly yoinked from https://github.com/Clybius/ComfyUI-Extra-Samplers
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from enum import Enum, auto
|
||||
from typing import Callable
|
||||
|
||||
import torch
|
||||
from comfy.utils import common_upscale
|
||||
from torch import FloatTensor, Generator, Tensor
|
||||
from torch.distributions import Laplace, StudentT
|
||||
|
||||
# ruff: noqa: D412, D413, D417, D212, D407, ANN002, ANN003, FBT001, FBT002, S311
|
||||
|
||||
|
||||
class NoiseType(Enum):
|
||||
GAUSSIAN = auto()
|
||||
UNIFORM = auto()
|
||||
BROWNIAN = auto()
|
||||
PERLIN = auto()
|
||||
STUDENTT = auto()
|
||||
HIGHRES_PYRAMID = auto()
|
||||
PYRAMID = auto()
|
||||
PYRAMID_MIX = auto()
|
||||
PINK = auto()
|
||||
LAPLACIAN = auto()
|
||||
POWER = auto()
|
||||
RAINBOW_MILD = auto()
|
||||
RAINBOW_INTENSE = auto()
|
||||
GREEN_TEST = auto()
|
||||
PYRAMID_OLD = auto()
|
||||
PYRAMID_BISLERP = auto()
|
||||
HIGHRES_PYRAMID_BISLERP = auto()
|
||||
PYRAMID_OLD_BISLERP = auto()
|
||||
PYRAMID_OLD_AREA = auto()
|
||||
PYRAMID_AREA = auto()
|
||||
HIGHRES_PYRAMID_AREA = auto()
|
||||
PYRAMID_DISCOUNT5 = auto()
|
||||
PYRAMID_MIX_BISLERP = auto()
|
||||
PYRAMID_MIX_AREA = auto()
|
||||
|
||||
@classmethod
|
||||
def get_names(cls, default=None, skip=None):
|
||||
if default is not None:
|
||||
yield default.name.lower()
|
||||
for nt in cls:
|
||||
if nt == default or (skip and nt in skip):
|
||||
continue
|
||||
yield nt.name.lower()
|
||||
|
||||
|
||||
class NoiseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def scale_noise(noise, factor=1.0, *, normalized=True, threshold_std_devs=2.5):
|
||||
if not normalized or noise.numel() == 0:
|
||||
return noise.mul_(factor) if factor != 1 else noise
|
||||
mean, std = noise.mean().item(), noise.std().item()
|
||||
threshold = threshold_std_devs / math.sqrt(noise.numel())
|
||||
if abs(mean) > threshold:
|
||||
noise -= mean
|
||||
if abs(1.0 - std) > threshold:
|
||||
noise /= std
|
||||
return noise.mul_(factor) if factor != 1 else noise
|
||||
|
||||
|
||||
def get_positions(block_shape: tuple[int, int]) -> Tensor:
|
||||
"""
|
||||
Generate position tensor.
|
||||
|
||||
Arguments:
|
||||
block_shape -- (height, width) of position tensor
|
||||
|
||||
Returns:
|
||||
position vector shaped (1, height, width, 1, 1, 2)
|
||||
"""
|
||||
bh, bw = block_shape
|
||||
return torch.stack(
|
||||
torch.meshgrid(
|
||||
[(torch.arange(b) + 0.5) / b for b in (bw, bh)],
|
||||
indexing="xy",
|
||||
),
|
||||
-1,
|
||||
).view(1, bh, bw, 1, 1, 2)
|
||||
|
||||
|
||||
def unfold_grid(vectors: Tensor) -> Tensor:
|
||||
"""
|
||||
Unfold vector grid to batched vectors.
|
||||
|
||||
Arguments:
|
||||
vectors -- grid vectors
|
||||
|
||||
Returns:
|
||||
batched grid vectors
|
||||
"""
|
||||
batch_size, _, gpy, gpx = vectors.shape
|
||||
return (
|
||||
torch.nn.functional.unfold(vectors, (2, 2))
|
||||
.view(batch_size, 2, 4, -1)
|
||||
.permute(0, 2, 3, 1)
|
||||
.view(batch_size, 4, gpy - 1, gpx - 1, 2)
|
||||
)
|
||||
|
||||
|
||||
def smooth_step(t: Tensor) -> Tensor:
|
||||
"""
|
||||
Smooth step function [0, 1] -> [0, 1].
|
||||
|
||||
Arguments:
|
||||
t -- input values (any shape)
|
||||
|
||||
Returns:
|
||||
output values (same shape as input values)
|
||||
"""
|
||||
return t * t * (3.0 - 2.0 * t)
|
||||
|
||||
|
||||
def perlin_noise_tensor(
|
||||
vectors: Tensor,
|
||||
positions: Tensor,
|
||||
step: Callable | None = None,
|
||||
) -> Tensor:
|
||||
"""
|
||||
Generate perlin noise from batched vectors and positions.
|
||||
|
||||
Arguments:
|
||||
vectors -- batched grid vectors shaped (batch_size, 4, grid_height, grid_width, 2)
|
||||
positions -- batched grid positions shaped (batch_size or 1, block_height, block_width, grid_height or 1, grid_width or 1, 2)
|
||||
|
||||
Keyword Arguments:
|
||||
step -- smooth step function [0, 1] -> [0, 1] (default: `smooth_step`)
|
||||
|
||||
Raises:
|
||||
Exception: if position and vector shapes do not match
|
||||
|
||||
Returns:
|
||||
(batch_size, block_height * grid_height, block_width * grid_width)
|
||||
"""
|
||||
if step is None:
|
||||
step = smooth_step
|
||||
|
||||
batch_size = vectors.shape[0]
|
||||
# grid height, grid width
|
||||
gh, gw = vectors.shape[2:4]
|
||||
# block height, block width
|
||||
bh, bw = positions.shape[1:3]
|
||||
|
||||
for i in range(2):
|
||||
if positions.shape[i + 3] not in (1, vectors.shape[i + 2]):
|
||||
msg = f"Blocks shapes do not match: vectors ({vectors.shape[1]}, {vectors.shape[2]}), positions {gh}, {gw})"
|
||||
raise NoiseError(msg)
|
||||
|
||||
if positions.shape[0] not in (1, batch_size):
|
||||
msg = f"Batch sizes do not match: vectors ({vectors.shape[0]}), positions ({positions.shape[0]})"
|
||||
raise NoiseError(msg)
|
||||
|
||||
vectors = vectors.view(batch_size, 4, 1, gh * gw, 2)
|
||||
positions = positions.view(positions.shape[0], bh * bw, -1, 2)
|
||||
|
||||
step_x = step(positions[..., 0])
|
||||
step_y = step(positions[..., 1])
|
||||
|
||||
row0 = torch.lerp(
|
||||
(vectors[:, 0] * positions).sum(dim=-1),
|
||||
(vectors[:, 1] * (positions - positions.new_tensor((1, 0)))).sum(dim=-1),
|
||||
step_x,
|
||||
)
|
||||
row1 = torch.lerp(
|
||||
(vectors[:, 2] * (positions - positions.new_tensor((0, 1)))).sum(dim=-1),
|
||||
(vectors[:, 3] * (positions - positions.new_tensor((1, 1)))).sum(dim=-1),
|
||||
step_x,
|
||||
)
|
||||
noise = torch.lerp(row0, row1, step_y)
|
||||
return (
|
||||
noise.view(
|
||||
batch_size,
|
||||
bh,
|
||||
bw,
|
||||
gh,
|
||||
gw,
|
||||
)
|
||||
.permute(0, 3, 1, 4, 2)
|
||||
.reshape(batch_size, gh * bh, gw * bw)
|
||||
)
|
||||
|
||||
|
||||
def perlin_noise(
|
||||
grid_shape: tuple[int, int],
|
||||
out_shape: tuple[int, int],
|
||||
batch_size: int = 1,
|
||||
generator: Generator | None = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Tensor:
|
||||
"""
|
||||
Generate perlin noise with given shape. `*args` and `**kwargs` are forwarded to `Tensor` creation.
|
||||
|
||||
Arguments:
|
||||
grid_shape -- Shape of grid (height, width).
|
||||
out_shape -- Shape of output noise image (height, width).
|
||||
|
||||
Keyword Arguments:
|
||||
batch_size -- (default: {1})
|
||||
generator -- random generator used for grid vectors (default: {None})
|
||||
|
||||
Raises:
|
||||
Exception: if grid and out shapes do not match
|
||||
|
||||
Returns:
|
||||
Noise image shaped (batch_size, height, width)
|
||||
"""
|
||||
# grid height and width
|
||||
gh, gw = grid_shape
|
||||
# output height and width
|
||||
oh, ow = out_shape
|
||||
# block height and width
|
||||
bh, bw = oh // gh, ow // gw
|
||||
|
||||
if oh != bh * gh:
|
||||
msg = f"Output height {oh} must be divisible by grid height {gh}"
|
||||
raise NoiseError(msg)
|
||||
if ow != bw * gw != 0:
|
||||
msg = f"Output width {ow} must be divisible by grid width {gw}"
|
||||
raise NoiseError(msg)
|
||||
|
||||
angle = torch.empty(
|
||||
[batch_size] + [s + 1 for s in grid_shape],
|
||||
*args,
|
||||
**kwargs,
|
||||
).uniform_(to=2.0 * math.pi, generator=generator)
|
||||
# random vectors on grid points
|
||||
vectors = unfold_grid(torch.stack((torch.cos(angle), torch.sin(angle)), dim=1))
|
||||
# positions inside grid cells [0, 1)
|
||||
positions = get_positions((bh, bw)).to(vectors)
|
||||
return perlin_noise_tensor(vectors, positions).squeeze(0)
|
||||
|
||||
|
||||
def rand_perlin_like(x):
|
||||
noise = torch.randn_like(x) / 2.0
|
||||
noise_height = noise.size(dim=2)
|
||||
noise_width = noise.size(dim=3)
|
||||
for _ in range(2):
|
||||
noise += perlin_noise(
|
||||
(noise_height, noise_width),
|
||||
(noise_height, noise_width),
|
||||
batch_size=x.shape[1], # This should be the number of channels.
|
||||
).to(x.device)
|
||||
return scale_noise(noise)
|
||||
|
||||
|
||||
def uniform_noise_like(x):
|
||||
return (torch.rand_like(x) - 0.5) * 3.46
|
||||
|
||||
|
||||
def highres_pyramid_noise_like(x, discount=0.7, upscale_mode="bilinear"):
|
||||
(
|
||||
b,
|
||||
c,
|
||||
h,
|
||||
w,
|
||||
) = x.shape # EDIT: w and h get over-written, rename for a different variant!
|
||||
orig_w, orig_h = w, h
|
||||
noise = uniform_noise_like(x)
|
||||
rs = torch.rand(4, dtype=torch.float32) * 2 + 2
|
||||
for i in range(4):
|
||||
r = rs[i]
|
||||
h, w = min(orig_h * 15, int(h * (r**i))), min(orig_w * 15, int(w * (r**i)))
|
||||
noise += common_upscale(
|
||||
torch.randn(b, c, h, w).to(x),
|
||||
orig_w,
|
||||
orig_h,
|
||||
upscale_mode,
|
||||
None,
|
||||
).mul_(discount**i)
|
||||
if h >= orig_h * 15 or w >= orig_w * 15:
|
||||
break # Lowest resolution is 1x1
|
||||
return scale_noise(noise)
|
||||
|
||||
|
||||
def pyramid_old_noise_like(
|
||||
x,
|
||||
generator=None,
|
||||
device="cpu",
|
||||
discount=0.8,
|
||||
upscale_mode="nearest-exact",
|
||||
):
|
||||
size = x.size()
|
||||
b, c, h, w = size
|
||||
orig_h, orig_w = h, w
|
||||
noise = torch.zeros(size=size, dtype=x.dtype, layout=x.layout, device=device)
|
||||
r = 1
|
||||
for i in range(5):
|
||||
r *= 2
|
||||
noise += common_upscale(
|
||||
torch.normal(
|
||||
mean=0,
|
||||
std=0.5**i,
|
||||
size=(b, c, h * r, w * r),
|
||||
dtype=x.dtype,
|
||||
layout=x.layout,
|
||||
generator=generator,
|
||||
device=device,
|
||||
),
|
||||
orig_w,
|
||||
orig_h,
|
||||
upscale_mode,
|
||||
None,
|
||||
).mul_(discount**i)
|
||||
return noise.to(device=x.device)
|
||||
|
||||
|
||||
# Copied from https://wandb.ai/johnowhitaker/multires_noise/reports/Multi-Resolution-Noise-for-Diffusion-Model-Training--VmlldzozNjYyOTU2
|
||||
def pyramid_noise_like(x, discount=0.7, upscale_mode="bilinear"):
|
||||
b, c, w, h = (
|
||||
x.shape
|
||||
) # NOTE: w and h get over-written, rename for a different variant!
|
||||
orig_w, orig_h = w, h
|
||||
noise = torch.randn_like(x)
|
||||
for i in range(10):
|
||||
r = torch.rand(1, device="cpu").item() * 2 + 2 # Rather than always going 2x,
|
||||
w, h = max(1, int(w / (r**i))), max(1, int(h / (r**i)))
|
||||
noise += common_upscale(
|
||||
torch.randn(b, c, w, h).to(x),
|
||||
orig_h,
|
||||
orig_w,
|
||||
upscale_mode,
|
||||
None,
|
||||
).mul_(
|
||||
discount**i,
|
||||
)
|
||||
if w == 1 or h == 1:
|
||||
break # Lowest resolution is 1x1
|
||||
return scale_noise(noise)
|
||||
|
||||
|
||||
def studentt_noise_like(x):
|
||||
noise = StudentT(loc=0, scale=0.2, df=1).rsample(x.size())
|
||||
s: FloatTensor = torch.quantile(noise.flatten(start_dim=1).abs(), 0.75, dim=-1)
|
||||
s = s.reshape(*s.shape, 1, 1, 1)
|
||||
noise = noise.clamp(-s, s)
|
||||
return torch.copysign(torch.pow(torch.abs(noise), 0.5), noise)
|
||||
|
||||
|
||||
def green_noise_like(x):
|
||||
# The comments said this didn't work and I had to learn the hard way. Turns out it's true!
|
||||
width, height = x.size(dim=2), x.size(dim=3)
|
||||
noise = torch.randn_like(x)
|
||||
scale = 1.0 / (width * height)
|
||||
fy = torch.fft.fftfreq(width, device=x.device)[:, None] ** 2
|
||||
fx = torch.fft.fftfreq(height, device=x.device) ** 2
|
||||
f = fy + fx
|
||||
power = torch.sqrt(f)
|
||||
power[0, 0] = 1
|
||||
noise = torch.fft.ifft2(torch.fft.fft2(noise) / torch.sqrt(power))
|
||||
noise *= scale / noise.std()
|
||||
noise = torch.real(noise).to(x.device)
|
||||
return scale_noise(noise)
|
||||
|
||||
|
||||
def generate_1f_noise(tensor, alpha, k, generator=None):
|
||||
"""Generate 1/f noise for a given tensor.
|
||||
|
||||
Args:
|
||||
tensor: The tensor to add noise to.
|
||||
alpha: The parameter that determines the slope of the spectrum.
|
||||
k: A constant.
|
||||
|
||||
Returns:
|
||||
A tensor with the same shape as `tensor` containing 1/f noise.
|
||||
"""
|
||||
fft = torch.fft.fft2(tensor)
|
||||
freq = torch.arange(1, len(fft) + 1, dtype=torch.float)
|
||||
spectral_density = k / freq**alpha
|
||||
return torch.randn(tensor.shape, generator=generator) * spectral_density
|
||||
|
||||
|
||||
def pink_noise_like(x):
|
||||
return scale_noise(generate_1f_noise(x, 2.0, 1.0)).to(x.device)
|
||||
|
||||
|
||||
def laplacian_noise_like(x):
|
||||
noise = torch.randn_like(x).div_(4.0)
|
||||
noise += Laplace(loc=0, scale=1.0).rsample(x.size()).to(noise.device)
|
||||
return scale_noise(noise)
|
||||
|
||||
|
||||
def power_noise_like(tensor, alpha=2, k=1): # This doesn't work properly right now
|
||||
"""Generate 1/f noise for a given tensor.
|
||||
|
||||
Args:
|
||||
tensor: The tensor to add noise to.
|
||||
alpha: The parameter that determines the slope of the spectrum.
|
||||
k: A constant.
|
||||
|
||||
Returns:
|
||||
A tensor with the same shape as `tensor` containing 1/f noise.
|
||||
"""
|
||||
tensor = torch.randn_like(tensor)
|
||||
fft = torch.fft.fft2(tensor)
|
||||
freq = torch.arange(1, len(fft) + 1, dtype=torch.float).reshape(
|
||||
(len(fft),) + (1,) * (tensor.dim() - 1),
|
||||
)
|
||||
spectral_density = k / freq**alpha
|
||||
noise = torch.rand(tensor.shape).mul_(spectral_density)
|
||||
mean = torch.mean(noise, dim=(-2, -1), keepdim=True).to(tensor.device)
|
||||
std = torch.std(noise, dim=(-2, -1), keepdim=True).to(tensor.device)
|
||||
return noise.to(tensor.device).sub_(mean).div_(std)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"NoiseType",
|
||||
"NoiseError",
|
||||
"scale_noise",
|
||||
"green_noise_like",
|
||||
"highres_pyramid_noise_like",
|
||||
"laplacian_noise_like",
|
||||
"pink_noise_like",
|
||||
"power_noise_like",
|
||||
"pyramid_noise_like",
|
||||
"pyramid_old_noise_like",
|
||||
"rand_perlin_like",
|
||||
"studentt_noise_like",
|
||||
"uniform_noise_like",
|
||||
)
|
||||
@@ -0,0 +1,912 @@
|
||||
# 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, SonarNormalizeNoiseNodeMixin
|
||||
from .noise import CustomNoiseItemBase
|
||||
from .noise_generation import scale_noise
|
||||
|
||||
# ruff: noqa: ANN003, FBT001, FBT002
|
||||
|
||||
PREVIEW_FORMAT = comfy.latent_formats.SD15()
|
||||
|
||||
|
||||
def make_preview_result(img, result, prefix="sonar_temp"):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
prefix_append = f"{prefix}_" + "".join(
|
||||
random.choice("abcdefghijklmnopqrstupvxyz") # noqa: S311
|
||||
for x in range(5)
|
||||
)
|
||||
full_output_folder, filename, counter, subfolder, _ = (
|
||||
folder_paths.get_save_image_path(prefix_append, output_dir)
|
||||
)
|
||||
filename = f"{filename}_{counter:05}_.png"
|
||||
file_path = os.path.join(full_output_folder, filename) # noqa: PTH118
|
||||
img.save(file_path, compress_level=1)
|
||||
|
||||
return {
|
||||
"ui": {
|
||||
"images": [
|
||||
{"filename": filename, "subfolder": subfolder, "type": "temp"},
|
||||
],
|
||||
},
|
||||
"result": result,
|
||||
}
|
||||
|
||||
|
||||
class ChannelMixer:
|
||||
def __init__(self, channel_count, common_mode, channel_correlation):
|
||||
self.channel_count = channel_count
|
||||
self.common_mode = common_mode
|
||||
self.channel_correlation = channel_correlation
|
||||
self.mixer = self.build() if common_mode is not None else None
|
||||
|
||||
def build(self):
|
||||
c = self.channel_count
|
||||
common_mode = self.common_mode
|
||||
correlation_count = c * (c - 1) // 2
|
||||
channel_correlation = self.channel_correlation[:correlation_count]
|
||||
channel_correlation = torch.cat(
|
||||
(
|
||||
channel_correlation * common_mode,
|
||||
torch.full(
|
||||
(correlation_count - channel_correlation.numel(),),
|
||||
common_mode,
|
||||
),
|
||||
),
|
||||
)
|
||||
channel_mixer = torch.eye(c)
|
||||
channel_mixer[*torch.tril_indices(c, c, offset=-1)] = channel_correlation
|
||||
channel_mixer += channel_mixer.tril(-1).mT
|
||||
channel_mixer = torch.linalg.ldl_factor(channel_mixer).LD
|
||||
dc = torch.diagonal_copy(channel_mixer)
|
||||
torch.diagonal(channel_mixer)[:] = 1.0
|
||||
channel_mixer *= dc.clamp_min(0).sqrt().unsqueeze(0)
|
||||
channel_mixer /= channel_mixer.norm(dim=1, keepdim=True)
|
||||
return channel_mixer
|
||||
|
||||
def to(self, *args: list, **kwargs: dict):
|
||||
if self.mixer is not None:
|
||||
self.mixer = self.mixer.to(*args, **kwargs)
|
||||
return self
|
||||
|
||||
def apply(self, noise, shape, copy=False):
|
||||
if self.mixer is None:
|
||||
return noise if not copy else noise.clone()
|
||||
b, c, h, w = shape
|
||||
if c != self.channel_count:
|
||||
raise ValueError("Channel count mismatch")
|
||||
noise = self.mixer @ noise.swapaxes(0, 1).reshape(c, -1)
|
||||
return noise.reshape(c, b, h, w).swapaxes(1, 0)
|
||||
|
||||
def __call__(self, *args: list, **kwargs: dict):
|
||||
return self.apply(*args, **kwargs)
|
||||
|
||||
|
||||
class PowerFilter:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
min_freq=0.0,
|
||||
max_freq=0.7071,
|
||||
stretch=1.0,
|
||||
rotate=0.0,
|
||||
pnorm=2.0,
|
||||
alpha=0.0,
|
||||
scale=1.0,
|
||||
rel_bw=0.125,
|
||||
oversample=4,
|
||||
compose_with: None | PowerFilter = None,
|
||||
compose_mode="max",
|
||||
):
|
||||
self.min_freq = min_freq
|
||||
self.max_freq = max(max_freq, min_freq)
|
||||
self.stretch = stretch
|
||||
self.rotate = rotate
|
||||
self.pnorm = pnorm
|
||||
self.alpha = alpha
|
||||
self.scale = scale
|
||||
self.rel_bw = rel_bw
|
||||
self.oversample = oversample
|
||||
self.compose_with = compose_with
|
||||
self.compose_mode = compose_mode
|
||||
|
||||
def clone(self):
|
||||
fargs = {
|
||||
k: getattr(self, k)
|
||||
for k in (
|
||||
"min_freq",
|
||||
"max_freq",
|
||||
"stretch",
|
||||
"rotate",
|
||||
"pnorm",
|
||||
"alpha",
|
||||
"scale",
|
||||
"rel_bw",
|
||||
"oversample",
|
||||
"compose_mode",
|
||||
)
|
||||
}
|
||||
fargs["compose_with"] = (
|
||||
self.compose_with.clone() if self.compose_with is not None else None
|
||||
)
|
||||
return self.__class__(**fargs)
|
||||
|
||||
@classmethod
|
||||
def compose(cls, a, b, compose_mode="max"):
|
||||
if a.shape != b.shape:
|
||||
raise ValueError("Filter compose size mismatch!")
|
||||
cf = {
|
||||
"max": torch.max,
|
||||
"min": torch.min,
|
||||
"add": torch.add,
|
||||
"sub": torch.sub,
|
||||
"mul": torch.mul,
|
||||
}.get(compose_mode, torch.max)
|
||||
return cf(a, b).clamp_(min=0.0)
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, op, shape, mix=1.0, normalization_factor=1.0):
|
||||
height, width = shape[-2:]
|
||||
hfreq_bins = width // 2 + 1
|
||||
|
||||
# Flat unit gain frequency response
|
||||
if mix < 1.0:
|
||||
flat = torch.ones(1, 1, height, hfreq_bins)
|
||||
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
|
||||
# space)
|
||||
fc = torch.complex(
|
||||
# real-fftfreq
|
||||
torch.linspace(0, 0.5, oversample * hfreq_bins),
|
||||
# normal fftfreq
|
||||
torch.linspace(
|
||||
-(height // 2) / height,
|
||||
((height - 1) // 2) / height,
|
||||
oversample * height,
|
||||
).unsqueeze(1),
|
||||
)
|
||||
# Rotate, stretch and p-norm
|
||||
if abs(self.rotate) >= 1e-3:
|
||||
fc *= torch.exp(1.0j * torch.deg2rad(torch.scalar_tensor(self.rotate)))
|
||||
if self.stretch > 1.0:
|
||||
fc.real *= self.stretch
|
||||
else:
|
||||
fc.imag *= 1.0 / self.stretch
|
||||
if abs(self.pnorm - 2.0) < 1e-3:
|
||||
d = fc.abs()
|
||||
else:
|
||||
d = (
|
||||
torch.view_as_real(fc)
|
||||
.abs()
|
||||
.pow(self.pnorm)
|
||||
.sum(-1)
|
||||
.pow(1.0 / self.pnorm)
|
||||
)
|
||||
|
||||
# filter gain function
|
||||
op = torch.empty_like(d)
|
||||
m_highpass = d >= self.min_freq
|
||||
m_lowpass = d < self.max_freq
|
||||
m_band = m_highpass & m_lowpass
|
||||
# 1 / f^alpha for the band-pass region
|
||||
op[m_band] = d[m_band].pow(-self.alpha)
|
||||
# easing gaussian (TODO: try cosine windows)
|
||||
m_lowpass = ~m_lowpass
|
||||
op[m_lowpass] = math.pow(self.max_freq, -self.alpha) * torch.exp(
|
||||
-(d[m_lowpass] - self.max_freq).square() / (rel_bw * self.max_freq) ** 2,
|
||||
)
|
||||
if self.min_freq > 0.0:
|
||||
m_highpass = ~m_highpass
|
||||
op[m_highpass] = math.pow(self.min_freq, -self.alpha) * torch.exp(
|
||||
-(d[m_highpass] - self.min_freq).square()
|
||||
/ (rel_bw * self.min_freq) ** 2,
|
||||
)
|
||||
op = torch.nn.functional.interpolate(
|
||||
op[None, None, ...],
|
||||
(height, hfreq_bins),
|
||||
mode="bilinear",
|
||||
align_corners=True,
|
||||
)
|
||||
op = op.roll(-(height // 2), -2) # ifftshift
|
||||
if self.alpha > 0:
|
||||
# In general, the mean offset should be kept as is, sampled from
|
||||
# N(0, 1 / sqrt(H*W) ). However, gain goes to inf when alpha>0.
|
||||
op[..., 0, 0] = 0
|
||||
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
|
||||
|
||||
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,
|
||||
x: Tensor,
|
||||
sigma_min: float | None,
|
||||
sigma_max: float | None,
|
||||
seed: int | None,
|
||||
cpu: bool = True,
|
||||
normalized=True,
|
||||
):
|
||||
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",
|
||||
)
|
||||
noise_sampler = BrownianTreeNoiseSampler(
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu,
|
||||
)
|
||||
else:
|
||||
|
||||
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(
|
||||
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,
|
||||
)
|
||||
img = (
|
||||
torch.cat(
|
||||
(
|
||||
*filter_preview,
|
||||
noise.mul_(1 / 3).tanh_().add_(1.0).mul_(128.0),
|
||||
),
|
||||
dim=-1,
|
||||
)
|
||||
.clamp(0, 255)
|
||||
.to(torch.uint8)
|
||||
)
|
||||
return Image.fromarray(img[0, 0].numpy())
|
||||
|
||||
|
||||
def rfft2_to_fft2(x):
|
||||
"""Apply hermitian-summetry to reconstruct the second half of a fft.
|
||||
|
||||
Only for previews.
|
||||
"""
|
||||
height, width = x.shape[-2:]
|
||||
x_r = x.roll(height // 2, -2) # torch.fft.fftshift(x, -2)
|
||||
x_l = x_r[..., 1 : -1 if width & 1 else None]
|
||||
x_l = torch.flip(x_l.conj(), dims=(-2, -1))
|
||||
if height & 1 == 0:
|
||||
x_l = x_l.roll(1, -2)
|
||||
return torch.cat((x_l, x_r), dim=-1)
|
||||
|
||||
|
||||
class PowerFilterNoiseItem(PowerNoiseItem):
|
||||
def __init__(self, factor, *, noise, normalize_noise, normalize_result, **kwargs):
|
||||
super().__init__(
|
||||
factor,
|
||||
noise=noise.clone(),
|
||||
normalize_noise=normalize_noise,
|
||||
normalize_result=normalize_result,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def clone_key(self, k):
|
||||
if k == "noise":
|
||||
return self.noise.clone()
|
||||
return super().clone_key(k)
|
||||
|
||||
def make_noise_sampler(
|
||||
self,
|
||||
x: Tensor,
|
||||
sigma_min: float | None,
|
||||
sigma_max: float | None,
|
||||
seed: int | None,
|
||||
cpu: bool = True,
|
||||
normalized=True,
|
||||
):
|
||||
shape, device = x.shape, x.device
|
||||
normalize_noise = self.get_normalize("normalize_noise", False) # noqa: FBT003
|
||||
normalize_result = self.get_normalize("normalize_result", normalized)
|
||||
filter_rfft = self.make_filter(shape).to(device, non_blocking=True)
|
||||
noise_sampler = self.noise.make_noise_sampler(
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed,
|
||||
cpu,
|
||||
normalized=normalize_noise,
|
||||
)
|
||||
|
||||
return self.make_noise_sampler_internal(
|
||||
x,
|
||||
noise_sampler,
|
||||
filter_rfft,
|
||||
normalized=normalize_result,
|
||||
)
|
||||
|
||||
def preview(self, size=(128, 128)):
|
||||
if getattr(self, "preview_type", None) != "custom":
|
||||
return super().preview(size=size)
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn((1, 4, *size), dtype=torch.float, device="cpu")
|
||||
ns = self.noise.make_noise_sampler(
|
||||
x,
|
||||
torch.scalar_tensor(0.0),
|
||||
torch.scalar_tensor(14.0),
|
||||
0,
|
||||
True, # noqa: FBT003
|
||||
normalized=self.normalize_noise is True,
|
||||
)
|
||||
filtered_ns = self.make_noise_sampler_internal(
|
||||
x,
|
||||
ns,
|
||||
self.make_filter(x.shape),
|
||||
self.normalize_result in (True, None),
|
||||
)
|
||||
filtered_noise = filtered_ns(
|
||||
torch.scalar_tensor(14.0),
|
||||
torch.scalar_tensor(10.0),
|
||||
)
|
||||
previewer = latent_preview.get_previewer(None, PREVIEW_FORMAT)
|
||||
default_preview = super().preview(size=size).convert("RGB")
|
||||
preview = previewer.decode_latent_to_preview(filtered_noise.cpu())
|
||||
default_preview.paste(
|
||||
preview.resize((size[-1], size[-2])),
|
||||
box=(size[-1] * 2, 0),
|
||||
)
|
||||
return default_preview
|
||||
|
||||
|
||||
class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls, *args: list, **kwargs: dict):
|
||||
result = super().INPUT_TYPES(*args, **kwargs)
|
||||
result["required"] |= {
|
||||
"time_brownian": ("BOOLEAN", {"default": False}),
|
||||
"alpha": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": -5.0,
|
||||
"max": 5.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"max_freq": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.7071,
|
||||
"min": 0.0,
|
||||
"max": 0.7071,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"min_freq": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.7071,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"stretch": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.01,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"rotate": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": -90,
|
||||
"max": 90,
|
||||
"step": 5,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"pnorm": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 2,
|
||||
"min": 0.125,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"mix": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"common_mode": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": -100.0,
|
||||
"max": 100.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"channel_correlation": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "1, 1, 1, 1, 1, 1",
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
},
|
||||
),
|
||||
"preview": (("none", "no_mix", "mix"),),
|
||||
}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return PowerNoiseItem
|
||||
|
||||
def go(
|
||||
self,
|
||||
preview="none",
|
||||
**kwargs,
|
||||
):
|
||||
result = super().go(**kwargs)
|
||||
if preview == "none":
|
||||
return result
|
||||
if preview == "no_mix":
|
||||
kwargs["mix"] = 1.0
|
||||
img = self.get_item_class()(preview_type=preview, **kwargs).preview()
|
||||
return make_preview_result(img, result)
|
||||
|
||||
|
||||
class SonarPowerFilterNoiseNode(SonarPowerNoiseNode, SonarNormalizeNoiseNodeMixin):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
|
||||
for k in (
|
||||
"min_freq",
|
||||
"max_freq",
|
||||
"stretch",
|
||||
"rotate",
|
||||
"pnorm",
|
||||
"alpha",
|
||||
"time_brownian",
|
||||
):
|
||||
del result["required"][k]
|
||||
result["required"] |= {
|
||||
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
|
||||
"sonar_power_filter": ("SONAR_POWER_FILTER",),
|
||||
"filter_norm_factor": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"normalize_result": (("default", "forced", "disabled"),),
|
||||
"normalize_noise": (("default", "forced", "disabled"),),
|
||||
}
|
||||
result["required"]["preview"] = ((*result["required"]["preview"][0], "custom"),)
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return PowerFilterNoiseItem
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
sonar_power_filter,
|
||||
filter_norm_factor,
|
||||
normalize_noise,
|
||||
normalize_result,
|
||||
preview="none",
|
||||
**kwargs: dict,
|
||||
):
|
||||
return super().go(
|
||||
factor=factor,
|
||||
noise=sonar_custom_noise,
|
||||
normalize_noise=self.get_normalize(normalize_noise),
|
||||
normalize_result=self.get_normalize(normalize_result),
|
||||
preview=preview,
|
||||
time_brownian=True,
|
||||
power_filter=sonar_power_filter,
|
||||
filter_norm_factor=filter_norm_factor,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class SonarPowerFilterNode:
|
||||
RETURN_TYPES = ("SONAR_POWER_FILTER",)
|
||||
CATEGORY = "advanced/noise"
|
||||
FUNCTION = "go"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"alpha": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": -5.0,
|
||||
"max": 5.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"max_freq": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.7071,
|
||||
"min": 0.0,
|
||||
"max": 0.7071,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"min_freq": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.7071,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"stretch": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.01,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"rotate": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": -90,
|
||||
"max": 90,
|
||||
"step": 5,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"pnorm": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 2,
|
||||
"min": 0.125,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"oversample": ("INT", {"default": 4, "min": 1, "max": 128}),
|
||||
"blur": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.125,
|
||||
"min": -10.0,
|
||||
"max": 10.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"scale": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": -100.0,
|
||||
"max": 100.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"compose_mode": (("max", "min", "add", "sub", "mul"),),
|
||||
},
|
||||
"optional": {
|
||||
"power_filter_opt": ("SONAR_POWER_FILTER",),
|
||||
},
|
||||
}
|
||||
|
||||
def go(
|
||||
self,
|
||||
min_freq=0.0,
|
||||
max_freq=0.7071,
|
||||
stretch=1.0,
|
||||
rotate=0.0,
|
||||
pnorm=2.0,
|
||||
alpha=0.0,
|
||||
blur=0.125,
|
||||
oversample=4,
|
||||
scale=1.0,
|
||||
compose_mode="max",
|
||||
power_filter_opt=None,
|
||||
):
|
||||
return (
|
||||
PowerFilter(
|
||||
min_freq=min_freq,
|
||||
max_freq=max_freq,
|
||||
stretch=stretch,
|
||||
rotate=rotate,
|
||||
pnorm=pnorm,
|
||||
alpha=alpha,
|
||||
scale=scale,
|
||||
rel_bw=blur,
|
||||
oversample=oversample,
|
||||
compose_mode=compose_mode,
|
||||
compose_with=power_filter_opt,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SonarPreviewFilterNode:
|
||||
RETURN_TYPES = ("SONAR_POWER_FILTER",)
|
||||
CATEGORY = "advanced/noise"
|
||||
FUNCTION = "go"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"sonar_power_filter": ("SONAR_POWER_FILTER",),
|
||||
"filter_gain": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1 / 3,
|
||||
"min": 0.0,
|
||||
"max": 1000000.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"kernel_gain": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1 / 3,
|
||||
"min": 0.0,
|
||||
"max": 1000000.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"norm_factor": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"preview_size": (
|
||||
(
|
||||
"128x128",
|
||||
"256x256",
|
||||
"384x256",
|
||||
"256x384",
|
||||
"768x512",
|
||||
"512x768",
|
||||
"768x768",
|
||||
"128x127",
|
||||
"127x128",
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
def go(
|
||||
self,
|
||||
sonar_power_filter,
|
||||
filter_gain=1 / 3,
|
||||
kernel_gain=1 / 3,
|
||||
norm_factor=1.0,
|
||||
preview_size="256x256",
|
||||
):
|
||||
filt = sonar_power_filter.clone()
|
||||
filt.preview_type = "custom"
|
||||
preview_size = tuple(int(val) for val in preview_size.split("x", 1))
|
||||
return make_preview_result(
|
||||
filt.preview(
|
||||
size=(preview_size[1], preview_size[0]),
|
||||
filter_gain=filter_gain,
|
||||
kernel_gain=kernel_gain,
|
||||
normalization_factor=norm_factor,
|
||||
),
|
||||
(filt,),
|
||||
)
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from sys import stderr
|
||||
from typing import Any, Callable, NamedTuple
|
||||
|
||||
import torch
|
||||
@@ -44,6 +45,8 @@ class SonarConfig(NamedTuple):
|
||||
|
||||
|
||||
class SonarBase:
|
||||
DEFAULT_NOISE_TYPE = noise.NoiseType.GAUSSIAN
|
||||
|
||||
def __init__(self, cfg: SonarConfig) -> None:
|
||||
self.history_d = None
|
||||
self.cfg = cfg
|
||||
@@ -59,11 +62,11 @@ class SonarBase:
|
||||
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
|
||||
if noise_sampler is not None and self.cfg.noise_type not in (
|
||||
None,
|
||||
noise.NoiseType.GAUSSIAN,
|
||||
self.DEFAULT_NOISE_TYPE,
|
||||
):
|
||||
# Possibly we should just use the supplied already-created noise sampler here.
|
||||
raise ValueError(
|
||||
"Unexpected noise_sampler presence with non-default noise type requested",
|
||||
print(
|
||||
"Sonar: Warning: Noise sampler supplied, overriding noise type from settings",
|
||||
file=stderr,
|
||||
)
|
||||
if self.cfg.custom_noise:
|
||||
noise_sampler = self.cfg.custom_noise.make_noise_sampler(
|
||||
@@ -72,14 +75,15 @@ class SonarBase:
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
)
|
||||
elif noise_sampler is None and self.cfg.noise_type:
|
||||
elif noise_sampler is None:
|
||||
noise_sampler = noise.get_noise_sampler(
|
||||
self.cfg.noise_type,
|
||||
self.cfg.noise_type or self.DEFAULT_NOISE_TYPE,
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=True,
|
||||
normalized=True,
|
||||
)
|
||||
self.noise_sampler = noise_sampler
|
||||
return noise_sampler
|
||||
@@ -100,6 +104,7 @@ class SonarBase:
|
||||
None,
|
||||
seed=self.extra_args.get("seed"),
|
||||
cpu=True,
|
||||
normalized=True,
|
||||
)
|
||||
self.history_d = ns(None, None)
|
||||
else:
|
||||
@@ -157,34 +162,42 @@ class SonarGuidanceMixin:
|
||||
if self.ref_latent.device != x.device:
|
||||
self.ref_latent = self.ref_latent.to(device=x.device)
|
||||
if self.guidance.guidance_type == GuidanceType.LINEAR:
|
||||
return self.guidance_linear(x)
|
||||
return self.guidance_linear(x, self.ref_latent, self.guidance.factor)
|
||||
if self.guidance.guidance_type == GuidanceType.EULER:
|
||||
return self.guidance_euler(step_index, x, denoised)
|
||||
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
|
||||
return self.guidance_euler(
|
||||
sigma,
|
||||
sigma_next,
|
||||
x,
|
||||
denoised,
|
||||
self.ref_latent,
|
||||
self.guidance.factor,
|
||||
)
|
||||
raise ValueError("Sonar: Guidance: Unknown guidance type")
|
||||
|
||||
@staticmethod
|
||||
def guidance_euler(
|
||||
self,
|
||||
step_index: int,
|
||||
sigma: Tensor,
|
||||
sigma_next: Tensor,
|
||||
x: Tensor,
|
||||
denoised: Tensor,
|
||||
):
|
||||
ref_latent: Tensor,
|
||||
factor: float = 0.2,
|
||||
) -> Tensor:
|
||||
avg_t = denoised.mean(dim=[1, 2, 3], keepdim=True)
|
||||
std_t = denoised.std(dim=[1, 2, 3], keepdim=True)
|
||||
ref_img_shift = self.ref_latent * std_t + avg_t
|
||||
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
|
||||
ref_img_shift = ref_latent * std_t + avg_t
|
||||
|
||||
d = sampling.to_d(x, sigma, ref_img_shift)
|
||||
dt = (sigma_next - sigma) * self.guidance.factor
|
||||
dt = (sigma_next - sigma) * factor
|
||||
return x + d * dt
|
||||
|
||||
def guidance_linear(
|
||||
self,
|
||||
x: Tensor,
|
||||
):
|
||||
@staticmethod
|
||||
def guidance_linear(x: Tensor, ref_latent: Tensor, factor: float = 0.2) -> Tensor:
|
||||
avg_t = x.mean(dim=[1, 2, 3], keepdim=True)
|
||||
std_t = x.std(dim=[1, 2, 3], keepdim=True)
|
||||
ref_img_shift = self.ref_latent * std_t + avg_t
|
||||
return (1.0 - self.guidance.factor) * x + self.guidance.factor * ref_img_shift
|
||||
ref_img_shift = ref_latent * std_t + avg_t
|
||||
return (1.0 - factor) * x + factor * ref_img_shift
|
||||
|
||||
|
||||
class SonarWithGuidance(SonarBase, SonarGuidanceMixin):
|
||||
@@ -386,14 +399,6 @@ class SonarEulerAncestral(SonarSampler):
|
||||
):
|
||||
if sonar_config is None:
|
||||
sonar_config = SonarConfig()
|
||||
if (
|
||||
noise_sampler is not None
|
||||
and sonar_config.noise_type != noise.NoiseType.GAUSSIAN
|
||||
):
|
||||
# Possibly we should just use the supplied already-created noise sampler here.
|
||||
raise ValueError(
|
||||
"Unexpected noise_sampler presence with non-default noise type requested",
|
||||
)
|
||||
s_in = x.new_ones([x.shape[0]])
|
||||
sonar = cls(
|
||||
eta,
|
||||
@@ -430,6 +435,8 @@ class SonarEulerAncestral(SonarSampler):
|
||||
|
||||
|
||||
class SonarDPMPPSDE(SonarSampler):
|
||||
DEFAULT_NOISE_TYPE = noise.NoiseType.BROWNIAN
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
eta: float = 1.0,
|
||||
@@ -560,15 +567,6 @@ class SonarDPMPPSDE(SonarSampler):
|
||||
):
|
||||
if sonar_config is None:
|
||||
sonar_config = SonarConfig()
|
||||
if (
|
||||
noise_sampler is not None
|
||||
and sonar_config.noise_type != noise.NoiseType.GAUSSIAN
|
||||
):
|
||||
# Possibly we should just use the supplied already-created noise sampler here.
|
||||
raise ValueError(
|
||||
"Unexpected noise_sampler presence with non-default noise type requested",
|
||||
)
|
||||
|
||||
s_in = x.new_ones([x.shape[0]])
|
||||
sonar = cls(
|
||||
eta,
|
||||
|
||||