Author SHA1 Message Date
blepping 25d1bf5631 Bump changelog date, minor doc update 2025-08-05 16:55:46 -06:00
blepping 5280a2ca11 Internal refactoring/cleanups
Added a SonarAdvancedVoronoiNoise node
2025-08-04 14:42:43 -06:00
blepping ee6410523e Convert more nodes to the new input types system
Make WaveletCFG less spammy in verbose mode
WaveletCFG will pass sigmas and other information to latent operations that support it
Add SonarCustomNoiseParameters node
Add replace/replace_keepsign/replace_avoidsign quantile norm modes
2025-07-29 13:04:03 -06:00
blepping 849b7266e7 Fix quantile norm scale_down mode
Fix advanced latent operation breakage
Handle another edge case in step detection
2025-07-25 22:16:50 -06:00
blepping e61fc24ebe Stage 1 2025-07-23 05:17:07 -06:00
8 changed files with 154 additions and 525 deletions
-11
View File
@@ -2,17 +2,6 @@
Note, only relatively significant changes to user-visible functionality will be included here. Most recent changes at the top.
## 20250808
Aside from the `sigmoid` quantile mode change and `SonarShuffledNoise`, these changes should not break workflows. Let me know if you experience anything unusual.
* The `sigmoid` quantile norm mode was renamed to `sigmoid_keepsign` since that's what it was doing. There is a replacement `sigmoid` quantile norm mode that doesn't care about sign.
* Quantile normalization can now take a negative quantile to consider values closest to zero the "extremes". Note: Experimental feature that may not be implemented correctly/subject to change.
* `SonarShuffledNoise` node reworked. Unfortunately, this will break workflows. If anyone has a burning need for the old version, let me know and I can bring it back as a separate node. The new approach should be better in general though.
* Fixed momentum sampler init parameter passing.
* Added more Voronoi noise octave modes.
* Added replace_2pt/3pt (and variants) quantile norm result modes that use multiple replacement values.
## 20250805
Once again, large set of changes/internal reorganization which may break stuff. If you run into problems or experience anything weird, please create an issue.
+2 -8
View File
@@ -461,7 +461,7 @@ The node has many options for calculating the distance between the feature point
* You can enter a `+` (plus symbol) separated list of modes. The modes will be calculated and the result will be the average. Distance modes all have the common parameter `dscale` which defaults to 1 and can be overridden. Result modes use `rscale`. See below for a description on passing parameters.
* It's possible to pass parameters to distance and result modes. Example with a result mode: `diff:idx1=0:idx2=1:rscale=0.5`
Some modes act as wrappers to other modes. All modes will just ignore parameters they don't understand. Each time a submode is called, one level of "_" at the beginning of parameter names is stripped off. It's not user-friendly but this does allow passing parameters to submodes. Since unknown parameters are ignored, you only need to bother with this if the mode that's calling the submode will use that parameter. Dumb example: `gradient_magnitude:name1=diff:name2=gradient_magnitude:_name1=f4:_name2=f4`. `gradient_magnitude` takes two submodes that it calls (specified with `name1` and `name2`). The top-level `gradient_magnitude` will consume the `name1` and `name2` parameters, strip one level of underscores off the parameter names and call the submodes.
**Note**: Some modes act as wrappers for other modes. Unfortunately, there isn't currently a good way to escape parameters. Modes ignore parameters they don't understand and the wrapper modes will pass through any parameters they don't use themselves so you _can_ pass parameters to the submodes as long as they don't conflict (and only up to one level).
#### Distance Modes
@@ -475,9 +475,7 @@ Modes listed with the defaults for parameters they support. These modes also sup
* `angle:idx=2` - idxs here range from 0 to 2.
* `angle_tanh:idx=2` - Same as `angle` but scales the result with tanh.
* `angle_sigmoid:idx=2` - Same as `angle` but scales the result with the sigmoid function.
* `fuzz:name=euclidean:fuzz=0.25` - Acts as a wrapper for another mode (specified with `name`). Will perturb the result by `fuzz` percent of the absolute maximum value. Or more simply, randomizes values by +/- `fuzz` percentage so if you set `fuzz=1` you will essentially get pure noise.
* `fractal_norm:scale=0.1:multiplier=10.0:mode=sin:name=euclidean` - This acts as a wrapper for another mode. `mode` may be one of `sin`, `cos`. It will adjust the input to the mode it wraps by `scale * sin(input * multiplier)` (assuming `mode=sin`).
* `weight:h=1.0:w=1.0:z=0.25:name=euclidean` - This acts as a wrapper for another mode and allows you to scale height/width/z (depth) before calling it.
* `fuzz:name=euclidean:fuzz=0.25` - Acts as a wrapper for another result mode (specified with `name`). Will perturb the result by `fuzz` percent of the absolute maximum value. Or more simply, randomizes values by +/- `fuzz` percentage so if you set `fuzz=1` you will essentially get pure noise.
#### Result Modes
@@ -493,10 +491,6 @@ Modes listed with the defaults for parameters they support. These modes also sup
* `cellid` - Returns a discrete value for the area of each cell (diffusion models hate this). You will need to dilute the Voronoi noise a lot to actually use this. It could also possibly be used for masking.
* `median_distance`
* `fuzz:name=f1:fuzz=0.25` - Works the same as `fuzz` in distance modes. See the description there.
* `fractal_norm:scale=0.1:multiplier=10.0:mode=sin:name=diff` - This acts as a wrapper for another mode. `mode` may be one of `sin`, `cos`. It will adjust the input to the mode it wraps by `scale * sin(input * multiplier)` (assuming `mode=sin`).
* `ridge:name=diff:exp=-1.0` - Wraps another mode and may enhance cell borders (doesn't seem super useful).
* `softmin:temperature=50.0` - Passes the result through softmin and can be used to smooth the output from distance modes like `angle` that may experience abrupt changes as you move through `z`. It also supports a `use_sorted` parameter that will apply this adjustment to the sorted values as well if it's present and set to anything. Higher temperatures will result in less of a smoothing effect.
* `gradient_magnitude:name1=f4:name2=f4:padding_mode=replicate` - Wraps two other modes. Seems pretty nice for adding detail when using low numbers of feature points. `padding_mode` can be set to modes that PyTorch's `pad` function supports.
#### Depth
+1 -1
View File
@@ -98,7 +98,7 @@ class SamplerNodeSonarBase:
max=15.0,
tooltip="Multiplier applied to the result of normal sampling.",
)
.req_field_rand_init_noise_type(
.req_field_init_noise_type(
tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),
default="gaussian",
tooltip="Noise type to use when momentum_init is set to RANDOM.",
+15 -26
View File
@@ -689,11 +689,11 @@ class SonarQuantileFilteredNoiseNode(SonarCustomNoiseNodeBase):
)
.req_float_quantile(
default=0.85,
min=-1.0,
min=0.0,
max=1.0,
step=0.001,
round=False,
tooltip="When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the input and how many of the values are extreme. (Experimental) You can also use a negative quantile to consider values closest to 0 to be 'extreme'.",
tooltip="When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the input and how many of the values are extreme.",
)
.req_field_dim(
("global", "0", "1", "2", "3", "4"),
@@ -766,28 +766,23 @@ class SonarQuantileFilteredNoiseNode(SonarCustomNoiseNodeBase):
class SonarShuffledNoiseNode(SonarCustomNoiseNodeBase):
DESCRIPTION = (
"Custom noise type that allows shuffling noise along dimensions you specify."
)
DESCRIPTION = "Custom noise type that allows shuffling noise along some dimension"
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseNoChainInputTypes()
.req_customnoise_custom_noise(tooltip="Custom noise type to filter.")
.req_string_dims(
default="1,-2,-1",
default="-1",
tooltip="Comma separated list of dimensions to shuffle. May be negative to count from the end.",
)
.req_string_percentages(
default="1.0,0.25,0.25",
tooltip="Comma separated list of percentages (0.0 to 1.0 for 100%) of elements to shuffle. Paired with the list of dimensions and wrap if it's shorter. For example, if you specified three dimensions and two percentages, the third dimension in the list would use the first percentage again.",
.req_bool_flatten(
tooltip="Controls whether to flatten starting from the dimension before the shuffle operation. May be slow as this requires flattening and then reshaping the tensor back to the correct shape. Flattening will occur between the lowest and highest dimension in the list, other dimensions will be ignored. If they are the same, then it will just flatten from the lowest dimension.",
)
.req_bool_fork_rng(
default=True,
tooltip="When enabled, the RNG state will be forked to generate the shuffle values.",
)
.req_bool_no_identity(
default=True,
tooltip="When enabled, ensures shuffle never ends up selecting the original element.",
.req_float_percentage(
default=1.0,
min=0.0,
max=1.0,
tooltip="Percentage of elements to shuffle in the specified dimensions.",
),
)
@@ -800,24 +795,18 @@ class SonarShuffledNoiseNode(SonarCustomNoiseNodeBase):
*,
factor: float,
dims: str,
percentages: str,
fork_rng: bool,
no_identity: bool,
flatten: bool,
percentage: float,
custom_noise: object,
):
dims = dims.strip()
dims = () if not dims else tuple(int(i) for i in dims.split(","))
percentages = percentages.strip()
percentages = (
() if not percentages else tuple(float(p) for p in percentages.split(","))
)
return super().go(
factor,
noise=custom_noise,
dims=dims,
percentages=percentages,
fork_rng=fork_rng,
no_identity=no_identity,
flatten=flatten,
percentage=percentage,
)
+4 -13
View File
@@ -404,11 +404,11 @@ class SonarAdvancedDistroNoiseNode(SonarCustomNoiseNodeBase):
"FLOAT",
{
"default": 0.85,
"min": -1.0,
"min": 0.0,
"max": 1.0,
"step": 0.001,
"round": False,
"tooltip": "When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the distribution and how many of the values are extreme. (Experimental) You can use a negative quantile to consider the values closest to zero as extreme.",
"tooltip": "When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the distribution and how many of the values are extreme.",
},
),
"quantile_norm_mode": (
@@ -657,18 +657,9 @@ class SonarAdvancedVoronoiNoiseNode(SonarCustomNoiseNodeBase):
f"Possible result modes: {_pretty_result_modes}",
)
.req_field_octave_mode(
(
"same_features",
"new_features",
"same_invert_odd",
"same_invert_even",
"same_roll_chan_up",
"same_roll_chan_down",
"same_roll_dir_up",
"same_roll_dir_down",
),
("same_features", "new_features"),
default="new_features",
tooltip="Only relevant when generating multiple octaves. Controls whether octaves share a set of feature points or if they are different for each octave (note that this is slower). Modes starting with 'same' will use the same feature points per octave but may transform them.",
tooltip="Only relevant when generating multiple octaves. Controls whether octaves share a set of feature points or if they are different for each octave (note that this is slower).",
)
.req_int_octaves(
default=3,
-48
View File
@@ -1907,54 +1907,6 @@ class ShuffledNoise(CustomNoiseItemBase):
*args,
normalized=True,
**kwargs,
):
factor = self.factor
dims = tuple(d if d >= 0 else x.ndim + d for d in self.dims)
if not all(d >= 0 and d < x.ndim for d in dims):
raise ValueError("Dimension out of range")
percentages = self.percentages
if not all(0.0 <= p <= 1.0 for p in percentages):
raise ValueError("Percentage out of range, must be between 0 and 1")
ns = self.noise.make_noise_sampler(
x,
*args,
sigma_min=sigma_min,
sigma_max=sigma_max,
normalized=normalized,
**kwargs,
)
if not percentages or not dims or all(p == 0 for p in percentages):
return ns
n_percentages = len(percentages)
fork_rng = self.fork_rng
no_identity = self.no_identity
def noise_sampler(sigma, sigma_next):
noise = scale_noise(
ns(sigma, sigma_next),
factor,
normalized=normalized,
)
with torch.random.fork_rng(enabled=fork_rng, devices=(noise.device,)):
for idx, dim in enumerate(dims):
noise = utils.elementwise_shuffle_by_dim(
noise,
dim=dim,
prob=percentages[idx % n_percentages],
no_identity=no_identity,
)
return noise
return noise_sampler
def make_noise_sampler_(
self,
x,
sigma_min,
sigma_max,
*args,
normalized=True,
**kwargs,
):
factor = self.factor
dims = {x.ndim + d if d < 0 else d for d in self.dims}
+83 -242
View File
@@ -6,7 +6,6 @@ from enum import Enum, auto
from typing import TYPE_CHECKING, Callable, ClassVar, NamedTuple
import torch
import torch.nn.functional as F # noqa: N812
from comfy.k_diffusion import sampling
from comfy.model_management import throw_exception_if_processing_interrupted
from torch import FloatTensor, Generator, Tensor
@@ -1293,42 +1292,6 @@ class VoronoiNoiseGenerator(NoiseGenerator):
MIN_DIMS = 4
MAX_DIMS = 4
voronoi_distance_modes = frozenset((
"angle_sigmoid",
"angle_tanh",
"angle",
"chebyshev",
"euclidean",
"fractal_norm",
"fuzz",
"manhatten",
"minkowski",
"quadratic",
"weight",
))
voronoi_result_modes = frozenset((
"cellid",
"diff",
"diff2",
"f",
"f1",
"f2",
"f3",
"f4",
"fractal_norm",
"fuzz",
"inv_f",
"inv_f1",
"inv_f2",
"inv_f3",
"inv_f4",
"gradient_magnitude",
"median_distance",
"ridge",
"softmin",
))
@classmethod
def ng_params(cls, *, no_super: bool = False):
result = {
@@ -1425,26 +1388,7 @@ class VoronoiNoiseGenerator(NoiseGenerator):
)
def get_feature_points(self, octave: int) -> torch.Tensor:
result = self.feature_points[octave % len(self.feature_points)]
odd_octave = (octave % 2) == 1
om = self.octave_mode
if (om == "same_invert_odd" and odd_octave) or (
om == "same_invert_even" and not odd_octave
):
return 1.0 - result
if octave > 0 and om in {"same_roll_chan_up", "same_roll_chan_down"}:
return torch.roll(
result,
(-1 if om == "same_roll_chan_up" else 1) * (octave % 3),
dims=(1,),
)
if octave > 0 and om in {"same_roll_dir_up", "same_roll_dir_down"}:
return torch.roll(
result,
(-1 if om == "same_roll_dir_up" else 1) * (octave % 3),
dims=(3,),
)
return result
return self.feature_points[octave % len(self.feature_points)]
def get_distance_mode(self, octave: int) -> torch.Tensor:
return self.distance_mode[octave % len(self.distance_mode)]
@@ -1452,29 +1396,17 @@ class VoronoiNoiseGenerator(NoiseGenerator):
def get_result_mode(self, octave: int) -> torch.Tensor:
return self.result_mode[octave % len(self.result_mode)]
def voronoi_call_mode(
self,
name: str,
*,
result: bool,
args: list | tuple = (),
kwargs: dict | None = None,
) -> torch.Tensor:
name = name.strip().lower()
modes = self.voronoi_result_modes if result else self.voronoi_distance_modes
mode_label = "result" if result else "distance"
if name not in modes:
errstr = f"Bad Voronoi {mode_label} mode {name}"
raise ValueError(errstr)
kwargs = (
{}
if kwargs is None
else {
k[1:] if k.startswith("_") and len(k) > 1 else k: v
for k, v in kwargs.items()
}
)
return getattr(self, f"_voronoi_{mode_label}_{name}")(*args, **kwargs)
voronoi_distance_modes = frozenset((
"euclidean",
"manhatten",
"chebyshev",
"minkowski",
"quadratic",
"angle",
"angle_tanh",
"angle_sigmoid",
"fuzz",
))
@staticmethod
def _voronoi_distance_euclidean(d: torch.Tensor, **_kwargs) -> torch.Tensor:
@@ -1539,59 +1471,19 @@ class VoronoiNoiseGenerator(NoiseGenerator):
.acos_()
)
def _voronoi_distance_weight(
self,
d: torch.Tensor,
*args,
name: str = "euclidean",
h: float | str = 1.0,
w: float | str = 1.0,
z: float | str = 0.25,
**kwargs,
) -> torch.Tensor:
weights = d.new_tensor((float(h), float(w), float(z)))
return self.voronoi_call_mode(
name,
result=False,
args=(d * weights, *args),
kwargs=kwargs,
)
def _voronoi_distance_fractal_norm(
self,
d: torch.Tensor,
*args,
name: str = "euclidean",
mode: str = "sin",
scale: str | float = 0.1,
multiplier: str | float = 10.0,
**kwargs,
) -> torch.Tensor:
if mode == "sin":
fun = torch.sin
elif mode == "cos":
fun = torch.cos
else:
raise ValueError(
"Bad mode parameter for fractal_norm distance mode, must be one of: sin, cos",
)
adjustment = float(scale) * fun(d * float(multiplier))
return self.voronoi_call_mode(
name,
result=False,
args=(d + adjustment, *args),
kwargs=kwargs,
)
def _voronoi_distance_fuzz(
self,
*args,
name: str = "euclidean",
name: str = "f1",
fuzz: float | str = 0.25,
**kwargs,
) -> torch.Tensor:
fuzz = float(fuzz)
result = self.voronoi_call_mode(name, result=False, args=args, kwargs=kwargs)
name = name.strip().lower()
if name not in self.voronoi_distance_modes:
errstr = f"Bad voronoi fuzz distance mode name: {name}"
raise ValueError(errstr)
result = getattr(self, f"_voronoi_distance_{name}")(*args, **kwargs)
rmin, rmax = result.aminmax()
fuzz = max(abs(rmin.item()), abs(rmax.item())) * fuzz
result += (
@@ -1602,6 +1494,48 @@ class VoronoiNoiseGenerator(NoiseGenerator):
)
return utils.normalize_to_scale(result, rmin.item(), rmax.item(), dim=(-2, -1))
def voronoi_distance(self, d: torch.Tensor, octave: int) -> torch.Tensor:
modes = self.get_distance_mode(octave).split("+")
result_scale_base = 1.0 / len(modes)
result = None
for mode in modes:
if ":" in mode:
mode_name, *mode_rest = mode.split(":")
mode_kwargs = dict(
tuple(v.strip() for v in di.split("=", 1)) for di in mode_rest
)
result_scale = result_scale_base * float(mode_kwargs.pop("dscale", 1.0))
else:
mode_name = mode
mode_kwargs = {}
result_scale = result_scale_base
if mode_name not in self.voronoi_distance_modes:
errstr = f"Bad distance mode {mode}"
raise ValueError(errstr)
handler = getattr(self, f"_voronoi_distance_{mode_name}")
curr_result = handler(d, **mode_kwargs).mul_(result_scale)
result = curr_result if result is None else result.add_(curr_result)
return result
voronoi_result_modes = frozenset((
"f",
"f1",
"f2",
"f3",
"f4",
"diff",
"diff2",
"inv_f",
"inv_f1",
"inv_f2",
"inv_f3",
"inv_f4",
"cellid",
"ridge",
"median_distance",
"fuzz",
))
@staticmethod
def _voronoi_result_f(
_d: torch.Tensor,
@@ -1675,9 +1609,12 @@ class VoronoiNoiseGenerator(NoiseGenerator):
exp: float | str = -10.0,
**kwargs,
) -> torch.Tensor:
name = name.strip().lower()
if name not in self.voronoi_result_modes:
errstr = f"Bad voronoi ridge result mode name: {name}"
raise ValueError(errstr)
return 1.0 - (
float(exp)
* self.voronoi_call_mode(name, result=True, args=args, kwargs=kwargs)
float(exp) * getattr(self, f"_voronoi_result_{name}")(*args, **kwargs)
)
@staticmethod
@@ -1688,76 +1625,6 @@ class VoronoiNoiseGenerator(NoiseGenerator):
) -> torch.Tensor:
return get_sorted().median(dim=-1).values
@staticmethod
def _voronoi_result_softmin(
d: torch.Tensor,
*_args,
temperature=50.0,
use_sorted=None,
d_orig: torch.Tensor,
get_sorted: Callable,
**_kwargs,
) -> torch.Tensor:
d_norm = d_orig.norm(dim=-1)
soft_weights = F.softmax(-d_norm * float(temperature), dim=-1)
eff_d = get_sorted() if use_sorted is not None else d
return (eff_d * soft_weights).sum(dim=-1)
def _voronoi_result_gradient_magnitude(
self,
*args,
name1: str = "f4",
name2: str = "f4",
pad_mode: str = "replicate",
**kwargs,
) -> torch.Tensor:
r1 = self.voronoi_call_mode(name1, result=True, args=args, kwargs=kwargs)
r1_padded = F.pad(r1, (1, 1, 1, 1), mode=pad_mode)
if name2 != name1:
r2 = self.voronoi_call_mode(name2, result=True, args=args, kwargs=kwargs)
r2_padded = F.pad(r2, (1, 1, 1, 1), mode=pad_mode)
else:
r2 = r1
r2_padded = r1_padded
dx = r1_padded[..., 1:-1, 2:] - r2_padded[..., 1:-1, :-2]
dy = r1_padded[..., 2:, 1:-1] - r2_padded[..., :-2, 1:-1]
return (dx**2 + dy**2).sqrt_()
def _voronoi_result_fractal_norm(
self,
d: torch.Tensor,
*args,
name: str = "diff",
mode: str = "sin",
scale: str | float = 0.1,
multiplier: str | float = 10.0,
**kwargs,
) -> torch.Tensor:
if mode == "sin":
fun = torch.sin
elif mode == "cos":
fun = torch.cos
else:
raise ValueError(
"Bad mode parameter for fractal_norm result mode, must be one of: sin, cos",
)
d_adjusted = float(scale) * fun(d * float(multiplier))
my_d_sorted = None
def my_get_sorted():
nonlocal my_d_sorted
if my_d_sorted is not None:
return my_d_sorted
my_d_sorted = d_adjusted.sort(dim=-1).values
return my_d_sorted
return self.voronoi_call_mode(
name,
result=True,
args=(d_adjusted, *args),
kwargs=kwargs | {"get_sorted": my_get_sorted},
)
def _voronoi_result_fuzz(
self,
*args,
@@ -1766,7 +1633,11 @@ class VoronoiNoiseGenerator(NoiseGenerator):
**kwargs,
) -> torch.Tensor:
fuzz = float(fuzz)
result = self.voronoi_call_mode(name, result=True, args=args, kwargs=kwargs)
name = name.strip().lower()
if name not in self.voronoi_result_modes:
errstr = f"Bad voronoi fuzz result mode name: {name}"
raise ValueError(errstr)
result = getattr(self, f"_voronoi_result_{name}")(*args, **kwargs)
rmin, rmax = result.aminmax()
fuzz = max(abs(rmin.item()), abs(rmax.item())) * fuzz
result += (
@@ -1777,37 +1648,7 @@ class VoronoiNoiseGenerator(NoiseGenerator):
)
return utils.normalize_to_scale(result, rmin.item(), rmax.item(), dim=(-2, -1))
def voronoi_distance(self, d: torch.Tensor, octave: int) -> torch.Tensor:
modes = self.get_distance_mode(octave).split("+")
result_scale_base = 1.0 / len(modes)
result = None
for mode in modes:
if ":" in mode:
mode_name, *mode_rest = mode.split(":")
mode_kwargs = dict(
tuple(val.strip() for val in di.split("=", 1)) for di in mode_rest
)
result_scale = result_scale_base * float(mode_kwargs.pop("dscale", 1.0))
else:
mode_name = mode
mode_kwargs = {}
result_scale = result_scale_base
curr_result = self.voronoi_call_mode(
mode_name,
result=False,
args=(d,),
kwargs=mode_kwargs,
).mul_(result_scale)
result = curr_result if result is None else result.add_(curr_result)
return result
def voronoi_result(
self,
d: torch.Tensor,
d_orig: torch.Tensor,
*,
octave: int,
) -> torch.Tensor:
def voronoi_result(self, d: torch.Tensor, octave: int) -> torch.Tensor:
modes = self.get_result_mode(octave).split("+")
result_scale_base = 1.0 / len(modes)
result = None
@@ -1820,10 +1661,6 @@ class VoronoiNoiseGenerator(NoiseGenerator):
d_sorted = d.sort(dim=-1).values
return d_sorted
base_kwargs = {
"d_orig": d_orig,
"get_sorted": get_sorted,
}
for mode in modes:
if ":" in mode:
mode_name, *mode_rest = mode.split(":")
@@ -1835,11 +1672,14 @@ class VoronoiNoiseGenerator(NoiseGenerator):
result_scale = result_scale_base
mode_name = mode
mode_kwargs = {}
curr_result = self.voronoi_call_mode(
mode_name,
result=True,
args=(d,),
kwargs=mode_kwargs | base_kwargs,
if mode_name not in self.voronoi_result_modes:
errstr = f"Bad result mode {mode}"
raise ValueError(errstr)
handler = getattr(self, f"_voronoi_result_{mode_name}")
curr_result = handler(
d,
get_sorted=get_sorted,
**mode_kwargs,
).mul_(result_scale)
result = curr_result if result is None else result.add_(curr_result)
return result
@@ -1864,9 +1704,10 @@ class VoronoiNoiseGenerator(NoiseGenerator):
fp = (fp * scale) % 1.0
# Toroidal wrapped difference
d_orig = d = (grid_3d - fp + 0.5) % 1.0 - 0.5 # Wrap to [-0.5, 0.5)
d = self.voronoi_distance(d.clone(), octave=octave)
return self.voronoi_result(d, d_orig, octave=octave)
d = (grid_3d - fp + 0.5) % 1.0 - 0.5 # Wrap to [-0.5, 0.5)
d = self.voronoi_distance(d, octave=octave)
return self.voronoi_result(d, octave=octave)
def generate(self, *args):
if self.grid_xyz is None or self.feature_points is None or self.z_max == 0:
+49 -176
View File
@@ -181,32 +181,13 @@ def _quantile_norm_replace(
*,
keep_sign: bool = False,
avoid_sign: bool = False,
count: int = 1,
count_flipping: bool = False,
**_kwargs: dict,
) -> torch.Tensor:
mask = noise.abs() <= nq
candidates = noise[mask].flatten()
n_candidates = candidates.numel()
idxs = torch.arange(noise.numel()) % n_candidates
cresult = candidates[idxs]
if count < 2:
candidates = cresult
else:
multiplier = 1.0 / count
cresult = cresult * multiplier # noqa: PLR6104
for i in range(1, count):
cresult += (
candidates[
torch.roll(
idxs,
i if not count_flipping or (i % 2) == 0 else -i,
dims=(-1,),
)
]
* multiplier
)
candidates = cresult.reshape(noise.shape)
candidates = candidates[torch.arange(noise.numel()) % candidates.numel()].reshape(
noise.shape,
)
if keep_sign or avoid_sign:
candidates = candidates.copysign_(noise.neg() if avoid_sign else noise)
return torch.where(mask, noise, candidates)
@@ -221,12 +202,9 @@ quantile_handlers = {
noise.tanh().mul_(nq.abs()),
noise,
),
"sigmoid_keepsign": lambda noise, nq, **_kwargs: noise.sigmoid()
"sigmoid": lambda noise, nq, **_kwargs: noise.sigmoid()
.mul_(nq.abs())
.copysign(noise),
"sigmoid": lambda noise, nq, **_kwargs: noise.sigmoid()
.mul_(nq.abs() * 2)
.sub_(nq.abs()),
"sigmoid_outliers": lambda noise, nq, **_kwargs: torch.where(
noise.abs() > nq,
noise.sigmoid().mul_(nq.abs()).copysign(noise),
@@ -312,54 +290,6 @@ quantile_handlers = {
"replace": _quantile_norm_replace,
"replace_keepsign": partial(_quantile_norm_replace, keep_sign=True),
"replace_avoidsign": partial(_quantile_norm_replace, avoid_sign=True),
"replace_2pt": partial(_quantile_norm_replace, count=2),
"replace_3pt": partial(_quantile_norm_replace, count=3),
"replace_2pt_flip": partial(_quantile_norm_replace, count=2, count_flipping=True),
"replace_3pt_flip": partial(_quantile_norm_replace, count=3, count_flipping=True),
"replace_2pt_keepsign": partial(
_quantile_norm_replace,
count=2,
keep_sign=True,
),
"replace_3pt_keepsign": partial(
_quantile_norm_replace,
count=3,
keep_sign=True,
),
"replace_2pt_flip_keepsign": partial(
_quantile_norm_replace,
count=2,
count_flipping=True,
keep_sign=True,
),
"replace_3pt_flip_keepsign": partial(
_quantile_norm_replace,
count=3,
count_flipping=True,
keep_sign=True,
),
"replace_2pt_avoidsign": partial(
_quantile_norm_replace,
count=2,
avoid_sign=True,
),
"replace_3pt_avoidsign": partial(
_quantile_norm_replace,
count=3,
avoid_sign=True,
),
"replace_2pt_flip_avoidsign": partial(
_quantile_norm_replace,
count=2,
count_flipping=True,
avoid_sign=True,
),
"replace_3pt_flip_avoidsign": partial(
_quantile_norm_replace,
count=3,
count_flipping=True,
avoid_sign=True,
),
}
@@ -367,14 +297,14 @@ quantile_handlers = {
def quantile_normalize(
noise: torch.Tensor,
*,
quantile: float | tuple | list = 0.75,
quantile: float = 0.75,
dim: int | None = 1,
flatten: bool = True,
nq_fac: float = 1.0,
pow_fac: float = 0.5,
strategy: str = "clamp",
strategy_handler=None,
eps=1e-08,
use_abs: bool = True,
) -> torch.Tensor:
if noise.numel() == 0:
return noise
@@ -389,18 +319,51 @@ def quantile_normalize(
pow_fac=pow_fac,
strategy=strategy,
strategy_handler=strategy_handler,
use_abs=use_abs,
)
return noise
if quantile is None or quantile >= 1 or quantile <= -1:
if quantile is None or quantile <= 0 or quantile >= 1:
return noise
centered = quantile < 0
absquantile = abs(quantile)
if not use_abs:
pos_mask = noise >= 0
neg_mask = ~pos_mask
result = torch.zeros_like(noise)
result[pos_mask] = quantile_normalize(
noise=noise[pos_mask],
quantile=quantile,
dim=dim,
flatten=flatten,
nq_fac=nq_fac,
pow_fac=pow_fac,
strategy=strategy,
strategy_handler=strategy_handler,
use_abs=True,
)
result[neg_mask] = quantile_normalize(
noise=noise[neg_mask],
quantile=quantile,
dim=dim,
flatten=flatten,
nq_fac=nq_fac,
pow_fac=pow_fac,
strategy=strategy,
strategy_handler=strategy_handler,
use_abs=True,
)
return result
orig_shape = noise.shape
if noise.ndim > 1 and flatten:
flatnoise = noise.flatten(start_dim=dim)
else:
flatten = False
flatnoise = noise
nq = torch.quantile(
flatnoise.abs(),
quantile,
dim=-1 if flatten else dim,
keepdim=True,
)
nq = nq.mul_(nq_fac)
handler = (
quantile_handlers.get(strategy)
if strategy_handler is None
@@ -408,42 +371,13 @@ def quantile_normalize(
)
if handler is None:
raise ValueError("Unknown strategy")
if not centered:
nq = torch.quantile(
flatnoise.abs(),
quantile,
dim=-1 if flatten else dim,
keepdim=True,
)
nq = nq.mul_(nq_fac).add_(eps)
# print(f"\nNQ: {nq}")
noise = handler(
flatnoise,
nq,
orig_noise=noise,
dim=dim,
flatten=flatten,
)
else:
absnoise = flatnoise.abs()
maxabs = absnoise.amax(dim=-1 if flatten else dim, keepdim=True)
proxy = flatnoise.sign().mul_(maxabs - absnoise)
nq_proxy = torch.quantile(
proxy.abs(),
absquantile,
dim=-1 if flatten else dim,
keepdim=True,
)
nq_proxy = nq_proxy.mul_(nq_fac).add_(eps)
# print(f"\nNQ proxy: {nq_proxy}")
out_proxy = handler(
proxy,
nq_proxy,
orig_noise=noise,
dim=dim,
flatten=flatten,
)
noise = out_proxy.sign().mul_(maxabs - out_proxy.abs())
noise = handler(
flatnoise,
nq,
orig_noise=noise,
dim=dim,
flatten=flatten,
)
if pow_fac not in {0.0, 1.0}:
noise = noise.abs().pow_(pow_fac).copysign(noise)
return noise if noise.shape == orig_shape else noise.reshape(orig_shape)
@@ -596,67 +530,6 @@ def pattern_break(
return blend_function(noise, result, percentage).to(dtype=orig_dtype)
def elementwise_shuffle_by_dim(
t: torch.Tensor,
*,
dim: int = -1,
prob: float = 1.0,
no_identity: bool = False,
generator=None,
) -> torch.Tensor:
orig_shape = t.shape
device = t.device
num_positions = math.prod(orig_shape[:dim] + orig_shape[dim + 1 :])
num_elements = orig_shape[dim]
tensor_2d = t.permute(
*tuple(d for d in range(t.dim()) if d != dim),
dim,
).reshape(-1, num_elements)
rand_perms = (
torch.arange(num_elements, device=device).expand(num_positions, -1).clone()
)
if prob < 1.0:
mask = torch.rand(num_positions, device=device, generator=generator) < prob
else:
mask = torch.ones(num_positions, device=device, dtype=torch.bool)
if no_identity:
offsets = torch.randint(
1,
num_elements,
(num_positions,),
device=device,
generator=generator,
)
rand_perms[mask] = (
torch.arange(num_elements, device=device) + offsets[mask][:, None]
) % num_elements
else:
rand_perms[mask] = torch.rand(
num_positions,
num_elements,
device=device,
generator=generator,
)[mask].argsort(dim=1)
shuffled_2d = torch.gather(tensor_2d, 1, rand_perms)
shuffled = shuffled_2d.reshape(
*orig_shape[:dim],
*orig_shape[dim + 1 :],
orig_shape[dim],
)
return shuffled.permute(
*tuple(d for d in range(t.dim() - 1) if d < dim),
t.dim() - 1,
*tuple(d for d in range(t.dim() - 1) if d >= dim),
).contiguous()
def trunc_decimals(x: torch.Tensor, decimals: int = 3) -> torch.Tensor:
x_i = x.trunc()
x_f = x - x_i