diff --git a/deforum_nodes/mapping.py b/deforum_nodes/mapping.py index 141ab19..44bd38b 100644 --- a/deforum_nodes/mapping.py +++ b/deforum_nodes/mapping.py @@ -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 * diff --git a/deforum_nodes/modules/better_resize/__init__.py b/deforum_nodes/modules/better_resize/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deforum_nodes/modules/better_resize/interp_methods.py b/deforum_nodes/modules/better_resize/interp_methods.py new file mode 100644 index 0000000..4a577f4 --- /dev/null +++ b/deforum_nodes/modules/better_resize/interp_methods.py @@ -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)) \ No newline at end of file diff --git a/deforum_nodes/modules/better_resize/resize_right.py b/deforum_nodes/modules/better_resize/resize_right.py new file mode 100644 index 0000000..5106a6d --- /dev/null +++ b/deforum_nodes/modules/better_resize/resize_right.py @@ -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) \ No newline at end of file diff --git a/deforum_nodes/modules/deforum_comfyui_helpers.py b/deforum_nodes/modules/deforum_comfyui_helpers.py index 3fca62b..e355cc8 100644 --- a/deforum_nodes/modules/deforum_comfyui_helpers.py +++ b/deforum_nodes/modules/deforum_comfyui_helpers.py @@ -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): diff --git a/deforum_nodes/nodes/deforum_advnoise_node.py b/deforum_nodes/nodes/deforum_advnoise_node.py new file mode 100644 index 0000000..3b309fa --- /dev/null +++ b/deforum_nodes/nodes/deforum_advnoise_node.py @@ -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 diff --git a/deforum_nodes/nodes/deforum_cnet_nodes.py b/deforum_nodes/nodes/deforum_cnet_nodes.py index 1b93f68..4401bef 100644 --- a/deforum_nodes/nodes/deforum_cnet_nodes.py +++ b/deforum_nodes/nodes/deforum_cnet_nodes.py @@ -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): diff --git a/deforum_nodes/nodes/deforum_noise_nodes.py b/deforum_nodes/nodes/deforum_noise_nodes.py new file mode 100644 index 0000000..e877190 --- /dev/null +++ b/deforum_nodes/nodes/deforum_noise_nodes.py @@ -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 + + diff --git a/deforum_nodes/nodes/deforum_video_nodes.py b/deforum_nodes/nodes/deforum_video_nodes.py index b4eddeb..c9ccecb 100644 --- a/deforum_nodes/nodes/deforum_video_nodes.py +++ b/deforum_nodes/nodes/deforum_video_nodes.py @@ -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"] diff --git a/requirements.txt b/requirements.txt index 9058b96..d16f8ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ opencv-contrib-python timm==0.6.5 +PyWavelets +opensimplex matplotlib pandas<=1.5.1 numexpr