optimized + Scheduling + % complete in console
added the ability to schedule some values in the FX nodes, as well as optimized some code to make them a little faster. Iv also added a % complete print in the console so you can see how far along the processing is =)
This commit is contained in:
@@ -45,7 +45,7 @@ current implementation requires you to break batches into a list and back into a
|
||||
Audio Tools (WIP): - Load audio, scans for BPM, crops audio to desired bars and duration
|
||||
-
|
||||
|
||||
other nodes that are a work in progress take the sliced audio/bpm/fps and hold an image for the duration.
|
||||
other nodes that are a work in progress takes the sliced audio/bpm/fps and holds an image for the duration.
|
||||
There is also a VHS converter node that allows you to load audio into the VHS video combine for audio insertion on the fly!
|
||||
|
||||

|
||||
@@ -56,44 +56,11 @@ Directory Crawler: - Simple node that loads all images in a directory and any su
|
||||

|
||||
|
||||
|
||||
Raw Code Node: - Simple node that loads Python and allows you to dev inside comfy without having to reload the instance every time
|
||||
Raw Code Node: - Simple node that loads python and allows you to dev inside comfy without having to reload the instance every time
|
||||
-
|
||||
Great for developing ideas and writing custom stuff quickly
|
||||
Great for deving out ideas and write custom stuff quickly
|
||||
|
||||
|
||||

|
||||
|
||||
Glitch: Video and image effect
|
||||
-
|
||||
Slices up your image or video to make a glitching feel
|
||||
|
||||

|
||||
|
||||
Ripple: Video and image effect
|
||||
-
|
||||
Ripples your video or image
|
||||
|
||||

|
||||
|
||||
Pixel Sort: Video and image
|
||||
-
|
||||
CAUTION: This node is a very heavy operation. It takes 5-10 seconds per frame. (WIP)
|
||||
|
||||

|
||||
|
||||
Hexagon: Video an image
|
||||
-
|
||||
This one is really fun. It masks your image and video in slices, but thats not all! Each slice acts as its own video or image when you start rotating and messing with the parameters.
|
||||
|
||||

|
||||
|
||||
Ascii: Video and image
|
||||
-
|
||||
This one allows for a TON of different styles. This node also works with Alt Codes like this: alt+3 = ♥ or alt+219 = █
|
||||
If you play with the spacing of 219 you can actually get a pixel art effect. ALSO, the last character in the list will always be applied to the highest luminance areas of the image. This is useful because you can do silly things like leave the last character as a blank space, allowing for negative space to be applied to light areas.
|
||||
|
||||
<img width="1136" alt="Screenshot 2024-04-29 192646" src="https://github.com/filliptm/ComfyUI_Fill-Nodes/assets/55672949/926287e9-e22a-4e64-9e4f-7fd6e096b558">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ from .fl_ripple import FL_Ripple
|
||||
from .fl_pixelsort import FL_PixelSort
|
||||
from .fl_hexagonalpattern import FL_HexagonalPattern
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"FL_ImageRandomizer": FL_ImageRandomizer,
|
||||
"FL_ImageCaptionSaver": FL_ImageCaptionSaver,
|
||||
|
||||
+24
-2
@@ -5,7 +5,8 @@ import sys
|
||||
|
||||
class FL_Ascii:
|
||||
def __init__(self):
|
||||
pass
|
||||
self.spacing_index = 0
|
||||
self.font_size_index = 0
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -42,7 +43,28 @@ class FL_Ascii:
|
||||
for b in range(batch_size):
|
||||
img_b = image[b] * 255.0
|
||||
img_b = Image.fromarray(img_b.numpy().astype('uint8'), 'RGB')
|
||||
result_b = ascii_art_effect(img_b, spacing, font_size, characters)
|
||||
|
||||
# Check if spacing is a list and get the current value
|
||||
if isinstance(spacing, list):
|
||||
if self.spacing_index >= len(spacing):
|
||||
print("Warning: Spacing list index out of range. Using the last value.")
|
||||
self.spacing_index = len(spacing) - 1
|
||||
current_spacing = spacing[self.spacing_index]
|
||||
self.spacing_index = (self.spacing_index + 1) % len(spacing)
|
||||
else:
|
||||
current_spacing = spacing
|
||||
|
||||
# Check if font_size is a list and get the current value
|
||||
if isinstance(font_size, list):
|
||||
if self.font_size_index >= len(font_size):
|
||||
print("Warning: Font size list index out of range. Using the last value.")
|
||||
self.font_size_index = len(font_size) - 1
|
||||
current_font_size = font_size[self.font_size_index]
|
||||
self.font_size_index = (self.font_size_index + 1) % len(font_size)
|
||||
else:
|
||||
current_font_size = font_size
|
||||
|
||||
result_b = ascii_art_effect(img_b, current_spacing, current_font_size, characters)
|
||||
result_b = torch.tensor(np.array(result_b)) / 255.0
|
||||
result[b] = result_b
|
||||
|
||||
|
||||
+21
-2
@@ -5,6 +5,9 @@ from glitch_this import ImageGlitcher
|
||||
import sys
|
||||
|
||||
class FL_Glitch:
|
||||
def __init__(self):
|
||||
self.seed_index = 0
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -36,14 +39,30 @@ class FL_Glitch:
|
||||
g = ImageGlitcher()
|
||||
out = []
|
||||
total_images = len(images)
|
||||
|
||||
# Convert seed to a list if it's a single value
|
||||
if not isinstance(seed, list):
|
||||
seed = [seed] * total_images
|
||||
|
||||
for i, image in enumerate(images, start=1):
|
||||
p = self.t2p(image)
|
||||
|
||||
g1 = g.glitch_image(p, glitch_amount, color_offset=color_offset, seed=seed)
|
||||
# Get the current seed value
|
||||
current_seed = seed[i - 1]
|
||||
|
||||
# Ensure current_seed is a single integer value
|
||||
if isinstance(current_seed, (int, float)):
|
||||
current_seed = int(current_seed)
|
||||
elif isinstance(current_seed, (list, tuple)):
|
||||
current_seed = current_seed[0]
|
||||
else:
|
||||
current_seed = current_seed.iloc[0]
|
||||
|
||||
g1 = g.glitch_image(p, glitch_amount, color_offset=color_offset, seed=current_seed)
|
||||
|
||||
r1 = g1.rotate(90, expand=True)
|
||||
|
||||
g2 = g.glitch_image(r1, glitch_amount, color_offset=color_offset, seed=seed)
|
||||
g2 = g.glitch_image(r1, glitch_amount, color_offset=color_offset, seed=current_seed)
|
||||
|
||||
f = g2.rotate(-90, expand=True)
|
||||
|
||||
|
||||
+37
-17
@@ -5,6 +5,14 @@ import math
|
||||
import sys
|
||||
|
||||
class FL_HexagonalPattern:
|
||||
def __init__(self):
|
||||
self.hexagon_size_index = 0
|
||||
self.shadow_offset_index = 0
|
||||
self.shadow_color_index = 0
|
||||
self.background_color_index = 0
|
||||
self.rotation_index = 0
|
||||
self.spacing_index = 0
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -37,14 +45,20 @@ class FL_HexagonalPattern:
|
||||
draw.regular_polygon((size // 2, size // 2, size // 2), 6, fill=255)
|
||||
return mask
|
||||
|
||||
def calculate_adjusted_hexagon_size(self, width, height, hexagon_size, spacing):
|
||||
horizontal_count = math.ceil(width / (hexagon_size * spacing))
|
||||
vertical_count = math.ceil(height / (hexagon_size * spacing * math.sqrt(3) / 2))
|
||||
def process_input_value(self, value, index):
|
||||
if isinstance(value, list):
|
||||
if index >= len(value):
|
||||
print(f"Warning: Value list index out of range. Using the last value.")
|
||||
index = len(value) - 1
|
||||
current_value = value[index]
|
||||
index = (index + 1) % len(value)
|
||||
else:
|
||||
current_value = value
|
||||
|
||||
adjusted_width = width / horizontal_count
|
||||
adjusted_height = height / (vertical_count * math.sqrt(3) / 2)
|
||||
if hasattr(current_value, 'values'):
|
||||
current_value = float(current_value.values[0])
|
||||
|
||||
return min(adjusted_width, adjusted_height) / spacing
|
||||
return current_value, index
|
||||
|
||||
def hexagonal_pattern(self, images, hexagon_size=100, shadow_offset=5, shadow_color="black", shadow_opacity=0.5,
|
||||
background_color="white", rotation=0.0, spacing=1.0):
|
||||
@@ -54,24 +68,30 @@ class FL_HexagonalPattern:
|
||||
p = self.t2p(img_tensor)
|
||||
width, height = p.size
|
||||
|
||||
adjusted_hexagon_size = self.calculate_adjusted_hexagon_size(width, height, hexagon_size, spacing)
|
||||
hexagon_mask = self.create_hexagon_mask(int(adjusted_hexagon_size))
|
||||
current_hexagon_size, self.hexagon_size_index = self.process_input_value(hexagon_size, self.hexagon_size_index)
|
||||
current_shadow_offset, self.shadow_offset_index = self.process_input_value(shadow_offset, self.shadow_offset_index)
|
||||
current_shadow_color, self.shadow_color_index = self.process_input_value(shadow_color, self.shadow_color_index)
|
||||
current_background_color, self.background_color_index = self.process_input_value(background_color, self.background_color_index)
|
||||
current_rotation, self.rotation_index = self.process_input_value(rotation, self.rotation_index)
|
||||
current_spacing, self.spacing_index = self.process_input_value(spacing, self.spacing_index)
|
||||
|
||||
output_image = Image.new("RGBA", (width, height), background_color)
|
||||
hexagon_mask = self.create_hexagon_mask(current_hexagon_size)
|
||||
|
||||
for y in range(0, height, int(adjusted_hexagon_size * spacing * math.sqrt(3) / 2)):
|
||||
for x in range(0, width, int(adjusted_hexagon_size * spacing)):
|
||||
if y % (2 * int(adjusted_hexagon_size * spacing * math.sqrt(3) / 2)) == int(adjusted_hexagon_size * spacing * math.sqrt(3) / 2):
|
||||
x += int(adjusted_hexagon_size * spacing) // 2
|
||||
output_image = Image.new("RGBA", (width, height), current_background_color)
|
||||
|
||||
cropped_hexagon = p.crop((x, y, x + int(adjusted_hexagon_size), y + int(adjusted_hexagon_size))).rotate(rotation, expand=True)
|
||||
for y in range(0, height, int(current_hexagon_size * current_spacing * math.sqrt(3) / 2)):
|
||||
for x in range(0, width, int(current_hexagon_size * current_spacing)):
|
||||
if y % (2 * int(current_hexagon_size * current_spacing * math.sqrt(3) / 2)) == int(current_hexagon_size * current_spacing * math.sqrt(3) / 2):
|
||||
x += int(current_hexagon_size * current_spacing) // 2
|
||||
|
||||
cropped_hexagon = p.crop((x, y, x + current_hexagon_size, y + current_hexagon_size)).rotate(current_rotation, expand=True)
|
||||
|
||||
shadow = Image.new("RGBA", cropped_hexagon.size, (0, 0, 0, 0))
|
||||
shadow_mask = hexagon_mask.copy().resize(cropped_hexagon.size)
|
||||
shadow.paste(shadow_color, (shadow_offset, shadow_offset), shadow_mask)
|
||||
shadow.paste(current_shadow_color, (current_shadow_offset, current_shadow_offset), shadow_mask)
|
||||
shadow.putalpha(int(255 * shadow_opacity))
|
||||
|
||||
output_image.paste(shadow, (x + shadow_offset, y + shadow_offset), shadow_mask)
|
||||
output_image.paste(shadow, (x + current_shadow_offset, y + current_shadow_offset), shadow_mask)
|
||||
output_image.paste(cropped_hexagon, (x, y), shadow_mask)
|
||||
|
||||
o = np.array(output_image.convert("RGB")).astype(np.float32) / 255.0
|
||||
@@ -87,4 +107,4 @@ class FL_HexagonalPattern:
|
||||
print()
|
||||
|
||||
out = torch.cat(out, 0)
|
||||
return (out,)
|
||||
return (out,)
|
||||
|
||||
+24
-5
@@ -2,8 +2,12 @@ import torch
|
||||
from PIL import Image
|
||||
from kornia.morphology import gradient
|
||||
import comfy.model_management
|
||||
import sys
|
||||
|
||||
class FL_ImagePixelator:
|
||||
def __init__(self):
|
||||
self.modulation_index = 0
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -11,6 +15,7 @@ class FL_ImagePixelator:
|
||||
"image": ("IMAGE", {}),
|
||||
"scale_factor": ("FLOAT", {"default": 0.0500, "min": 0.0100, "max": 0.2000, "step": 0.0100}),
|
||||
"kernel_size": ("INT", {"default": 3, "max": 10, "step": 1}),
|
||||
"modulation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,35 +23,49 @@ class FL_ImagePixelator:
|
||||
FUNCTION = "pixelate_image"
|
||||
CATEGORY = "🏵️Fill Nodes"
|
||||
|
||||
def pixelate_image(self, image, scale_factor, kernel_size):
|
||||
def pixelate_image(self, image, scale_factor, kernel_size, modulation):
|
||||
if isinstance(image, torch.Tensor):
|
||||
if image.dim() == 4: # Batch dimension is present
|
||||
output_images = []
|
||||
total_frames = image.shape[0]
|
||||
for i, single_image in enumerate(image, start=1):
|
||||
single_image = single_image.unsqueeze(0) # Add batch dimension
|
||||
single_image = self.apply_pixelation_tensor(single_image, scale_factor)
|
||||
modulated_scale_factor = self.apply_modulation(scale_factor, modulation, total_frames)
|
||||
single_image = self.apply_pixelation_tensor(single_image, modulated_scale_factor)
|
||||
single_image = self.process(single_image, kernel_size)
|
||||
output_images.append(single_image)
|
||||
print(f"Processing frame {i}/{total_frames}")
|
||||
progress = i / total_frames * 100
|
||||
sys.stdout.write(f"\rProcessing frames: {progress:.2f}%")
|
||||
sys.stdout.flush()
|
||||
image = torch.cat(output_images, dim=0) # Concatenate processed images along batch dimension
|
||||
elif image.dim() == 3: # No batch dimension, single image
|
||||
image = image.unsqueeze(0) # Add batch dimension
|
||||
image = self.apply_pixelation_tensor(image, scale_factor)
|
||||
modulated_scale_factor = self.apply_modulation(scale_factor, modulation, 1)
|
||||
image = self.apply_pixelation_tensor(image, modulated_scale_factor)
|
||||
image = self.process(image, kernel_size)
|
||||
image = image.squeeze(0) # Remove batch dimension
|
||||
print("Processing single image")
|
||||
else:
|
||||
return (None,)
|
||||
elif isinstance(image, Image.Image):
|
||||
image = self.apply_pixelation_pil(image, scale_factor)
|
||||
modulated_scale_factor = self.apply_modulation(scale_factor, modulation, 1)
|
||||
image = self.apply_pixelation_pil(image, modulated_scale_factor)
|
||||
image = self.process(image, kernel_size)
|
||||
print("Processing single PIL image")
|
||||
else:
|
||||
return (None,)
|
||||
|
||||
# Print a new line after the progress update
|
||||
print()
|
||||
|
||||
return (image,)
|
||||
|
||||
def apply_modulation(self, scale_factor, modulation, total_frames):
|
||||
modulation_factor = 1 + modulation * torch.sin(2 * torch.pi * torch.tensor(self.modulation_index / total_frames))
|
||||
modulated_scale_factor = scale_factor * modulation_factor.item()
|
||||
self.modulation_index += 1
|
||||
return modulated_scale_factor
|
||||
|
||||
def apply_pixelation_pil(self, input_image, scale_factor):
|
||||
width, height = input_image.size
|
||||
new_size = (int(width * scale_factor), int(height * scale_factor))
|
||||
|
||||
+7
-6
@@ -21,7 +21,7 @@ class FL_PixelSort:
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "pixel_sort_hue"
|
||||
FUNCTION = "pixel_sort_saturation"
|
||||
CATEGORY = "🏵️Fill Nodes"
|
||||
|
||||
def t2p(self, t):
|
||||
@@ -30,17 +30,17 @@ class FL_PixelSort:
|
||||
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
return p
|
||||
|
||||
def hue(self, pixel):
|
||||
def saturation(self, pixel):
|
||||
r, g, b = pixel
|
||||
h, _, _ = rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
|
||||
return h
|
||||
_, s, _ = rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
|
||||
return s
|
||||
|
||||
def pixel_sort_hue(self, images, direction="Horizontal", threshold=0.5, smoothing=0.1, rotation=0):
|
||||
def pixel_sort_saturation(self, images, direction="Horizontal", threshold=0.5, smoothing=0.1, rotation=0):
|
||||
out = []
|
||||
total_images = len(images)
|
||||
for i, img in enumerate(images, start=1):
|
||||
p = self.t2p(img)
|
||||
sorted_image = self.sort_pixels(p, self.hue, threshold, smoothing, rotation)
|
||||
sorted_image = self.sort_pixels(p, self.saturation, threshold, smoothing, rotation)
|
||||
o = np.array(sorted_image.convert("RGB")).astype(np.float32) / 255.0
|
||||
o = torch.from_numpy(o).unsqueeze(0)
|
||||
out.append(o)
|
||||
@@ -63,6 +63,7 @@ class FL_PixelSort:
|
||||
edges = np.maximum(edges, 0)
|
||||
edges = np.minimum(edges, 1)
|
||||
edges = np.convolve(edges.flatten(), np.ones(int(smoothing * pixels.shape[1])), 'same').reshape(edges.shape)
|
||||
|
||||
intervals = [np.flatnonzero(row) for row in edges]
|
||||
|
||||
for row, key in enumerate(values):
|
||||
|
||||
+21
-8
@@ -5,6 +5,9 @@ import math
|
||||
import sys
|
||||
|
||||
class FL_Ripple:
|
||||
def __init__(self):
|
||||
self.modulation_index = 0
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -15,6 +18,9 @@ class FL_Ripple:
|
||||
"amplitude": ("FLOAT", {"default": 10.0, "min": 0.1, "max": 50.0, "step": 0.1}),
|
||||
"frequency": ("FLOAT", {"default": 20.0, "min": 1.0, "max": 100.0, "step": 0.1}),
|
||||
"phase": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360.0, "step": 1.0}),
|
||||
"center_x": ("FLOAT", {"default": 50.0, "min": 0.0, "max": 100.0, "step": 0.1}),
|
||||
"center_y": ("FLOAT", {"default": 50.0, "min": 0.0, "max": 100.0, "step": 0.1}),
|
||||
"modulation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,23 +34,28 @@ class FL_Ripple:
|
||||
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
return p
|
||||
|
||||
def ripple(self, images, amplitude=10.0, frequency=20.0, phase=0.0):
|
||||
def ripple(self, images, amplitude=10.0, frequency=20.0, phase=0.0, center_x=50.0, center_y=50.0, modulation=0.0):
|
||||
out = []
|
||||
total_images = len(images)
|
||||
for i, img in enumerate(images, start=1):
|
||||
p = self.t2p(img)
|
||||
width, height = p.size
|
||||
center_x = width // 2
|
||||
center_y = height // 2
|
||||
center_x_pixel = int(center_x / 100 * width)
|
||||
center_y_pixel = int(center_y / 100 * height)
|
||||
|
||||
x, y = np.meshgrid(np.arange(width), np.arange(height))
|
||||
dx = x - center_x
|
||||
dy = y - center_y
|
||||
dx = x - center_x_pixel
|
||||
dy = y - center_y_pixel
|
||||
distance = np.sqrt(dx ** 2 + dy ** 2)
|
||||
|
||||
angle = distance / frequency * 2 * np.pi + np.radians(phase)
|
||||
offset_x = (amplitude * np.sin(angle)).astype(int)
|
||||
offset_y = (amplitude * np.cos(angle)).astype(int)
|
||||
# Apply modulation to amplitude and frequency
|
||||
modulation_factor = 1 + modulation * math.sin(2 * math.pi * self.modulation_index / total_images)
|
||||
modulated_amplitude = amplitude * modulation_factor
|
||||
modulated_frequency = frequency * modulation_factor
|
||||
|
||||
angle = distance / modulated_frequency * 2 * np.pi + np.radians(phase)
|
||||
offset_x = (modulated_amplitude * np.sin(angle)).astype(int)
|
||||
offset_y = (modulated_amplitude * np.cos(angle)).astype(int)
|
||||
|
||||
sample_x = np.clip(x + offset_x, 0, width - 1)
|
||||
sample_y = np.clip(y + offset_y, 0, height - 1)
|
||||
@@ -56,6 +67,8 @@ class FL_Ripple:
|
||||
o = torch.from_numpy(o).unsqueeze(0)
|
||||
out.append(o)
|
||||
|
||||
self.modulation_index += 1
|
||||
|
||||
# Print progress update
|
||||
progress = i / total_images * 100
|
||||
sys.stdout.write(f"\rProcessing images: {progress:.2f}%")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
librosa
|
||||
sounddevice
|
||||
wave
|
||||
glitch_this
|
||||
Reference in New Issue
Block a user