Squashed
This commit is contained in:
+14
-7
@@ -74,7 +74,9 @@ from .nodes.masks.flex_masks import (
|
||||
FlexMaskMorph,
|
||||
FlexMaskWarp,
|
||||
FlexMaskTransform,
|
||||
FlexMaskMath
|
||||
FlexMaskMath,
|
||||
FlexMaskOpacity,
|
||||
FlexMaskVoronoiScheduled,
|
||||
)
|
||||
|
||||
|
||||
@@ -98,7 +100,10 @@ from .nodes.masks.particle_system_masks import (
|
||||
ParticleSpeedModulation,
|
||||
)
|
||||
|
||||
|
||||
# from .nodes.masks.opacity_masks import(
|
||||
# DepthBasedMaskOpacity,
|
||||
# FlexDepthBasedMaskOpacity,
|
||||
# )
|
||||
|
||||
from .nodes.masks.utility_nodes import (
|
||||
_mfc,
|
||||
@@ -132,6 +137,7 @@ from .nodes.flex.feature_modulation import (
|
||||
)
|
||||
|
||||
|
||||
|
||||
import os
|
||||
import folder_paths
|
||||
|
||||
@@ -207,6 +213,12 @@ NODE_CLASS_MAPPINGS = {
|
||||
"FlexImagePixelate":FlexImagePixelate,
|
||||
"FlexImageColorGrade":FlexImageColorGrade,
|
||||
"FlexImageTiltShift":FlexImageTiltShift,
|
||||
"FlexMaskOpacity":FlexMaskOpacity,
|
||||
"FlexMaskVoronoiScheduled":FlexMaskVoronoiScheduled,
|
||||
|
||||
#opacity xp
|
||||
# "FlexDepthBasedMaskOpacity":FlexDepthBasedMaskOpacity,
|
||||
# "DepthBasedMaskOpacity":DepthBasedMaskOpacity,
|
||||
|
||||
|
||||
#garb
|
||||
@@ -214,11 +226,6 @@ NODE_CLASS_MAPPINGS = {
|
||||
"MovingShape": MovingShape,
|
||||
"_mfc":_mfc,
|
||||
"TextMaskNode":TextMaskNode,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -716,3 +716,32 @@ add_node_config("FlexImageTiltShift", {
|
||||
- `focus_shape`: Shape of the focus area ("rectangle" or "ellipse")
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexMaskOpacity", {
|
||||
"TOP_DESCRIPTION": "Applies opacity modulation to the mask based on a selected feature.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `max_opacity`: Maximum opacity to apply to the mask (0.0 to 1.0). Higher values allow for more opaque masks.
|
||||
|
||||
The actual opacity applied is determined by the product of max_opacity, feature value, and strength.
|
||||
|
||||
This node is useful for creating masks that fade in and out based on the selected feature, allowing for smooth transitions and effects that respond to various inputs like audio, time, or other extracted features.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexMaskVoronoiScheduled", {
|
||||
"TOP_DESCRIPTION": "Generates a Voronoi noise mask with parameters modulated by a selected feature according to a specified formula.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `distance_metric`: Method used to calculate distances in the Voronoi diagram. Options include various mathematical norms and custom patterns.
|
||||
- `scale`: Base scale of the Voronoi cells (0.1 to 10.0). Larger values create bigger cells.
|
||||
- `detail`: Number of Voronoi cells (10 to 1000). More cells create more intricate patterns.
|
||||
- `randomness`: Degree of randomness in cell placement (0.0 to 5.0). Higher values create more chaotic patterns.
|
||||
- `seed`: Random seed for reproducible results (0 to 2^64 - 1).
|
||||
- `x_offset`: Horizontal offset of the Voronoi pattern (-1000.0 to 1000.0).
|
||||
- `y_offset`: Vertical offset of the Voronoi pattern (-1000.0 to 1000.0).
|
||||
- `control_parameter`: Which parameter to modulate based on the feature ("scale", "detail", "randomness", "seed", "x_offset", "y_offset").
|
||||
- `formula`: Mathematical formula used to map the feature value to the control parameter. Options include "Linear", "Quadratic", "Cubic", "Sinusoidal", and "Exponential".
|
||||
- `a` and `b`: Parameters for fine-tuning the chosen formula (0.1 to 10.0).
|
||||
|
||||
Credit for the heavy lifting for this node goes to https://github.com/alanhuang67/
|
||||
"""
|
||||
})
|
||||
@@ -101,10 +101,14 @@ class AudioFeature(BaseFeature):
|
||||
end_time = (frame_index + 1) * self.frame_duration
|
||||
start_sample = int(start_time * self.sample_rate)
|
||||
end_sample = int(end_time * self.sample_rate)
|
||||
return self.audio[start_sample:end_sample]
|
||||
if start_sample >= len(self.audio):
|
||||
return np.array([]) # Return empty array if we've run out of audio
|
||||
return self.audio[start_sample:min(end_sample, len(self.audio))]
|
||||
|
||||
def _amplitude_envelope(self):
|
||||
return np.array([np.max(np.abs(self._get_audio_frame(i))) for i in range(self.num_frames)])
|
||||
def safe_max(frame):
|
||||
return np.max(np.abs(frame)) if frame.size > 0 else 0
|
||||
return np.array([safe_max(self._get_audio_frame(i)) for i in range(self.num_frames)])
|
||||
|
||||
def _rms_energy(self):
|
||||
return np.array([np.sqrt(np.mean(self._get_audio_frame(i)**2)) for i in range(self.num_frames)])
|
||||
|
||||
+168
-1
@@ -1,6 +1,9 @@
|
||||
from .mask_base import FlexMaskBase
|
||||
from .mask_utils import morph_mask, warp_mask, transform_mask, combine_masks
|
||||
import math
|
||||
import numpy as np
|
||||
from .voronoi_noise import VoronoiNoise #NOTE credit for Voronoi goes to Alan Huang https://github.com/alanhuang67/
|
||||
from comfy.model_management import get_torch_device
|
||||
|
||||
class FlexMaskMorph(FlexMaskBase):
|
||||
@classmethod
|
||||
@@ -86,4 +89,168 @@ class FlexMaskMath(FlexMaskBase):
|
||||
|
||||
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur, mask_b, combination_method, **kwargs):
|
||||
return (self.apply_mask_operation(masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur,
|
||||
mask_b=mask_b, combination_method=combination_method, **kwargs),)
|
||||
mask_b=mask_b, combination_method=combination_method, **kwargs),)
|
||||
|
||||
class FlexMaskOpacity(FlexMaskBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"max_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, max_opacity: float, **kwargs) -> np.ndarray:
|
||||
opacity = max_opacity * feature_value * strength
|
||||
return mask * opacity
|
||||
|
||||
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur, max_opacity, **kwargs):
|
||||
return (self.apply_mask_operation(masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur,
|
||||
max_opacity=max_opacity, **kwargs),)
|
||||
|
||||
#NOTE credit for the heavy lifting in this class and all of the noise class goes to Alan Huang https://github.com/alanhuang67/
|
||||
class FlexMaskVoronoiScheduled(FlexMaskBase):
|
||||
formulas = {
|
||||
"Linear": lambda t, a, b: t * a / b,
|
||||
"Quadratic": lambda t, a, b: (t * a / b) ** 2,
|
||||
"Cubic": lambda t, a, b: (t * a / b) ** 3,
|
||||
"Sinusoidal": lambda t, a, b: math.sin(math.pi * t * a / b / 2),
|
||||
"Exponential": lambda t, a, b: math.exp(t * a / b) - 1,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"distance_metric": ([
|
||||
"euclidean", "manhattan", "chebyshev", "minkowski",
|
||||
"elliptical", "kaleidoscope_star", "kaleidoscope_wave",
|
||||
"kaleidoscope_radiation_α", "kaleidoscope_radiation_β",
|
||||
"kaleidoscope_radiation_γ"
|
||||
],),
|
||||
"scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
||||
"detail": ("INT", {"default": 100, "min": 10, "max": 1000, "step": 10}),
|
||||
"randomness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.1}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"x_offset": ("FLOAT", {"default": 0.0, "min": -1000.0, "max": 1000.0, "step": 0.1}),
|
||||
"y_offset": ("FLOAT", {"default": 0.0, "min": -1000.0, "max": 1000.0, "step": 0.1}),
|
||||
"control_parameter": (["scale", "detail", "randomness", "seed", "x_offset", "y_offset"],),
|
||||
"formula": (list(cls.formulas.keys()),),
|
||||
"a": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
||||
"b": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
def generate_schedule(self, formula, feature_value, a, b):
|
||||
t = feature_value
|
||||
return self.formulas[formula](t, a, b)
|
||||
|
||||
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float,
|
||||
distance_metric: str, scale: float, detail: int, randomness: float,
|
||||
seed: int, x_offset: float, y_offset: float, control_parameter: str,
|
||||
formula: str, a: float, b: float, **kwargs) -> np.ndarray:
|
||||
|
||||
height, width = mask.shape[:2]
|
||||
|
||||
# Generate schedule value
|
||||
schedule_value = self.generate_schedule(formula, feature_value, a, b)
|
||||
|
||||
# Adjust the controlled parameter based on the schedule value and strength
|
||||
if control_parameter == "scale":
|
||||
scale *= (1 + schedule_value * strength)
|
||||
elif control_parameter == "detail":
|
||||
detail = int(detail * (1 + schedule_value * strength))
|
||||
elif control_parameter == "randomness":
|
||||
randomness *= (1 + schedule_value * strength)
|
||||
elif control_parameter == "seed":
|
||||
seed = int(seed + (schedule_value * strength * 1000000))
|
||||
elif control_parameter == "x_offset":
|
||||
x_offset *= (1 + schedule_value * strength)
|
||||
elif control_parameter == "y_offset":
|
||||
y_offset *= (1 + schedule_value * strength)
|
||||
|
||||
# Create VoronoiNoise instance
|
||||
voronoi = VoronoiNoise(
|
||||
width=width,
|
||||
height=height,
|
||||
scale=[scale],
|
||||
detail=[detail],
|
||||
seed=[seed],
|
||||
randomness=[randomness],
|
||||
X=[x_offset],
|
||||
Y=[y_offset],
|
||||
distance_metric=distance_metric,
|
||||
batch_size=1,
|
||||
device=get_torch_device()
|
||||
)
|
||||
|
||||
# Generate Voronoi noise
|
||||
voronoi_tensor = voronoi()
|
||||
|
||||
# Convert to numpy array and extract the first channel (they're all the same)
|
||||
voronoi_mask = voronoi_tensor[0, :, :, 0].cpu().numpy()
|
||||
|
||||
return voronoi_mask
|
||||
|
||||
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold,
|
||||
invert, subtract_original, grow_with_blur, distance_metric,
|
||||
scale, detail, randomness, seed, x_offset, y_offset,
|
||||
control_parameter, formula, a, b, **kwargs):
|
||||
return (self.apply_mask_operation(masks, feature, feature_pipe, strength,
|
||||
feature_threshold, invert, subtract_original,
|
||||
grow_with_blur, distance_metric=distance_metric,
|
||||
scale=scale, detail=detail, randomness=randomness,
|
||||
seed=seed, x_offset=x_offset, y_offset=y_offset,
|
||||
control_parameter=control_parameter,
|
||||
formula=formula, a=a, b=b, **kwargs),)
|
||||
|
||||
#TODO
|
||||
# class FlexMaskVoronoiShape(FlexMaskBase):
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(cls):
|
||||
# return {
|
||||
# **super().INPUT_TYPES(),
|
||||
# "required": {
|
||||
# **super().INPUT_TYPES()["required"],
|
||||
# "max_num_points": ("INT", {"default": 50, "min": 4, "max": 1000, "step": 1}),
|
||||
# "max_point_jitter": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
# "shape_type": (get_available_shapes(),),
|
||||
# "max_shape_size": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
# "control_mode": (["num_points", "point_jitter", "shape_size", "all"],),
|
||||
# }
|
||||
# }
|
||||
|
||||
# def process_mask(self, mask: np.ndarray, feature_value: float, strength: float,
|
||||
# max_num_points: int, max_point_jitter: float, shape_type: str,
|
||||
# max_shape_size: float, control_mode: str, **kwargs) -> np.ndarray:
|
||||
# if control_mode == "num_points" or control_mode == "all":
|
||||
# num_points = max(4, int(max_num_points * feature_value * strength))
|
||||
# else:
|
||||
# num_points = max_num_points
|
||||
|
||||
# if control_mode == "point_jitter" or control_mode == "all":
|
||||
# point_jitter = max_point_jitter * feature_value * strength
|
||||
# else:
|
||||
# point_jitter = max_point_jitter
|
||||
|
||||
# if control_mode == "shape_size" or control_mode == "all":
|
||||
# shape_size = max_shape_size * feature_value * strength
|
||||
# else:
|
||||
# shape_size = max_shape_size
|
||||
|
||||
# return generate_voronoi_shapes_mask(mask.shape[:2], num_points, point_jitter, shape_type, shape_size)
|
||||
|
||||
# def main_function(self, masks, feature, feature_pipe, strength, feature_threshold,
|
||||
# invert, subtract_original, grow_with_blur, max_num_points,
|
||||
# max_point_jitter, shape_type, max_shape_size, control_mode, **kwargs):
|
||||
# return (self.apply_mask_operation(masks, feature, feature_pipe, strength,
|
||||
# feature_threshold, invert, subtract_original,
|
||||
# grow_with_blur, max_num_points=max_num_points,
|
||||
# max_point_jitter=max_point_jitter,
|
||||
# shape_type=shape_type,
|
||||
# max_shape_size=max_shape_size,
|
||||
# control_mode=control_mode, **kwargs),)
|
||||
+120
-1
@@ -1,8 +1,12 @@
|
||||
import numpy as np
|
||||
import cv2
|
||||
from scipy import ndimage
|
||||
from scipy.ndimage import gaussian_filter
|
||||
from scipy.ndimage import gaussian_filter,distance_transform_edt
|
||||
from ..node_utilities import string_to_rgb
|
||||
from scipy.spatial import Voronoi
|
||||
from .shape_utils import get_available_shapes,create_shape_mask
|
||||
from scipy.spatial import Voronoi
|
||||
from skimage import draw
|
||||
|
||||
def apply_easing(t, easing_type):
|
||||
if easing_type == 'linear':
|
||||
@@ -43,6 +47,121 @@ def morph_mask(mask, morph_type, kernel_size, iterations, progress_callback=None
|
||||
|
||||
return mask
|
||||
|
||||
def generate_shape(shape_type, size, center):
|
||||
x, y = center
|
||||
if shape_type == "circle":
|
||||
return cv2.circle(np.zeros(size, dtype=np.float32), (int(x), int(y)), int(size[0]/20), 1, -1)
|
||||
elif shape_type == "star":
|
||||
points = np.array([[x, y-30], [x+10, y-10], [x+30, y-10], [x+15, y+5],
|
||||
[x+20, y+30], [x, y+15], [x-20, y+30], [x-15, y+5],
|
||||
[x-30, y-10], [x-10, y-10]], dtype=np.int32)
|
||||
return cv2.fillPoly(np.zeros(size, dtype=np.float32), [points], 1)
|
||||
elif shape_type == "oval":
|
||||
return cv2.ellipse(np.zeros(size, dtype=np.float32), (int(x), int(y)), (int(size[0]/15), int(size[0]/30)), 0, 0, 360, 1, -1)
|
||||
elif shape_type == "parallelogram":
|
||||
points = np.array([[x-20, y-10], [x+20, y-10], [x+30, y+10], [x-10, y+10]], dtype=np.int32)
|
||||
return cv2.fillPoly(np.zeros(size, dtype=np.float32), [points], 1)
|
||||
elif shape_type == "dodecagon":
|
||||
radius = size[0] // 40
|
||||
angle = np.linspace(0, 2*np.pi, 13)[:-1]
|
||||
points = np.array([np.column_stack([x + radius * np.cos(angle), y + radius * np.sin(angle)])], dtype=np.int32)
|
||||
return cv2.fillPoly(np.zeros(size, dtype=np.float32), points, 1)
|
||||
else: # default to point
|
||||
return cv2.circle(np.zeros(size, dtype=np.float32), (int(x), int(y)), 1, 1, -1)
|
||||
|
||||
|
||||
def generate_voronoi_mask(shape, num_points, point_jitter, edge_width):
|
||||
height, width = shape
|
||||
|
||||
# Ensure at least 4 points
|
||||
num_points = max(4, num_points)
|
||||
|
||||
# Generate base grid of points
|
||||
x = np.linspace(0, width, int(np.sqrt(num_points)))
|
||||
y = np.linspace(0, height, int(np.sqrt(num_points)))
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
points = np.column_stack((xx.ravel(), yy.ravel()))
|
||||
|
||||
# Add jitter to points
|
||||
jitter = np.random.uniform(-point_jitter, point_jitter, points.shape) * np.array([width, height])
|
||||
points += jitter
|
||||
|
||||
# Ensure points are within the image bounds and unique
|
||||
points = np.clip(points, [0, 0], [width - 1, height - 1])
|
||||
points = np.unique(points, axis=0)
|
||||
|
||||
# If we have less than 4 unique points, add some random points
|
||||
while len(points) < 4:
|
||||
new_point = np.random.rand(2) * [width, height]
|
||||
points = np.vstack((points, new_point))
|
||||
points = np.unique(points, axis=0)
|
||||
|
||||
# Generate Voronoi diagram
|
||||
vor = Voronoi(points)
|
||||
|
||||
# Create mask
|
||||
mask = np.zeros(shape, dtype=np.float32)
|
||||
|
||||
# Draw Voronoi edges
|
||||
for simplex in vor.ridge_vertices:
|
||||
if -1 not in simplex:
|
||||
p1, p2 = vor.vertices[simplex]
|
||||
rr, cc = draw.line(int(p1[1]), int(p1[0]), int(p2[1]), int(p2[0]))
|
||||
valid = (rr >= 0) & (rr < height) & (cc >= 0) & (cc < width)
|
||||
mask[rr[valid], cc[valid]] = 1
|
||||
|
||||
# Apply edge width
|
||||
if edge_width > 0:
|
||||
distance = distance_transform_edt(1 - mask)
|
||||
mask = np.where(distance < edge_width * min(height, width), 1, 0)
|
||||
|
||||
return mask.astype(np.float32)
|
||||
|
||||
def generate_voronoi_shapes_mask(shape, num_points, point_jitter, shape_type, shape_size, shape_params=None):
|
||||
height, width = shape
|
||||
|
||||
# Generate points
|
||||
x = np.linspace(0, width, int(np.sqrt(num_points)))
|
||||
y = np.linspace(0, height, int(np.sqrt(num_points)))
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
points = np.column_stack((xx.ravel(), yy.ravel()))
|
||||
|
||||
# Add jitter to points
|
||||
jitter = np.random.uniform(-point_jitter, point_jitter, points.shape) * np.array([width, height])
|
||||
points += jitter
|
||||
|
||||
# Ensure points are within the image bounds and unique
|
||||
points = np.clip(points, [0, 0], [width - 1, height - 1])
|
||||
points = np.unique(points, axis=0)
|
||||
|
||||
# If we have less than 4 unique points, add some random points
|
||||
while len(points) < 4:
|
||||
new_point = np.random.rand(2) * [width, height]
|
||||
points = np.vstack((points, new_point))
|
||||
points = np.unique(points, axis=0)
|
||||
|
||||
# Generate Voronoi diagram
|
||||
vor = Voronoi(points)
|
||||
|
||||
# Create mask
|
||||
mask = np.zeros(shape, dtype=np.float32)
|
||||
|
||||
# Generate shapes at Voronoi points
|
||||
for point in vor.points:
|
||||
x, y = point.astype(int)
|
||||
size = int(shape_size * min(height, width))
|
||||
|
||||
if shape_type == "random":
|
||||
current_shape_type = np.random.choice(get_available_shapes())
|
||||
else:
|
||||
current_shape_type = shape_type
|
||||
|
||||
shape_mask = create_shape_mask(shape, (x, y), current_shape_type, size, shape_params)
|
||||
mask = np.maximum(mask, shape_mask)
|
||||
|
||||
return mask
|
||||
|
||||
|
||||
###TRANSFORM
|
||||
def warp_affine(mask: np.ndarray, M: np.ndarray) -> np.ndarray:
|
||||
height, width = mask.shape[:2]
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import numpy as np
|
||||
from matplotlib.path import Path
|
||||
|
||||
def create_circle(center, size, params):
|
||||
return lambda x, y: ((x - center[0])**2 + (y - center[1])**2 <= size**2)
|
||||
|
||||
def create_ellipse(center, size, params):
|
||||
aspect_ratio = params.get('aspect_ratio', 0.5)
|
||||
return lambda x, y: ((x - center[0])**2 / size**2 + (y - center[1])**2 / (size * aspect_ratio)**2 <= 1)
|
||||
|
||||
def create_rectangle(center, size, params):
|
||||
aspect_ratio = params.get('aspect_ratio', 1)
|
||||
return lambda x, y: (abs(x - center[0]) <= size) & (abs(y - center[1]) <= size * aspect_ratio)
|
||||
|
||||
def create_polygon(center, size, params):
|
||||
sides = params.get('sides', 3)
|
||||
rotation = params.get('rotation', 0)
|
||||
angles = np.linspace(0, 2*np.pi, sides, endpoint=False) + rotation
|
||||
polygon_x = center[0] + size * np.cos(angles)
|
||||
polygon_y = center[1] + size * np.sin(angles)
|
||||
path = Path(np.column_stack([polygon_x, polygon_y]))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_star(center, size, params):
|
||||
num_points = params.get('num_points', 5)
|
||||
inner_radius = size * params.get('inner_radius_ratio', 0.5)
|
||||
rotation = params.get('rotation', 0)
|
||||
angles = np.linspace(0, 2*np.pi, 2*num_points, endpoint=False) + rotation
|
||||
radii = np.array([size, inner_radius] * num_points)
|
||||
star_x = center[0] + radii * np.cos(angles)
|
||||
star_y = center[1] + radii * np.sin(angles)
|
||||
path = Path(np.column_stack([star_x, star_y]))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_cross(center, size, params):
|
||||
thickness = params.get('thickness', 0.2)
|
||||
return lambda x, y: ((abs(x - center[0]) <= size * thickness) | (abs(y - center[1]) <= size * thickness)) & \
|
||||
((abs(x - center[0]) <= size) & (abs(y - center[1]) <= size))
|
||||
|
||||
def create_heart(center, size, params):
|
||||
def heart_curve(t):
|
||||
x = 16 * np.sin(t)**3
|
||||
y = 13 * np.cos(t) - 5 * np.cos(2*t) - 2 * np.cos(3*t) - np.cos(4*t)
|
||||
return x, -y
|
||||
t = np.linspace(0, 2*np.pi, 100)
|
||||
heart_x, heart_y = heart_curve(t)
|
||||
heart_x = center[0] + size * heart_x / np.max(np.abs(heart_x))
|
||||
heart_y = center[1] + size * heart_y / np.max(np.abs(heart_y))
|
||||
path = Path(np.column_stack([heart_x, heart_y]))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_crescent(center, size, params):
|
||||
inner_radius = size * params.get('inner_radius_ratio', 0.7)
|
||||
offset = size * params.get('offset_ratio', 0.3)
|
||||
return lambda x, y: ((x - center[0])**2 + (y - center[1])**2 <= size**2) & \
|
||||
((x - (center[0] + offset))**2 + (y - center[1])**2 > inner_radius**2)
|
||||
|
||||
def create_arrow(center, size, params):
|
||||
width = params.get('width', 0.5) * size
|
||||
head_width = params.get('head_width', 1.5) * width
|
||||
head_length = params.get('head_length', 0.3) * size
|
||||
rotation = params.get('rotation', 0)
|
||||
points = np.array([
|
||||
[0, -width/2], [size-head_length, -width/2], [size-head_length, -head_width/2],
|
||||
[size, 0], [size-head_length, head_width/2], [size-head_length, width/2],
|
||||
[0, width/2]
|
||||
])
|
||||
points = points - [size/2, 0] # Center the arrow
|
||||
cos_theta, sin_theta = np.cos(rotation), np.sin(rotation)
|
||||
rotation_matrix = np.array([[cos_theta, -sin_theta], [sin_theta, cos_theta]])
|
||||
rotated_points = np.dot(points, rotation_matrix.T)
|
||||
translated_points = rotated_points + center
|
||||
path = Path(translated_points)
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_plus(center, size, params):
|
||||
thickness = params.get('thickness', 0.2)
|
||||
return lambda x, y: (((abs(x - center[0]) <= size * thickness) & (abs(y - center[1]) <= size)) |
|
||||
((abs(x - center[0]) <= size) & (abs(y - center[1]) <= size * thickness)))
|
||||
|
||||
def create_donut(center, size, params):
|
||||
inner_radius = size * params.get('inner_radius_ratio', 0.5)
|
||||
return lambda x, y: ((x - center[0])**2 + (y - center[1])**2 <= size**2) & \
|
||||
((x - center[0])**2 + (y - center[1])**2 > inner_radius**2)
|
||||
|
||||
def create_spiral(center, size, params):
|
||||
turns = params.get('turns', 3)
|
||||
thickness = params.get('thickness', 0.1)
|
||||
def spiral(t):
|
||||
return size * t * np.array([np.cos(2*np.pi*turns*t), np.sin(2*np.pi*turns*t)]) / turns
|
||||
t = np.linspace(0, 1, 1000)
|
||||
spiral_points = spiral(t) + center
|
||||
path = Path(spiral_points, [Path.MOVETO] + [Path.LINETO] * (len(spiral_points) - 1))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y])) | \
|
||||
(((x - center[0])**2 + (y - center[1])**2 <= (size*thickness)**2))
|
||||
|
||||
def create_gear(center, size, params):
|
||||
num_teeth = params.get('num_teeth', 20)
|
||||
tooth_depth = params.get('tooth_depth', 0.1)
|
||||
rotation = params.get('rotation', 0)
|
||||
angles = np.linspace(0, 2*np.pi, num_teeth*2, endpoint=False) + rotation
|
||||
radii = np.array([size, size * (1 - tooth_depth)] * num_teeth)
|
||||
gear_x = center[0] + radii * np.cos(angles)
|
||||
gear_y = center[1] + radii * np.sin(angles)
|
||||
path = Path(np.column_stack([gear_x, gear_y]))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_flower(center, size, params):
|
||||
num_petals = params.get('num_petals', 5)
|
||||
petal_width = params.get('petal_width', 0.3)
|
||||
rotation = params.get('rotation', 0)
|
||||
t = np.linspace(0, 2*np.pi, 1000)
|
||||
r = size * np.abs(np.sin(num_petals/2 * t)) ** (1/petal_width)
|
||||
flower_x = center[0] + r * np.cos(t + rotation)
|
||||
flower_y = center[1] + r * np.sin(t + rotation)
|
||||
path = Path(np.column_stack([flower_x, flower_y]))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_trapezoid(center, size, params):
|
||||
top_width = params.get('top_width', 0.5)
|
||||
rotation = params.get('rotation', 0)
|
||||
points = np.array([[-size, -size], [size, -size], [size*top_width, size], [-size*top_width, size]])
|
||||
cos_theta, sin_theta = np.cos(rotation), np.sin(rotation)
|
||||
rotation_matrix = np.array([[cos_theta, -sin_theta], [sin_theta, cos_theta]])
|
||||
rotated_points = np.dot(points, rotation_matrix.T)
|
||||
translated_points = rotated_points + center
|
||||
path = Path(translated_points)
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_parallelogram(center, size, params):
|
||||
skew = params.get('skew', 0.5)
|
||||
rotation = params.get('rotation', 0)
|
||||
points = np.array([[-size+size*skew, -size], [size+size*skew, -size], [size, size], [-size, size]])
|
||||
cos_theta, sin_theta = np.cos(rotation), np.sin(rotation)
|
||||
rotation_matrix = np.array([[cos_theta, -sin_theta], [sin_theta, cos_theta]])
|
||||
rotated_points = np.dot(points, rotation_matrix.T)
|
||||
translated_points = rotated_points + center
|
||||
path = Path(translated_points)
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_rhombus(center, size, params):
|
||||
aspect_ratio = params.get('aspect_ratio', 0.5)
|
||||
rotation = params.get('rotation', 0)
|
||||
points = np.array([[0, -size], [size*aspect_ratio, 0], [0, size], [-size*aspect_ratio, 0]])
|
||||
cos_theta, sin_theta = np.cos(rotation), np.sin(rotation)
|
||||
rotation_matrix = np.array([[cos_theta, -sin_theta], [sin_theta, cos_theta]])
|
||||
rotated_points = np.dot(points, rotation_matrix.T)
|
||||
translated_points = rotated_points + center
|
||||
path = Path(translated_points)
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_semicircle(center, size, params):
|
||||
rotation = params.get('rotation', 0)
|
||||
t = np.linspace(0, np.pi, 100)
|
||||
semicircle_x = center[0] + size * np.cos(t + rotation)
|
||||
semicircle_y = center[1] + size * np.sin(t + rotation)
|
||||
points = np.column_stack([semicircle_x, semicircle_y])
|
||||
points = np.vstack([points, [center[0], center[1]]])
|
||||
path = Path(points)
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_quarter_circle(center, size, params):
|
||||
rotation = params.get('rotation', 0)
|
||||
t = np.linspace(0, np.pi/2, 50)
|
||||
quarter_circle_x = center[0] + size * np.cos(t + rotation)
|
||||
quarter_circle_y = center[1] + size * np.sin(t + rotation)
|
||||
points = np.column_stack([quarter_circle_x, quarter_circle_y])
|
||||
points = np.vstack([points, [center[0], center[1]]])
|
||||
path = Path(points)
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_pie_slice(center, size, params):
|
||||
angle = params.get('angle', np.pi/4)
|
||||
rotation = params.get('rotation', 0)
|
||||
t = np.linspace(0, angle, 50)
|
||||
pie_x = center[0] + size * np.cos(t + rotation)
|
||||
pie_y = center[1] + size * np.sin(t + rotation)
|
||||
points = np.column_stack([pie_x, pie_y])
|
||||
points = np.vstack([[center[0], center[1]], points])
|
||||
path = Path(points)
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_teardrop(center, size, params):
|
||||
rotation = params.get('rotation', 0)
|
||||
t = np.linspace(0, 2*np.pi, 100)
|
||||
r = size * (1 - np.cos(t)) / 2
|
||||
teardrop_x = center[0] + r * np.cos(t + rotation)
|
||||
teardrop_y = center[1] + r * np.sin(t + rotation)
|
||||
path = Path(np.column_stack([teardrop_x, teardrop_y]))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_starburst(center, size, params):
|
||||
num_points = params.get('num_points', 8)
|
||||
inner_radius = size * params.get('inner_radius_ratio', 0.5)
|
||||
rotation = params.get('rotation', 0)
|
||||
angles = np.linspace(0, 2*np.pi, num_points*2, endpoint=False) + rotation
|
||||
radii = np.array([size, inner_radius] * num_points)
|
||||
starburst_x = center[0] + radii * np.cos(angles)
|
||||
starburst_y = center[1] + radii * np.sin(angles)
|
||||
path = Path(np.column_stack([starburst_x, starburst_y]))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_cloud(center, size, params):
|
||||
num_bulbs = params.get('num_bulbs', 5)
|
||||
bulb_size = size * 0.5
|
||||
angles = np.linspace(0, 2*np.pi, num_bulbs, endpoint=False)
|
||||
cloud_x = center[0] + size * 0.5 * np.cos(angles)
|
||||
cloud_y = center[1] + size * 0.25 * np.sin(angles)
|
||||
path = None
|
||||
for x, y in zip(cloud_x, cloud_y):
|
||||
circle = Path.circle((x, y), bulb_size)
|
||||
path = circle if path is None else path.union(circle)
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
def create_raindrop(center, size, params):
|
||||
rotation = params.get('rotation', 0)
|
||||
t = np.linspace(0, 2*np.pi, 100)
|
||||
r = size * (1 - 0.5*np.sin(t/2))
|
||||
raindrop_x = center[0] + r * np.cos(t + rotation)
|
||||
raindrop_y = center[1] + r * np.sin(t + rotation)
|
||||
path = Path(np.column_stack([raindrop_x, raindrop_y]))
|
||||
return lambda x, y: path.contains_points(np.column_stack([x, y]))
|
||||
|
||||
# Dictionary mapping shape names to their creation functions
|
||||
SHAPE_FUNCTIONS = {
|
||||
"circle": create_circle,
|
||||
"ellipse": create_ellipse,
|
||||
"rectangle": create_rectangle,
|
||||
"triangle": lambda center, size, params: create_polygon(center, size, {"sides": 3, **params}),
|
||||
"square": lambda center, size, params: create_rectangle(center, size, {"aspect_ratio": 1, **params}),
|
||||
"pentagon": lambda center, size, params: create_polygon(center, size, {"sides": 5, **params}),
|
||||
"hexagon": lambda center, size, params: create_polygon(center, size, {"sides": 6, **params}),
|
||||
"octagon": lambda center, size, params: create_polygon(center, size, {"sides": 8, **params}),
|
||||
"star": create_star,
|
||||
"cross": create_cross,
|
||||
"heart": create_heart,
|
||||
"crescent": create_crescent,
|
||||
"arrow": create_arrow,
|
||||
"plus": create_plus,
|
||||
"donut": create_donut,
|
||||
"spiral": create_spiral,
|
||||
"gear": create_gear,
|
||||
"flower": create_flower,
|
||||
"trapezoid": create_trapezoid,
|
||||
"parallelogram": create_parallelogram,
|
||||
"rhombus": create_rhombus,
|
||||
"semicircle": create_semicircle,
|
||||
"quarter_circle": create_quarter_circle,
|
||||
"pie_slice": create_pie_slice,
|
||||
"teardrop": create_teardrop,
|
||||
"starburst": create_starburst,
|
||||
"cloud": create_cloud,
|
||||
"raindrop": create_raindrop,
|
||||
}
|
||||
|
||||
def create_shape_mask(shape, center, shape_type, size, params=None):
|
||||
height, width = shape
|
||||
y, x = np.ogrid[:height, :width]
|
||||
params = params or {}
|
||||
|
||||
if shape_type in SHAPE_FUNCTIONS:
|
||||
mask_func = SHAPE_FUNCTIONS[shape_type](center, size, params)
|
||||
# Ensure x and y have the same shape
|
||||
x_flat, y_flat = np.broadcast_arrays(x, y)
|
||||
points = np.column_stack([x_flat.ravel(), y_flat.ravel()])
|
||||
mask = mask_func(points[:, 0], points[:, 1]).reshape(shape)
|
||||
return mask.astype(np.float32)
|
||||
else:
|
||||
return np.zeros(shape, dtype=np.float32)
|
||||
|
||||
# Function to get all available shape types
|
||||
def get_available_shapes():
|
||||
return list(SHAPE_FUNCTIONS.keys()) + ["random"]
|
||||
@@ -0,0 +1,128 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import numpy as np
|
||||
|
||||
#NOTE credit for this class and much of the heavy lifting in the Flex Voronoi nodes goes to Alan Huang
|
||||
# https://github.com/alanhuang67/
|
||||
class VoronoiNoise(nn.Module):
|
||||
def __init__(self, width, height, scale, detail, seed, randomness, X=[0], Y=[0], distance_metric='euclidean', batch_size=1, device='cpu'):
|
||||
super(VoronoiNoise, self).__init__()
|
||||
self.width, self.height = width, height
|
||||
self.scale = torch.tensor(self._adjust_list_length(scale, batch_size), dtype=torch.float, device=device)
|
||||
self.detail = self._adjust_list_length(detail, batch_size)
|
||||
self.seed = self._adjust_list_length(seed, batch_size)
|
||||
self.randomness = torch.tensor(self._adjust_list_length(randomness, batch_size), dtype=torch.float, device=device)
|
||||
self.X = torch.tensor(self._adjust_list_length(X, batch_size), dtype=torch.float, device=device)
|
||||
self.Y = torch.tensor(self._adjust_list_length(Y, batch_size), dtype=torch.float, device=device)
|
||||
self.distance_metric = distance_metric
|
||||
self.batch_size = batch_size
|
||||
self.device = device
|
||||
|
||||
@staticmethod
|
||||
def _adjust_list_length(lst, length):
|
||||
return lst + [lst[-1]] * (length - len(lst)) if len(lst) < length else lst
|
||||
|
||||
def forward(self):
|
||||
noise_batch = []
|
||||
for b in range(self.batch_size):
|
||||
torch.manual_seed(self.seed[b])
|
||||
center_x = self.width // 2
|
||||
center_y = self.height // 2
|
||||
sqrt_detail = int(np.sqrt(self.detail[b]))
|
||||
spacing = max(self.width, self.height) / sqrt_detail
|
||||
offsets_x = torch.arange(-sqrt_detail // 2, sqrt_detail // 2 + 1, device=self.device) * spacing
|
||||
offsets_y = torch.arange(-sqrt_detail // 2, sqrt_detail // 2 + 1, device=self.device) * spacing
|
||||
grid_x, grid_y = torch.meshgrid(offsets_x, offsets_y, indexing='xy')
|
||||
points = torch.stack([grid_x.flatten(), grid_y.flatten()], dim=-1)
|
||||
random_offsets = (torch.rand_like(points) * 2 - 1) * self.randomness[b] * spacing / 2
|
||||
points += random_offsets
|
||||
points[len(points) // 2] = torch.tensor([0, 0], device=self.device)
|
||||
points *= self.scale[b]
|
||||
points += torch.tensor([self.X[b], self.Y[b]], device=self.device)
|
||||
points += torch.tensor([center_x, center_y], device=self.device)
|
||||
x_coords = torch.arange(self.width, device=self.device)
|
||||
y_coords = torch.arange(self.height, device=self.device)
|
||||
grid_x, grid_y = torch.meshgrid(x_coords, y_coords, indexing='xy')
|
||||
grid = torch.stack([grid_x, grid_y], dim=-1).float()
|
||||
|
||||
if self.distance_metric == 'euclidean':
|
||||
distances = torch.sqrt(((grid.unsqueeze(2) - points) ** 2).sum(dim=-1))
|
||||
elif self.distance_metric == 'manhattan':
|
||||
distances = torch.abs(grid.unsqueeze(2) - points).sum(dim=-1)
|
||||
elif self.distance_metric == 'chebyshev':
|
||||
distances = torch.abs(grid.unsqueeze(2) - points).max(dim=-1).values
|
||||
elif self.distance_metric == 'minkowski':
|
||||
p = 3
|
||||
distances = (torch.abs(grid.unsqueeze(2) - points) ** p).sum(dim=-1) ** (1/p)
|
||||
elif self.distance_metric == 'elliptical':
|
||||
# 确定长轴和短轴
|
||||
if self.width > self.height:
|
||||
a = self.width / self.height
|
||||
b = 1
|
||||
else:
|
||||
a = 1
|
||||
b = self.height / self.width
|
||||
# 放大或缩小椭圆的比例,使效果更显著
|
||||
scale_factor_a = 3.5 # 调整长轴比例以增强效果
|
||||
scale_factor_b = 1 # 调整短轴比例以增强效果
|
||||
a *= scale_factor_a
|
||||
b *= scale_factor_b
|
||||
distances = torch.sqrt(((grid.unsqueeze(2) - points) ** 2 / torch.tensor([a, b], device=self.device)).sum(dim=-1))
|
||||
|
||||
elif self.distance_metric == 'kaleidoscope_star':
|
||||
def kaleidoscope_star_shape(theta):
|
||||
return torch.abs(torch.sin(8 * theta)) # 调整8以生成不同数量的星点
|
||||
|
||||
theta = torch.atan2(grid[..., 1].unsqueeze(2) - points[..., 1], grid[..., 0].unsqueeze(2) - points[..., 0])
|
||||
radius = torch.sqrt((grid[..., 0].unsqueeze(2) - points[..., 0]) ** 2 + (grid[..., 1].unsqueeze(2) - points[..., 1]) ** 2)
|
||||
star_radius = kaleidoscope_star_shape(theta)
|
||||
distances = torch.abs(radius - star_radius * radius / star_radius.max())
|
||||
distances = 1 - distances / distances.max() # Adjust contrast
|
||||
elif self.distance_metric == 'kaleidoscope_wave':
|
||||
def kaleidoscope_wave_shape(theta):
|
||||
return 1 + 0.3 * torch.sin(4 * theta + radius / 10) # 结合角度和半径生成波浪
|
||||
|
||||
theta = torch.atan2(grid[..., 1].unsqueeze(2) - points[..., 1], grid[..., 0].unsqueeze(2) - points[..., 0])
|
||||
radius = torch.sqrt((grid[..., 0].unsqueeze(2) - points[..., 0]) ** 2 + (grid[..., 1].unsqueeze(2) - points[..., 1]) ** 2)
|
||||
wave_radius = kaleidoscope_wave_shape(theta)
|
||||
distances = torch.abs(radius - wave_radius * radius / wave_radius.max())
|
||||
distances = 1 - distances / distances.max() # Adjust contrast
|
||||
elif self.distance_metric == 'kaleidoscope_radiation_α':
|
||||
theta = torch.atan2(grid[..., 1].unsqueeze(2) - points[..., 1], grid[..., 0].unsqueeze(2) - points[..., 0])
|
||||
radius = torch.sqrt((grid[..., 0].unsqueeze(2) - points[..., 0])**2 + (grid[..., 1].unsqueeze(2) - points[..., 1])**2)
|
||||
distances = torch.abs(torch.sin(6 * theta) * radius)
|
||||
distances = 1 - distances / distances.max() # 调整对比度
|
||||
elif self.distance_metric == 'kaleidoscope_radiation_β':
|
||||
def kaleidoscope_2_shape(theta):
|
||||
return 1 + 0.5 * torch.sin(5 * theta)
|
||||
theta = torch.atan2(grid[..., 1].unsqueeze(2) - points[..., 1], grid[..., 0].unsqueeze(2) - points[..., 0])
|
||||
radius = torch.sqrt((grid[..., 0].unsqueeze(2) - points[..., 0]) ** 2 + (grid[..., 1].unsqueeze(2) - points[..., 1]) ** 2)
|
||||
kaleidoscope_2_radius = kaleidoscope_2_shape(theta)
|
||||
distances = torch.abs(radius - kaleidoscope_2_radius * radius / kaleidoscope_2_radius.max())
|
||||
distances = 1 - distances / distances.max() # Adjust contrast
|
||||
elif self.distance_metric == 'kaleidoscope_radiation_γ':
|
||||
def kaleidoscope_diamond_shape(theta):
|
||||
# 使用菱形对称结构生成形状
|
||||
return torch.abs(torch.sin(4 * theta)) # 调整4以生成不同数量的菱角
|
||||
|
||||
theta = torch.atan2(grid[..., 1].unsqueeze(2) - points[..., 1], grid[..., 0].unsqueeze(2) - points[..., 0])
|
||||
radius = torch.sqrt((grid[..., 0].unsqueeze(2) - points[..., 0]) ** 2 + (grid[..., 1].unsqueeze(2) - points[..., 1]) ** 2)
|
||||
diamond_radius = kaleidoscope_diamond_shape(theta)
|
||||
distances = torch.abs(radius - diamond_radius * radius / diamond_radius.max())
|
||||
distances = 1 - distances / distances.max() # 调整对比度
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported distance metric: {self.distance_metric}")
|
||||
|
||||
min_distances, _ = distances.min(dim=-1)
|
||||
single_noise = min_distances
|
||||
single_noise_flat = single_noise.view(-1)
|
||||
local_min = single_noise_flat.min()
|
||||
local_max = single_noise_flat.max()
|
||||
normalized_noise = (single_noise - local_min) / (local_max - local_min)
|
||||
final_output = normalized_noise.unsqueeze(0).unsqueeze(-1).repeat(1, 1, 1, 3).cpu()
|
||||
noise_batch.append(final_output)
|
||||
|
||||
return torch.cat(noise_batch, dim=0)
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui_ryanonyheinside"
|
||||
description = "Custom nodes introducing particle simulations, optical flow, audio manipulation & reactivity, and temporal masks"
|
||||
version = "1.3.0"
|
||||
version = "1.4.1"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib","openunmix","mido"]
|
||||
|
||||
|
||||
@@ -235,6 +235,8 @@ See `requirements.txt` for a list of dependencies.
|
||||
|
||||
## Contributing
|
||||
|
||||
*Credit to https://github.com/alanhuang67/ComfyUI-FAI-Node for Voronoi implementation
|
||||
|
||||
Contributions are welcome! Both to the code and EXAMPLE WORKFLOWS!!! If you'd like to contribute:
|
||||
|
||||
1. Fork the repository
|
||||
|
||||
Reference in New Issue
Block a user