This commit is contained in:
ryanontheinstide
2024-08-28 09:30:06 -04:00
parent 0abd3fca09
commit 67ecd4aaaa
9 changed files with 741 additions and 12 deletions
+14 -7
View File
@@ -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,
}
+29
View File
@@ -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/
"""
})
+6 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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]
+273
View File
@@ -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"]
+128
View File
@@ -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
View File
@@ -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"]
+2
View File
@@ -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