proximity et al
This commit is contained in:
+7
-1
@@ -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,
|
||||
|
||||
@@ -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": """
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user