From 843420a4fb6ce415a4b2c7bd8d446aba440a5f4e Mon Sep 17 00:00:00 2001 From: ryanontheinstide Date: Sat, 7 Sep 2024 17:52:54 -0400 Subject: [PATCH] proximity et al --- __init__.py | 8 +- node_configs.py | 36 ++++ nodes/flex/feature_modulation.py | 42 +++- nodes/flex/proximity_feature.py | 119 ++++++------ nodes/flex/proximity_feature_extractor.py | 225 +++++++++++++--------- pyproject.toml | 2 +- readme.md | 3 +- 7 files changed, 275 insertions(+), 160 deletions(-) diff --git a/__init__.py b/__init__.py index fb4b637..4860673 100644 --- a/__init__.py +++ b/__init__.py @@ -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, diff --git a/node_configs.py b/node_configs.py index f09c01e..3e713ab 100644 --- a/node_configs.py +++ b/node_configs.py @@ -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": """ diff --git a/nodes/flex/feature_modulation.py b/nodes/flex/feature_modulation.py index 02b68de..262f4c9 100644 --- a/nodes/flex/feature_modulation.py +++ b/nodes/flex/feature_modulation.py @@ -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): diff --git a/nodes/flex/proximity_feature.py b/nodes/flex/proximity_feature.py index d24535a..281646c 100644 --- a/nodes/flex/proximity_feature.py +++ b/nodes/flex/proximity_feature.py @@ -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]) \ No newline at end of file + return torch.from_numpy(self.points) \ No newline at end of file diff --git a/nodes/flex/proximity_feature_extractor.py b/nodes/flex/proximity_feature_extractor.py index 9de0ca1..1f92cc7 100644 --- a/nodes/flex/proximity_feature_extractor.py +++ b/nodes/flex/proximity_feature_extractor.py @@ -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,) \ No newline at end of file + return (output_tensor,) + + def parse_color(self, color_string): + return tuple(map(int, color_string.strip("()").split(","))) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e3e8e88..17b37fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/readme.md b/readme.md index df50997..c952172 100644 --- a/readme.md +++ b/readme.md @@ -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