Noise Nodes
This commit is contained in:
@@ -15,6 +15,14 @@ from .nodes.deforum_interpolation_nodes import *
|
||||
from .nodes.deforum_image_nodes import *
|
||||
from .nodes.deforum_iteration_nodes import *
|
||||
from .nodes.deforum_legacy_nodes import *
|
||||
try:
|
||||
from .nodes.deforum_noise_nodes import AddCustomNoiseNode
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
from .nodes.deforum_advnoise_node import AddAdvancedNoiseNode
|
||||
except:
|
||||
pass
|
||||
from .nodes.deforum_prompt_nodes import *
|
||||
from .nodes.redirect_console_node import DeforumRedirectConsole
|
||||
from .nodes.deforum_sampler_nodes import *
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
from math import pi
|
||||
|
||||
try:
|
||||
import torch
|
||||
except ImportError:
|
||||
torch = None
|
||||
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
numpy = None
|
||||
|
||||
if numpy is None and torch is None:
|
||||
raise ImportError("Must have either Numpy or PyTorch but both not found")
|
||||
|
||||
|
||||
def set_framework_dependencies(x):
|
||||
if type(x) is numpy.ndarray:
|
||||
to_dtype = lambda a: a
|
||||
fw = numpy
|
||||
else:
|
||||
to_dtype = lambda a: a.to(x.dtype)
|
||||
fw = torch
|
||||
eps = fw.finfo(fw.float32).eps
|
||||
return fw, to_dtype, eps
|
||||
|
||||
|
||||
def support_sz(sz):
|
||||
def wrapper(f):
|
||||
f.support_sz = sz
|
||||
return f
|
||||
return wrapper
|
||||
|
||||
|
||||
@support_sz(4)
|
||||
def cubic(x):
|
||||
fw, to_dtype, eps = set_framework_dependencies(x)
|
||||
absx = fw.abs(x)
|
||||
absx2 = absx ** 2
|
||||
absx3 = absx ** 3
|
||||
return ((1.5 * absx3 - 2.5 * absx2 + 1.) * to_dtype(absx <= 1.) +
|
||||
(-0.5 * absx3 + 2.5 * absx2 - 4. * absx + 2.) *
|
||||
to_dtype((1. < absx) & (absx <= 2.)))
|
||||
|
||||
|
||||
@support_sz(4)
|
||||
def lanczos2(x):
|
||||
fw, to_dtype, eps = set_framework_dependencies(x)
|
||||
return (((fw.sin(pi * x) * fw.sin(pi * x / 2) + eps) /
|
||||
((pi**2 * x**2 / 2) + eps)) * to_dtype(abs(x) < 2))
|
||||
|
||||
|
||||
@support_sz(6)
|
||||
def lanczos3(x):
|
||||
fw, to_dtype, eps = set_framework_dependencies(x)
|
||||
return (((fw.sin(pi * x) * fw.sin(pi * x / 3) + eps) /
|
||||
((pi**2 * x**2 / 3) + eps)) * to_dtype(abs(x) < 3))
|
||||
|
||||
|
||||
@support_sz(2)
|
||||
def linear(x):
|
||||
fw, to_dtype, eps = set_framework_dependencies(x)
|
||||
return ((x + 1) * to_dtype((-1 <= x) & (x < 0)) + (1 - x) *
|
||||
to_dtype((0 <= x) & (x <= 1)))
|
||||
|
||||
|
||||
@support_sz(1)
|
||||
def box(x):
|
||||
fw, to_dtype, eps = set_framework_dependencies(x)
|
||||
return to_dtype((-1 <= x) & (x < 0)) + to_dtype((0 <= x) & (x <= 1))
|
||||
@@ -0,0 +1,428 @@
|
||||
from typing import Tuple
|
||||
import warnings
|
||||
from math import ceil
|
||||
from . import interp_methods
|
||||
from fractions import Fraction
|
||||
|
||||
|
||||
class NoneClass:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
import torch
|
||||
from torch import nn
|
||||
nnModuleWrapped = nn.Module
|
||||
except ImportError:
|
||||
warnings.warn('No PyTorch found, will work only with Numpy')
|
||||
torch = None
|
||||
nnModuleWrapped = NoneClass
|
||||
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
warnings.warn('No Numpy found, will work only with PyTorch')
|
||||
numpy = None
|
||||
|
||||
|
||||
if numpy is None and torch is None:
|
||||
raise ImportError("Must have either Numpy or PyTorch but both not found")
|
||||
|
||||
|
||||
def resize(input, scale_factors=None, out_shape=None,
|
||||
interp_method=interp_methods.cubic, support_sz=None,
|
||||
antialiasing=True, by_convs=False, scale_tolerance=None,
|
||||
max_numerator=10, pad_mode='constant'):
|
||||
# get properties of the input tensor
|
||||
in_shape, n_dims = input.shape, input.ndim
|
||||
|
||||
# fw stands for framework that can be either numpy or torch,
|
||||
# determined by the input type
|
||||
fw = numpy if type(input) is numpy.ndarray else torch
|
||||
eps = fw.finfo(fw.float32).eps
|
||||
device = input.device if fw is torch else None
|
||||
|
||||
# set missing scale factors or output shapem one according to another,
|
||||
# scream if both missing. this is also where all the defults policies
|
||||
# take place. also handling the by_convs attribute carefully.
|
||||
scale_factors, out_shape, by_convs = set_scale_and_out_sz(in_shape,
|
||||
out_shape,
|
||||
scale_factors,
|
||||
by_convs,
|
||||
scale_tolerance,
|
||||
max_numerator,
|
||||
eps, fw)
|
||||
|
||||
# sort indices of dimensions according to scale of each dimension.
|
||||
# since we are going dim by dim this is efficient
|
||||
sorted_filtered_dims_and_scales = [(dim, scale_factors[dim], by_convs[dim],
|
||||
in_shape[dim], out_shape[dim])
|
||||
for dim in sorted(range(n_dims),
|
||||
key=lambda ind: scale_factors[ind])
|
||||
if scale_factors[dim] != 1.]
|
||||
|
||||
# unless support size is specified by the user, it is an attribute
|
||||
# of the interpolation method
|
||||
if support_sz is None:
|
||||
support_sz = interp_method.support_sz
|
||||
|
||||
# output begins identical to input and changes with each iteration
|
||||
output = input
|
||||
|
||||
# iterate over dims
|
||||
for (dim, scale_factor, dim_by_convs, in_sz, out_sz
|
||||
) in sorted_filtered_dims_and_scales:
|
||||
# STEP 1- PROJECTED GRID: The non-integer locations of the projection
|
||||
# of output pixel locations to the input tensor
|
||||
projected_grid = get_projected_grid(in_sz, out_sz,
|
||||
scale_factor, fw, dim_by_convs,
|
||||
device)
|
||||
|
||||
# STEP 1.5: ANTIALIASING- If antialiasing is taking place, we modify
|
||||
# the window size and the interpolation method (see inside function)
|
||||
cur_interp_method, cur_support_sz = apply_antialiasing_if_needed(
|
||||
interp_method,
|
||||
support_sz,
|
||||
scale_factor,
|
||||
antialiasing)
|
||||
|
||||
# STEP 2- FIELDS OF VIEW: for each output pixels, map the input pixels
|
||||
# that influence it. Also calculate needed padding and update grid
|
||||
# accoedingly
|
||||
field_of_view = get_field_of_view(projected_grid, cur_support_sz, fw,
|
||||
eps, device)
|
||||
|
||||
# STEP 2.5- CALCULATE PAD AND UPDATE: according to the field of view,
|
||||
# the input should be padded to handle the boundaries, coordinates
|
||||
# should be updated. actual padding only occurs when weights are
|
||||
# aplied (step 4). if using by_convs for this dim, then we need to
|
||||
# calc right and left boundaries for each filter instead.
|
||||
pad_sz, projected_grid, field_of_view = calc_pad_sz(in_sz, out_sz,
|
||||
field_of_view,
|
||||
projected_grid,
|
||||
scale_factor,
|
||||
dim_by_convs, fw,
|
||||
device)
|
||||
|
||||
# STEP 3- CALCULATE WEIGHTS: Match a set of weights to the pixels in
|
||||
# the field of view for each output pixel
|
||||
weights = get_weights(cur_interp_method, projected_grid, field_of_view)
|
||||
|
||||
# STEP 4- APPLY WEIGHTS: Each output pixel is calculated by multiplying
|
||||
# its set of weights with the pixel values in its field of view.
|
||||
# We now multiply the fields of view with their matching weights.
|
||||
# We do this by tensor multiplication and broadcasting.
|
||||
# if by_convs is true for this dim, then we do this action by
|
||||
# convolutions. this is equivalent but faster.
|
||||
if not dim_by_convs:
|
||||
output = apply_weights(output, field_of_view, weights, dim, n_dims,
|
||||
pad_sz, pad_mode, fw)
|
||||
else:
|
||||
output = apply_convs(output, scale_factor, in_sz, out_sz, weights,
|
||||
dim, pad_sz, pad_mode, fw)
|
||||
return output
|
||||
|
||||
|
||||
def get_projected_grid(in_sz, out_sz, scale_factor, fw, by_convs, device=None):
|
||||
# we start by having the ouput coordinates which are just integer locations
|
||||
# in the special case when usin by_convs, we only need two cycles of grid
|
||||
# points. the first and last.
|
||||
grid_sz = out_sz if not by_convs else scale_factor.numerator
|
||||
out_coordinates = fw_arange(grid_sz, fw, device)
|
||||
|
||||
# This is projecting the ouput pixel locations in 1d to the input tensor,
|
||||
# as non-integer locations.
|
||||
# the following fomrula is derived in the paper
|
||||
# "From Discrete to Continuous Convolutions" by Shocher et al.
|
||||
return (out_coordinates / float(scale_factor) +
|
||||
(in_sz - 1) / 2 - (out_sz - 1) / (2 * float(scale_factor)))
|
||||
|
||||
|
||||
def get_field_of_view(projected_grid, cur_support_sz, fw, eps, device):
|
||||
# for each output pixel, map which input pixels influence it, in 1d.
|
||||
# we start by calculating the leftmost neighbor, using half of the window
|
||||
# size (eps is for when boundary is exact int)
|
||||
left_boundaries = fw_ceil(projected_grid - cur_support_sz / 2 - eps, fw)
|
||||
|
||||
# then we simply take all the pixel centers in the field by counting
|
||||
# window size pixels from the left boundary
|
||||
ordinal_numbers = fw_arange(ceil(cur_support_sz - eps), fw, device)
|
||||
return left_boundaries[:, None] + ordinal_numbers
|
||||
|
||||
|
||||
def calc_pad_sz(in_sz, out_sz, field_of_view, projected_grid, scale_factor,
|
||||
dim_by_convs, fw, device):
|
||||
if not dim_by_convs:
|
||||
# determine padding according to neighbor coords out of bound.
|
||||
# this is a generalized notion of padding, when pad<0 it means crop
|
||||
pad_sz = [-field_of_view[0, 0].item(),
|
||||
field_of_view[-1, -1].item() - in_sz + 1]
|
||||
|
||||
# since input image will be changed by padding, coordinates of both
|
||||
# field_of_view and projected_grid need to be updated
|
||||
field_of_view += pad_sz[0]
|
||||
projected_grid += pad_sz[0]
|
||||
|
||||
else:
|
||||
# only used for by_convs, to calc the boundaries of each filter the
|
||||
# number of distinct convolutions is the numerator of the scale factor
|
||||
num_convs, stride = scale_factor.numerator, scale_factor.denominator
|
||||
|
||||
# calculate left and right boundaries for each conv. left can also be
|
||||
# negative right can be bigger than in_sz. such cases imply padding if
|
||||
# needed. however if# both are in-bounds, it means we need to crop,
|
||||
# practically apply the conv only on part of the image.
|
||||
left_pads = -field_of_view[:, 0]
|
||||
|
||||
# next calc is tricky, explanation by rows:
|
||||
# 1) counting output pixels between the first position of each filter
|
||||
# to the right boundary of the input
|
||||
# 2) dividing it by number of filters to count how many 'jumps'
|
||||
# each filter does
|
||||
# 3) multiplying by the stride gives us the distance over the input
|
||||
# coords done by all these jumps for each filter
|
||||
# 4) to this distance we add the right boundary of the filter when
|
||||
# placed in its leftmost position. so now we get the right boundary
|
||||
# of that filter in input coord.
|
||||
# 5) the padding size needed is obtained by subtracting the rightmost
|
||||
# input coordinate. if the result is positive padding is needed. if
|
||||
# negative then negative padding means shaving off pixel columns.
|
||||
right_pads = (((out_sz - fw_arange(num_convs, fw, device) - 1) # (1)
|
||||
// num_convs) # (2)
|
||||
* stride # (3)
|
||||
+ field_of_view[:, -1] # (4)
|
||||
- in_sz + 1) # (5)
|
||||
|
||||
# in the by_convs case pad_sz is a list of left-right pairs. one per
|
||||
# each filter
|
||||
|
||||
pad_sz = list(zip(left_pads, right_pads))
|
||||
|
||||
return pad_sz, projected_grid, field_of_view
|
||||
|
||||
|
||||
def get_weights(interp_method, projected_grid, field_of_view):
|
||||
# the set of weights per each output pixels is the result of the chosen
|
||||
# interpolation method applied to the distances between projected grid
|
||||
# locations and the pixel-centers in the field of view (distances are
|
||||
# directed, can be positive or negative)
|
||||
weights = interp_method(projected_grid[:, None] - field_of_view)
|
||||
|
||||
# we now carefully normalize the weights to sum to 1 per each output pixel
|
||||
sum_weights = weights.sum(1, keepdims=True)
|
||||
sum_weights[sum_weights == 0] = 1
|
||||
return weights / sum_weights
|
||||
|
||||
|
||||
def apply_weights(input, field_of_view, weights, dim, n_dims, pad_sz, pad_mode,
|
||||
fw):
|
||||
# for this operation we assume the resized dim is the first one.
|
||||
# so we transpose and will transpose back after multiplying
|
||||
tmp_input = fw_swapaxes(input, dim, 0, fw)
|
||||
|
||||
# apply padding
|
||||
tmp_input = fw_pad(tmp_input, fw, pad_sz, pad_mode)
|
||||
|
||||
# field_of_view is a tensor of order 2: for each output (1d location
|
||||
# along cur dim)- a list of 1d neighbors locations.
|
||||
# note that this whole operations is applied to each dim separately,
|
||||
# this is why it is all in 1d.
|
||||
# neighbors = tmp_input[field_of_view] is a tensor of order image_dims+1:
|
||||
# for each output pixel (this time indicated in all dims), these are the
|
||||
# values of the neighbors in the 1d field of view. note that we only
|
||||
# consider neighbors along the current dim, but such set exists for every
|
||||
# multi-dim location, hence the final tensor order is image_dims+1.
|
||||
neighbors = tmp_input[field_of_view]
|
||||
|
||||
# weights is an order 2 tensor: for each output location along 1d- a list
|
||||
# of weights matching the field of view. we augment it with ones, for
|
||||
# broadcasting, so that when multiplies some tensor the weights affect
|
||||
# only its first dim.
|
||||
tmp_weights = fw.reshape(weights, (*weights.shape, * [1] * (n_dims - 1)))
|
||||
|
||||
# now we simply multiply the weights with the neighbors, and then sum
|
||||
# along the field of view, to get a single value per out pixel
|
||||
tmp_output = (neighbors * tmp_weights).sum(1)
|
||||
|
||||
# we transpose back the resized dim to its original position
|
||||
return fw_swapaxes(tmp_output, 0, dim, fw)
|
||||
|
||||
|
||||
def apply_convs(input, scale_factor, in_sz, out_sz, weights, dim, pad_sz,
|
||||
pad_mode, fw):
|
||||
# for this operations we assume the resized dim is the last one.
|
||||
# so we transpose and will transpose back after multiplying
|
||||
input = fw_swapaxes(input, dim, -1, fw)
|
||||
|
||||
# the stride for all convs is the denominator of the scale factor
|
||||
stride, num_convs = scale_factor.denominator, scale_factor.numerator
|
||||
|
||||
# prepare an empty tensor for the output
|
||||
tmp_out_shape = list(input.shape)
|
||||
tmp_out_shape[-1] = out_sz
|
||||
tmp_output = fw_empty(tuple(tmp_out_shape), fw, input.device)
|
||||
|
||||
# iterate over the conv operations. we have as many as the numerator
|
||||
# of the scale-factor. for each we need boundaries and a filter.
|
||||
for conv_ind, (pad_sz, filt) in enumerate(zip(pad_sz, weights)):
|
||||
# apply padding (we pad last dim, padding can be negative)
|
||||
pad_dim = input.ndim - 1
|
||||
tmp_input = fw_pad(input, fw, pad_sz, pad_mode, dim=pad_dim)
|
||||
|
||||
# apply convolution over last dim. store in the output tensor with
|
||||
# positional strides so that when the loop is comlete conv results are
|
||||
# interwind
|
||||
tmp_output[..., conv_ind::num_convs] = fw_conv(tmp_input, filt, stride)
|
||||
|
||||
return fw_swapaxes(tmp_output, -1, dim, fw)
|
||||
|
||||
|
||||
def set_scale_and_out_sz(in_shape, out_shape, scale_factors, by_convs,
|
||||
scale_tolerance, max_numerator, eps, fw):
|
||||
# eventually we must have both scale-factors and out-sizes for all in/out
|
||||
# dims. however, we support many possible partial arguments
|
||||
if scale_factors is None and out_shape is None:
|
||||
raise ValueError("either scale_factors or out_shape should be "
|
||||
"provided")
|
||||
if out_shape is not None:
|
||||
# if out_shape has less dims than in_shape, we defaultly resize the
|
||||
# first dims for numpy and last dims for torch
|
||||
out_shape = (list(out_shape) + list(in_shape[len(out_shape):])
|
||||
if fw is numpy
|
||||
else list(in_shape[:-len(out_shape)]) + list(out_shape))
|
||||
if scale_factors is None:
|
||||
# if no scale given, we calculate it as the out to in ratio
|
||||
# (not recomended)
|
||||
scale_factors = [out_sz / in_sz for out_sz, in_sz
|
||||
in zip(out_shape, in_shape)]
|
||||
if scale_factors is not None:
|
||||
# by default, if a single number is given as scale, we assume resizing
|
||||
# two dims (most common are images with 2 spatial dims)
|
||||
scale_factors = (scale_factors
|
||||
if isinstance(scale_factors, (list, tuple))
|
||||
else [scale_factors, scale_factors])
|
||||
# if less scale_factors than in_shape dims, we defaultly resize the
|
||||
# first dims for numpy and last dims for torch
|
||||
scale_factors = (list(scale_factors) + [1] *
|
||||
(len(in_shape) - len(scale_factors)) if fw is numpy
|
||||
else [1] * (len(in_shape) - len(scale_factors)) +
|
||||
list(scale_factors))
|
||||
if out_shape is None:
|
||||
# when no out_shape given, it is calculated by multiplying the
|
||||
# scale by the in_shape (not recomended)
|
||||
out_shape = [ceil(scale_factor * in_sz)
|
||||
for scale_factor, in_sz in
|
||||
zip(scale_factors, in_shape)]
|
||||
# next part intentionally after out_shape determined for stability
|
||||
# we fix by_convs to be a list of truth values in case it is not
|
||||
if not isinstance(by_convs, (list, tuple)):
|
||||
by_convs = [by_convs] * len(out_shape)
|
||||
|
||||
# next loop fixes the scale for each dim to be either frac or float.
|
||||
# this is determined by by_convs and by tolerance for scale accuracy.
|
||||
for ind, (sf, dim_by_convs) in enumerate(zip(scale_factors, by_convs)):
|
||||
# first we fractionaize
|
||||
if dim_by_convs:
|
||||
frac = Fraction(1/sf).limit_denominator(max_numerator)
|
||||
frac = Fraction(numerator=frac.denominator, denominator=frac.numerator)
|
||||
|
||||
# if accuracy is within tolerance scale will be frac. if not, then
|
||||
# it will be float and the by_convs attr will be set false for
|
||||
# this dim
|
||||
if scale_tolerance is None:
|
||||
scale_tolerance = eps
|
||||
if dim_by_convs and abs(frac - sf) < scale_tolerance:
|
||||
scale_factors[ind] = frac
|
||||
else:
|
||||
scale_factors[ind] = float(sf)
|
||||
by_convs[ind] = False
|
||||
|
||||
return scale_factors, out_shape, by_convs
|
||||
|
||||
|
||||
def apply_antialiasing_if_needed(interp_method, support_sz, scale_factor,
|
||||
antialiasing):
|
||||
# antialiasing is "stretching" the field of view according to the scale
|
||||
# factor (only for downscaling). this is low-pass filtering. this
|
||||
# requires modifying both the interpolation (stretching the 1d
|
||||
# function and multiplying by the scale-factor) and the window size.
|
||||
scale_factor = float(scale_factor)
|
||||
if scale_factor >= 1.0 or not antialiasing:
|
||||
return interp_method, support_sz
|
||||
cur_interp_method = (lambda arg: scale_factor *
|
||||
interp_method(scale_factor * arg))
|
||||
cur_support_sz = support_sz / scale_factor
|
||||
return cur_interp_method, cur_support_sz
|
||||
|
||||
|
||||
def fw_ceil(x, fw):
|
||||
if fw is numpy:
|
||||
return fw.int_(fw.ceil(x))
|
||||
else:
|
||||
return x.ceil().long()
|
||||
|
||||
|
||||
def fw_floor(x, fw):
|
||||
if fw is numpy:
|
||||
return fw.int_(fw.floor(x))
|
||||
else:
|
||||
return x.floor().long()
|
||||
|
||||
|
||||
def fw_cat(x, fw):
|
||||
if fw is numpy:
|
||||
return fw.concatenate(x)
|
||||
else:
|
||||
return fw.cat(x)
|
||||
|
||||
|
||||
def fw_swapaxes(x, ax_1, ax_2, fw):
|
||||
if fw is numpy:
|
||||
return fw.swapaxes(x, ax_1, ax_2)
|
||||
else:
|
||||
return x.transpose(ax_1, ax_2)
|
||||
|
||||
|
||||
def fw_pad(x, fw, pad_sz, pad_mode, dim=0):
|
||||
if pad_sz == (0, 0):
|
||||
return x
|
||||
if fw is numpy:
|
||||
pad_vec = [(0, 0)] * x.ndim
|
||||
pad_vec[dim] = pad_sz
|
||||
return fw.pad(x, pad_width=pad_vec, mode=pad_mode)
|
||||
else:
|
||||
if x.ndim < 3:
|
||||
x = x[None, None, ...]
|
||||
|
||||
pad_vec = [0] * ((x.ndim - 2) * 2)
|
||||
pad_vec[0:2] = pad_sz
|
||||
return fw.nn.functional.pad(x.transpose(dim, -1), pad=pad_vec,
|
||||
mode=pad_mode).transpose(dim, -1)
|
||||
|
||||
|
||||
def fw_conv(input, filter, stride):
|
||||
# we want to apply 1d conv to any nd array. the way to do it is to reshape
|
||||
# the input to a 4D tensor. first two dims are singeletons, 3rd dim stores
|
||||
# all the spatial dims that we are not convolving along now. then we can
|
||||
# apply conv2d with a 1xK filter. This convolves the same way all the other
|
||||
# dims stored in the 3d dim. like depthwise conv over these.
|
||||
# TODO: numpy support
|
||||
reshaped_input = input.reshape(1, 1, -1, input.shape[-1])
|
||||
reshaped_output = torch.nn.functional.conv2d(reshaped_input,
|
||||
filter.view(1, 1, 1, -1),
|
||||
stride=(1, stride))
|
||||
return reshaped_output.reshape(*input.shape[:-1], -1)
|
||||
|
||||
|
||||
def fw_arange(upper_bound, fw, device):
|
||||
if fw is numpy:
|
||||
return fw.arange(upper_bound)
|
||||
else:
|
||||
return fw.arange(upper_bound, device=device)
|
||||
|
||||
|
||||
def fw_empty(shape, fw, device):
|
||||
if fw is numpy:
|
||||
return fw.empty(shape)
|
||||
else:
|
||||
return fw.empty(size=(*shape,), device=device)
|
||||
@@ -119,7 +119,7 @@ def generate_seed_list(max_frames, mode='fixed', start_seed=0, step=1):
|
||||
else:
|
||||
raise ValueError("Invalid mode specified. Choose among 'fixed', 'random', 'ladder', 'incrementing', 'decrementing'.")
|
||||
|
||||
def find_next_index(output_dir, filename_prefix):
|
||||
def find_next_index(output_dir, filename_prefix, format):
|
||||
"""
|
||||
Finds the next index for an MP4 file given an output directory and a filename prefix.
|
||||
|
||||
@@ -132,7 +132,7 @@ def find_next_index(output_dir, filename_prefix):
|
||||
"""
|
||||
# Compile a regular expression pattern to match the filenames
|
||||
# This assumes the index is at the end of the filename, before the .mp4 extension
|
||||
pattern = re.compile(rf"^{re.escape(filename_prefix)}_(\d+)\.mp4$")
|
||||
pattern = re.compile(rf"^{re.escape(filename_prefix)}_(\d+)\.{format}$")
|
||||
|
||||
max_index = -1
|
||||
for filename in os.listdir(output_dir):
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
import secrets
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
import math
|
||||
|
||||
import pywt
|
||||
import torch.nn.functional as F
|
||||
from opensimplex import OpenSimplex
|
||||
|
||||
|
||||
|
||||
class AddAdvancedNoiseNode:
|
||||
"""
|
||||
A node to add various types of advanced noise to an image using PyTorch.
|
||||
"""
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, text, autorefresh):
|
||||
# Force re-evaluation of the node
|
||||
if autorefresh == "Yes":
|
||||
return float("NaN")
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"noise_type": (["wavelet", "value", "flow", "turbulence",
|
||||
"ridged_multifractal", "reaction_diffusion", "voronoi", "simplex"],), # "gabor"
|
||||
"amount": ("FLOAT", {
|
||||
"default": 0.1,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"seed": ("INT", {
|
||||
"default": None,
|
||||
"min": 0,
|
||||
"max": 2 ** 32 - 1,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
# Voronoi specific parameters
|
||||
"num_points": ("INT", {
|
||||
"default": 100,
|
||||
"min": 10,
|
||||
"max": 1000,
|
||||
"step": 10,
|
||||
"display": "number"
|
||||
}),
|
||||
# Simplex Noise specific parameters
|
||||
"scale": ("FLOAT", {
|
||||
"default": 0.1,
|
||||
"min": 0.01,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
"octaves": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 10,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"persistence": ("FLOAT", {
|
||||
"default": 0.5,
|
||||
"min": 0.1,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"display": "number"
|
||||
}),
|
||||
"lacunarity": ("FLOAT", {
|
||||
"default": 2.0,
|
||||
"min": 1.0,
|
||||
"max": 3.0,
|
||||
"step": 0.1,
|
||||
"display": "number"
|
||||
}),
|
||||
# Wavelet Noise specific parameters
|
||||
|
||||
"wavelet": (["haar", "db1", "sym2", "coif1", "bior1.3", "rbio1.3"],),
|
||||
"mode": (["symmetric", "periodic", "reflect", "zero-padding"],),
|
||||
# Additional parameters specific to Gabor noise
|
||||
"frequency": ("FLOAT", {
|
||||
"default": 0.1,
|
||||
"min": 0.01,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
"theta": ("FLOAT", {
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 2 * math.pi,
|
||||
"step": 0.01,
|
||||
"display": "slider"
|
||||
}),
|
||||
"sigma_x": ("FLOAT", {
|
||||
"default": 10.0,
|
||||
"min": 1.0,
|
||||
"max": 50.0,
|
||||
"step": 1.0,
|
||||
"display": "number"
|
||||
}),
|
||||
"sigma_y": ("FLOAT", {
|
||||
"default": 10.0,
|
||||
"min": 1.0,
|
||||
"max": 50.0,
|
||||
"step": 1.0,
|
||||
"display": "number"
|
||||
}),
|
||||
# Value Noise specific parameters
|
||||
"res": ("INT", {
|
||||
"default": 16,
|
||||
"min": 4,
|
||||
"max": 64,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
# Additional parameters for Flow Noise
|
||||
"flow_scale": ("FLOAT", {
|
||||
"default": 0.1,
|
||||
"min": 0.01,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
"flow_angle": ("FLOAT", {
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 2 * np.pi,
|
||||
"step": 0.1,
|
||||
"display": "number"
|
||||
}),
|
||||
# Reaction Diffusion Noise
|
||||
"steps": ("INT", {
|
||||
"default": 10,
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"Du": ("FLOAT", {
|
||||
"default": 0.16,
|
||||
"min": 0.01,
|
||||
"max": 0.5,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
"Dv": ("FLOAT", {
|
||||
"default": 0.08,
|
||||
"min": 0.01,
|
||||
"max": 0.5,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
"feed_rate": ("FLOAT", {
|
||||
"default": 0.035,
|
||||
"min": 0.01,
|
||||
"max": 0.1,
|
||||
"step": 0.001,
|
||||
"display": "slider"
|
||||
}),
|
||||
"kill_rate": ("FLOAT", {
|
||||
"default": 0.06,
|
||||
"min": 0.01,
|
||||
"max": 0.1,
|
||||
"step": 0.001,
|
||||
"display": "slider"
|
||||
}),
|
||||
# Additional parameters specific to each noise type can be defined here
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE","IMAGE",)
|
||||
RETURN_NAMES = ("NOISED_IMAGE","NOISE",)
|
||||
|
||||
FUNCTION = "add_advanced_noise"
|
||||
CATEGORY = "Advanced Noise"
|
||||
display_name = "Add Advanced Noise"
|
||||
|
||||
def add_advanced_noise(self, images, noise_type, amount, seed=None, **kwargs):
|
||||
B, H, W, C = images.shape
|
||||
if seed is not None:
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
|
||||
# Adjusted noise function dictionary
|
||||
noise_function = {
|
||||
"voronoi": generate_voronoi_noise,
|
||||
"simplex": lambda H, W, **kw: generate_simplex_noise(H, W, **kw).unsqueeze(-1).repeat(1, 1, 1, C),
|
||||
"wavelet": lambda H, W, **kw: generate_wavelet_noise(H, W, **kw).unsqueeze(-1).repeat(1, 1, 1, C),
|
||||
"gabor": lambda H, W, **kw: generate_gabor_noise(H, W, **kw),
|
||||
"value": lambda H, W, **kw: generate_value_noise(H, W, **kw).unsqueeze(-1).repeat(1, 1, 1, C),
|
||||
"flow": lambda H, W, **kw: generate_flow_noise(H, W, **kw),
|
||||
"turbulence": lambda H, W, **kw: generate_turbulence_noise(H, W, **kw),
|
||||
"ridged_multifractal": lambda H, W, **kw: generate_ridged_multifractal_noise(H, W, **kw),
|
||||
"reaction_diffusion": lambda H, W, **kw: generate_reaction_diffusion_noise(H, W, **kw),
|
||||
|
||||
# Additional noise functions adjusted similarly
|
||||
}.get(noise_type, lambda H, W, **kw: torch.zeros((B, H, W, C)))
|
||||
|
||||
# Generate the noise pattern
|
||||
noise = noise_function(H, W, seed=seed, **kwargs)
|
||||
# Adjust for the case where noise is not returned with a batch dimension or channel dimension
|
||||
if noise.dim() == 2: # Assuming noise is of shape (H, W)
|
||||
noise = noise.unsqueeze(0).unsqueeze(-1) # Add batch and channel dimension
|
||||
noise = noise.expand(B, -1, -1, C) # Expand to match (B, H, W, C)
|
||||
elif noise.dim() == 3: # Assuming noise is of shape (B, H, W)
|
||||
noise = noise.unsqueeze(-1) # Add channel dimension
|
||||
noise = noise.expand(-1, -1, -1, C) # Expand to match (B, H, W, C)
|
||||
|
||||
# Ensure noise tensor device matches the images tensor device
|
||||
noise = noise.to(images.device)
|
||||
# from ..modules.better_resize.resize_right import resize
|
||||
# belnd_img = resize(images, noise.shape, antialiasing=False)
|
||||
# Apply the noise to the images
|
||||
noisy_images = images.clone() + amount * noise.clone()
|
||||
|
||||
return (torch.clamp(noisy_images, 0, 1),noise,)
|
||||
|
||||
def generate_voronoi_noise(H, W, num_points=100, seed=None, *args, **kwargs):
|
||||
"""
|
||||
Generates a 2D Voronoi (Worley) Noise pattern.
|
||||
|
||||
Args:
|
||||
H, W (int): The height and width of the output noise pattern.
|
||||
num_points (int): Number of seed points to generate.
|
||||
seed (int, optional): Random seed for reproducibility.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: A tensor containing the Voronoi noise pattern.
|
||||
"""
|
||||
if seed is not None:
|
||||
torch.manual_seed(seed)
|
||||
np.random.seed(seed)
|
||||
|
||||
# Generate random seed points
|
||||
seeds_x = np.random.uniform(0, W, num_points)
|
||||
seeds_y = np.random.uniform(0, H, num_points)
|
||||
seeds = np.stack((seeds_x, seeds_y), axis=-1)
|
||||
|
||||
# Initialize the noise tensor
|
||||
noise = torch.full((H, W), float('inf'))
|
||||
|
||||
# Compute distance from each pixel to the closest seed point
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
distances = torch.tensor([((x - sx) ** 2 + (y - sy) ** 2) for sx, sy in seeds])
|
||||
noise[y, x] = torch.min(distances).sqrt()
|
||||
|
||||
# Normalize the noise pattern
|
||||
noise = (noise - noise.min()) / (noise.max() - noise.min())
|
||||
|
||||
return noise
|
||||
|
||||
def generate_simplex_noise(H, W, scale=0.05, octaves=4, persistence=0.5, lacunarity=2.0, seed=None, **kwargs):
|
||||
if seed is not None:
|
||||
gen = OpenSimplex(seed)
|
||||
else:
|
||||
gen = OpenSimplex()
|
||||
|
||||
noise_pattern = np.zeros((H, W), dtype=np.float32)
|
||||
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
nx = x / W - 0.5 # Normalize x coordinate to [-0.5, 0.5]
|
||||
ny = y / H - 0.5 # Normalize y coordinate to [-0.5, 0.5]
|
||||
noise_val = 0
|
||||
amplitude = 1.0
|
||||
frequency = 1.0
|
||||
max_value = 0.0
|
||||
for o in range(octaves):
|
||||
noise_val += gen.noise2(x=nx * scale * frequency, y=ny * scale * frequency) * amplitude
|
||||
max_value += amplitude
|
||||
amplitude *= persistence
|
||||
frequency *= lacunarity
|
||||
|
||||
# Normalize each point individually
|
||||
noise_pattern[y, x] = (noise_val / max_value + 1) / 2 # Normalize to [0, 1]
|
||||
|
||||
return torch.from_numpy(noise_pattern)
|
||||
|
||||
|
||||
def generate_wavelet_noise(H, W, octaves=3, wavelet='haar', mode='symmetric', **kwargs):
|
||||
seed = kwargs.get('seed', secrets.randbelow(2 ** 32))
|
||||
if seed is not None:
|
||||
np.random.seed(seed)
|
||||
|
||||
# Generate initial random noise
|
||||
noise = np.random.randn(H, W)
|
||||
|
||||
# Perform wavelet decomposition and reconstruction
|
||||
coeffs = pywt.wavedec2(noise, wavelet=wavelet, mode=mode, level=octaves)
|
||||
wavelet_noise = pywt.waverec2(coeffs, wavelet=wavelet, mode=mode)
|
||||
|
||||
# Normalize the wavelet noise to have values between 0 and 1
|
||||
wavelet_noise = (wavelet_noise - np.min(wavelet_noise)) / (np.max(wavelet_noise) - np.min(wavelet_noise))
|
||||
|
||||
# Convert to a PyTorch tensor and add a channel dimension since input is assumed to be BxHxWxC
|
||||
wavelet_noise_tensor = torch.from_numpy(wavelet_noise).float().unsqueeze(0).unsqueeze(0)
|
||||
|
||||
# Interpolate to match input size (H, W)
|
||||
# Note: 'align_corners=False' is generally used for non-exact sizes but you might want to experiment
|
||||
# with 'align_corners=True' or 'recompute_scale_factor=True' depending on your specific use case
|
||||
wavelet_noise_tensor = F.interpolate(wavelet_noise_tensor, size=(H, W), mode='bilinear', align_corners=False)
|
||||
|
||||
# Remove the extra dimensions added for interpolation if necessary
|
||||
wavelet_noise_tensor = wavelet_noise_tensor.squeeze()
|
||||
|
||||
return wavelet_noise_tensor
|
||||
|
||||
|
||||
def generate_gabor_kernel(frequency, theta, sigma_x, sigma_y, n_stds=3, grid_size=28):
|
||||
"""
|
||||
Generates a Gabor kernel.
|
||||
|
||||
Args:
|
||||
frequency (float): The frequency of the sinusoidal component.
|
||||
theta (float): Orientation of the Gabor filter in radians.
|
||||
sigma_x, sigma_y (float): Standard deviation of the Gaussian envelope along x and y axes.
|
||||
n_stds (int): Number of standard deviations to consider for the kernel size.
|
||||
grid_size (int): The size of the output tensor.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: A 2D tensor with the Gabor kernel.
|
||||
"""
|
||||
xmax = ymax = grid_size // 2
|
||||
xmin = ymin = -xmax
|
||||
(y, x) = torch.meshgrid(torch.arange(ymin, ymax + 1), torch.arange(xmin, xmax + 1))
|
||||
x = x.float()
|
||||
y = y.float()
|
||||
|
||||
# Convert theta to a tensor to use with torch.cos and torch.sin
|
||||
theta_tensor = torch.tensor(theta)
|
||||
|
||||
rotx = x * torch.cos(theta_tensor) + y * torch.sin(theta_tensor)
|
||||
roty = -x * torch.sin(theta_tensor) + y * torch.cos(theta_tensor)
|
||||
|
||||
g = torch.zeros(y.shape)
|
||||
g = torch.exp(-0.5 * ((rotx ** 2 / sigma_x ** 2) + (roty ** 2 / sigma_y ** 2))) * torch.cos(
|
||||
2 * np.pi * frequency * rotx)
|
||||
|
||||
return g
|
||||
|
||||
def generate_gabor_noise(H, W, frequency=0.1, theta=0.0, sigma_x=10.0, sigma_y=10.0, batch_size=1, channels=3, **kwargs):
|
||||
"""
|
||||
Generates a 2D Gabor Noise pattern and scales it to the size of the input images.
|
||||
|
||||
Args:
|
||||
H, W (int): Height and width of the output noise pattern.
|
||||
frequency (float): Frequency of the sinusoidal component.
|
||||
theta (float): Orientation of the Gabor filter in radians.
|
||||
sigma_x, sigma_y (float): Standard deviation of the Gaussian envelope.
|
||||
batch_size (int): Number of images in the batch.
|
||||
channels (int): Number of channels in the images.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: A tensor containing the Gabor noise pattern, resized to match input images dimensions.
|
||||
"""
|
||||
gabor_kernel = generate_gabor_kernel(frequency, theta, sigma_x, sigma_y, grid_size=min(H, W))
|
||||
|
||||
# Center the kernel in a larger tensor matching the image dimensions
|
||||
pad_height = (H - gabor_kernel.size(0)) // 2
|
||||
pad_width = (W - gabor_kernel.size(1)) // 2
|
||||
pad_height_extra = H - gabor_kernel.size(0) - pad_height * 2
|
||||
pad_width_extra = W - gabor_kernel.size(1) - pad_width * 2
|
||||
|
||||
# Pad the kernel to match the target size
|
||||
gabor_padded = F.pad(gabor_kernel, [pad_width, pad_width + pad_width_extra, pad_height, pad_height_extra], "constant", 0)
|
||||
|
||||
# Ensure the pattern has the correct dimensions: batch_size x H x W x channels
|
||||
gabor_padded = gabor_padded.unsqueeze(0) # Add a batch dimension
|
||||
gabor_padded = gabor_padded.unsqueeze(-1) # Add a channel dimension
|
||||
gabor_padded = gabor_padded.expand(batch_size, -1, -1, channels) # Expand to match the full dimensions
|
||||
|
||||
return gabor_padded
|
||||
|
||||
|
||||
def generate_value_noise(H, W, res=16, seed=None, **kwargs):
|
||||
"""
|
||||
Generates a 2D Value Noise pattern with edge handling.
|
||||
|
||||
Args:
|
||||
H, W (int): Height and width of the output noise pattern.
|
||||
res (int): Resolution of the noise grid (smaller values mean smoother noise).
|
||||
seed (int, optional): Seed for random number generator.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: A tensor containing the Value noise pattern.
|
||||
"""
|
||||
if seed is not None:
|
||||
torch.manual_seed(seed)
|
||||
|
||||
# Create a grid of random values
|
||||
grid_res_x, grid_res_y = W // res + 1, H // res + 1
|
||||
random_values = torch.rand((grid_res_y, grid_res_x))
|
||||
|
||||
# Initialize output noise pattern
|
||||
noise_pattern = torch.zeros((H, W))
|
||||
|
||||
# Generate the noise
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
# Grid cell coordinates in the random grid
|
||||
cell_x, cell_y = x // res, y // res
|
||||
|
||||
# Ensure indices stay within bounds
|
||||
next_cell_x = min(cell_x + 1, grid_res_x - 1)
|
||||
next_cell_y = min(cell_y + 1, grid_res_y - 1)
|
||||
|
||||
# Local x and y in the grid cell
|
||||
local_x, local_y = (x % res) / res, (y % res) / res
|
||||
|
||||
# Corners of the cell in the grid
|
||||
c00 = random_values[cell_y, cell_x]
|
||||
c10 = random_values[cell_y, next_cell_x]
|
||||
c01 = random_values[next_cell_y, cell_x]
|
||||
c11 = random_values[next_cell_y, next_cell_x]
|
||||
|
||||
# Interpolate between grid corner values
|
||||
nx0 = lerp(c00, c10, fade(local_x))
|
||||
nx1 = lerp(c01, c11, fade(local_x))
|
||||
nxy = lerp(nx0, nx1, fade(local_y))
|
||||
|
||||
noise_pattern[y, x] = nxy
|
||||
|
||||
# Normalize the noise pattern
|
||||
noise_pattern = (noise_pattern - noise_pattern.min()) / (noise_pattern.max() - noise_pattern.min())
|
||||
|
||||
return noise_pattern
|
||||
|
||||
def lerp(a, b, t):
|
||||
"""Linear interpolation between a and b with t in [0, 1]."""
|
||||
return a + t * (b - a)
|
||||
|
||||
def fade(t):
|
||||
"""Fade function as defined by Ken Perlin; eases coordinate values."""
|
||||
return t * t * t * (t * (t * 6 - 15) + 10)
|
||||
|
||||
|
||||
def generate_flow_noise_pattern(H, W, scale=0.1, angle=0.0, seed=None):
|
||||
"""
|
||||
Generates a 2D Flow Noise pattern using a base noise to perturb the sampling space of another noise layer.
|
||||
|
||||
Args:
|
||||
H, W (int): Height and width of the output noise pattern.
|
||||
scale (float): Scale of the underlying noise.
|
||||
angle (float): Base flow direction in radians.
|
||||
seed (int, optional): Random seed for reproducibility.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: A tensor containing the Flow noise pattern.
|
||||
"""
|
||||
# Base noise for perturbation
|
||||
base_noise = generate_simplex_noise(H, W, scale=scale, seed=seed)
|
||||
# Generate gradient from angle for flow direction
|
||||
flow_x = torch.cos(torch.tensor(angle))
|
||||
flow_y = torch.sin(torch.tensor(angle))
|
||||
|
||||
# Apply perturbation based on the base noise and flow direction
|
||||
perturb_x = base_noise * flow_x
|
||||
perturb_y = base_noise * flow_y
|
||||
|
||||
# Another layer of noise for actual flow appearance
|
||||
flow_noise = generate_simplex_noise(H, W, scale=scale / 2, seed=seed if seed is None else seed + 1)
|
||||
|
||||
# Final flow noise is a combination of perturbed coordinates and the flow noise layer
|
||||
final_noise = flow_noise + perturb_x + perturb_y
|
||||
|
||||
# Normalize the noise pattern
|
||||
final_noise = (final_noise - final_noise.min()) / (final_noise.max() - final_noise.min())
|
||||
|
||||
return final_noise
|
||||
|
||||
|
||||
def generate_flow_noise(H, W, flow_scale=0.1, flow_angle=0.0, seed=None, batch_size=1, channels=3, **kwargs):
|
||||
"""
|
||||
Generates a 2D Flow Noise pattern and resizes it to match the input image size.
|
||||
|
||||
Args:
|
||||
H, W (int): Height and width of the input images.
|
||||
scale, angle: Parameters for the flow noise generation.
|
||||
seed (int, optional): Seed for random number generator.
|
||||
batch_size (int): The batch size of the input images.
|
||||
channels (int): The channel count of the input images.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: A tensor of the Flow noise pattern, matched to input dimensions.
|
||||
"""
|
||||
# Generate the base noise pattern
|
||||
base_noise = generate_flow_noise_pattern(H, W, flow_scale, flow_angle, seed)
|
||||
# Ensure the noise tensor has the same dimensions as the input: (batch_size, H, W, channels)
|
||||
# Interpolate noise to match the input size if needed
|
||||
noise_resized = F.interpolate(base_noise.unsqueeze(0).unsqueeze(0), size=(H, W), mode='bilinear',
|
||||
align_corners=False)
|
||||
|
||||
# Expand to match the batch and channel dimensions
|
||||
noise_resized = noise_resized.repeat(batch_size, channels, 1, 1)
|
||||
noise_resized = noise_resized.permute(0, 2, 3, 1) # Rearrange dimensions to match (B, H, W, C)
|
||||
|
||||
return noise_resized
|
||||
|
||||
def generate_turbulence_noise(H, W, scale=0.05, octaves=4, persistence=0.5, lacunarity=2.0, seed=None, **kwargs):
|
||||
if seed is not None:
|
||||
gen = OpenSimplex(seed)
|
||||
else:
|
||||
gen = OpenSimplex()
|
||||
|
||||
noise_pattern = np.zeros((H, W), dtype=np.float32)
|
||||
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
nx = x / W - 0.5 # Normalize x coordinate to [-0.5, 0.5]
|
||||
ny = y / H - 0.5 # Normalize y coordinate to [-0.5, 0.5]
|
||||
noise_val = 0
|
||||
amplitude = 1.0
|
||||
frequency = 1.0
|
||||
max_value = 0.0
|
||||
for o in range(octaves):
|
||||
# The key change for turbulence: using the absolute value of the noise
|
||||
noise_val += abs(gen.noise2(x=nx * scale * frequency, y=ny * scale * frequency)) * amplitude
|
||||
max_value += amplitude
|
||||
amplitude *= persistence
|
||||
frequency *= lacunarity
|
||||
|
||||
# Normalize each point individually
|
||||
noise_pattern[y, x] = (noise_val / max_value)
|
||||
|
||||
# Normalize the whole pattern to [0, 1] range if desired
|
||||
noise_pattern = (noise_pattern - noise_pattern.min()) / (noise_pattern.max() - noise_pattern.min())
|
||||
|
||||
return torch.from_numpy(noise_pattern)
|
||||
|
||||
def generate_ridged_multifractal_noise(H, W, scale=0.05, octaves=4, persistence=0.5, lacunarity=2.0, seed=None, **kwargs):
|
||||
if seed is not None:
|
||||
gen = OpenSimplex(seed)
|
||||
else:
|
||||
gen = OpenSimplex()
|
||||
|
||||
noise_pattern = np.zeros((H, W), dtype=np.float32)
|
||||
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
nx = x / W - 0.5 # Normalize x coordinate to [-0.5, 0.5]
|
||||
ny = y / H - 0.5 # Normalize y coordinate to [-0.5, 0.5]
|
||||
noise_val = 0
|
||||
amplitude = 1.0
|
||||
frequency = 1.0
|
||||
max_value = 0.0
|
||||
for o in range(octaves):
|
||||
signal = gen.noise2(x=nx * scale * frequency, y=ny * scale * frequency)
|
||||
# Transformation for ridged noise: invert and square the signal
|
||||
signal = 1.0 - abs(signal)
|
||||
signal *= signal
|
||||
noise_val += signal * amplitude
|
||||
max_value += amplitude
|
||||
amplitude *= persistence
|
||||
frequency *= lacunarity
|
||||
|
||||
# Normalize each point individually
|
||||
noise_pattern[y, x] = (noise_val / max_value)
|
||||
|
||||
# Normalize the whole pattern to [0, 1] range if desired
|
||||
noise_pattern = (noise_pattern - noise_pattern.min()) / (noise_pattern.max() - noise_pattern.min())
|
||||
|
||||
return torch.from_numpy(noise_pattern)
|
||||
|
||||
|
||||
def generate_reaction_diffusion_noise(H, W, steps=100, Du=0.16, Dv=0.08, feed_rate=0.035, kill_rate=0.06, seed=None, **kwargs):
|
||||
"""
|
||||
Generates a 2D Reaction-Diffusion pattern using a simplified Gray-Scott model.
|
||||
|
||||
Args:
|
||||
H, W (int): Height and width of the output noise pattern.
|
||||
steps (int): Number of simulation steps.
|
||||
Du, Dv (float): Diffusion rates for substances A and B.
|
||||
feed_rate, kill_rate (float): Rates for feed and kill reactions.
|
||||
seed (int, optional): Seed for initializing the pattern.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: A tensor containing the Reaction-Diffusion noise pattern.
|
||||
"""
|
||||
if seed is not None:
|
||||
torch.manual_seed(seed)
|
||||
np.random.seed(seed)
|
||||
|
||||
# Initialize A and B concentrations with A being fully saturated and B having a noise pattern
|
||||
A = torch.ones((H, W), dtype=torch.float32)
|
||||
B = torch.rand((H, W), dtype=torch.float32) * 0.25 # Starting with a low concentration of B
|
||||
|
||||
# Laplacian kernel for diffusion
|
||||
laplacian_kernel = torch.tensor([[0.05, 0.2, 0.05],
|
||||
[0.2, -1.0, 0.2],
|
||||
[0.05, 0.2, 0.05]], dtype=torch.float32)
|
||||
|
||||
for _ in range(steps):
|
||||
LA = torch.nn.functional.conv2d(A.unsqueeze(0).unsqueeze(0), laplacian_kernel.unsqueeze(0).unsqueeze(0), padding='same').squeeze()
|
||||
LB = torch.nn.functional.conv2d(B.unsqueeze(0).unsqueeze(0), laplacian_kernel.unsqueeze(0).unsqueeze(0), padding='same').squeeze()
|
||||
|
||||
# Reaction-diffusion equations
|
||||
AB2 = A * B.pow(2)
|
||||
dA = Du * LA - AB2 + feed_rate * (1 - A)
|
||||
dB = Dv * LB + AB2 - (feed_rate + kill_rate) * B
|
||||
|
||||
A += dA
|
||||
B += dB
|
||||
|
||||
# Normalize the B concentration pattern to be between 0 and 1
|
||||
pattern = (B - B.min()) / (B.max() - B.min())
|
||||
|
||||
return pattern.unsqueeze(0) # Add a channel dimension
|
||||
@@ -11,7 +11,6 @@ class DeforumControlNetApply:
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
FUNCTION = "apply_controlnet"
|
||||
display_name = "Apply ControlNet [safe]"
|
||||
|
||||
CATEGORY = "deforum"
|
||||
|
||||
def apply_controlnet(self, conditioning, control_net, strength, image=None):
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
import secrets
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
|
||||
|
||||
class AddCustomNoiseNode:
|
||||
"""
|
||||
A node to add various types of noise to an image using PyTorch.
|
||||
"""
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, text, autorefresh):
|
||||
# Force re-evaluation of the node
|
||||
if autorefresh == "Yes":
|
||||
return float("NaN")
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"noise_type": (
|
||||
["speckle", "uniform", "rayleigh", "exponential",
|
||||
"gamma", "random_valued_impulse", "laplace", "perlin", "brownian", "quantization", "shot",
|
||||
"multiplicative", "flicker", "fractal", "cellular", "gaussian", "thermal", "salt_pepper", "poisson",],), #TODO ["blue", "anisotropic"]
|
||||
"amount": ("FLOAT", {
|
||||
"default": 0.1,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"seed": ("INT", {
|
||||
"default": None,
|
||||
"min": 0,
|
||||
"max": 2 ** 32 - 1,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"temperature_map": ("IMAGE",),
|
||||
"mean": ("FLOAT", {
|
||||
"default": 0.0,
|
||||
"min": -1.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
"std": ("FLOAT", {
|
||||
"default": 0.1,
|
||||
"min": 0.01,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "number"
|
||||
}),
|
||||
"prob": ("FLOAT", {
|
||||
"default": 0.05,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "slider"
|
||||
}),
|
||||
"scale": ("FLOAT", {
|
||||
"default": 0.1,
|
||||
"min": 0.01,
|
||||
"max": 10.0,
|
||||
"step": 0.1,
|
||||
"display": "number"
|
||||
}),
|
||||
"temp_scale": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.1,
|
||||
"max": 10.0,
|
||||
"step": 0.1,
|
||||
"display": "number"
|
||||
}),
|
||||
"scale_factor": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.1,
|
||||
"max": 10.0,
|
||||
"step": 0.1,
|
||||
"display": "number"
|
||||
}),
|
||||
"location": ("FLOAT", {
|
||||
"default": 0.0,
|
||||
"min": -10.0,
|
||||
"max": 10.0,
|
||||
"step": 0.1,
|
||||
"display": "number"
|
||||
}),
|
||||
"res_x": ("INT", {
|
||||
"default": 10,
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"res_y": ("INT", {
|
||||
"default": 10,
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"octaves": ("INT", {
|
||||
"default": 5,
|
||||
"min": 1,
|
||||
"max": 10,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"persistence": ("FLOAT", {
|
||||
"default": 0.5,
|
||||
"min": 0.01,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "slider"
|
||||
}),
|
||||
"num_points": ("INT", {
|
||||
"default": 100,
|
||||
"min": 10,
|
||||
"max": 1000,
|
||||
"step": 10,
|
||||
"display": "number"
|
||||
}),
|
||||
"direction": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.00,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "slider"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE","IMAGE",)
|
||||
RETURN_NAMES = ("NOISED_IMAGE","NOISE",)
|
||||
FUNCTION = "add_noise"
|
||||
display_name = "Add Custom Noise"
|
||||
CATEGORY = "deforum"
|
||||
def add_noise(self, images, noise_type, amount, seed=None, temperature_map=None, **kwargs):
|
||||
|
||||
image, noise = add_noise_torch(images.clone(), noise_type, seed, amount, temperature_map, **kwargs)
|
||||
return (image,noise,)
|
||||
|
||||
|
||||
|
||||
def add_noise_torch(images, noise_type='gaussian', seed=None, amount=0.1, temperature_map=None, **kwargs):
|
||||
"""
|
||||
Add various types of noise to an image tensor using PyTorch.
|
||||
|
||||
Parameters:
|
||||
images (torch.Tensor): The input images tensor of shape (B, C, H, W).
|
||||
noise_type (str): Type of noise to add. Supports 'gaussian', 'salt_pepper', 'poisson', 'speckle',
|
||||
'uniform', 'rayleigh', 'exponential', 'gamma', 'random_valued_impulse'.
|
||||
seed (int): Seed value for randomness. Default is None.
|
||||
amount (float): General parameter to control noise amount, interpretation depends on noise_type.
|
||||
**kwargs: Additional noise-specific parameters.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: The noisy images tensor of the same shape as input.
|
||||
"""
|
||||
B, C, H, W = images.shape
|
||||
|
||||
noise = images
|
||||
|
||||
if seed is not None:
|
||||
torch.manual_seed(seed)
|
||||
else:
|
||||
seed = secrets.randbelow(2 ** 32)
|
||||
if noise_type == 'gaussian':
|
||||
mean = kwargs.get('mean', 0.0)
|
||||
std = kwargs.get('std', 0.1)
|
||||
noisy_images = images + amount * torch.randn_like(images) * std + mean
|
||||
|
||||
elif noise_type == 'thermal':
|
||||
if temperature_map is None:
|
||||
raise ValueError("Temperature map must be provided for thermal noise type.")
|
||||
# Normalize temperature map to have a meaningful scale for noise
|
||||
temp_min = temperature_map.min()
|
||||
temp_max = temperature_map.max()
|
||||
normalized_temp_map = (temperature_map - temp_min) / (temp_max - temp_min)
|
||||
# Scale temperature map to control the amount of thermal noise
|
||||
temp_scale = kwargs.get('temp_scale', 1.0) # Scale factor for temperature effect
|
||||
std_map = amount * normalized_temp_map * temp_scale
|
||||
# Generate thermal noise based on temperature map
|
||||
noise = torch.randn_like(images) * std_map.unsqueeze(
|
||||
1) # Ensure std_map matches the channel dimension of images
|
||||
noisy_images = images + noise
|
||||
|
||||
elif noise_type == 'salt_pepper':
|
||||
prob = kwargs.get('prob', 0.05) # Probability for both salt and pepper
|
||||
mask = torch.rand_like(images) < prob
|
||||
images[mask] = torch.rand(1).item() * mask[mask] # Salt
|
||||
mask = torch.rand_like(images) < prob
|
||||
images[mask] = 0 # Pepper
|
||||
noisy_images = images
|
||||
elif noise_type == 'poisson':
|
||||
noisy_images = torch.poisson(images * amount) / amount
|
||||
|
||||
elif noise_type == 'speckle':
|
||||
noise = torch.randn_like(images)
|
||||
noisy_images = images + images * noise * amount
|
||||
|
||||
elif noise_type == 'uniform':
|
||||
noise = torch.rand_like(images)
|
||||
noisy_images = images + amount * (noise - 0.5)
|
||||
|
||||
elif noise_type == 'rayleigh':
|
||||
scale = kwargs.get('scale', 0.1)
|
||||
noise = torch.sqrt(-2 * scale * torch.log(1 - torch.rand_like(images)))
|
||||
noisy_images = images + amount * noise
|
||||
|
||||
elif noise_type == 'exponential':
|
||||
scale = kwargs.get('scale', 0.1)
|
||||
noise = torch.distributions.Exponential(scale).sample(images.shape)
|
||||
noisy_images = images + amount * noise
|
||||
noise = torch.clamp(1 / noise, 0, 1.0) * amount
|
||||
elif noise_type == 'gamma':
|
||||
shape = kwargs.get('shape', 2.0)
|
||||
scale = kwargs.get('scale', 0.1)
|
||||
noise = torch.distributions.Gamma(shape, scale).sample(images.shape)
|
||||
noisy_images = images + amount * noise
|
||||
noise = torch.clamp(1 / noise, 0, 1.0) * amount
|
||||
|
||||
elif noise_type == 'random_valued_impulse':
|
||||
prob = kwargs.get('prob', 0.05)
|
||||
mask = torch.rand_like(images) < prob
|
||||
# Fix: Use logical NOT operator on the mask
|
||||
inverted_mask = ~mask
|
||||
noise = torch.rand_like(images) * mask
|
||||
noisy_images = images * inverted_mask.float() + noise
|
||||
noise = (torch.clamp(noise * 255.0, 0, 1.0) * mask) * amount
|
||||
elif noise_type == 'laplace':
|
||||
location = kwargs.get('location', 0.0)
|
||||
scale = kwargs.get('scale', 0.1)
|
||||
noise = torch.distributions.Laplace(location, scale).sample(images.shape)
|
||||
noisy_images = images + amount * noise
|
||||
noise = torch.clamp(noise * 255.0, 0, 1.0) * amount
|
||||
|
||||
elif noise_type == 'perlin':
|
||||
# Hypothetical function call - you need to implement or integrate a Perlin noise generator.
|
||||
noise = generate_perlin_noise(B, C, H, W, **kwargs)
|
||||
noisy_images = images + amount * noise
|
||||
|
||||
elif noise_type == 'brownian':
|
||||
scale = kwargs.get('scale', 0.1)
|
||||
noise = generate_brownian_noise(images.shape, scale, seed)
|
||||
noisy_images = images + amount * noise / torch.max(noise)
|
||||
elif noise_type == 'quantization':
|
||||
# Quantization levels
|
||||
levels = kwargs.get('levels', 256) # Default is 256 levels for an 8-bit image
|
||||
# Quantize the image
|
||||
max_val = images.max()
|
||||
quantized_images = torch.round(images * (levels - 1) / max_val) * max_val / (levels - 1)
|
||||
# Calculate quantization noise as the difference between original and quantized images
|
||||
noise = images - quantized_images
|
||||
# Optionally scale the noise
|
||||
noisy_images = images + amount * noise
|
||||
noise = torch.clamp(noise, 0, 1.0)
|
||||
elif noise_type == 'shot':
|
||||
# Scale images to simulate intensity as photon counts
|
||||
scale_factor = kwargs.get('scale_factor', 1.0) # Scale factor to adjust the intensity
|
||||
scaled_images = images * scale_factor
|
||||
|
||||
# Apply Poisson distribution to simulate shot noise
|
||||
# Since Poisson function expects lambda > 0, ensure scaled_images is positive
|
||||
noisy_images = torch.poisson(scaled_images) / scale_factor
|
||||
elif noise_type == 'multiplicative':
|
||||
# Amount parameter controls the variance of the multiplicative noise
|
||||
mean = kwargs.get('mean', 1.0) # Multiplicative noise mean. Typically, it should be around 1.
|
||||
std = kwargs.get('std', 0.1) # Standard deviation for the multiplicative noise
|
||||
|
||||
# Generate multiplicative noise
|
||||
noise = torch.randn_like(images) * std + mean
|
||||
|
||||
# Apply the multiplicative noise
|
||||
noisy_images = images * noise
|
||||
elif noise_type == 'blue':
|
||||
# Generate approximate Blue Noise
|
||||
noise = generate_approx_blue_noise((B, C, H, W), seed=seed, **kwargs)
|
||||
|
||||
# Apply the Blue Noise pattern to the images
|
||||
# The amount parameter controls the visibility of the blue noise
|
||||
noisy_images = images + amount * noise
|
||||
elif noise_type == 'flicker':
|
||||
# Create frequency domain noise
|
||||
# Create frequency domain noise
|
||||
real_part = torch.randn((B, C, H, W // 2 + 1), device=images.device, generator=torch.manual_seed(seed))
|
||||
imag_part = torch.randn((B, C, H, W // 2 + 1), device=images.device, generator=torch.manual_seed(seed))
|
||||
freq_domain_noise = real_part + 1j * imag_part
|
||||
|
||||
# Correctly compute frequencies for 1/f scaling
|
||||
frequencies_x = torch.fft.rfftfreq(W, d=1 / W).to(images.device)
|
||||
frequencies_y = torch.fft.fftfreq(H, d=1 / H).to(images.device)
|
||||
|
||||
# Use broadcasting to create a meshgrid of frequencies
|
||||
freq_mesh_x = frequencies_x[None, None, None, :] # Add dimensions for B, C, and H for broadcasting
|
||||
freq_mesh_y = frequencies_y[None, None, :, None] # Add dimensions for B, C, and W for broadcasting
|
||||
|
||||
# Calculate the hypotenuse of the frequency meshgrid to apply 1/f scaling
|
||||
freq_mesh = torch.sqrt(freq_mesh_x ** 2 + freq_mesh_y ** 2)
|
||||
|
||||
# Avoid division by zero by adding a small value, no need to expand as broadcasting handles it
|
||||
freq_domain_noise /= (freq_mesh + 1e-5)
|
||||
|
||||
# Convert back to the spatial domain
|
||||
noise = torch.fft.irfft(freq_domain_noise, n=W, dim=-1)
|
||||
|
||||
# Apply noise to images
|
||||
noisy_images = images + amount * noise
|
||||
elif noise_type == 'anisotropic':
|
||||
# Generate base noise
|
||||
base_noise = torch.randn_like(images)
|
||||
# Apply a directional gradient
|
||||
dir_min = kwargs.get('dir_min', 0.0)
|
||||
dir_max = kwargs.get('dir_min', 1.0)
|
||||
direction = torch.tensor([dir_max, dir_min])
|
||||
gradient = torch.outer(torch.arange(H), direction[0]) + torch.outer(torch.arange(W), direction[1])
|
||||
gradient = gradient.unsqueeze(0).unsqueeze(0).to(images.device)
|
||||
# Apply directional bias to the noise
|
||||
noise = base_noise * gradient
|
||||
noisy_images = images + amount * noise
|
||||
elif noise_type == 'fractal':
|
||||
res_x = kwargs.get('res_x', 10) # Base resolution for Perlin noise
|
||||
res_y = kwargs.get('res_y', 10)
|
||||
octaves = kwargs.get('octaves', 5) # Number of noise layers
|
||||
persistence = kwargs.get('persistence', 0.5) # Amplitude decay per layer
|
||||
noise = generate_fractal_noise(B, C, H, W, (res_x, res_y), octaves, persistence)
|
||||
noisy_images = images + amount * noise
|
||||
elif noise_type == 'cellular':
|
||||
num_points = kwargs.get('num_points', 100) # Default number of points
|
||||
cellular_noise = generate_cellular_noise(H, W, num_points=num_points)
|
||||
# Expand Cellular noise to match input dimensions
|
||||
noise = cellular_noise.unsqueeze(0).unsqueeze(0).repeat(B, C, 1, 1)
|
||||
noisy_images = images + amount * noise
|
||||
else:
|
||||
raise ValueError(f"Unsupported noise type: {noise_type}")
|
||||
|
||||
return torch.clamp(noisy_images, 0, 1), noise
|
||||
|
||||
|
||||
def generate_perlin_noise_2d(shape, res, seed=None):
|
||||
"""
|
||||
Generate a 2D numpy array of Perlin noise.
|
||||
|
||||
Args:
|
||||
shape: The shape of the generated array (height, width).
|
||||
res: The resolution of the noise in terms of number of cells in each dimension.
|
||||
|
||||
Returns:
|
||||
A 2D numpy array of Perlin noise.
|
||||
"""
|
||||
|
||||
if seed is not None:
|
||||
np.random.seed(seed) # Set the seed if provided
|
||||
|
||||
def interpolate(t):
|
||||
return t * t * t * (t * (t * 6 - 15) + 10)
|
||||
|
||||
def gradient(h, x, y):
|
||||
vectors = np.array([[0,1],[0,-1],[1,0],[-1,0]])
|
||||
g = vectors[h % 4]
|
||||
return g[:, :, 0] * x + g[:, :, 1] * y
|
||||
|
||||
grid_x, grid_y = np.meshgrid(np.arange(shape[1]), np.arange(shape[0]))
|
||||
|
||||
# Rescale grid to fit res
|
||||
grid_x = grid_x * res[0] / shape[1]
|
||||
grid_y = grid_y * res[1] / shape[0]
|
||||
|
||||
# Determine grid cell coordinates
|
||||
x0 = grid_x.astype(int)
|
||||
y0 = grid_y.astype(int)
|
||||
|
||||
# Relative x and y coordinates within each cell
|
||||
x_rel = grid_x - x0
|
||||
y_rel = grid_y - y0
|
||||
|
||||
# Random gradients
|
||||
np.random.seed(0) # Optional: for reproducibility
|
||||
gradients = np.random.randint(0, 4, (res[0] + 1, res[1] + 1))
|
||||
|
||||
# Compute the dot-product
|
||||
n0 = gradient(gradients[y0, x0], x_rel, y_rel)
|
||||
n1 = gradient(gradients[y0, x0 + 1], x_rel - 1, y_rel)
|
||||
ix0 = interpolate(n0) + (interpolate(n1) - interpolate(n0)) * interpolate(x_rel)
|
||||
|
||||
n0 = gradient(gradients[y0 + 1, x0], x_rel, y_rel - 1)
|
||||
n1 = gradient(gradients[y0 + 1, x0 + 1], x_rel - 1, y_rel - 1)
|
||||
ix1 = interpolate(n0) + (interpolate(n1) - interpolate(n0)) * interpolate(x_rel)
|
||||
|
||||
value = ix0 + (ix1 - ix0) * interpolate(y_rel)
|
||||
return (value - np.min(value)) / (np.max(value) - np.min(value))
|
||||
|
||||
def generate_perlin_noise(B, C, H, W, res_x, res_y, seed=None, *args, **kwargs):
|
||||
"""
|
||||
Generates Perlin noise for a batch of images in PyTorch.
|
||||
|
||||
Args:
|
||||
B, C, H, W: Dimensions of the output tensor.
|
||||
res_x, res_y: Resolution of the Perlin noise grid.
|
||||
|
||||
Returns:
|
||||
A PyTorch tensor of shape (B, C, H, W) containing Perlin noise.
|
||||
"""
|
||||
noise = np.zeros((B, C, H, W))
|
||||
for b in range(B):
|
||||
for c in range(C):
|
||||
noise[b, c] = generate_perlin_noise_2d((H, W), (res_x, res_y), seed=seed)
|
||||
return torch.FloatTensor(noise)
|
||||
|
||||
|
||||
def generate_brownian_noise(shape, scale=1.0, seed=None):
|
||||
"""
|
||||
Generate Brownian (Red) noise for a given tensor shape.
|
||||
|
||||
Parameters:
|
||||
- shape (tuple): The shape of the output tensor (e.g., (B, C, H, W)).
|
||||
- scale (float): Scaling factor for noise intensity. Default is 1.0.
|
||||
- seed (int): Optional seed for reproducibility. Default is None.
|
||||
|
||||
Returns:
|
||||
- torch.Tensor: Tensor containing Brownian noise of the specified shape.
|
||||
"""
|
||||
if seed is not None:
|
||||
torch.manual_seed(seed)
|
||||
|
||||
# Initialize a tensor of random noise
|
||||
random_noise = torch.randn(shape)
|
||||
|
||||
# Cumulatively sum the noise along the last dimension to simulate Brownian motion
|
||||
brownian_noise = torch.cumsum(random_noise, dim=-1) * scale
|
||||
|
||||
# Normalize the noise to have values between 0 and 1
|
||||
brownian_noise -= brownian_noise.min()
|
||||
brownian_noise /= brownian_noise.max()
|
||||
|
||||
return brownian_noise
|
||||
|
||||
|
||||
def generate_approx_blue_noise(shape, seed=None, min_dist=1.0, sample_fraction=0.1, *args, **kwargs):
|
||||
"""
|
||||
Generates an approximate Blue Noise pattern using a simplified, naive approach.
|
||||
Note: This is computationally expensive and not optimized for performance or quality.
|
||||
|
||||
Parameters:
|
||||
shape (tuple): Shape of the output tensor (B, C, H, W).
|
||||
seed (int): Random seed for reproducibility.
|
||||
min_dist (float): Minimum distance between points. Controls the density.
|
||||
sample_fraction (float): Fraction of points to sample, lower means sparser.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Tensor containing an approximate Blue Noise pattern.
|
||||
"""
|
||||
if seed is not None:
|
||||
torch.manual_seed(seed)
|
||||
|
||||
_, _, H, W = shape
|
||||
pattern = torch.zeros((H, W))
|
||||
|
||||
# Number of points to sample
|
||||
num_points = int(H * W * sample_fraction)
|
||||
|
||||
# Randomly sample points with minimum distance
|
||||
for _ in range(num_points):
|
||||
x, y = torch.randint(0, H, (1,)), torch.randint(0, W, (1,))
|
||||
|
||||
# Check if any existing point is within min_dist
|
||||
y_grid, x_grid = torch.meshgrid(torch.arange(H), torch.arange(W), indexing='ij')
|
||||
distances = torch.sqrt((x_grid - x) ** 2 + (y_grid - y) ** 2)
|
||||
if torch.min(distances * (pattern > 0).float()) >= min_dist or torch.sum(pattern) == 0:
|
||||
pattern[x, y] = 1
|
||||
|
||||
# Convert the pattern to match the input shape, assuming binary pattern applied across all channels
|
||||
blue_noise_pattern = pattern.repeat(shape[1], 1, 1).unsqueeze(0).repeat(shape[0], 1, 1, 1)
|
||||
|
||||
return blue_noise_pattern.float()
|
||||
|
||||
|
||||
def generate_fractal_noise(B, C, H, W, res, octaves=5, persistence=0.5, *args, **kwargs):
|
||||
"""
|
||||
Generates fractal noise for a batch of images in PyTorch.
|
||||
|
||||
Args:
|
||||
B (int): Batch size.
|
||||
C (int): Number of channels.
|
||||
H, W (int): Height and Width of the images.
|
||||
res (tuple): Resolution of the base Perlin noise (res_x, res_y).
|
||||
octaves (int): Number of layers of noise to generate.
|
||||
persistence (float): Amplitude decay per octave (determines "roughness").
|
||||
|
||||
Returns:
|
||||
A PyTorch tensor of shape (B, C, H, W) containing fractal noise.
|
||||
"""
|
||||
noise = torch.zeros((B, C, H, W))
|
||||
amplitude = 1.0
|
||||
frequency = 1.0
|
||||
max_amplitude = 0.0
|
||||
|
||||
for _ in range(octaves):
|
||||
perlin_noise = generate_perlin_noise(B, C, H, W, res_x=(int(res[0] * frequency)), res_y=int(res[1] * frequency))
|
||||
noise += perlin_noise * amplitude
|
||||
max_amplitude += amplitude
|
||||
amplitude *= persistence
|
||||
frequency *= 2
|
||||
|
||||
# Normalize the noise
|
||||
noise /= max_amplitude
|
||||
|
||||
return noise
|
||||
|
||||
|
||||
def generate_cellular_noise(H, W, num_points=100, *args, **kwargs):
|
||||
"""
|
||||
Generates a 2D Cellular (Worley) Noise pattern using PyTorch.
|
||||
|
||||
Args:
|
||||
H, W (int): The height and width of the output noise pattern.
|
||||
num_points (int): Number of seed points to generate.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: A tensor containing the Cellular noise pattern.
|
||||
"""
|
||||
# Ensure PyTorch is using the same device as the input images
|
||||
device = kwargs.get('device', 'cpu')
|
||||
|
||||
# Generate random points (the seeds of the Cellular noise)
|
||||
points = torch.rand(num_points, 2, device=device) * torch.tensor([W, H], device=device)
|
||||
|
||||
# Initialize the noise pattern
|
||||
noise = torch.full((H, W), float('inf'), device=device)
|
||||
|
||||
# Compute distance from each pixel to the nearest point using broadcasting
|
||||
y_coords, x_coords = torch.meshgrid(torch.arange(H, device=device), torch.arange(W, device=device), indexing='ij')
|
||||
for point in points:
|
||||
dist = torch.sqrt((x_coords - point[0]) ** 2 + (y_coords - point[1]) ** 2)
|
||||
noise = torch.min(noise, dist)
|
||||
# Normalize the noise pattern
|
||||
noise = (noise - torch.min(noise)) / (torch.max(noise) - torch.min(noise))
|
||||
return noise
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class DeforumVideoSaveNode:
|
||||
def fn(self, image, filename_prefix, fps, codec, pixel_format, format, quality, dump_by, dump_every, dump_now, skip_save, deforum_frame_data={}):
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||
filename_prefix, self.output_dir)
|
||||
counter = find_next_index(full_output_folder, filename_prefix)
|
||||
counter = find_next_index(full_output_folder, filename_prefix, format)
|
||||
|
||||
#frame_idx = deforum_frame_data.get["frame_idx"]
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
opencv-contrib-python
|
||||
timm==0.6.5
|
||||
PyWavelets
|
||||
opensimplex
|
||||
matplotlib
|
||||
pandas<=1.5.1
|
||||
numexpr
|
||||
|
||||
Reference in New Issue
Block a user