From d02bb5b1ba4e2e04f1c400520af1aece5cb1147f Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Sat, 31 Aug 2024 10:01:55 -0400 Subject: [PATCH] Rings and wave propagation, skip features for flex images, etc --- __init__.py | 26 +++-- node_configs.py | 44 ++++++-- nodes/images/flex_images.py | 16 +-- nodes/images/image_base.py | 195 ++--------------------------------- nodes/masks/flex_masks.py | 157 +++++++++++++++++++++++++++- nodes/masks/mask_base.py | 11 +- nodes/masks/utility_nodes.py | 10 +- pyproject.toml | 2 +- readme.md | 6 ++ 9 files changed, 248 insertions(+), 219 deletions(-) diff --git a/__init__.py b/__init__.py index 1a459e9..58180e8 100644 --- a/__init__.py +++ b/__init__.py @@ -70,15 +70,7 @@ from .nodes.flex.midi_feature_extractor import( MIDILoadAndExtract, ) -from .nodes.masks.flex_masks import ( - FlexMaskMorph, - FlexMaskWarp, - FlexMaskTransform, - FlexMaskMath, - FlexMaskOpacity, - FlexMaskVoronoiScheduled, - FlexMaskBinary, -) + @@ -117,6 +109,19 @@ from .nodes.masks.utility_nodes import ( from .nodes.images.image_utility_nodes import ( DyeImage, ) + +from .nodes.masks.flex_masks import ( + FlexMaskMorph, + FlexMaskWarp, + FlexMaskTransform, + FlexMaskMath, + FlexMaskOpacity, + FlexMaskVoronoiScheduled, + FlexMaskBinary, + FlexMaskWavePropagation, + FlexMaskEmanatingRings, +) + from .nodes.images.flex_images import ( FlexImageEdgeDetect, FlexImagePosterize, @@ -192,7 +197,8 @@ NODE_CLASS_MAPPINGS = { "FlexMaskBinary": FlexMaskBinary, "FlexMaskOpacity": FlexMaskOpacity, "FlexMaskVoronoiScheduled": FlexMaskVoronoiScheduled, - + "FlexMaskWavePropagation": FlexMaskWavePropagation, + "FlexMaskEmanatingRings": FlexMaskEmanatingRings, #audio "AudioSeparator": AudioSeparator, diff --git a/node_configs.py b/node_configs.py index 233c93e..8a10b1a 100644 --- a/node_configs.py +++ b/node_configs.py @@ -315,7 +315,6 @@ add_node_config("ParticleColorModulation", { add_node_config("FlexMaskBase", { "BASE_DESCRIPTION": """ -## Common Parameters - `feature`: The feature used to modulate the mask operation (FEATURE type) - `feature_pipe`: The feature pipe containing frame information (FEATURE_PIPE type) - `feature_threshold`: Threshold for feature activation (0.0 to 1.0) @@ -392,8 +391,8 @@ add_node_config("FlexMaskVoronoiScheduled", { - `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". +- `feature_param`: 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 feature 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/ @@ -415,11 +414,42 @@ The binary mask operation is applied with strength determined by the selected fe """ }) +add_node_config("FlexMaskEmanatingRings", { + "TOP_DESCRIPTION": "Creates dynamic, expanding ring patterns that emanate from the edges of the input mask.", + "ADDITIONAL_INFO": """ +- `num_rings`: Number of concentric rings to generate (1 to 50). More rings create a denser, more complex pattern. +- `max_ring_width`: Maximum width of each ring as a fraction of the total distance (0.01 to 0.9). Larger values create wider, more prominent rings that overlap more. +- `wave_speed`: Speed at which the rings expand outward (0.01 to 0.5). Higher values create faster-moving, more dynamic patterns. +- `feature_param`: Determines which aspect of the effect is modulated by the input feature. + - Options: "num_rings" (varies ring count), "ring_width" (adjusts ring thickness), "wave_speed" (changes expansion rate), "all" (modulates all parameters) + +This node creates a mesmerizing effect of rings expanding from the edges of the input mask. The rings start thick at the mask boundary and thin out as they move outward, creating a pulsating, wave-like appearance. The effect can be subtle and smooth or bold and dynamic depending on the parameter settings. + +The feature input can be used to dynamically adjust the effect over time, allowing for rhythmic pulsing (e.g., synced to audio) or gradual evolution of the pattern. When the feature value is below the threshold, the animation continues but no new rings are generated, creating a smooth transition effect. +""" +}) + +add_node_config("FlexMaskWavePropagation", { + "TOP_DESCRIPTION": "Good luck with this one...Simulates wave-like abstract distortions propagating from the edges of the input mask.", + "ADDITIONAL_INFO": """ +- `wave_speed`: Controls the rate of wave propagation (0.1 to 100.0). Higher values create faster-moving, more rapidly evolving patterns. +- `wave_amplitude`: Determines the intensity of the wave effect (0.1 to 2.0). Larger values create more pronounced, exaggerated distortions. +- `wave_decay`: Rate at which waves fade out over time (0.9 to 10.0). Lower values cause waves to dissipate quickly, while higher values allow waves to persist and interact more. +- `wave_frequency`: Frequency of the wave oscillations (0.01 to 10.0). Higher values create more rapid, ripple-like effects, while lower values produce smoother, more gradual undulations. +- `max_wave_field`: Maximum allowed intensity for the wave field (10.0 to 10000.0). This parameter prevents the effect from becoming too extreme over time. + +This node creates a dynamic, fluid-like effect where waves seem to emanate from the edges of the input mask. The waves propagate outward, interacting with each other and creating complex, evolving patterns. The effect can range from subtle, water-like ripples to intense, psychedelic distortions depending on the parameter settings. + +The wave propagation is particularly sensitive to the interplay between `wave_speed`, `wave_amplitude`, and `wave_decay`. High speed with low decay can create a turbulent, chaotic effect, while lower speed with higher decay produces a more serene, flowing appearance. + +The feature input modulates the intensity of new waves being generated, allowing for dynamic control over the effect's strength. This can be used to create pulsating effects synchronized with audio or other time-varying inputs. +""" +}) + add_node_config("BaseFeatureNode", { "BASE_DESCRIPTION": """ Features are used to modulate mask operations in FlexMask nodes. -## Common Parameters - `frame_rate`: Frame rate of the video - `frame_count`: Total number of frames """ @@ -719,15 +749,13 @@ This node is particularly useful for creating dynamic, feature-driven animations NODE_CONFIGS["FlexImageBase"] = { "BASE_DESCRIPTION": """ - -## Common Parameters - `images`: Input image sequence (IMAGE type) - `feature`: Feature used to modulate the effect (FEATURE type) - `feature_pipe`: Feature pipe containing frame information (FEATURE_PIPE type) - `strength`: Overall strength of the effect (0.0 to 1.0) - `feature_threshold`: Minimum feature value to apply the effect (0.0 to 1.0) -- `modulate_param`: Parameter to be modulated by the feature -- `modulation_mode`: How the feature modulates the parameter ("relative" or "absolute") +- `feature_param`: Parameter to be modulated by the feature +- `feature_mode`: How the feature modulates the parameter ("relative" or "absolute"). Relative mode adjusts the parameter based on its current value, while absolute mode directly sets the parameter to the feature value. """ } diff --git a/nodes/images/flex_images.py b/nodes/images/flex_images.py index 866aece..f2dd281 100644 --- a/nodes/images/flex_images.py +++ b/nodes/images/flex_images.py @@ -19,7 +19,7 @@ class FlexImageEdgeDetect(FlexImageBase): @classmethod def get_modifiable_params(cls): - return ["low_threshold", "high_threshold"] + return ["low_threshold", "high_threshold", "None"] def apply_effect_internal(self, image: np.ndarray, low_threshold: float, high_threshold: float, **kwargs) -> np.ndarray: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) @@ -47,7 +47,7 @@ class FlexImagePosterize(FlexImageBase): @classmethod def get_modifiable_params(cls): - return ["max_levels", "dither_strength", "channel_separation", "gamma"] + return ["max_levels", "dither_strength", "channel_separation", "gamma", "None"] def apply_effect_internal(self, image: np.ndarray, max_levels: int, dither_strength: float, channel_separation: float, gamma: float, **kwargs) -> np.ndarray: @@ -105,7 +105,7 @@ class FlexImageKaleidoscope(FlexImageBase): @classmethod def get_modifiable_params(cls): - return ["segments", "zoom", "rotation", "precession", "speed"] + return ["segments", "zoom", "rotation", "precession", "speed", "None"] def apply_effect_internal(self, image: np.ndarray, segments: int, center_x: float, center_y: float, zoom: float, rotation: float, precession: float, speed: float, **kwargs) -> np.ndarray: @@ -204,7 +204,7 @@ class FlexImageGlitch(FlexImageBase): @classmethod def get_modifiable_params(cls): - return ["shift_amount", "scan_lines", "color_shift"] + return ["shift_amount", "scan_lines", "color_shift", "None"] def apply_effect_internal(self, image: np.ndarray, shift_amount: float, scan_lines: int, color_shift: float, **kwargs) -> np.ndarray: h, w = image.shape[:2] @@ -241,7 +241,7 @@ class FlexImageChromaticAberration(FlexImageBase): @classmethod def get_modifiable_params(cls): - return ["shift_amount", "angle"] + return ["shift_amount", "angle", "None"] def apply_effect_internal(self, image: np.ndarray, shift_amount: float, angle: float, **kwargs) -> np.ndarray: h, w = image.shape[:2] @@ -272,7 +272,7 @@ class FlexImagePixelate(FlexImageBase): @classmethod def get_modifiable_params(cls): - return ["pixel_size"] + return ["pixel_size", "None"] def apply_effect_internal(self, image: np.ndarray, pixel_size: int, **kwargs) -> np.ndarray: h, w = image.shape[:2] @@ -309,7 +309,7 @@ class FlexImageBloom(FlexImageBase): @classmethod def get_modifiable_params(cls): - return ["threshold", "blur_amount", "intensity"] + return ["threshold", "blur_amount", "intensity", "None"] def apply_effect_internal(self, image: np.ndarray, threshold: float, blur_amount: float, intensity: float, **kwargs) -> np.ndarray: @@ -342,7 +342,7 @@ class FlexImageTiltShift(FlexImageBase): @classmethod def get_modifiable_params(cls): - return ["blur_amount", "focus_position_x", "focus_position_y", "focus_width", "focus_height"] + return ["blur_amount", "focus_position_x", "focus_position_y", "focus_width", "focus_height", "None"] def apply_effect_internal(self, image: np.ndarray, blur_amount: float, focus_position_x: float, focus_position_y: float, focus_width: float, focus_height: float, diff --git a/nodes/images/image_base.py b/nodes/images/image_base.py index f89eef1..0c08051 100644 --- a/nodes/images/image_base.py +++ b/nodes/images/image_base.py @@ -1,181 +1,3 @@ -# import torch -# import numpy as np -# from abc import ABC, abstractmethod -# from tqdm import tqdm -# from comfy.utils import ProgressBar -# from .image_utils import preserve_original_colors, apply_blur -# from ... import RyanOnTheInside - -# class ImageBase(RyanOnTheInside, ABC): -# @classmethod -# def INPUT_TYPES(cls): -# return { -# "required": { -# "images": ("IMAGE",), -# "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), -# "blend_mode": (["normal", "multiply", "screen", "overlay", "soft_light"],), -# "preserve_colors": ("BOOLEAN", {"default": False}), -# "apply_to_alpha": ("BOOLEAN", {"default": False}), -# } -# } - -# CATEGORY = "RyanOnTheInside/Images" - -# def __init__(self): -# self.pre_processors = [] -# self.post_processors = [] -# self.progress_bar = None -# self.tqdm_bar = None -# self.current_progress = 0 -# self.total_steps = 0 - -# def add_pre_processor(self, func): -# self.pre_processors.append(func) -# return self - -# def add_post_processor(self, func): -# self.post_processors.append(func) -# return self - -# def pre_process(self, image): -# for processor in self.pre_processors: -# image = processor(image) -# return image - -# def post_process(self, image): -# for processor in self.post_processors: -# image = processor(image) -# return image - -# def start_progress(self, total_steps, desc="Processing"): -# self.progress_bar = ProgressBar(total_steps) -# self.tqdm_bar = tqdm(total=total_steps, desc=desc, leave=False) -# self.current_progress = 0 -# self.total_steps = total_steps - -# def update_progress(self, step=1): -# self.current_progress += step -# if self.progress_bar: -# self.progress_bar.update(step) -# if self.tqdm_bar: -# self.tqdm_bar.update(step) - -# def end_progress(self): -# if self.tqdm_bar: -# self.tqdm_bar.close() -# self.progress_bar = None -# self.tqdm_bar = None -# self.current_progress = 0 -# self.total_steps = 0 - -# @abstractmethod -# def process_image(self, image: np.ndarray, strength: float, **kwargs) -> np.ndarray: -# """ -# Process a single image. This method must be implemented by child classes. -# """ -# pass - -# def apply_image_operation(self, processed_images: torch.Tensor, original_images: torch.Tensor, strength: float, blend_mode: str, preserve_colors: bool, apply_to_alpha: bool, **kwargs) -> torch.Tensor: -# processed_images_np = processed_images.cpu().numpy() if isinstance(processed_images, torch.Tensor) else processed_images -# original_images_np = original_images.cpu().numpy() if isinstance(original_images, torch.Tensor) else original_images -# num_frames = processed_images_np.shape[0] - -# self.start_progress(num_frames, desc="Applying image operation") - -# result = [] -# for processed_image, original_image in zip(processed_images_np, original_images_np): -# # Pre-processing -# processed_image = self.pre_process(processed_image) - -# if preserve_colors: -# processed_image = preserve_original_colors(original_image, processed_image) - -# if not apply_to_alpha and original_image.shape[-1] == 4: -# processed_image[..., -1] = original_image[..., -1] - -# # Post-processing -# processed_image = self.post_process(processed_image) - -# # Ensure the final image is clipped between 0 and 1 -# processed_image = np.clip(processed_image, 0, 1) - -# result.append(processed_image) -# self.update_progress() - -# self.end_progress() - -# return torch.from_numpy(np.stack(result)).float() - -# @abstractmethod -# def main_function(self, *args, **kwargs) -> torch.Tensor: -# """ -# Main entry point for the node. This method must be implemented by child classes. -# It should call apply_image_operation with the appropriate arguments. -# """ -# pass - - - -# import torch -# import numpy as np -# from abc import abstractmethod -# from .image_base import ImageBase - -# class FlexImageBase(ImageBase): - -# BLEND_MODES = ["multiply", "add", "subtract", "override"] -# @classmethod -# def INPUT_TYPES(cls): -# return { -# "required": { -# **super().INPUT_TYPES()["required"], -# "feature": ("FEATURE",), -# "feature_pipe": ("FEATURE_PIPE",), -# "feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), -# "feature_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), -# "feature_blend_mode": (cls.BLEND_MODES,), -# } -# } - -# CATEGORY = "RyanOnTheInside/FlexImages" -# RETURN_TYPES = ("IMAGE",) -# FUNCTION = "main_function" - -# @abstractmethod -# def process_image(self, image: np.ndarray, feature_value: float, strength: float, **kwargs) -> np.ndarray: -# pass - -# def apply_image_operation(self, images, feature, feature_pipe, strength, blend_mode, preserve_colors, apply_to_alpha, feature_threshold, feature_strength, feature_blend_mode, **kwargs): -# num_frames = feature_pipe.frame_count -# original_images = images.clone() - -# self.start_progress(num_frames, desc="Applying flex image operation") - -# result = [] -# for i in range(num_frames): -# image = images[i].numpy() -# feature_value = feature.get_value_at_frame(i) - -# if feature_value >= feature_threshold: -# # Apply the effect with the feature value directly controlling the intensity -# processed_image = self.process_image(image, feature_value * feature_strength, strength, **kwargs) -# else: -# processed_image = image - -# result.append(processed_image) -# self.update_progress() - -# self.end_progress() - -# processed_images = torch.from_numpy(np.stack(result)).float() -# return super().apply_image_operation(processed_images, original_images, strength, blend_mode, preserve_colors, apply_to_alpha, **kwargs) - -# @abstractmethod -# def main_function(self, images, feature, feature_pipe, strength, blend_mode, preserve_colors, apply_to_alpha, feature_threshold, feature_strength, feature_blend_mode, **kwargs): -# pass - - - import torch import numpy as np from abc import ABC, abstractmethod @@ -193,8 +15,8 @@ class FlexImageBase(RyanOnTheInside, ABC): "feature_pipe": ("FEATURE_PIPE",), "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), "feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), - "modulate_param": (cls.get_modifiable_params(), {"default": cls.get_modifiable_params()[0]}), - "modulation_mode": (["relative", "absolute"], {"default": "relative"}), + "feature_param": (cls.get_modifiable_params(), {"default": cls.get_modifiable_params()[0]}), + "feature_mode": (["relative", "absolute"], {"default": "relative"}), } } @@ -227,7 +49,7 @@ class FlexImageBase(RyanOnTheInside, ABC): else: # absolute return param_value * feature_value * strength - def apply_effect(self, images, feature, feature_pipe, strength, feature_threshold, **kwargs): + def apply_effect(self, images, feature, feature_pipe, strength, feature_threshold, feature_param, feature_mode, **kwargs): num_frames = feature_pipe.frame_count images_np = images.cpu().numpy() @@ -239,7 +61,10 @@ class FlexImageBase(RyanOnTheInside, ABC): feature_value = feature.get_value_at_frame(i) if feature_value >= feature_threshold: - processed_image = self.process_image(image, feature_value, strength, **kwargs) + processed_image = self.process_image(image, feature_value, strength, + feature_param=feature_param, + feature_mode=feature_mode, + **kwargs) else: processed_image = image @@ -261,13 +86,13 @@ class FlexImageBase(RyanOnTheInside, ABC): return (result_tensor,) def process_image(self, image: np.ndarray, feature_value: float, strength: float, - modulate_param: str, modulation_mode: str, **kwargs) -> np.ndarray: + feature_param: str, feature_mode: str, **kwargs) -> np.ndarray: # Modulate the selected parameter for param_name in self.get_modifiable_params(): if param_name in kwargs: - if param_name == modulate_param: + if param_name == feature_param: kwargs[param_name] = self.modulate_param(param_name, kwargs[param_name], - feature_value, strength, modulation_mode) + feature_value, strength, feature_mode) # Call the child class's implementation return self.apply_effect_internal(image, **kwargs) diff --git a/nodes/masks/flex_masks.py b/nodes/masks/flex_masks.py index 6adfbac..f3d3953 100644 --- a/nodes/masks/flex_masks.py +++ b/nodes/masks/flex_masks.py @@ -1,10 +1,12 @@ -from .mask_base import FlexMaskBase +from .mask_base import FlexMaskBase, MaskBase 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 import cv2 +from .mask_base import FlexMaskBase +from scipy.ndimage import distance_transform_edt class FlexMaskMorph(FlexMaskBase): @classmethod @@ -303,6 +305,159 @@ class FlexMaskBinary(FlexMaskBase): +class FlexMaskWavePropagation(FlexMaskBase): + @classmethod + def INPUT_TYPES(cls): + cls.feature_threshold_default = 0.25 + return { + **super().INPUT_TYPES(), + "required": { + **super().INPUT_TYPES()["required"], + "wave_speed": ("FLOAT", {"default": 50.0, "min": 0.1, "max": 100.0, "step": 0.5}), + "wave_amplitude": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 2.0, "step": 0.05}), + "wave_decay": ("FLOAT", {"default": 5.0, "min": 0.9, "max": 10.0, "step": 0.001}), + "wave_frequency": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 10.0, "step": 0.01}), + "max_wave_field": ("FLOAT", {"default": 750.0, "min": 10.0, "max": 10000.0, "step": 10.0}), + } + } + + def __init__(self): + super().__init__() + self.wave_field = None + self.frame_count = 0 + + def process_mask_below_threshold(self, mask, feature_value, strength, **kwargs): + self.wave_field = None + self.frame_count = 0 + return mask + + def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, + wave_speed: float, wave_amplitude: float, wave_decay: float, + wave_frequency: float, max_wave_field: float, **kwargs) -> np.ndarray: + height, width = mask.shape + + if self.wave_field is None: + self.wave_field = np.zeros((height, width), dtype=np.float32) + + # Find mask boundary + kernel = np.ones((3,3), np.uint8) + boundary = cv2.dilate(mask.astype(np.uint8), kernel, iterations=1) - mask.astype(np.uint8) + + # Reset wave field where the mask is not present + self.wave_field[mask == 0] *= wave_decay + + # Emit wave from boundary and propagate + self.wave_field += boundary * feature_value * wave_amplitude + self.wave_field = cv2.GaussianBlur(self.wave_field, (0, 0), sigmaX=wave_speed) + + # Apply decay + self.wave_field *= wave_decay + + # Normalize wave field if it exceeds max_wave_field + max_value = np.max(np.abs(self.wave_field)) + if max_value > max_wave_field: + self.wave_field *= (max_wave_field / max_value) + + time_factor = self.frame_count * wave_frequency + wave_pattern = np.sin(self.wave_field + time_factor) * 0.5 + 0.5 + + # Combine with original mask + result_mask = np.clip(mask + wave_pattern * strength, 0, 1) + + # Print debug information + print(f"Frame: {self.frame_count}") + print(f"Wave field min/max: {self.wave_field.min():.4f} / {self.wave_field.max():.4f}") + print(f"Wave pattern min/max: {wave_pattern.min():.4f} / {wave_pattern.max():.4f}") + print(f"Result mask min/max: {result_mask.min():.4f} / {result_mask.max():.4f}") + print("---") + + self.frame_count += 1 + + return result_mask.astype(np.float32) + + def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, + invert, subtract_original, grow_with_blur, **kwargs): + # Reset wave_field and frame_count for each new feature input + self.wave_field = None + self.frame_count = 0 + return (self.apply_mask_operation(masks, feature, feature_pipe, strength, + feature_threshold, invert, subtract_original, + grow_with_blur, **kwargs),) + + + + +class FlexMaskEmanatingRings(FlexMaskBase): + @classmethod + def INPUT_TYPES(cls): + cls.feature_threshold_default = 0.25 + return { + **super().INPUT_TYPES(), + "required": { + **super().INPUT_TYPES()["required"], + "num_rings": ("INT", {"default": 4, "min": 1, "max": 50, "step": 1}), + "max_ring_width": ("FLOAT", {"default": 0.5, "min": 0.01, "max": 0.9, "step": 0.01}), + "wave_speed": ("FLOAT", {"default": 0.05, "min": 0.01, "max": 0.5, "step": 0.01}), + "feature_param": (["num_rings", "ring_width", "wave_speed", "all"],), + } + } + + def __init__(self): + super().__init__() + self.time = 0 + + def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, + num_rings: int, max_ring_width: float, wave_speed: float, + feature_param: str, **kwargs) -> np.ndarray: + # Adjust parameters based on feature_value and feature_param + if feature_param in ["num_rings", "all"]: + adjusted_num_rings = max(1, int(num_rings * feature_value * strength)) + else: + adjusted_num_rings = num_rings + + if feature_param in ["ring_width", "all"]: + adjusted_max_ring_width = max_ring_width * feature_value * strength + else: + adjusted_max_ring_width = max_ring_width + + if feature_param in ["wave_speed", "all"]: + adjusted_wave_speed = wave_speed * feature_value * strength + else: + adjusted_wave_speed = wave_speed + + distance = distance_transform_edt(1 - mask) + max_distance = np.max(distance) + normalized_distance = distance / max_distance + + # Create emanating rings + rings = np.zeros_like(mask) + for i in range(adjusted_num_rings): + ring_progress = (self.time * adjusted_wave_speed + i / adjusted_num_rings) % 1 + ring_width = adjusted_max_ring_width * (1 - ring_progress) # Rings get thinner as they move out + ring_outer = normalized_distance < ring_progress + ring_inner = normalized_distance < (ring_progress - ring_width) + rings = np.logical_or(rings, np.logical_xor(ring_outer, ring_inner)) + + # Combine with original mask + result = np.logical_or(mask, rings).astype(np.float32) + + self.time += 1 + return result + + def process_mask_below_threshold(self, mask: np.ndarray, feature_value: float, strength: float, **kwargs) -> np.ndarray: + # Continue the animation don't create new rings + return self.process_mask(mask, 0, strength, **kwargs) + + def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, + invert, subtract_original, grow_with_blur, num_rings, + max_ring_width, wave_speed, feature_param, **kwargs): + self.time = 0 # Reset time for each new feature input + return (self.apply_mask_operation(masks, feature, feature_pipe, strength, + feature_threshold, invert, subtract_original, + grow_with_blur, num_rings=num_rings, + max_ring_width=max_ring_width, wave_speed=wave_speed, + feature_param=feature_param, **kwargs),) + #TODO # class FlexMaskVoronoiShape(FlexMaskBase): # @classmethod diff --git a/nodes/masks/mask_base.py b/nodes/masks/mask_base.py index bde8fcb..d403696 100644 --- a/nodes/masks/mask_base.py +++ b/nodes/masks/mask_base.py @@ -917,14 +917,15 @@ class OpticalFlowMaskBase(MaskBase, ABC): #TODO check if input mask is blank and just skip, then check children class FlexMaskBase(MaskBase): + feature_threshold_default=0.0 @classmethod - def INPUT_TYPES(cls): + def INPUT_TYPES(cls): return { "required": { **super().INPUT_TYPES()["required"], "feature": ("FEATURE",), "feature_pipe": ("FEATURE_PIPE",), - "feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "feature_threshold": ("FLOAT", {"default": cls.feature_threshold_default, "min": 0.0, "max": 1.0, "step": 0.01}), } } @@ -950,7 +951,11 @@ class FlexMaskBase(MaskBase): if feature_value >= feature_threshold: processed_mask = self.process_mask(mask, feature_value, strength, **kwargs) else: - processed_mask = mask + if hasattr(self, 'process_mask_below_threshold'): + processed_mask = self.process_mask_below_threshold(mask, feature_value, strength, **kwargs) + else: + processed_mask = mask + result.append(processed_mask) self.update_progress() diff --git a/nodes/masks/utility_nodes.py b/nodes/masks/utility_nodes.py index 08eba38..5b4830b 100644 --- a/nodes/masks/utility_nodes.py +++ b/nodes/masks/utility_nodes.py @@ -247,8 +247,8 @@ class _mfc: "threshold": ("INT", { "default": 0, "min": 0, "max": 127, "step": 1, }), } } - #TODO add image - RETURN_TYPES = ("MASK",) + + RETURN_TYPES = ("MASK", "IMAGE") FUNCTION = "execute" CATEGORY = "/RyanOnTheInside/masks/" @@ -262,4 +262,8 @@ class _mfc: mask = (temp >= lower_bound) & (temp <= upper_bound) mask = mask.all(dim=-1) mask = mask.float() - return (mask, ) \ No newline at end of file + + # Create an image of the mask + mask_image = mask.unsqueeze(-1).repeat(1, 1, 1, 3) + + return (mask, mask_image) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index dbfe81c..211e7f2 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.4.7" +version = "1.4.8" 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 3a11f9b..0a13f1f 100644 --- a/readme.md +++ b/readme.md @@ -28,6 +28,12 @@ RyanOnTheInside node pack introduces dynamic features to ComfyUI: *Examples showcasing various effects using particle emitters, vortices, and other node features* +##### 🆕 Recent Updates + +- 8/30/24 - **FlexMaskEmanatingRings**: Create dynamic, expanding ring patterns emanating from mask edges. +- 8/30/24 - **FlexMaskWavePropagation**: Simulate wave-like distortions propagating from mask boundaries. +- 8/29/24 - **Added feature reactivity to paricle simulations** + ## 🚀 Quick Start Getting started with the RyanOnTheInside node pack is easy: