From 0bcd5c3c7cff850e5acab35532d973cf4d6bc94c Mon Sep 17 00:00:00 2001 From: Fill <55672949+filliptm@users.noreply.github.com> Date: Sat, 11 May 2024 08:54:22 +0900 Subject: [PATCH] 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 =) --- README.md | 39 +++--------------------------- __init__.py | 2 ++ fl_ascii.py | 26 ++++++++++++++++++-- fl_glitch.py | 23 ++++++++++++++++-- fl_hexagonalpattern.py | 54 +++++++++++++++++++++++++++++------------- fl_image_pixelator.py | 29 +++++++++++++++++++---- fl_pixelsort.py | 13 +++++----- fl_ripple.py | 29 ++++++++++++++++------- requirements.txt | 1 - 9 files changed, 139 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index d9283b8..d6a0a5c 100644 --- a/README.md +++ b/README.md @@ -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! ![image](https://github.com/filliptm/ComfyUI_Fill-Nodes/assets/55672949/e1b642e2-29d7-442a-a657-a32ca0fac9c4) @@ -56,44 +56,11 @@ Directory Crawler: - Simple node that loads all images in a directory and any su ![image](https://github.com/filliptm/ComfyUI_Fill-Nodes/assets/55672949/7f6862c7-60dc-4561-8b58-72b489903107) -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 ![image](https://github.com/filliptm/ComfyUI_Fill-Nodes/assets/55672949/db439865-e3c5-4e52-b37c-c3ba601c0840) -Glitch: Video and image effect -- -Slices up your image or video to make a glitching feel - -![image](https://github.com/filliptm/ComfyUI_Fill-Nodes/assets/55672949/b9bc2f82-19e3-4877-bb98-0801c4ceb96f) - -Ripple: Video and image effect -- -Ripples your video or image - -![image](https://github.com/filliptm/ComfyUI_Fill-Nodes/assets/55672949/660983b1-1090-400b-9c92-4e5d3a1eb2b6) - -Pixel Sort: Video and image -- -CAUTION: This node is a very heavy operation. It takes 5-10 seconds per frame. (WIP) - -![image](https://github.com/filliptm/ComfyUI_Fill-Nodes/assets/55672949/7ab1785a-fab7-4206-bf9b-fef48896b518) - -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. - -![image](https://github.com/filliptm/ComfyUI_Fill-Nodes/assets/55672949/06ddaae1-2c1e-41c0-af7f-d713f1bc6d91) - -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. - -Screenshot 2024-04-29 192646 - - - diff --git a/__init__.py b/__init__.py index c95e3b4..aed2dd0 100644 --- a/__init__.py +++ b/__init__.py @@ -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, diff --git a/fl_ascii.py b/fl_ascii.py index 19c1026..7f30402 100644 --- a/fl_ascii.py +++ b/fl_ascii.py @@ -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 diff --git a/fl_glitch.py b/fl_glitch.py index d990945..47514bc 100644 --- a/fl_glitch.py +++ b/fl_glitch.py @@ -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) diff --git a/fl_hexagonalpattern.py b/fl_hexagonalpattern.py index 500c175..1a80515 100644 --- a/fl_hexagonalpattern.py +++ b/fl_hexagonalpattern.py @@ -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,) \ No newline at end of file + return (out,) diff --git a/fl_image_pixelator.py b/fl_image_pixelator.py index 833c2a4..ff42c1c 100644 --- a/fl_image_pixelator.py +++ b/fl_image_pixelator.py @@ -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)) diff --git a/fl_pixelsort.py b/fl_pixelsort.py index 91413e0..70e2523 100644 --- a/fl_pixelsort.py +++ b/fl_pixelsort.py @@ -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): diff --git a/fl_ripple.py b/fl_ripple.py index cb8df81..285b67c 100644 --- a/fl_ripple.py +++ b/fl_ripple.py @@ -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}%") diff --git a/requirements.txt b/requirements.txt index c2bd5a0..73a30a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ librosa sounddevice -wave glitch_this \ No newline at end of file