This commit is contained in:
RyanOnTheInside
2024-09-03 16:12:30 -04:00
parent 7d59071edf
commit 37f99600d8
7 changed files with 238 additions and 10 deletions
+6
View File
@@ -131,6 +131,8 @@ from .nodes.masks.flex_masks import (
FlexMaskWavePropagation,
FlexMaskEmanatingRings,
FlexMaskRandomShapes,
FlexMaskDepthChamber,
# FlexMaskDepthChamberRelative, #NOTE work in progress
)
@@ -156,6 +158,7 @@ from .nodes.flex.feature_modulation import (
FeatureOscillator,
FeatureScaler,
FeatureSmoothing,
FeatureFade,
)
@@ -212,6 +215,8 @@ NODE_CLASS_MAPPINGS = {
"FlexMaskWavePropagation": FlexMaskWavePropagation,
"FlexMaskEmanatingRings": FlexMaskEmanatingRings,
"FlexMaskRandomShapes": FlexMaskRandomShapes,
"FlexMaskDepthChamber": FlexMaskDepthChamber,
# "FlexMaskDepthChamberRelative": FlexMaskDepthChamberRelative,
#audio
"AudioSeparator": AudioSeparator,
@@ -237,6 +242,7 @@ NODE_CLASS_MAPPINGS = {
"FeatureOscillator": FeatureOscillator,
"FeatureScaler": FeatureScaler,
"FeatureSmoothing": FeatureSmoothing,
"FeatureFade": FeatureFade,
#images
'FlexImageEdgeDetect': FlexImageEdgeDetect,
+41 -4
View File
@@ -322,6 +322,20 @@ add_node_config("FlexMaskBase", {
"""
})
add_node_config("FlexMaskDepthChamber", {
"TOP_DESCRIPTION": "Applies a depth-based mask modulation using a depth map and specified front and back depth values.",
"ADDITIONAL_INFO": """
- `depth_map`: Input depth map (IMAGE type)
- `z_front`: Front depth value for the mask (0.0 to 1.0). Default is 1.0.
- `z_back`: Back depth value for the mask (0.0 to 1.0). Default is 0.0.
- `feature_param`: Parameter to modulate based on the feature. Options are "none", "z_front", "z_back", "both".
- `feature_mode`: Mode of feature modulation. Options are "squeeze" and "expand".
This node creates a mask based on the depth values in the input depth map. The mask is modulated by the specified front and back depth values, and can be further adjusted using a feature input to dynamically change the depth range.
"""
})
add_node_config("FlexMaskMorph", {
"TOP_DESCRIPTION": "Applies morphological operations to the mask, modulated by a selected feature.",
"ADDITIONAL_INFO": """
@@ -755,6 +769,18 @@ add_node_config("FeatureOscillator", {
"""
})
add_node_config("FeatureFade", {
"TOP_DESCRIPTION": "Fades between two features based on a fader value or a control feature.",
"ADDITIONAL_INFO": """
- `feature1`: First input feature to fade from (FEATURE type)
- `feature2`: Second input feature to fade to (FEATURE type)
- `fader`: Static fader value to control the blend between feature1 and feature2 (0.0 to 1.0). 0.0 is 100 percent feature1, 1.0 is 100 percent feature2.
- `control_feature`: Optional feature to dynamically control the fader value (FEATURE type). If provided, the fader value will be modulated by this feature.
Shoutout @cyncratic
"""
})
add_node_config("FeatureToWeightsStrategy", {
"TOP_DESCRIPTION": "Converts a FEATURE input into a WEIGHTS_STRATEGY for use with IPAdapter nodes.",
"ADDITIONAL_INFO": """
@@ -859,7 +885,7 @@ add_node_config("UtilityNode",{
"BASE_DESCRIPTION":"Various Utils"
})
add_node_config("ImageInterval", {
add_node_config("ImageIntervalSelect", {
"TOP_DESCRIPTION": "Selects images from a sequence at specified intervals.",
"ADDITIONAL_INFO": """
- `image`: Input image sequence (IMAGE type)
@@ -869,7 +895,7 @@ add_node_config("ImageInterval", {
"""
})
add_node_config("ImageChunk", {
add_node_config("ImageChunks", {
"TOP_DESCRIPTION": "Concatenates images into a grid.",
"ADDITIONAL_INFO": """
- `image`: Input image sequence (IMAGE type)
@@ -880,8 +906,8 @@ add_node_config("ImageChunk", {
"""
})
add_node_config("VideoChunk", {
"TOP_DESCRIPTION": "Chunks images into grids for video processing.",
add_node_config("VideoChunks", {
"TOP_DESCRIPTION": "Chunks images into grids.",
"ADDITIONAL_INFO": """
- `image`: Input image sequence (IMAGE type)
- `chunk_size`: Number of images per grid (default: 4, min: 1)
@@ -892,6 +918,17 @@ add_node_config("VideoChunk", {
"""
})
add_node_config("SwapDevice", {
"TOP_DESCRIPTION": "Transfers the image and mask tensors to the specified device (CPU or CUDA).",
"ADDITIONAL_INFO": """
- `device`: The target device to transfer the tensors to. Options are "cpu" or "cuda".
- `image`: (Optional) The image tensor to transfer. If not provided, a zero tensor is created.
- `mask`: (Optional) The mask tensor to transfer. If not provided, a zero tensor is created.
This node checks if the specified device is available and transfers the image and mask tensors to that device. If the device is not available, it raises a ValueError.
"""
})
add_node_config("ImageShuffle", {
"TOP_DESCRIPTION": "Shuffles images in groups of specified size.",
"ADDITIONAL_INFO": """
+42 -1
View File
@@ -105,7 +105,7 @@ class FeatureMixer(FeatureModulationBase):
"release": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}),
"smoothing": ("FLOAT", {"default": 0.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}),
"rise_detection_threshold": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.05}),
"rise_detection_threshold": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
"rise_smoothing_factor": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 2.0, "step": 0.05}),
**super().INPUT_TYPES()["required"], # Include the invert_output option
}
@@ -338,3 +338,44 @@ class FeatureOscillator(FeatureModulationBase):
processed_feature = self.create_processed_feature(feature, blended, "Oscillated", invert_output)
return (processed_feature, self.visualize(processed_feature))
#NOTE separated from FeatureMath for ease of use.
class FeatureFade(FeatureModulationBase):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"feature1": ("FEATURE",),
"feature2": ("FEATURE",),
"fader": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
**super().INPUT_TYPES()["required"],
},
"optional": {
"control_feature": ("FEATURE",),
}
}
RETURN_TYPES = ("FEATURE", "IMAGE")
RETURN_NAMES = ("FEATURE", "FEATURE_VISUALIZATION")
def modulate(self, feature1, feature2, fader, invert_output, control_feature=None):
values1 = [feature1.get_value_at_frame(i) for i in range(feature1.frame_count)]
values2 = [feature2.get_value_at_frame(i) for i in range(feature2.frame_count)]
# Ensure both features have the same length
min_length = min(len(values1), len(values2))
values1 = values1[:min_length]
values2 = values2[:min_length]
if control_feature:
control_values = [control_feature.get_value_at_frame(i) for i in range(control_feature.frame_count)]
control_values = control_values[:min_length]
fader_values = [(v - min(control_values)) / (max(control_values) - min(control_values)) if max(control_values) > min(control_values) else 0.5 for v in control_values]
else:
fader_values = [fader] * min_length
combined = [(1 - f) * v1 + f * v2 for v1, v2, f in zip(values1, values2, fader_values)]
processed_feature = self.create_processed_feature(feature1, combined, "Faded", invert_output)
return (processed_feature, self.visualize(processed_feature))
+137
View File
@@ -591,6 +591,143 @@ class FlexMaskRandomShapes(FlexMaskBase):
easing_function=easing_function,
shape_type=shape_type,
feature_param=feature_param, **kwargs),)
import torch
class FlexMaskDepthChamber(FlexMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"depth_map": ("IMAGE",),
"z_front": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"z_back": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"feature_param": (["none", "z_front", "z_back", "both"],),
"feature_mode": (["squeeze", "expand"],),
}
}
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float,
depth_map: torch.Tensor, z_front: float, z_back: float, feature_param: str,
feature_mode: str, **kwargs) -> np.ndarray:
frame_index = kwargs.get('frame_index', 0)
depth_map_frame = depth_map[frame_index].cpu().numpy()
depth_map_frame = depth_map_frame[:, :, 0]
# Adjust z_front and z_back based on feature_mode and feature_param
if feature_param != "none":
if feature_mode == "squeeze":
if feature_param in ["z_front", "both"]:
z_front = z_front - (z_front - z_back) * strength * feature_value / 2 if z_front > z_back else z_front + (z_back - z_front) * strength * feature_value / 2
if feature_param in ["z_back", "both"]:
z_back = z_back + (z_front - z_back) * strength * feature_value / 2 if z_back < z_front else z_back - (z_back - z_front) * strength * feature_value / 2
elif feature_mode == "expand":
if feature_param in ["z_front", "both"]:
z_front = min(1.0, z_front + (z_front - z_back) * strength * feature_value / 2) if z_front > z_back else max(0.0, z_front - (z_back - z_front) * strength * feature_value / 2)
if feature_param in ["z_back", "both"]:
z_back = max(0.0, z_back - (z_front - z_back) * strength * feature_value / 2) if z_back < z_front else min(1.0, z_back + (z_back - z_front) * strength * feature_value / 2)
# Create the depth mask
if z_back < z_front:
depth_mask = (depth_map_frame >= z_back) & (depth_map_frame <= z_front)
else:
depth_mask = (depth_map_frame >= z_back) | (depth_map_frame <= z_front)
depth_mask_resized = cv2.resize(depth_mask.astype(np.float32), (mask.shape[1], mask.shape[0]))
return depth_mask_resized
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold,
invert, subtract_original, grow_with_blur, depth_map, z_front, z_back,
feature_param, feature_mode, **kwargs):
return (self.apply_mask_operation(masks, feature, feature_pipe, strength,
feature_threshold, invert, subtract_original,
grow_with_blur, depth_map=depth_map, z_front=z_front, z_back=z_back,
feature_param=feature_param, feature_mode=feature_mode,
**kwargs),)
from typing import List
import torch.nn.functional as F
class FlexMaskDepthChamberRelative(FlexMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"depth_map": ("IMAGE",),
"z1": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"z2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"feature_param": (["none", "z1", "z2", "both"],),
"feature_mode": (["squeeze", "expand"],),
}
}
def calculate_roi_size(self, mask: torch.Tensor) -> float:
# Calculate the bounding box of the mask
y_indices, x_indices = torch.where(mask > 0)
if len(y_indices) == 0 or len(x_indices) == 0:
return 0.0
height = y_indices.max().item() - y_indices.min().item()
width = x_indices.max().item() - x_indices.min().item()
return height * width
def calculate_reference_size(self, masks: List[torch.Tensor]) -> float:
# Calculate the mean or median size of the ROI across all frames
sizes = [self.calculate_roi_size(mask) for mask in masks]
return torch.median(torch.tensor(sizes)).item() # or torch.mean(torch.tensor(sizes)).item()
def process_mask(self, mask: torch.Tensor, feature_value: float, strength: float,
depth_map: torch.Tensor, z1: float, z2: float, feature_param: str,
feature_mode: str, reference_size: float, **kwargs) -> torch.Tensor:
frame_index = kwargs.get('frame_index', 0)
depth_map_frame = depth_map[frame_index, :, :, 0]
# Calculate the ROI size for the current frame
roi_size = self.calculate_roi_size(mask)
if feature_param == "z1":
z1 = z1 * (roi_size / reference_size)
elif feature_param == "z2":
z2 = z2 * (roi_size / reference_size)
elif feature_param == "both":
z1 = z1 * (roi_size / reference_size)
z2 = z2 * (roi_size / reference_size)
# Ensure z1 is less than z2
z1, z2 = min(z1, z2), max(z1, z2)
if feature_mode == "squeeze":
depth_mask = (depth_map_frame >= z1) & (depth_map_frame <= z2)
elif feature_mode == "expand":
depth_mask = (depth_map_frame < z1) | (depth_map_frame > z2)
depth_mask_resized = F.interpolate(depth_mask.unsqueeze(0).unsqueeze(0).float(), size=mask.shape[-2:], mode='nearest').squeeze(0).squeeze(0)
output_mask = mask.float() * depth_mask_resized
return output_mask
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold,
invert, subtract_original, grow_with_blur, depth_map, z1, z2,
feature_param, feature_mode, **kwargs):
reference_size = self.calculate_reference_size(masks)
output_masks = []
for frame_index, mask in enumerate(masks):
output_mask = self.process_mask(mask, feature_value=1.0, strength=1.0,
depth_map=depth_map, z1=z1, z2=z2,
feature_param=feature_param, feature_mode=feature_mode,
reference_size=reference_size, frame_index=frame_index)
output_masks.append(output_mask)
# Stack the list of tensors into a single tensor
output_masks_tensor = torch.stack(output_masks)
return (output_masks_tensor,)
#TODO
# class FlexMaskVoronoiShape(FlexMaskBase):
# @classmethod
+2 -2
View File
@@ -3,9 +3,9 @@ import torch
import torchvision
from .. import RyanOnTheInside
from abc import ABC
class UtilityNode(RyanOnTheInside, ABC):
CATEGORY="UTILITY"
CATEGORY="RyanOnTheInside/Utility"
class ImageIntervalSelect(UtilityNode):
+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.1"
version = "1.5.3"
license = {file = "LICENSE"}
dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib","openunmix","mido"]
+9 -2
View File
@@ -1,7 +1,8 @@
# ComfyUI RyanOnTheInside Node Pack
## Overview
RyanOnTheInside node pack introduces dynamic features to ComfyUI:
### RyanOnTheInside node pack introduces dynamic features to ComfyUI:
**Dynamic Reactivity**: Almost all nodes in this pack can be made to react to audio, MIDI, motion, time, color, depth, brightness, and more, allowing for incredibly dynamic and responsive workflows.**
- 🎆 **Particle Systems**: Create mesmerizing, fluid-like effects
- 🎵 **Audio & MIDI Processing**: Separate instruments and create audio-reactive visuals
@@ -9,6 +10,9 @@ RyanOnTheInside node pack introduces dynamic features to ComfyUI:
- 🌊 **Optical Flow**: Generate masks based on motion in videos
- ⏳ **Temporal Effects**: Apply time-based transformations to masks and images
<table style="border-collapse: collapse; border: none;">
<tr>
<td style="border: none; padding: 0 2px 2px 0;"><img src="./assets/particle_systems.gif" width="300" alt="RyanOnTheInside particle demo header"></td>
@@ -29,6 +33,9 @@ RyanOnTheInside node pack introduces dynamic features to ComfyUI:
*Examples showcasing various effects using particle emitters, vortices, and other node features*
##### 🆕 Recent Updates
- 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.
- 9/1/24 - **Utility Nodes**: Added many utility nodes for batch manipulation and more
- 8/31/24 - **FlexMaskRandomShapes**: Create dynamic shapes. Cooler than it sounds!
- 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.
@@ -50,7 +57,7 @@ For a range of example and tutorial workflows showcasing the capabilities of thi
## ❓ Help and Documentation
For detailed information on each and every node, click the ❓ icon present in the top-right corner of every node.
For detailed information on each and every node, click the ❓ icon present in the top-right corner of the node.
![Help and Documentation](./assets/help.png)