proximity et al

This commit is contained in:
ryanontheinstide
2024-09-07 17:52:54 -04:00
parent b6f89349ca
commit 843420a4fb
7 changed files with 275 additions and 160 deletions
+7 -1
View File
@@ -75,6 +75,8 @@ from .nodes.flex.proximity_feature_extractor import(
LocationFromMask,
ProximityFeatureNode,
ProximityVisualizer,
LocationFromPoint,
LocationTransform,
)
@@ -169,6 +171,7 @@ from .nodes.flex.feature_modulation import (
FeatureFade,
PreviewFeature,
FeatureMath,
FeatureRebase,
)
@@ -248,7 +251,9 @@ NODE_CLASS_MAPPINGS = {
"LocationFromMask": LocationFromMask,
"ProximityFeatureNode": ProximityFeatureNode,
"ProximityVisualizer": ProximityVisualizer,
"LocationFromPoint": LocationFromPoint,
"LocationTransform": LocationTransform,
#feature modulation
"FeatureMixer": FeatureMixer,
"FeatureCombine": FeatureCombine,
@@ -258,6 +263,7 @@ NODE_CLASS_MAPPINGS = {
"FeatureFade": FeatureFade,
"FeatureMath": FeatureMath,
"PreviewFeature": PreviewFeature,
"FeatureRebase": FeatureRebase,
#images
'FlexImageEdgeDetect': FlexImageEdgeDetect,
"FlexImagePosterize": FlexImagePosterize,
+36
View File
@@ -617,6 +617,30 @@ This node generates locations from the input masks using the specified method. T
"""
})
add_node_config("LocationFromPoint", {
"TOP_DESCRIPTION": "Generates locations from specified x, y, and z coordinates.",
"ADDITIONAL_INFO": """
- `x`: X-coordinate of the location (FLOAT, default: 0.0, min: 0.0, step: 0.01)
- `y`: Y-coordinate of the location (FLOAT, default: 0.0, min: 0.0, step: 0.01)
- `batch_count`: Number of locations to generate (INT, default: 1, min: 1)
- `z`: Z-coordinate of the location (FLOAT, default: 0.0, min: 0.0, max: 1.0, step: 0.01)
This node generates a batch of locations based on the specified x, y, and z coordinates.
"""
})
add_node_config("LocationTransform", {
"TOP_DESCRIPTION": "Transforms locations based on a feature and specified transformation type.",
"ADDITIONAL_INFO": """
- `locations`: Input locations to be transformed (LOCATION type)
- `feature`: Feature used to modulate the transformation (FEATURE type)
- `transformation_type`: Type of transformation to apply ("translate" or "scale")
- `transformation_value`: Value of the transformation (FLOAT, default: 1.0)
This node transforms the input locations based on the specified transformation type and value, modulated by the input feature.
"""
})
add_node_config("EmitterMovement", {
"TOP_DESCRIPTION": """These parameters work together to create complex, periodic movements for particle emitters.
By adjusting frequencies and amplitudes, you can achieve various patterns like circles,
@@ -749,6 +773,18 @@ Outputs:
"""
})
add_node_config("FeatureRebase", {
"TOP_DESCRIPTION": "Rebases feature values within specified thresholds.",
"ADDITIONAL_INFO": """
- `feature`: Input feature to be rebased (FEATURE type)
- `lower_threshold`: Lower threshold for feature values (FLOAT, default: 0.0, min: 0.0, max: 1.0, step: 0.01)
- `upper_threshold`: Upper threshold for feature values (FLOAT, default: 1.0, min: 0.0, max: 1.0, step: 0.01)
- `invert_output`: Whether to invert the output feature values (BOOLEAN, default: False)
This node rebases the input feature values within the specified thresholds and normalizes them.
"""
})
add_node_config("FeatureMath", {
"TOP_DESCRIPTION": "Performs mathematical operations between a feature's values and a float value.",
"ADDITIONAL_INFO": """
+40 -2
View File
@@ -319,6 +319,7 @@ class FeatureSmoothing(FeatureModulationBase):
def modulate(self, feature, smoothing_type, window_size, alpha, sigma, invert_output):
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
original_min = min(values)
if smoothing_type == "moving_average":
smoothed = np.convolve(values, np.ones(window_size), 'valid') / window_size
@@ -335,7 +336,12 @@ class FeatureSmoothing(FeatureModulationBase):
kernel = kernel / np.sum(kernel)
smoothed = np.convolve(values, kernel, mode='same')
processed_feature = self.create_processed_feature(feature, smoothed, "Smoothed", invert_output)
# Adjust the smoothed values to ensure the minimum value remains unchanged
smoothed_min = min(smoothed)
adjustment = original_min - smoothed_min
adjusted_smoothed = [v + adjustment for v in smoothed]
processed_feature = self.create_processed_feature(feature, adjusted_smoothed, "Smoothed", invert_output)
return (processed_feature, self.visualize(processed_feature))
class FeatureOscillator(FeatureModulationBase):
@@ -414,7 +420,39 @@ class FeatureFade(FeatureModulationBase):
processed_feature = self.create_processed_feature(feature1, combined, "Faded", invert_output)
return (processed_feature, self.visualize(processed_feature))
class FeatureRebase(FeatureModulationBase):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"feature": ("FEATURE",),
"lower_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"upper_threshold": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
**super().INPUT_TYPES()["required"],
}
}
RETURN_TYPES = ("FEATURE", "IMAGE")
RETURN_NAMES = ("FEATURE", "FEATURE_VISUALIZATION")
FUNCTION = "rebase"
def rebase(self, feature, lower_threshold, upper_threshold, invert_output):
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
# Apply thresholds
rebased_values = [v if lower_threshold <= v <= upper_threshold else 0 for v in values]
# Re-normalize the values
min_val, max_val = min(rebased_values), max(rebased_values)
if min_val == max_val:
normalized = [0 for _ in rebased_values] # All values are the same, normalize to 0
else:
normalized = [(v - min_val) / (max_val - min_val) for v in rebased_values]
processed_feature = self.create_processed_feature(feature, normalized, "Rebased", invert_output)
return (processed_feature, self.visualize(processed_feature))
class PreviewFeature(FeatureModulationBase):
@classmethod
def INPUT_TYPES(cls):
+56 -63
View File
@@ -6,69 +6,65 @@ import numpy as np
from .features import BaseFeature
class ProximityFeature(BaseFeature):
def __init__(self, name, anchor_locations, query_locations, frame_rate, frame_count, distance_metric='euclidean'):
def __init__(self, name, anchor_locations, query_locations, frame_rate, frame_count, frame_dimensions, normalization_method='frame'):
super().__init__(name, "proximity", frame_rate, frame_count)
self.anchor_locations = anchor_locations
self.query_locations = query_locations
self.distance_metric = distance_metric
self.closest_distances = None
self.global_min = None
self.global_max = None
self.frame_diagonal = np.sqrt(frame_dimensions[0]**2 + frame_dimensions[1]**2)
self.proximity_values = None
self.normalization_method = normalization_method
def extract(self):
self.closest_distances = []
all_distances = []
from scipy.spatial.distance import cdist
proximities = []
for anchor, query in zip(self.anchor_locations, self.query_locations):
frame_distances = []
for a in anchor:
for q in query:
if self.distance_metric == 'euclidean':
distance = np.sqrt((a.x - q.x)**2 + (a.y - q.y)**2 + (a.z - q.z)**2)
elif self.distance_metric == 'manhattan':
distance = abs(a.x - q.x) + abs(a.y - q.y) + abs(a.z - q.z)
elif self.distance_metric == 'chebyshev':
distance = max(abs(a.x - q.x), abs(a.y - q.y), abs(a.z - q.z))
else:
raise ValueError(f"Unsupported distance metric: {self.distance_metric}")
frame_distances.append(distance)
closest_distance = min(frame_distances)
self.closest_distances.append(closest_distance)
all_distances.extend(frame_distances)
self.closest_distances = np.array(self.closest_distances)
self.global_min = np.min(self.closest_distances)
self.global_max = np.max(self.closest_distances)
return self.normalize()
def normalize(self):
if self.global_max > self.global_min:
# Normalize the closest distances to the range [0, 1]
self.proximity_values = (self.closest_distances - self.global_min) / (self.global_max - self.global_min)
if len(anchor) == 0 or len(query) == 0:
proximities.append(self.frame_diagonal if self.normalization_method == 'frame' else float('inf'))
else:
distances = cdist(anchor.points, query.points)
min_distance = np.min(distances)
proximities.append(min_distance)
proximities = np.array(proximities)
if self.normalization_method == 'frame':
self.proximity_values = 1 - np.clip(proximities / self.frame_diagonal, 0, 1)
elif self.normalization_method == 'minmax':
min_proximity = np.min(proximities)
max_proximity = np.max(proximities)
if max_proximity > min_proximity:
self.proximity_values = 1 - (proximities - min_proximity) / (max_proximity - min_proximity)
else:
self.proximity_values = np.ones_like(proximities)
else:
self.proximity_values = np.zeros_like(self.closest_distances)
raise ValueError(f"Unknown normalization method: {self.normalization_method}")
self.smooth_proximities()
return self
def smooth_proximities(self, window_size=5):
kernel = np.ones(window_size) / window_size
self.proximity_values = np.convolve(self.proximity_values, kernel, mode='same')
def get_value_at_frame(self, frame_index):
return self.proximity_values[frame_index]
class Location:
def __init__(self, x, y, z=0.5):
if isinstance(x, (list, tuple, np.ndarray)):
self.points = []
for i in range(len(x)):
xi = x[i]
yi = y[i] if isinstance(y, (list, tuple, np.ndarray)) else y
zi = z[i] if isinstance(z, (list, tuple, np.ndarray)) else z
self.points.append(Location(xi, yi, zi))
def __init__(self, x, y, z=None):
x = np.asarray(x).reshape(-1)
y = np.asarray(y).reshape(-1)
if z is not None:
z = np.asarray(z).reshape(-1)
# Ensure z has the same length as x and y, with a small tolerance for floating-point imprecision
if abs(len(z) - len(x)) <= 1: # Allow for off-by-one errors
z = z[:len(x)] if len(z) > len(x) else np.pad(z, (0, len(x) - len(z)), 'constant', constant_values=np.nan)
elif len(z) != len(x):
raise ValueError(f"Mismatch in dimensions: x,y have length {len(x)}, but z has length {len(z)}")
self.points = np.column_stack((x, y, z))
else:
self.x = float(x)
self.y = float(y)
self.z = float(z)
self.points = [self]
self.points = np.column_stack((x, y))
def __len__(self):
return len(self.points)
@@ -76,24 +72,21 @@ class Location:
def __getitem__(self, idx):
return self.points[idx]
def __iter__(self):
return iter(self.points)
@property
def x(self):
return self.points[:, 0]
def __repr__(self):
if len(self) == 1:
return f"Location(x={self.x:.2f}, y={self.y:.2f}, z={self.z:.2f})"
else:
return f"Location(points={len(self)})"
@property
def y(self):
return self.points[:, 1]
@property
def z(self):
return self.points[:, 2] if self.points.shape[1] > 2 else None
@classmethod
def from_tensor(cls, tensor):
if tensor.dim() == 1:
return cls(tensor[0].item(), tensor[1].item(), tensor[2].item() if len(tensor) > 2 else 0.5)
elif tensor.dim() == 2:
return cls(tensor[:, 0].tolist(), tensor[:, 1].tolist(),
tensor[:, 2].tolist() if tensor.shape[1] > 2 else [0.5] * tensor.shape[0])
else:
raise ValueError("Tensor must have 1 or 2 dimensions")
return cls(tensor[:, 0], tensor[:, 1], tensor[:, 2] if tensor.shape[1] > 2 else None)
def to_tensor(self):
return torch.tensor([[p.x, p.y, p.z] for p in self.points])
return torch.from_numpy(self.points)
+133 -92
View File
@@ -5,6 +5,7 @@ from .feature_pipe import FeaturePipe
import numpy as np
import cv2
import torch
from scipy.spatial.distance import cdist # Add this import
class ProximityFeatureNode(FirstFeature):
@classmethod
@@ -15,17 +16,17 @@ class ProximityFeatureNode(FirstFeature):
"frame_rate": ("FLOAT", {"default": 30.0, "min": 1.0, "max": 120.0, "step": 0.1}),
"anchor_locations": ("LOCATION",),
"query_locations": ("LOCATION",),
"distance_metric": (["euclidean", "manhattan", "chebyshev"],),
"normalization_method": (["frame", "minmax"],),
}
}
RETURN_TYPES = ("FEATURE", "FEATURE_PIPE")
RETURN_NAMES = ("proximity_feature", "feature_pipe")
FUNCTION = "create_feature"
CATEGORY = "RyanOnTheInside/FlexFeatures"
def create_feature(self, video_frames, frame_rate, anchor_locations, query_locations, distance_metric):
def create_feature(self, video_frames, frame_rate, anchor_locations, query_locations, normalization_method):
feature_pipe = FeaturePipe(frame_rate, video_frames)
frame_dimensions = (video_frames.shape[2], video_frames.shape[1]) # width, height
proximity_feature = ProximityFeature(
name="proximity_feature",
@@ -33,16 +34,12 @@ class ProximityFeatureNode(FirstFeature):
query_locations=query_locations,
frame_rate=frame_rate,
frame_count=len(video_frames),
distance_metric=distance_metric
frame_dimensions=frame_dimensions,
normalization_method=normalization_method
)
self.start_progress(len(video_frames), desc="Calculating proximity")
proximity_feature.extract()
for _ in range(len(video_frames)):
self.update_progress()
self.end_progress()
return (proximity_feature, feature_pipe)
@@ -53,7 +50,7 @@ class LocationFromMask(RyanOnTheInside):
return {
"required": {
"masks": ("MASK",),
"method": (["mask_center", "mask_boundary", "mask_top_left", "mask_bottom_right"],),
"method": (["mask_boundary", "mask_center"],),
},
"optional": {
"depth_maps": ("IMAGE",),
@@ -62,47 +59,106 @@ class LocationFromMask(RyanOnTheInside):
RETURN_TYPES = ("LOCATION",)
FUNCTION = "generate_locations"
CATEGORY = "RyanOnTheInside/Proximity"
def generate_locations(self, masks, method, depth_maps=None):
num_frames = masks.shape[0]
locations = []
# self.start_progress(num_frames, desc="Generating locations")
for i in range(num_frames):
for i in range(masks.shape[0]):
mask = masks[i].cpu().numpy()
y, x = np.where(mask > 0.5)
if len(x) == 0 or len(y) == 0:
location = Location(mask.shape[1] / 2, mask.shape[0] / 2)
else:
if method == "mask_center":
center_x, center_y = np.mean(x), np.mean(y)
location = Location(center_x, center_y)
elif method == "mask_boundary":
contour = np.column_stack((x, y))
epsilon = 0.01 * cv2.arcLength(contour, True)
approx = cv2.approxPolyDP(contour, epsilon, True)
location = Location(approx[:, 0, 0].tolist(), approx[:, 0, 1].tolist())
elif method == "mask_top_left":
location = Location(np.min(x), np.min(y))
elif method == "mask_bottom_right":
location = Location(np.max(x), np.max(y))
if method == "mask_boundary":
contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
boundary = np.vstack(contours).squeeze()
else:
raise ValueError(f"Unsupported method: {method}")
if depth_maps is not None:
for point in location:
point.z = depth_maps[i, int(point.y), int(point.x)].mean().item()
boundary = np.array([[0, 0]]) # Fallback to a single point
elif method == "mask_center":
y, x = np.where(mask > 0.5)
if len(x) > 0 and len(y) > 0:
center_x, center_y = np.mean(x), np.mean(y)
boundary = np.array([[center_x, center_y]])
else:
boundary = np.array([[mask.shape[1] / 2, mask.shape[0] / 2]])
if depth_maps is not None:
depth_map = depth_maps[i].cpu().numpy()
# Ensure boundary points are within the depth map dimensions
valid_points = (boundary[:, 0] < depth_map.shape[1]) & (boundary[:, 1] < depth_map.shape[0])
boundary = boundary[valid_points]
# Extract depth values for valid boundary points
# Use the average of the three channels as the depth value
z = np.mean(depth_map[boundary[:, 1].astype(int), boundary[:, 0].astype(int)], axis=-1)
location = Location(boundary[:, 0], boundary[:, 1], z)
else:
location = Location(boundary[:, 0], boundary[:, 1])
locations.append(location)
# self.update_progress()
# self.end_progress()
return (locations,)
class LocationFromPoint(RyanOnTheInside):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"x": ("FLOAT", {"default": 0.0,"min":0.0,"step":0.01}),
"y": ("FLOAT", {"default": 0.0,"min":0.0,"step":0.01}),
"batch_count": ("INT", {"default": 1, "min": 1}),
"z": ("FLOAT", {"default": 0.0, "max":1.0, "min":0.0,"step":0.01}),
}
}
RETURN_TYPES = ("LOCATION",)
FUNCTION = "generate_locations"
def generate_locations(self, x, y, batch_count, z):
locations = []
for _ in range(batch_count):
location = Location(np.array([x]), np.array([y]), np.array([z]))
locations.append(location)
return (locations,)
class LocationTransform(RyanOnTheInside):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"locations": ("LOCATION",),
"feature": ("FEATURE",),
"transformation_type": (["translate", "scale"],),
"transformation_value": ("FLOAT", {"default": 1.0}),
}
}
RETURN_TYPES = ("LOCATION",)
FUNCTION = "transform_locations"
def transform_locations(self, locations, feature, transformation_type, transformation_value):
transformed_locations = []
for frame_index, location in enumerate(locations):
feature_value = feature.get_value_at_frame(frame_index)
if transformation_type == "translate":
new_x = location.x + feature_value * transformation_value
new_y = location.y + feature_value * transformation_value
if location.z is not None:
new_z = location.z + feature_value * transformation_value
transformed_location = Location(new_x, new_y, new_z)
else:
transformed_location = Location(new_x, new_y)
elif transformation_type == "scale":
new_x = location.x * feature_value * transformation_value
new_y = location.y * feature_value * transformation_value
if location.z is not None:
new_z = location.z * feature_value * transformation_value
transformed_location = Location(new_x, new_y, new_z)
else:
transformed_location = Location(new_x, new_y)
transformed_locations.append(transformed_location)
return (transformed_locations,)
class ProximityVisualizer(RyanOnTheInside):
@classmethod
@@ -116,44 +172,22 @@ class ProximityVisualizer(RyanOnTheInside):
"anchor_color": ("STRING", {"default": "(255,0,0)"}),
"query_color": ("STRING", {"default": "(0,255,0)"}),
"line_color": ("STRING", {"default": "(0,0,255)"}),
"anchor_boundary_color": ("STRING", {"default": "(255,128,0)"}),
"query_boundary_color": ("STRING", {"default": "(0,255,128)"}),
"point_size": ("INT", {"default": 5, "min": 1, "max": 20}),
"line_thickness": ("INT", {"default": 2, "min": 1, "max": 10}),
"boundary_thickness": ("INT", {"default": 2, "min": 1, "max": 10}),
"text_color": ("STRING", {"default": "(255,255,255)"}),
"font_scale": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 2.0, "step": 0.1}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "visualize_proximity"
CATEGORY = "RyanOnTheInside/Visualization"
def parse_color(self, color_string):
return tuple(map(int, color_string.strip("()").split(",")))
def draw_boundary(self, frame, locations, color, thickness):
print(f"Number of points in locations: {len(locations)}") # Debugging statement
if len(locations) < 3:
return # Not enough points to form a boundary
from scipy.spatial import ConvexHull
points = np.array([(loc.x, loc.y) for loc in locations])
hull = ConvexHull(points)
hull_points = points[hull.vertices]
hull_points = np.round(hull_points).astype(int)
cv2.polylines(frame, [hull_points], True, color, thickness)
def visualize_proximity(self, video_frames, anchor_locations, query_locations, proximity_feature,
anchor_color, query_color, line_color, anchor_boundary_color, query_boundary_color,
point_size, line_thickness, boundary_thickness):
anchor_color, query_color, line_color, text_color, font_scale):
anchor_color = self.parse_color(anchor_color)
query_color = self.parse_color(query_color)
line_color = self.parse_color(line_color)
anchor_boundary_color = self.parse_color(anchor_boundary_color)
query_boundary_color = self.parse_color(query_boundary_color)
text_color = self.parse_color(text_color)
output_frames = []
for frame_index in range(len(video_frames)):
frame = video_frames[frame_index].cpu().numpy()
frame = (frame * 255).astype(np.uint8)
@@ -162,42 +196,49 @@ class ProximityVisualizer(RyanOnTheInside):
anchor = anchor_locations[frame_index]
query = query_locations[frame_index]
# Draw boundaries formed by the location points
self.draw_boundary(frame, anchor, anchor_boundary_color, boundary_thickness)
self.draw_boundary(frame, query, query_boundary_color, boundary_thickness)
# Draw anchor points
# Draw all anchor and query points (using only x and y coordinates)
for point in anchor:
cv2.circle(frame, (int(point.x), int(point.y)), point_size, anchor_color, -1)
# Draw query points
cv2.circle(frame, (int(point[0]), int(point[1])), 2, anchor_color, -1)
for point in query:
cv2.circle(frame, (int(point.x), int(point.y)), point_size, query_color, -1)
cv2.circle(frame, (int(point[0]), int(point[1])), 2, query_color, -1)
# Find and draw the closest pair of points
closest_anchor = None
closest_query = None
min_distance = float('inf')
# Find the closest pair of points
if len(anchor) > 0 and len(query) > 0:
distances = cdist(anchor.points[:, :2], query.points[:, :2]) # Use only x and y for distance calculation
min_idx = np.unravel_index(distances.argmin(), distances.shape)
closest_anchor = anchor[min_idx[0]]
closest_query = query[min_idx[1]]
for a in anchor:
for q in query:
distance = np.sqrt((a.x - q.x)**2 + (a.y - q.y)**2)
if distance < min_distance:
min_distance = distance
closest_anchor = a
closest_query = q
# Draw line between closest points (using only x and y coordinates)
cv2.line(frame,
(int(closest_anchor[0]), int(closest_anchor[1])),
(int(closest_query[0]), int(closest_query[1])),
line_color, 2)
if closest_anchor and closest_query:
cv2.line(frame, (int(closest_anchor.x), int(closest_anchor.y)),
(int(closest_query.x), int(closest_query.y)), line_color, line_thickness)
# Display coordinates of closest points
anchor_text = f"Anchor: ({int(closest_anchor[0])}, {int(closest_anchor[1])}"
query_text = f"Query: ({int(closest_query[0])}, {int(closest_query[1])}"
if anchor.z is not None and query.z is not None:
anchor_text += f", {closest_anchor[2]:.2f})"
query_text += f", {closest_query[2]:.2f})"
else:
anchor_text += ") [2D]"
query_text += ") [2D]"
cv2.putText(frame, anchor_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, 1)
cv2.putText(frame, query_text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, 1)
# Add proximity value to the frame
proximity_value = proximity_feature.get_value_at_frame(frame_index)
cv2.putText(frame, f"Proximity: {proximity_value:.2f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
cv2.FONT_HERSHEY_SIMPLEX, 1, text_color, 2)
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
output_frames.append(frame)
output_tensor = torch.from_numpy(np.stack(output_frames)).float() / 255.0
return (output_tensor,)
return (output_tensor,)
def parse_color(self, color_string):
return tuple(map(int, color_string.strip("()").split(",")))
+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.5.7"
version = "1.6.0"
license = {file = "LICENSE"}
dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib","openunmix","mido"]
+2 -1
View File
@@ -33,6 +33,7 @@
*Examples showcasing various effects using particle emitters, vortices, and other node features*
##### 🆕 Recent Updates
- 9/10/24 - **Proximity Feature**: Adds proximity as a driving reactivity feature! Allows for the distince of objects from one another to control other nodes.
- 9/5/24 - **FlexImageParallax**: Add illusory depth.
- 9/3/24 - **FlexMaskDepthChamber**: Mask anything within a given depth range
- 9/3/24 - **FeatureFade**: another feature modifier to allow mixing of features, controled by features. Think mixing depth and motion features according to a kick drum or somthing. Ridiculous.
@@ -67,7 +68,7 @@ Particles are now reactive!
### 🎛️ Flex Features
Dynamic control over various aspects of your workflow:
- Modulate IPAdapters, Masks, Images, and Particles based on extracted features
- Features include: Audio, MIDI, Motion, Depth, Color, Time, and more
- Features include: Audio, MIDI, Motion, Proximity Depth, Color, Time, and more
- Create adaptive, responsive effects that evolve with your input data
### 🎆 Particle Systems