This commit is contained in:
ryanontheinstide
2024-10-26 14:00:01 -04:00
parent c9d5f054dc
commit eefa76397b
13 changed files with 203 additions and 736 deletions
+1 -1
View File
@@ -253,7 +253,7 @@ from .nodes.audio.flex_audio import (
FlexAudioTimeStretch,
)
from .nodes.latents.latent_base import (
from .nodes.latents.flex_latents import (
FlexLatentInterpolate,
EmbeddingGuidedLatentInterpolate,
FlexLatentBlend,
+1 -2
View File
@@ -496,8 +496,7 @@ class EmptyMaskFromAudio(AudioNodeBase):
# if not folder_paths.exists_annotated_filepath(audio):
# return "Invalid audio file: {}".format(audio)
# return True
#TODO
class EmptyImageAndMaskFromAudio(AudioNodeBase):
@classmethod
def INPUT_TYPES(cls):
+2 -2
View File
@@ -1,13 +1,13 @@
import torch
import numpy as np
from abc import ABC, abstractmethod
from abc import abstractmethod
from tqdm import tqdm
from comfy.utils import ProgressBar
from ... import RyanOnTheInside
from ..flex.flex_base import FlexBase
from .audio_utils import pitch_shift, time_stretch
class FlexAudioBase(FlexBase):
class FlexAudioBase(FlexBase, RyanOnTheInside):
@classmethod
def INPUT_TYPES(cls):
base_input_types = super().INPUT_TYPES()
+81 -44
View File
@@ -1,24 +1,22 @@
import torch
import numpy as np
from abc import ABC, abstractmethod
from tqdm import tqdm
from comfy.utils import ProgressBar
from ... import RyanOnTheInside
from ..flex.flex_base import FlexBase
import cv2
class FlexDepthBase(RyanOnTheInside, ABC):
#NOTE: in hindsight, much of this would have been better suited as mask-based operations
class FlexDepthBase(RyanOnTheInside, FlexBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"depth_maps": ("IMAGE",),
"feature": ("FEATURE",),
"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}),
"feature_param": (cls.get_modifiable_params(), {"default": cls.get_modifiable_params()[0]}),
"feature_mode": (["relative", "absolute"], {"default": "relative"}),
}
},
# Optional inputs are inherited from FlexBase
}
CATEGORY = "RyanOnTheInside/DepthEffects"
@@ -28,16 +26,6 @@ class FlexDepthBase(RyanOnTheInside, ABC):
def __init__(self):
self.progress_bar = None
def start_progress(self, total_steps, desc="Processing"):
self.progress_bar = ProgressBar(total_steps)
def update_progress(self):
if self.progress_bar:
self.progress_bar.update(1)
def end_progress(self):
self.progress_bar = None
@classmethod
@abstractmethod
def get_modifiable_params(cls):
@@ -50,27 +38,64 @@ class FlexDepthBase(RyanOnTheInside, ABC):
else: # absolute
return param_value * feature_value * strength
def apply_effect(self, depth_maps, feature, feature_pipe, strength, feature_threshold, feature_param, feature_mode, **kwargs):
num_frames = feature_pipe.frame_count
def apply_effect(
self,
depth_maps,
strength,
feature_threshold,
feature_param,
feature_mode,
opt_feature=None,
opt_feature_pipe=None,
**kwargs
):
if (opt_feature is None) != (opt_feature_pipe is None):
raise ValueError(
"Both opt_feature and opt_feature_pipe must be provided together, or neither should be provided."
)
num_frames = depth_maps.shape[0]
depth_maps_np = depth_maps.cpu().numpy()
self.start_progress(num_frames, desc=f"Applying {self.__class__.__name__}")
if opt_feature is None and opt_feature_pipe is None:
self.start_progress(num_frames, desc=f"Applying {self.__class__.__name__}")
result = []
for i in range(num_frames):
depth_map = depth_maps_np[i]
feature_value = feature.get_value_at_frame(i)
kwargs['frame_index'] = i
if feature_value >= feature_threshold:
processed_depth_map = self.process_depth_map(depth_map, feature_value, strength,
feature_param=feature_param,
feature_mode=feature_mode,
**kwargs)
else:
processed_depth_map = depth_map
result = []
for i in range(num_frames):
processed_depth_map = self.process_depth_map(
depth_maps_np[i],
0.5, # Default feature value when no feature is provided
strength,
feature_param=feature_param,
feature_mode=feature_mode,
frame_index=i,
**kwargs
)
result.append(processed_depth_map)
self.update_progress()
else:
num_frames = opt_feature_pipe.frame_count
self.start_progress(num_frames, desc=f"Applying {self.__class__.__name__}")
result.append(processed_depth_map)
self.update_progress()
result = []
for i in range(num_frames):
depth_map = depth_maps_np[i]
feature_value = opt_feature.get_value_at_frame(i)
kwargs['frame_index'] = i
if feature_value >= feature_threshold:
processed_depth_map = self.process_depth_map(
depth_map,
feature_value,
strength,
feature_param=feature_param,
feature_mode=feature_mode,
**kwargs
)
else:
processed_depth_map = depth_map
result.append(processed_depth_map)
self.update_progress()
self.end_progress()
@@ -81,20 +106,30 @@ class FlexDepthBase(RyanOnTheInside, ABC):
result_tensor = torch.from_numpy(result_np).float()
# Ensure the tensor is in BHWC format
if result_tensor.shape[1] != depth_maps.shape[1]: # Adjust as needed
if result_tensor.shape[1] != depth_maps.shape[1]:
result_tensor = result_tensor.permute(0, 2, 3, 1)
return (result_tensor,)
def process_depth_map(self, depth_map: np.ndarray, feature_value: float, strength: float,
feature_param: str, feature_mode: str, **kwargs) -> np.ndarray:
def process_depth_map(
self,
depth_map: np.ndarray,
feature_value: float,
strength: float,
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 == feature_param:
kwargs[param_name] = self.modulate_param(param_name, kwargs[param_name],
feature_value, strength, feature_mode)
if param_name in kwargs and param_name == feature_param:
kwargs[param_name] = self.modulate_param(
param_name,
kwargs[param_name],
feature_value,
strength,
feature_mode
)
# Call the child class's implementation
return self.apply_effect_internal(depth_map, **kwargs)
@@ -284,3 +319,5 @@ class DepthRippleEffect(FlexDepthBase):
modified_depth = np.clip(modified_depth, 0.0, 1.0)
return modified_depth
+1 -1
View File
@@ -5,7 +5,7 @@ import folder_paths
from server import PromptServer
from aiohttp import web
import shutil
from .midi_feature import MIDIFeature
from .features_midi import MIDIFeature
from .feature_extractors import FeatureExtractorBase
class MIDILoadAndExtract(FeatureExtractorBase):
-163
View File
@@ -1,163 +0,0 @@
import mido
import os
from .feature_pipe import FeaturePipe
import folder_paths
from server import PromptServer
from aiohttp import web
import shutil
from .midi_feature import MIDIFeature
from .feature_extractors import FeatureExtractorBase
class MIDILoadAndExtract(FeatureExtractorBase):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"midi_file": (folder_paths.get_filename_list("midi_files"),),
"track_selection": (["all"],),
"attribute": (MIDIFeature.get_attribute_names(), {"default": "Note On/Off"}),
"frame_rate": ("FLOAT", {"default": 30, "min": 0.1, "max": 120, "step": 0.1}),
"video_frames": ("IMAGE",),
"chord_only": ("BOOLEAN", {"default": False}),
"notes": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("MIDI", "FEATURE", "FEATURE_PIPE")
FUNCTION = "process_midi"
CATEGORY = "RyanOnTheInside/Audio"
def process_midi(self, midi_file, track_selection, notes, attribute, frame_rate, video_frames, chord_only=False):
try:
midi_path = folder_paths.get_full_path("midi_files", midi_file)
if not midi_path or not os.path.exists(midi_path):
raise FileNotFoundError(f"MIDI file not found: {midi_file}")
midi_data = mido.MidiFile(midi_path)
selected_notes = [int(n.strip()) for n in notes.split(',') if n.strip().isdigit()]
feature_pipe = FeaturePipe(frame_rate, video_frames)
# Convert friendly attribute name to internal attribute name
internal_attribute = MIDIFeature.get_attribute_value(attribute)
feature = MIDIFeature(f"midi_{internal_attribute}", midi_data, internal_attribute,
feature_pipe.frame_rate, feature_pipe.frame_count,
notes=selected_notes, chord_only=chord_only)
feature.extract()
return (midi_data, feature, feature_pipe)
except Exception as e:
# error_msg = f"Error in MIDILoadAndExtract.process_midi: {type(e).__name__}: {str(e)}\n"
# error_msg += traceback.format_exc()
# print(error_msg)
raise RuntimeError(f"Error processing MIDI file: {type(e).__name__}: {str(e)}")
@classmethod
def analyze_midi(cls, midi_path):
midi_data = mido.MidiFile(midi_path)
tracks = ["all"]
all_notes = set()
track_notes = {}
for i, track in enumerate(midi_data.tracks):
track_notes[str(i)] = set()
for msg in track:
if msg.type == 'note_on':
track_notes[str(i)].add(msg.note)
all_notes.add(msg.note)
if len(track_notes[str(i)]) == 0:
tracks.append(f"{i}: (Empty)")
else:
tracks.append(f"{i}: {track.name or f'Track {i}'}")
return {
"tracks": tracks,
"all_notes": ",".join(map(str, sorted(set(all_notes)))),
"track_notes": {k: ",".join(map(str, sorted(v))) for k, v in track_notes.items()}
}
@classmethod
def VALIDATE_INPUTS(cls, midi_file, track_selection, notes, attribute, frame_rate, video_frames):
if not folder_paths.exists_and_is_file(midi_file):
return "MIDI file not found: {}".format(midi_file)
if notes != "all":
try:
note_list = [int(n.strip()) for n in notes.split(',') if n.strip()]
if not all(0 <= n <= 127 for n in note_list):
return "Invalid note value. All notes must be between 0 and 127."
except ValueError:
return "Invalid notes format. Please provide comma-separated integers or 'all'."
return True
routes = PromptServer.instance.routes
@PromptServer.instance.routes.post('/get_track_notes')
async def get_track_notes(request):
data = await request.json()
midi_file = data.get('midi_file')
if not midi_file:
return web.json_response({"error": "Missing required parameters"}, status=400)
midi_path = folder_paths.get_full_path("midi_files", midi_file)
if not midi_path or not os.path.exists(midi_path):
return web.json_response({"error": "MIDI file not found"}, status=404)
analysis = MIDILoadAndExtract.analyze_midi(midi_path)
return web.json_response(analysis)
@routes.post('/upload_midi')
async def upload_midi(request):
data = await request.post()
midi_file = data['file']
if midi_file and midi_file.filename:
safe_filename = os.path.basename(midi_file.filename)
midi_dir = folder_paths.get_folder_paths("midi_files")[0]
if not os.path.exists(midi_dir):
os.makedirs(midi_dir, exist_ok=True)
midi_path = os.path.join(midi_dir, safe_filename)
with open(midi_path, 'wb') as f:
shutil.copyfileobj(midi_file.file, f)
midi_files = folder_paths.get_filename_list("midi_files")
analysis = MIDILoadAndExtract.analyze_midi(midi_path)
return web.json_response({
"status": "success",
"uploaded_file": safe_filename,
"midi_files": midi_files,
"analysis": analysis
})
else:
return web.json_response({"status": "error", "message": "No file uploaded"}, status=400)
@PromptServer.instance.routes.post('/refresh_midi_data')
async def refresh_midi_data(request):
data = await request.json()
midi_file = data.get('midi_file')
track_selection = data.get('track_selection')
if not midi_file:
return web.json_response({"error": "Missing required parameters"}, status=400)
midi_path = folder_paths.get_full_path("midi_files", midi_file)
if not midi_path or not os.path.exists(midi_path):
return web.json_response({"error": "MIDI file not found"}, status=404)
analysis = MIDILoadAndExtract.analyze_midi(midi_path)
# Filter notes based on track selection
if track_selection != "all":
track_index = track_selection.split(':')[0]
analysis['all_notes'] = analysis['track_notes'].get(track_index, "")
return web.json_response(analysis)
-92
View File
@@ -1,92 +0,0 @@
from .features import BaseFeature
import torch
import numpy as np
import numpy as np
from .features import BaseFeature
class ProximityFeature(BaseFeature):
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.frame_diagonal = np.sqrt(frame_dimensions[0]**2 + frame_dimensions[1]**2)
self.proximity_values = None
self.normalization_method = normalization_method
def extract(self):
from scipy.spatial.distance import cdist
proximities = []
for anchor, query in zip(self.anchor_locations, self.query_locations):
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:
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=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.points = np.column_stack((x, y))
def __len__(self):
return len(self.points)
def __getitem__(self, idx):
return self.points[idx]
@property
def x(self):
return self.points[:, 0]
@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):
return cls(tensor[:, 0], tensor[:, 1], tensor[:, 2] if tensor.shape[1] > 2 else None)
def to_tensor(self):
return torch.from_numpy(self.points)
-279
View File
@@ -1,279 +0,0 @@
from .proximity_feature import Location, ProximityFeature
from .feature_extractors import FirstFeature
from ... import RyanOnTheInside
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
def INPUT_TYPES(cls):
return {
"required": {
"video_frames": ("IMAGE",),
"frame_rate": ("FLOAT", {"default": 30.0, "min": 1.0, "max": 120.0, "step": 0.1}),
"anchor_locations": ("LOCATION",),
"query_locations": ("LOCATION",),
"normalization_method": (["frame", "minmax"],),
}
}
RETURN_TYPES = ("FEATURE", "FEATURE_PIPE")
RETURN_NAMES = ("proximity_feature", "feature_pipe")
FUNCTION = "create_feature"
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",
anchor_locations=anchor_locations,
query_locations=query_locations,
frame_rate=frame_rate,
frame_count=len(video_frames),
frame_dimensions=frame_dimensions,
normalization_method=normalization_method
)
# Ensure anchor and query points have the same number of columns
anchor_dim = anchor_locations[0].points.shape[1]
query_dim = query_locations[0].points.shape[1]
if anchor_dim != query_dim:
if anchor_dim == 3:
for location in anchor_locations:
location.points = location.points[:, :2]
elif query_dim == 3:
for location in query_locations:
location.points = location.points[:, :2]
print("Depth ignored. Depth must be included for all points or no points.")
# self.start_progress(len(video_frames), desc="Calculating proximity")
proximity_feature.extract()
# self.end_progress()
return (proximity_feature, feature_pipe)
class ProximityFeatureInput(RyanOnTheInside):
CATEGORY="RyanOnTheInside/Proximity"
class LocationFromMask(ProximityFeatureInput):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"masks": ("MASK",),
"method": (["mask_boundary", "mask_center"],),
},
"optional": {
"depth_maps": ("IMAGE",),
}
}
RETURN_TYPES = ("LOCATION",)
FUNCTION = "generate_locations"
def generate_locations(self, masks, method, depth_maps=None):
locations = []
for i in range(masks.shape[0]):
mask = masks[i].cpu().numpy()
if np.sum(mask) == 0: # Check if the mask is empty
print(f"No mask found in frame {i}.")
locations.append(Location(np.array([-1]), np.array([-1]), np.array([-1]) if depth_maps is not None else None))
continue
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:
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]
if boundary.size == 0: # Check if boundary is empty after filtering
print(f"No valid boundary points in frame {i}.")
locations.append(Location(np.array([-1]), np.array([-1]), np.array([-1])))
continue
# Extract depth values for valid boundary points
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)
return (locations,)
class LocationFromPoint(ProximityFeatureInput):
@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}),
"z": ("FLOAT", {"default": 0.0, "max":1.0, "min":0.0,"step":0.01}),
"batch_count": ("INT", {"default": 1, "min": 1}),
}
}
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(ProximityFeatureInput):
@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
def INPUT_TYPES(cls):
return {
"required": {
"video_frames": ("IMAGE",),
"anchor_locations": ("LOCATION",),
"query_locations": ("LOCATION",),
"proximity_feature": ("FEATURE",),
"anchor_color": ("STRING", {"default": "(255,0,0)"}),
"query_color": ("STRING", {"default": "(0,255,0)"}),
"line_color": ("STRING", {"default": "(0,0,255)"}),
"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"
def visualize_proximity(self, video_frames, anchor_locations, query_locations, proximity_feature,
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)
text_color = self.parse_color(text_color)
output_frames = []
height, width = video_frames.shape[1:3] # Extract height and width from video_frames
# Calculate the frame diagonal from the video frames
frame_diagonal = np.sqrt(width**2 + height**2)
scale_factor = frame_diagonal / proximity_feature.frame_diagonal
for frame_index in range(len(video_frames)):
frame = video_frames[frame_index].cpu().numpy()
frame = (frame * 255).astype(np.uint8)
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
anchor = anchor_locations[frame_index]
query = query_locations[frame_index]
# Calculate scaling factors based on the frame diagonal
# Draw all anchor and query points (using only x and y coordinates)
for point in anchor:
scaled_point = (int(point[0] * scale_factor), int(point[1] * scale_factor))
cv2.circle(frame, scaled_point, 2, anchor_color, -1)
for point in query:
scaled_point = (int(point[0] * scale_factor), int(point[1] * scale_factor))
cv2.circle(frame, scaled_point, 2, query_color, -1)
# 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]]
# Draw line between closest points (using only x and y coordinates)
cv2.line(frame,
(int(closest_anchor[0] * scale_factor), int(closest_anchor[1] * scale_factor)),
(int(closest_query[0] * scale_factor), int(closest_query[1] * scale_factor)),
line_color, 2)
# Display coordinates of closest points
anchor_text = f"Anchor: ({int(closest_anchor[0] * scale_factor)}, {int(closest_anchor[1] * scale_factor)}"
query_text = f"Query: ({int(closest_query[0] * scale_factor)}, {int(closest_query[1] * scale_factor)}"
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, 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,)
def parse_color(self, color_string):
return tuple(map(int, color_string.strip("()").split(",")))
+1 -1
View File
@@ -1,7 +1,7 @@
import cv2
import torch
import numpy as np
from .image_base import FlexImageBase
from .flex_image_base import FlexImageBase
from scipy.ndimage import gaussian_filter
import torch.nn.functional as F
from .image_utils import transform_image
+105
View File
@@ -0,0 +1,105 @@
import numpy as np
import torch
from abc import abstractmethod
from ... import RyanOnTheInside
from ..flex.flex_base import FlexBase
class FlexLatentBase(RyanOnTheInside, FlexBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"latents": ("LATENT",),
},
# Optional inputs are inherited from FlexBase
}
CATEGORY = "RyanOnTheInside/FlexLatent"
RETURN_TYPES = ("LATENT",)
FUNCTION = "apply_effect"
def apply_effect(
self,
latents,
strength,
feature_threshold,
feature_param,
feature_mode,
opt_feature=None,
opt_feature_pipe=None,
**kwargs
):
if (opt_feature is None) != (opt_feature_pipe is None):
raise ValueError(
"Both opt_feature and opt_feature_pipe must be provided together, or neither should be provided."
)
latents_np = latents["samples"].cpu().numpy()
num_frames = latents_np.shape[0]
self.start_progress(num_frames, desc=f"Applying {self.__class__.__name__}")
result = []
for i in range(num_frames):
latent = latents_np[i]
feature_value = 1.0 # Default feature value
apply_effect = True # Default to applying the effect
if opt_feature is not None and opt_feature_pipe is not None:
feature_value = opt_feature.get_value_at_frame(i)
apply_effect = feature_value >= feature_threshold
kwargs['frame_index'] = i
kwargs['feature_value'] = feature_value # Add feature_value to kwargs
kwargs['strength'] = strength # Add strength to kwargs
kwargs['feature_param'] = feature_param # Add feature_param to kwargs
kwargs['feature_mode'] = feature_mode # Add feature_mode to kwargs
if apply_effect:
processed_latent = self.process_latent(
latent,
**kwargs
)
else:
processed_latent = latent
result.append(processed_latent)
self.update_progress()
self.end_progress()
result_np = np.stack(result)
result_tensor = torch.from_numpy(result_np).float()
return ({"samples": result_tensor},)
def process_latent(
self,
latent: np.ndarray,
**kwargs
) -> np.ndarray:
feature_value = kwargs['feature_value']
strength = kwargs['strength']
feature_param = kwargs['feature_param']
feature_mode = kwargs['feature_mode']
# Modulate the selected parameter
for param_name in self.get_modifiable_params():
if param_name in kwargs and param_name == feature_param:
kwargs[param_name] = self.modulate_param(
param_name,
kwargs[param_name],
feature_value,
strength,
feature_mode
)
# Call the subclass's implementation
return self.apply_effect_internal(latent, **kwargs)
@abstractmethod
def apply_effect_internal(self, latent: np.ndarray, **kwargs) -> np.ndarray:
"""To be implemented by subclasses."""
pass
@@ -1,94 +1,4 @@
# custom_nodes/ComfyUI_RyanOnTheInside/nodes/latents/latent_base.py
import numpy as np
import torch
from abc import ABC, abstractmethod
from comfy.utils import ProgressBar
from ... import RyanOnTheInside
#NOTE: this is a work in progress, it sucks
class FlexLatentBase(RyanOnTheInside, ABC):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"latents": ("LATENT",),
"feature": ("FEATURE",),
"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}),
"feature_param": (cls.get_modifiable_params(), {"default": cls.get_modifiable_params()[0]}),
"feature_mode": (["relative", "absolute"], {"default": "relative"}),
}
}
CATEGORY = "RyanOnTheInside/FlexLatent"
RETURN_TYPES = ("LATENT",)
FUNCTION = "apply_effect"
def __init__(self):
self.progress_bar = None
def start_progress(self, total_steps, desc="Processing"):
self.progress_bar = ProgressBar(total_steps)
def update_progress(self):
if self.progress_bar:
self.progress_bar.update(1)
def end_progress(self):
self.progress_bar = None
@classmethod
@abstractmethod
def get_modifiable_params(cls):
"""Return a list of parameter names that can be modulated."""
return []
def modulate_param(self, param_name, param_value, feature_value, strength, mode):
if mode == "relative":
return param_value * (1 + (feature_value - 0.5) * strength)
else: # absolute
return param_value * feature_value * strength
def apply_effect(self, latents, feature, feature_pipe, strength, feature_threshold, feature_param, feature_mode, **kwargs):
num_frames = feature_pipe.frame_count
latents_np = latents["samples"].cpu().numpy()
self.start_progress(num_frames, desc=f"Applying {self.__class__.__name__}")
result = []
for i in range(num_frames):
latent = latents_np[i]
feature_value = feature.get_value_at_frame(i)
kwargs['frame_index'] = i
kwargs['feature_value'] = feature_value
kwargs['strength'] = strength
kwargs['feature_param'] = feature_param
kwargs['feature_mode'] = feature_mode
if feature_value >= feature_threshold:
processed_latent = self.process_latent(latent, **kwargs)
else:
processed_latent = latent
result.append(processed_latent)
self.update_progress()
self.end_progress()
result_np = np.stack(result)
result_tensor = torch.from_numpy(result_np).float()
return ({"samples": result_tensor},)
@abstractmethod
def process_latent(self, latent: np.ndarray, **kwargs) -> np.ndarray:
"""Process the latent using subclass-specific logic."""
# custom_nodes/ComfyUI_RyanOnTheInside/nodes/latents/flex_latent_interpolate.py
from .flex_latent_base import FlexLatentBase
import numpy as np
class FlexLatentInterpolate(FlexLatentBase):
@@ -105,26 +15,14 @@ class FlexLatentInterpolate(FlexLatentBase):
def get_modifiable_params(cls):
return ["None"]
def process_latent(self, latent: np.ndarray, **kwargs) -> np.ndarray:
def apply_effect_internal(self, latent: np.ndarray, **kwargs) -> np.ndarray:
feature_value = kwargs['feature_value']
strength = kwargs['strength']
feature_param = kwargs['feature_param']
feature_mode = kwargs['feature_mode']
latent_2 = kwargs['latent_2']
interpolation_mode = kwargs['interpolation_mode']
frame_index = kwargs['frame_index']
# Modulate any parameters if needed
if feature_param in self.get_modifiable_params():
modulated_value = self.modulate_param(
feature_param,
kwargs.get(feature_param, 0),
feature_value,
strength,
feature_mode
)
kwargs[feature_param] = modulated_value
latent_2_np = latent_2["samples"].cpu().numpy()[kwargs['frame_index']]
latent_2_np = latent_2["samples"].cpu().numpy()[frame_index]
# Perform interpolation
t = np.clip(feature_value * strength, 0.0, 1.0)
@@ -154,31 +52,6 @@ class FlexLatentInterpolate(FlexLatentBase):
coef2 = np.sin(t * omega) / sin_omega
result = coef1 * latent1 + coef2 * latent2
return result.reshape(latent1.shape)
# custom_nodes/ComfyUI_RyanOnTheInside/nodes/latents/embedding_guided_latent_interpolate.py
def spherical_interpolation(self, latent1, latent2, t):
# Flatten the latents
latent1_flat = latent1.flatten()
latent2_flat = latent2.flatten()
# Compute dot product and norms
dot = np.dot(latent1_flat, latent2_flat)
norm1 = np.linalg.norm(latent1_flat)
norm2 = np.linalg.norm(latent2_flat)
denominator = norm1 * norm2 + 1e-8 # Avoid division by zero
omega = np.arccos(np.clip(dot / denominator, -1.0, 1.0))
if np.isclose(omega, 0):
return latent1
else:
sin_omega = np.sin(omega)
coef1 = np.sin((1 - t) * omega) / sin_omega
coef2 = np.sin(t * omega) / sin_omega
result = coef1 * latent1 + coef2 * latent2
return result.reshape(latent1.shape)
import numpy as np
class EmbeddingGuidedLatentInterpolate(FlexLatentBase):
@classmethod
@@ -196,28 +69,15 @@ class EmbeddingGuidedLatentInterpolate(FlexLatentBase):
def get_modifiable_params(cls):
return ["interpolation_mode", "None"]
def process_latent(self, latent: np.ndarray, **kwargs) -> np.ndarray:
def apply_effect_internal(self, latent: np.ndarray, **kwargs) -> np.ndarray:
feature_value = kwargs['feature_value']
strength = kwargs['strength']
feature_param = kwargs['feature_param']
feature_mode = kwargs['feature_mode']
latent_2 = kwargs['latent_2']
embedding_1 = kwargs['embedding_1']
embedding_2 = kwargs['embedding_2']
interpolation_mode = kwargs['interpolation_mode']
frame_index = kwargs['frame_index']
# Modulate any parameters if needed
if feature_param in self.get_modifiable_params():
modulated_value = self.modulate_param(
feature_param,
kwargs.get(feature_param, 0),
feature_value,
strength,
feature_mode
)
kwargs[feature_param] = modulated_value
latent_2_np = latent_2["samples"].cpu().numpy()[frame_index]
embedding_1_np = embedding_1.cpu().numpy()[frame_index]
embedding_2_np = embedding_2.cpu().numpy()[frame_index]
@@ -261,7 +121,6 @@ class EmbeddingGuidedLatentInterpolate(FlexLatentBase):
coef2 = np.sin(t * omega) / sin_omega
result = coef1 * latent1 + coef2 * latent2
return result.reshape(latent1.shape)
class FlexLatentBlend(FlexLatentBase):
@classmethod
@@ -278,7 +137,7 @@ class FlexLatentBlend(FlexLatentBase):
def get_modifiable_params(cls):
return ["blend_strength", "None"]
def process_latent(self, latent: np.ndarray, **kwargs) -> np.ndarray:
def apply_effect_internal(self, latent: np.ndarray, **kwargs) -> np.ndarray:
feature_value = kwargs['feature_value']
strength = kwargs['strength']
feature_param = kwargs['feature_param']
@@ -323,7 +182,7 @@ class FlexLatentBlend(FlexLatentBase):
else:
# Default to Add if mode is unrecognized
return latent1 + latent2
class FlexLatentNoise(FlexLatentBase):
@classmethod
def INPUT_TYPES(cls):
@@ -338,7 +197,7 @@ class FlexLatentNoise(FlexLatentBase):
def get_modifiable_params(cls):
return ["noise_level", "None"]
def process_latent(self, latent: np.ndarray, **kwargs) -> np.ndarray:
def apply_effect_internal(self, latent: np.ndarray, **kwargs) -> np.ndarray:
feature_value = kwargs['feature_value']
strength = kwargs['strength']
feature_param = kwargs['feature_param']
@@ -355,7 +214,7 @@ class FlexLatentNoise(FlexLatentBase):
strength,
feature_mode
)
# Ensure noise_level remains within [0, 1]
# Ensure noise_level remains within [0.0, 1.0]
noise_level = np.clip(noise_level, 0.0, 1.0)
# Generate noise
@@ -369,4 +228,5 @@ class FlexLatentNoise(FlexLatentBase):
# Add noise to the latent
result = latent + noise
return result
return result