Delete nodes directory

This commit is contained in:
Fill
2024-08-07 02:43:13 -07:00
committed by GitHub
parent 7f335ee7ba
commit a56fa85830
73 changed files with 0 additions and 7929 deletions
-144
View File
@@ -1,144 +0,0 @@
import os
import torch
import numpy as np
from matplotlib import font_manager
from PIL import Image, ImageDraw, ImageFont
from .sup import ROOT_FONTS
from comfy.utils import ProgressBar
def parse_fonts() -> dict:
mgr = font_manager.FontManager()
return {f"{font.name[0].upper()}/{font.name}": font.fname for font in mgr.ttflist}
class FL_Ascii:
# Retrieve the environment variable and convert to lowercase
env_var_value = os.getenv("FL_USE_SYSTEM_FONTS", 'false').strip().lower()
# Scan the fonts folder for available fonts
if env_var_value.strip() in ('true', '1', 't'):
FONTS = parse_fonts()
else:
FONTS = {f"{str(font)}": str(font) for font in ROOT_FONTS.glob("*.[to][tf][f]")}
FONTS = {f"{str(font.stem)}": str(font) for font in ROOT_FONTS.glob("*.[to][tf][f]")}
print(f"LOADED {len(FONTS)} FONTS")
FONT_NAMES = sorted(FONTS.keys())
FONT_NAMES.sort(key=lambda i: i.lower())
DESCRIPTION = """
FL_Ascii is a class that converts an image into ASCII art using specified characters, font, spacing, and font size.
You can select either local or system fonts based on an environment variable. The class provides customization options
such as using a sequence of characters or mapping characters based on pixel intensity. The spacing and font size can
be specified as single values or lists to vary across the image. This tool is useful for creating stylized visual
representations of images with ASCII characters.
"""
def __init__(self):
self.spacing_index = 0
self.font_size_index = 0
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"spacing": ("INT", {
"default": 20,
"min": 1,
"step": 1,
}),
"font_size": ("INT", {
"default": 20,
"min": 1,
"step": 1,
}),
"characters": ("STRING", {
"default": "\._♥♦♣MachineDelusions♣♦♥_./",
"description": "characters to use"
}),
"font": (s.FONT_NAMES, {"default": "combo+"}),
"sequence_toggle": (["off", "on"], {
"default": "off",
"description": "toggle to type characters in sequence"
}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_ascii_art_effect"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_ascii_art_effect(self, image: torch.Tensor, spacing: int, font_size: int, characters, font: str, sequence_toggle: str):
batch_size = image.shape[0]
result = torch.zeros_like(image)
# get the local folder or system font
font = self.FONTS[font]
pbar = ProgressBar(batch_size)
for b in range(batch_size):
img_b = image[b] * 255.0
img_b = Image.fromarray(img_b.numpy().astype('uint8'), 'RGB')
# 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, font, sequence_toggle)
result_b = torch.tensor(np.array(result_b)) / 255.0
result[b] = result_b
pbar.update_absolute(b)
print(f"[FL_Ascii] {b+1} of {batch_size}")
return (result,)
def ascii_art_effect(image: torch.Tensor, spacing: int, font_size: int, characters, font_file, sequence_toggle):
small_image = image.resize((image.size[0] // spacing, image.size[1] // spacing), Image.Resampling.NEAREST)
ascii_image = Image.new('RGB', image.size, (0, 0, 0))
try:
font = ImageFont.truetype(font_file, font_size)
except Exception as e:
print(f"Error loading font '{font_file}' with size {font_size}: {str(e)}")
# Fallback to a default font or font size
font = ImageFont.load_default()
draw_image = ImageDraw.Draw(ascii_image)
char_index = 0
pbar = ProgressBar(small_image.height)
for i in range(small_image.height):
for j in range(small_image.width):
r, g, b = small_image.getpixel((j, i))
if sequence_toggle == "on":
char = characters[char_index % len(characters)]
else:
k = (r + g + b) // 3
char = characters[k * len(characters) // 256]
char_index += 1
draw_image.text(
(j * spacing, i * spacing),
char,
font=font,
fill=(r, g, b)
)
pbar.update_absolute(i)
return ascii_image
-192
View File
@@ -1,192 +0,0 @@
from dataclasses import dataclass
import torch
import torch.nn as nn
from comfy.model_patcher import ModelPatcher
from typing import Union
T = torch.Tensor
def exists(val):
return val is not None
def default(val, d):
if exists(val):
return val
return d
class StyleAlignedArgs:
def __init__(self, share_attn: str) -> None:
self.adain_keys = "k" in share_attn
self.adain_values = "v" in share_attn
self.adain_queries = "q" in share_attn
share_attention: bool = True
adain_queries: bool = True
adain_keys: bool = True
adain_values: bool = True
def calc_mean_std(feat, eps: float = 1e-5) -> "tuple[T, T]":
feat_std = (feat.var(dim=-2, keepdims=True) + eps).sqrt()
feat_mean = feat.mean(dim=-2, keepdims=True)
return feat_mean, feat_std
def expand_first(feat: T, scale=1.0) -> T:
b = feat.shape[0]
feat_style = torch.stack((feat[0], feat[b // 2])).unsqueeze(1)
if scale == 1:
feat_style = feat_style.expand(2, b // 2, *feat.shape[1:])
else:
feat_style = feat_style.repeat(1, b // 2, 1, 1, 1)
feat_style = torch.cat([feat_style[:, :1], scale * feat_style[:, 1:]], dim=1)
return feat_style.reshape(*feat.shape)
def concat_first(feat: T, dim=2, scale=1.0) -> T:
feat_style = expand_first(feat, scale=scale)
return torch.cat((feat, feat_style), dim=dim)
def enhanced_adain(feat: T, style_feat: T) -> T:
if style_feat is None:
return feat
feat_mean, feat_std = calc_mean_std(feat)
style_mean, style_std = calc_mean_std(style_feat)
# Enhanced AdaIN with a learnable scaling factor
scaling_factor = torch.nn.Parameter(torch.ones_like(feat_std))
feat_normalized = (feat - feat_mean) / feat_std
return scaling_factor * (feat_normalized * style_std + style_mean)
class EnhancedSharedAttentionProcessor:
def __init__(self, args: StyleAlignedArgs, scale: float):
self.args = args
self.scale = scale
def __call__(self, q, k, v, extra_options):
style_feat = extra_options.get('style_feat', None)
if self.args.adain_queries and style_feat is not None:
q = enhanced_adain(q, style_feat)
if self.args.adain_keys and style_feat is not None:
k = enhanced_adain(k, style_feat)
if self.args.adain_values and style_feat is not None:
v = enhanced_adain(v, style_feat)
if self.args.share_attention:
k = concat_first(k, -2, scale=self.scale)
v = concat_first(v, -2)
return q, k, v
def get_norm_layers(
layer: nn.Module,
norm_layers_: "dict[str, list[Union[nn.GroupNorm, nn.LayerNorm]]]",
share_layer_norm: bool,
share_group_norm: bool,
):
if isinstance(layer, nn.LayerNorm) and share_layer_norm:
norm_layers_["layer"].append(layer)
if isinstance(layer, nn.GroupNorm) and share_group_norm:
norm_layers_["group"].append(layer)
else:
for child_layer in layer.children():
get_norm_layers(
child_layer, norm_layers_, share_layer_norm, share_group_norm
)
def register_norm_forward(
norm_layer: Union[nn.GroupNorm, nn.LayerNorm],
) -> Union[nn.GroupNorm, nn.LayerNorm]:
if not hasattr(norm_layer, "orig_forward"):
setattr(norm_layer, "orig_forward", norm_layer.forward)
orig_forward = norm_layer.orig_forward
def forward_(hidden_states: T, *args, **kwargs) -> T:
style_feat = kwargs.get('style_feat', None)
n = hidden_states.shape[-2]
hidden_states = concat_first(hidden_states, dim=-2)
hidden_states = enhanced_adain(hidden_states, style_feat)
hidden_states = orig_forward(hidden_states)
return hidden_states[..., :n, :]
norm_layer.forward = forward_
return norm_layer
def register_shared_norm(
model: ModelPatcher,
share_group_norm: bool = True,
share_layer_norm: bool = True,
):
norm_layers = {"group": [], "layer": []}
get_norm_layers(model.model, norm_layers, share_layer_norm, share_group_norm)
print(
f"Patching {len(norm_layers['group'])} group norms, {len(norm_layers['layer'])} layer norms."
)
return [register_norm_forward(layer) for layer in norm_layers["group"]] + [
register_norm_forward(layer) for layer in norm_layers["layer"]
]
SHARE_NORM_OPTIONS = ["both", "group", "layer", "disabled"]
SHARE_ATTN_OPTIONS = ["q+k", "q+k+v", "disabled"]
class FL_BatchAlign:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"share_norm": (SHARE_NORM_OPTIONS,),
"share_attn": (SHARE_ATTN_OPTIONS,),
"scale": ("FLOAT", {"default": 1, "min": -2, "max": 2, "step": 0.1}),
}
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
CATEGORY = "🏵️Fill Nodes/experiments"
def patch(
self,
model: ModelPatcher,
share_norm: str,
share_attn: str,
scale: float,
):
m = model.clone()
share_group_norm = share_norm in ["group", "both"]
share_layer_norm = share_norm in ["layer", "both"]
register_shared_norm(model, share_group_norm, share_layer_norm)
args = StyleAlignedArgs(share_attn)
m.set_model_attn1_patch(EnhancedSharedAttentionProcessor(args, scale))
return (m,)
def consistency_loss(batch_images: T) -> T:
"""Calculate consistency loss to penalize differences within the batch."""
mean_image = batch_images.mean(dim=0, keepdim=True)
loss = ((batch_images - mean_image) ** 2).mean()
return loss
class ConsistencyEnforcedFLBatchAlign(FL_BatchAlign):
def patch(
self,
model: ModelPatcher,
share_norm: str,
share_attn: str,
scale: float,
):
m = super().patch(model, share_norm, share_attn, scale)
# Apply consistency loss (example, in practice this should be integrated into the training loop)
batch_images = get_batch_images() # Assume this function retrieves batch images
loss = consistency_loss(batch_images)
# Here you would typically backpropagate this loss if in a training loop
return (m, loss)
-11
View File
@@ -1,11 +0,0 @@
class FL_BulletHellGame:
@classmethod
def INPUT_TYPES(s):
return {"required": {}}
RETURN_TYPES = ()
FUNCTION = "execute"
CATEGORY = "🏵️Fill Nodes/games"
def execute(self):
return ()
-54
View File
@@ -1,54 +0,0 @@
import os
import csv
import io
class FL_CaptionToCSV:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_directory": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("CSV",)
FUNCTION = "create_csv"
CATEGORY = "🏵️Fill Nodes/Captioning"
OUTPUT_NODE = True
def create_csv(self, image_directory):
# Get all image files and their corresponding caption files
image_files = [f for f in os.listdir(image_directory) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
image_files.sort() # Sort files to ensure consistent order
# Prepare CSV data
csv_data = []
for image_file in image_files:
caption_file = os.path.splitext(image_file)[0] + '.txt'
caption_path = os.path.join(image_directory, caption_file)
try:
with open(caption_path, 'r', encoding='utf-8') as f:
caption = f.read().strip()
except FileNotFoundError:
caption = "No caption found"
csv_data.append([image_file, caption])
# Create CSV in memory
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['image_file', 'caption']) # Header
writer.writerows(csv_data)
# Get the CSV content as a string
csv_content = output.getvalue()
# Convert to bytes for compatibility with other nodes
csv_bytes = csv_content.encode('utf-8')
return (csv_bytes,)
@classmethod
def IS_CHANGED(cls, image_directory):
return float("NaN")
-57
View File
@@ -1,57 +0,0 @@
from pathlib import Path
from .sup import ROOT, AlwaysEqualProxy, parse_dynamic
class FL_CodeNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"code_input": ("STRING", {"default": "outputs[0] = 'hello, world!'", "multiline": True, "dynamicPrompts": False}),
"file": ("STRING", {"default": "./res/hello.py", "multiline": False, "dynamicPrompts": False}),
"use_file": ("BOOLEAN", {"default": False})
}}
CATEGORY = "🏵️Fill Nodes/utility"
RETURN_TYPES = tuple(AlwaysEqualProxy("*") for _ in range(4))
RETURN_NAMES = tuple(f"output_{i}" for i in range(4))
DESCRIPTION = """
FL_CodeNode is designed to execute custom user-provided Python code. The code can be directly entered as a string input or loaded from a specified file. This class processes dynamic inputs and provides four generic output slots. The execution environment includes predefined 'inputs' and 'outputs' dictionaries to facilitate interaction with the code. Proper error handling is included to ensure informative feedback in case of execution failures. This node is ideal for users needing to integrate custom logic or algorithms into their workflows.
"""
FUNCTION = "execute"
@classmethod
def IS_CHANGED(cls) -> float:
return float("nan")
def execute(self, code_input, file, use_file, **kwargs):
outputs = {i: None for i in range(4)}
inputs = kwargs.copy()
inputs.update({i: v for i, v in enumerate(kwargs.values())})
if use_file:
# load the referenced file
code_input = ""
if not (fname := Path(ROOT / file)).is_file():
print(fname)
if not (fname := Path(file)).is_file():
print(fname)
fname = None
if fname is not None:
try:
with open(str(fname), 'r') as f:
code_input = f.read()
except Exception as e:
raise RuntimeError(f"[FL_CodeNode] error loading code file: {e}")
print(code_input)
# sanitize?
# code_input = code_input
try:
exec(code_input, {"inputs": inputs, "outputs": outputs})
except Exception as e:
raise RuntimeError(f"Error executing user code: {e}")
return tuple(outputs[i] for i in range(4))
-15
View File
@@ -1,15 +0,0 @@
class FL_ColorPicker:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"selected_color": ("STRING", {"default": "#FF0000"})
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "get_color"
CATEGORY = "ui"
def get_color(self, selected_color):
return (selected_color,)
-89
View File
@@ -1,89 +0,0 @@
import os
import numpy as np
import torch
from PIL import Image, ImageOps
from comfy.utils import ProgressBar
class FL_DirectoryCrawl:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"directory_path": ("STRING", {"default": ""}),
"file_type": (["images", "text"],),
"max_files": ("INT", {"default": 100, "min": 1, "max": 10000}),
}
}
RETURN_TYPES = ("IMAGE", "STRING") # Output a batch of images or list of text contents
FUNCTION = "load_batch"
CATEGORY = "🏵️Fill Nodes/utility"
def load_batch(self, directory_path, file_type, max_files):
if not directory_path:
raise ValueError("Directory path is not provided.")
file_paths = self.crawl_directories(directory_path, file_type)
if not file_paths:
raise ValueError(f"No {file_type} found in the specified directory and its subdirectories.")
file_paths = file_paths[:max_files] # Limit the number of files
if file_type == "images":
return self.load_image_batch(file_paths)
else:
return self.load_text_batch(file_paths)
def load_image_batch(self, image_paths):
batch_images = []
pbar = ProgressBar(len(image_paths))
for idx, img_path in enumerate(image_paths):
image = Image.open(img_path)
image = ImageOps.exif_transpose(image) # Correct orientation
image = image.convert("RGB")
image_np = np.array(image).astype(np.float32) / 255.0
batch_images.append(image_np)
pbar.update_absolute(idx)
# Pad images to the largest dimensions
max_h = max(img.shape[0] for img in batch_images)
max_w = max(img.shape[1] for img in batch_images)
padded_images = []
for img in batch_images:
h, w, c = img.shape
padded = np.zeros((max_h, max_w, c), dtype=np.float32)
padded[:h, :w, :] = img
padded_images.append(padded)
batch_images_np = np.stack(padded_images, axis=0)
batch_images_tensor = torch.from_numpy(batch_images_np)
return (batch_images_tensor, "")
def load_text_batch(self, text_paths):
text_contents = []
pbar = ProgressBar(len(text_paths))
for idx, txt_path in enumerate(text_paths):
with open(txt_path, 'r', encoding='utf-8') as file:
content = file.read()
text_contents.append(content)
pbar.update_absolute(idx)
return (torch.zeros(1), "\n---\n".join(text_contents)) # Return empty tensor for IMAGE type
def crawl_directories(self, directory, file_type):
if file_type == "images":
supported_formats = ["jpg", "jpeg", "png", "bmp", "gif"]
else:
supported_formats = ["txt"]
file_paths = []
for root, dirs, files in os.walk(directory):
for file in files:
if file.split('.')[-1].lower() in supported_formats:
full_path = os.path.join(root, file)
file_paths.append(full_path)
return file_paths
-80
View File
@@ -1,80 +0,0 @@
import os
import numpy as np
import requests
from PIL import Image
from moviepy.editor import ImageSequenceClip
import torch
import tempfile
import json
class FL_SendToDiscordWebhook:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"webhook_url": ("STRING", {"default": "https://discord.com/api/webhooks/YOUR_WEBHOOK_HASH"}),
"frame_rate": ("INT", {"default": 12, "min": 1, "max": 60, "step": 1}),
"save_locally": ("BOOLEAN", {"default": True}),
"bot_username": ("STRING", {"default": "ComfyUI Bot"}),
"message": ("STRING", {"default": "Here's your image/video:", "multiline": True}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_and_upload"
CATEGORY = "🏵️Fill Nodes/Discord"
OUTPUT_NODE = True
def generate_and_upload(self, images, webhook_url: str, frame_rate: int, save_locally: bool, bot_username: str,
message: str):
if save_locally:
output_dir = os.path.join(os.path.dirname(__file__), "outputs")
os.makedirs(output_dir, exist_ok=True)
else:
output_dir = tempfile.gettempdir()
filename = f"discord_upload_{int(torch.rand(1).item() * 10000)}"
# Prepare the webhook data
webhook_data = {
"username": bot_username,
"content": message,
}
if len(images) == 1:
file_path = os.path.join(output_dir, f"{filename}.png")
single_image = 255.0 * images[0].cpu().numpy()
single_image_pil = Image.fromarray(single_image.astype(np.uint8))
single_image_pil.save(file_path)
with open(file_path, "rb") as file_data:
files = {
"payload_json": (None, json.dumps(webhook_data)),
"file": (f"{filename}.png", file_data)
}
response = requests.post(webhook_url, files=files)
else:
frames = [255.0 * image.cpu().numpy() for image in images]
file_path = os.path.join(output_dir, f"{filename}.mp4")
clip = ImageSequenceClip(frames, fps=frame_rate)
clip.write_videofile(file_path, codec="libx264", fps=frame_rate)
with open(file_path, 'rb') as file_data:
files = {
"payload_json": (None, json.dumps(webhook_data)),
"file": (f"{filename}.mp4", file_data)
}
response = requests.post(webhook_url, files=files)
if response.status_code == 204:
message = "Successfully uploaded to Discord."
else:
message = f"Failed to upload. Status code: {response.status_code} - {response.text}"
if not save_locally:
os.remove(file_path)
return (message,)
-100
View File
@@ -1,100 +0,0 @@
import torch
import torch.nn.functional as F
from comfy.utils import ProgressBar
class FL_Dither:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"dither_method": (["Floyd-Steinberg", "Random", "Ordered", "Bayer"],),
"num_colors": ("INT", {"default": 2, "min": 2, "max": 256, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_dither"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_dither(self, images, dither_method, num_colors):
device = images.device
total_images = images.shape[0]
pbar = ProgressBar(total_images)
result = []
for idx in range(total_images):
img = images[idx].unsqueeze(0) # Add batch dimension
dithered_img = self.dither_image(img, dither_method, num_colors, device)
result.append(dithered_img)
pbar.update_absolute(idx + 1)
return (torch.cat(result, dim=0),)
def dither_image(self, image, method, num_colors, device):
if method == "Floyd-Steinberg":
return self.floyd_steinberg_dither(image, num_colors, device)
elif method == "Random":
return self.random_dither(image, num_colors, device)
elif method == "Ordered":
return self.ordered_dither(image, num_colors, device)
elif method == "Bayer":
return self.bayer_dither(image, num_colors, device)
else:
return image
def floyd_steinberg_dither(self, img, num_colors, device):
img = img.clone()
h, w = img.shape[2], img.shape[3]
for y in range(h):
for x in range(w):
old_pixel = img[:, :, y, x].clone()
new_pixel = torch.round(old_pixel * (num_colors - 1)) / (num_colors - 1)
img[:, :, y, x] = new_pixel
error = old_pixel - new_pixel
if x + 1 < w:
img[:, :, y, x + 1] += error * 7 / 16
if y + 1 < h:
if x > 0:
img[:, :, y + 1, x - 1] += error * 3 / 16
img[:, :, y + 1, x] += error * 5 / 16
if x + 1 < w:
img[:, :, y + 1, x + 1] += error * 1 / 16
return img
def random_dither(self, img, num_colors, device):
noise = torch.rand_like(img) / (num_colors * 2)
img = torch.floor((img + noise) * (num_colors - 1)) / (num_colors - 1)
return img
def ordered_dither(self, img, num_colors, device):
bayer_matrix = torch.tensor([
[0, 8, 2, 10],
[12, 4, 14, 6],
[3, 11, 1, 9],
[15, 7, 13, 5]
], device=device).float() / 16.0
h, w = img.shape[2], img.shape[3]
bayer_tiled = bayer_matrix.repeat(h // 4 + 1, w // 4 + 1)[:h, :w]
thresholds = bayer_tiled.unsqueeze(0).unsqueeze(0).repeat(img.shape[0], img.shape[1], 1, 1)
img = torch.floor((img + thresholds / num_colors) * (num_colors - 1)) / (num_colors - 1)
return img
def bayer_dither(self, img, num_colors, device):
bayer_matrix = torch.tensor([
[0, 8, 2, 10],
[12, 4, 14, 6],
[3, 11, 1, 9],
[15, 7, 13, 5]
], device=device).float() / 16.0
h, w = img.shape[2], img.shape[3]
bayer_tiled = bayer_matrix.repeat(h // 4 + 1, w // 4 + 1)[:h, :w]
thresholds = bayer_tiled.unsqueeze(0).unsqueeze(0).repeat(img.shape[0], img.shape[1], 1, 1)
quantized = torch.round(img * (num_colors - 1)) / (num_colors - 1)
dithered = torch.where(img > thresholds, quantized + 1 / (num_colors - 1), quantized)
return torch.clamp(dithered, 0, 1)
-159
View File
@@ -1,159 +0,0 @@
import torch
import aiohttp
import asyncio
from PIL import Image
import io
import os
import sys
from tqdm import tqdm
import base64
class FL_GPT_Vision:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": ("STRING", {"default": "", "multiline": False, "hidden": True}),
"model": (["gpt-4o-mini", "gpt-4o", "gpt-4-vision-preview"],),
"system_prompt": ("STRING", {
"default": "You are a helpful assistant that describes images accurately and concisely.",
"multiline": True}),
"request_prompt": ("STRING", {"default": "Describe this image in detail.", "multiline": True}),
"output_directory": ("STRING", {"default": ""}),
"overwrite": ("BOOLEAN", {"default": False}),
"max_tokens": ("INT", {"default": 300, "min": 1, "max": 4096}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.1}),
"detail": (["auto", "low", "high"],),
"batch_size": ("INT", {"default": 5, "min": 1, "max": 20}),
},
"optional": {
"images": ("IMAGE",),
"input_directory": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("message", "output_directory")
FUNCTION = "generate_captions"
CATEGORY = "🏵️Fill Nodes/GPT"
async def process_image(self, session, img, img_filename, output_directory, overwrite, api_key, model,
system_prompt, request_prompt, max_tokens, temperature, detail):
caption_filename = os.path.splitext(img_filename)[0] + ".txt"
img_path = os.path.join(output_directory, img_filename)
caption_path = os.path.join(output_directory, caption_filename)
if not overwrite and os.path.exists(caption_path):
return None
# Save the image
img.save(img_path)
# Encode image to base64
buffered = io.BytesIO()
img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
payload = {
"model": model,
"messages": [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": [
{
"type": "text",
"text": request_prompt
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{img_str}",
"detail": detail
}
}
]
}
],
"max_tokens": max_tokens,
"temperature": temperature
}
try:
async with session.post("https://api.openai.com/v1/chat/completions", json=payload) as response:
response.raise_for_status()
data = await response.json()
caption = data['choices'][0]['message']['content']
# Save the caption
with open(caption_path, 'w', encoding='utf-8') as f:
f.write(caption)
return caption
except aiohttp.ClientResponseError as e:
print(f"Error processing {img_filename}: {str(e)}")
return None
async def process_batch(self, batch, session, *args):
tasks = [self.process_image(session, img, filename, *args) for img, filename in batch]
return await asyncio.gather(*tasks)
def generate_captions(self, api_key, model, system_prompt, request_prompt, output_directory, overwrite, max_tokens,
temperature, detail, batch_size, images=None, input_directory=None):
try:
if not api_key:
raise ValueError("API key is required")
if images is None and not input_directory:
raise ValueError("Either 'images' or 'input_directory' must be provided")
if not os.path.exists(output_directory):
os.makedirs(output_directory)
image_list = []
if images is not None:
for i, img in enumerate(images):
pil_img = Image.fromarray((img.squeeze().cpu().numpy() * 255).astype('uint8'))
image_list.append((pil_img, f"image_{i}.jpg"))
if input_directory:
if not os.path.exists(input_directory):
raise ValueError(f"Input directory does not exist: {input_directory}")
for filename in os.listdir(input_directory):
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
img_path = os.path.join(input_directory, filename)
pil_img = Image.open(img_path)
image_list.append((pil_img, filename))
total_images = len(image_list)
if total_images == 0:
raise ValueError("No images found to process")
batches = [image_list[i:i + batch_size] for i in range(0, total_images, batch_size)]
async def main():
async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {api_key}"}) as session:
all_captions = []
for batch in tqdm(batches, desc="Processing batches", file=sys.stdout):
batch_captions = await self.process_batch(batch, session, output_directory, overwrite, api_key,
model, system_prompt, request_prompt, max_tokens,
temperature, detail)
all_captions.extend(batch_captions)
return all_captions
captions = asyncio.run(main())
# Print summary
print(f"\nTotal images processed: {total_images}")
print(f"Images and captions saved in: {output_directory}")
return (f"Captions generated and saved in {output_directory}", output_directory)
except Exception as e:
error_message = f"Error: {str(e)}"
print(error_message)
return (error_message, "")
-77
View File
@@ -1,77 +0,0 @@
import torch
import numpy as np
from PIL import Image
from glitch_this import ImageGlitcher
from comfy.utils import ProgressBar
class FL_Glitch:
def __init__(self):
self.seed_index = 0
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"glitch_amount": ("FLOAT", {"default": 3.0, "min": 0.1, "max": 10.0, "step": 0.01}),
"color_offset": (["Disable", "Enable"],),
"seed": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "glitch"
CATEGORY = "🏵️Fill Nodes/VFX"
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def s2b(self, v):
return v == "Enable"
def glitch(self, images, glitch_amount=1, color_offset="Disable", seed=0):
color_offset = self.s2b(color_offset)
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
pbar = ProgressBar(total_images)
for i, image in enumerate(images, start=1):
p = self.t2p(image)
# 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=current_seed)
f = g2.rotate(-90, expand=True)
o = np.array(f.convert("RGB")).astype(np.float32) / 255.0
o = torch.from_numpy(o).unsqueeze(0)
out.append(o)
pbar.update_absolute(i)
out = torch.cat(out, 0)
return (out,)
-84
View File
@@ -1,84 +0,0 @@
import json
import numpy as np
import torch
from PIL import Image
class GradientImageGenerator:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"width": ("INT", {"default": 512, "min": 64, "max": 4096}),
"height": ("INT", {"default": 512, "min": 64, "max": 4096}),
"color_mode": (["RGB", "HSV"],),
"interpolation": (["Linear", "Ease In", "Ease Out", "Ease In-Out"],),
"gradient_colors": ("STRING", {"default": "[]"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_gradient"
CATEGORY = "image/generation"
def generate_gradient(self, width, height, color_mode, interpolation, gradient_colors):
# Parse gradient colors
gradient_colors = json.loads(gradient_colors)
colors = []
positions = []
for stop in gradient_colors:
pos = stop['pos']
color = stop['color']
positions.append(pos)
colors.append(color)
# Create the gradient image
image = np.zeros((height, width, 3), dtype=np.float32)
# Apply interpolation to positions if needed
if interpolation != "Linear":
x = np.linspace(0, 1, width)
if interpolation == "Ease In":
x = x ** 2
elif interpolation == "Ease Out":
x = 1 - (1 - x) ** 2
elif interpolation == "Ease In-Out":
x = np.where(x < 0.5, 2 * x ** 2, 1 - (-2 * x + 2) ** 2 / 2)
positions = np.interp(x, [0, 1], [0, 1])
# Generate gradient
for i in range(width):
pos = i / (width - 1)
if pos <= positions[0]:
color = colors[0]
elif pos >= positions[-1]:
color = colors[-1]
else:
for j in range(len(positions) - 1):
if positions[j] <= pos < positions[j + 1]:
t = (pos - positions[j]) / (positions[j + 1] - positions[j])
color = [
(1 - t) * colors[j][k] + t * colors[j + 1][k]
for k in range(3)
]
break
image[:, i] = [c / 255.0 for c in color] # Normalize color values to [0, 1]
# Convert to HSV if needed
if color_mode == "HSV":
image_rgb = (image * 255).astype(np.uint8)
image_hsv = Image.fromarray(image_rgb, mode="RGB").convert("HSV")
image = np.array(image_hsv).astype(np.float32) / 255.0
# Convert to PyTorch tensor
image_tensor = torch.from_numpy(image).unsqueeze(0) # Add batch dimension
return (image_tensor,)
NODE_CLASS_MAPPINGS = {
"GradientImageGenerator": GradientImageGenerator
}
NODE_DISPLAY_NAME_MAPPINGS = {
"GradientImageGenerator": "Gradient Image Generator"
}
-189
View File
@@ -1,189 +0,0 @@
import sys
import subprocess
import importlib.util
import os
import time
import threading
# Check if huggingface_hub is installed, if not, install it
if importlib.util.find_spec("huggingface_hub") is None:
print("huggingface_hub is not installed. Installing it now...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "huggingface_hub"])
print("huggingface_hub has been installed.")
import torch
from PIL import Image
import io
from huggingface_hub import HfApi, create_repo, repo_exists
from tqdm import tqdm
class FL_HFHubModelUploader:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": ("STRING", {"multiline": False}),
"owner": ("STRING", {"default": ""}),
"repo_name": ("STRING", {"default": "my-awesome-model"}),
"readme_content": (
"STRING", {"multiline": True, "default": "# My Awesome Model\n\nThis is a great model!"}),
"create_new_repo": (["True", "False"],),
"image_folder_path": ("STRING", {"default": "images"}),
"repo_type": (["model", "dataset", "space"],),
},
"optional": {
"image": ("IMAGE",),
"model_card_header": ("IMAGE",),
"zip_file": ("ZIP",),
"zip_filename": ("STRING", {"default": "archive"}),
"zip_folder_path": ("STRING", {"default": "zipped_content"}),
"model_file_path": ("STRING", {"default": ""}),
"model_repo_path": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "upload_to_hub"
CATEGORY = "🏵️Fill Nodes/Hugging Face"
def upload_to_hub(self, api_key: str, owner: str, repo_name: str, readme_content: str, create_new_repo: str,
image_folder_path: str, repo_type: str, image: torch.Tensor = None,
model_card_header: torch.Tensor = None, zip_file: bytes = None,
zip_filename: str = "archive", zip_folder_path: str = "zipped_content",
model_file_path: str = "", model_repo_path: str = "") -> tuple[str]:
# Initialize Hugging Face API
api = HfApi(token=api_key)
# Ensure zip_filename ends with .zip
if not zip_filename.lower().endswith('.zip'):
zip_filename += '.zip'
try:
# Construct full repo_id
full_repo_id = f"{owner}/{repo_name}"
# Step 1: Create a new repository or check if it exists
create_new_repo = create_new_repo == "True"
if create_new_repo:
repo_url = create_repo(repo_id=full_repo_id, token=api_key, exist_ok=True, repo_type=repo_type)
print(f"Repository created or already exists: {repo_url}")
else:
if not repo_exists(repo_id=full_repo_id, token=api_key):
return (
f"Error: Repository {full_repo_id} does not exist. Please create it first or use the 'Create New Repo' option.",)
repo_url = f"https://huggingface.co/{full_repo_id}"
print(f"Using existing repository: {repo_url}")
# Step 2: Prepare and upload files
max_retries = 3
for attempt in range(max_retries):
try:
# Upload the main image if provided
if image is not None:
main_image = Image.fromarray((image.squeeze().cpu().numpy() * 255).astype('uint8'))
main_img_byte_arr = io.BytesIO()
main_image.save(main_img_byte_arr, format='PNG')
main_img_byte_arr = main_img_byte_arr.getvalue()
api.upload_file(
path_or_fileobj=main_img_byte_arr,
path_in_repo=f"{image_folder_path}/model_image.png",
repo_id=full_repo_id,
token=api_key
)
print("Main image uploaded successfully")
# Upload the model card header image if provided
if model_card_header is not None:
header_image = Image.fromarray(
(model_card_header.squeeze().cpu().numpy() * 255).astype('uint8'))
header_img_byte_arr = io.BytesIO()
header_image.save(header_img_byte_arr, format='PNG')
header_img_byte_arr = header_img_byte_arr.getvalue()
api.upload_file(
path_or_fileobj=header_img_byte_arr,
path_in_repo="model_card_header.png",
repo_id=full_repo_id,
token=api_key
)
print("Model card header image uploaded successfully")
# Add the header image to the README content
readme_content = f"![Model Card Header](model_card_header.png)\n\n{readme_content}"
# Upload ZIP file if provided
if zip_file is not None:
api.upload_file(
path_or_fileobj=zip_file,
path_in_repo=f"{zip_folder_path}/{zip_filename}",
repo_id=full_repo_id,
token=api_key
)
print(f"ZIP file uploaded successfully as {zip_filename}")
# Upload model file from absolute path if provided
if model_file_path and model_repo_path:
if os.path.exists(model_file_path):
file_size = os.path.getsize(model_file_path)
# Create a progress bar
pbar = tqdm(total=100, unit='%', desc="Uploading model file")
# Function to update progress bar
def update_progress():
progress = 0
while progress < 95:
time.sleep(0.5)
increment = min(5, 95 - progress)
progress += increment
pbar.update(increment)
# Start progress update in a separate thread
progress_thread = threading.Thread(target=update_progress)
progress_thread.start()
# Perform the actual upload
with open(model_file_path, 'rb') as file:
api.upload_file(
path_or_fileobj=file,
path_in_repo=model_repo_path,
repo_id=full_repo_id,
token=api_key
)
# Ensure progress reaches 100%
progress_thread.join()
pbar.update(100 - pbar.n)
pbar.close()
print(f"Model file uploaded successfully to {model_repo_path}")
else:
print(f"Error: Model file not found at {model_file_path}")
# Upload README
api.upload_file(
path_or_fileobj=readme_content.encode('utf-8'),
path_in_repo="README.md",
repo_id=full_repo_id,
token=api_key
)
print("README uploaded successfully")
break # If successful, break out of the retry loop
except Exception as e:
if "Repository Not Found" in str(e) and attempt < max_retries - 1:
print(f"Repository not found. Retrying in 5 seconds... (Attempt {attempt + 1}/{max_retries})")
time.sleep(5)
else:
raise
return (f"Successfully uploaded to {repo_url}",)
except Exception as e:
return (f"Error: {str(e)}",)
@classmethod
def IS_CHANGED(cls, api_key, owner, repo_name, readme_content, create_new_repo, image_folder_path, repo_type,
image, model_card_header, zip_file, zip_filename, zip_folder_path, model_file_path, model_repo_path):
return float("NaN")
-199
View File
@@ -1,199 +0,0 @@
import sys
import subprocess
import importlib.util
import os
import time
import threading
import io
# Check if huggingface_hub is installed, if not, install it
if importlib.util.find_spec("huggingface_hub") is None:
print("huggingface_hub is not installed. Installing it now...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "huggingface_hub"])
print("huggingface_hub has been installed.")
import torch
from PIL import Image
from huggingface_hub import HfApi, create_repo, repo_exists
from tqdm import tqdm
class FL_HF_Character:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": ("STRING", {"multiline": False}),
"owner": ("STRING", {"default": ""}),
"repo_name": ("STRING", {"default": "my-awesome-model"}),
"studio_name": ("STRING", {"default": ""}),
"project_name": ("STRING", {"default": ""}),
"character_name": ("STRING", {"default": ""}),
"create_new_repo": (["True", "False"],),
"repo_type": (["model", "dataset", "space"],),
},
"optional": {
"lora_file": ("STRING", {"default": ""}),
"dataset_zip": ("ZIP",),
"caption_layout": ("IMAGE",),
"csv_file": ("CSV",),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "upload_to_hub"
CATEGORY = "🏵️Fill Nodes/Hugging Face"
def upload_to_hub(self, api_key: str, owner: str, repo_name: str, studio_name: str, project_name: str,
character_name: str, create_new_repo: str, repo_type: str,
lora_file: str = "", dataset_zip: bytes = None,
caption_layout: torch.Tensor = None, csv_file: bytes = None) -> tuple[str]:
# Initialize Hugging Face API
api = HfApi(token=api_key)
try:
# Construct full repo_id
full_repo_id = f"{owner}/{repo_name}"
# Step 1: Create a new repository or check if it exists
create_new_repo = create_new_repo == "True"
if create_new_repo:
repo_url = create_repo(repo_id=full_repo_id, token=api_key, exist_ok=True, repo_type=repo_type)
print(f"Repository created or already exists: {repo_url}")
else:
if not repo_exists(repo_id=full_repo_id, token=api_key):
return (
f"Error: Repository {full_repo_id} does not exist. Please create it first or use the 'Create New Repo' option.",)
repo_url = f"https://huggingface.co/{full_repo_id}"
print(f"Using existing repository: {repo_url}")
# Step 2: Create directory structure
base_path = f"{studio_name}/{project_name}/{character_name}"
# Step 3: Upload files
if lora_file:
self.upload_file_with_progress(api, lora_file, f"{base_path}/lora", full_repo_id, api_key, "LoRA")
if dataset_zip is not None:
self.upload_zip(api, dataset_zip, f"{base_path}/dataset", full_repo_id, api_key, "Dataset")
if caption_layout is not None:
self.upload_image(api, caption_layout, base_path, full_repo_id, api_key, "caption_layout")
if csv_file is not None:
self.upload_csv(api, csv_file, base_path, full_repo_id, api_key)
return (f"Successfully uploaded to {repo_url}/{base_path}",)
except Exception as e:
return (f"Error: {str(e)}",)
def upload_file_with_progress(self, api, file_path, repo_dir, full_repo_id, api_key, file_type):
if file_path and os.path.exists(file_path):
file_size = os.path.getsize(file_path)
file_name = os.path.basename(file_path)
repo_path = f"{repo_dir}/{file_name}"
pbar = tqdm(total=100, unit='%', desc=f"Uploading {file_type} file")
def update_progress():
progress = 0
while progress < 95:
time.sleep(0.5)
increment = min(5, 95 - progress)
progress += increment
pbar.update(increment)
progress_thread = threading.Thread(target=update_progress)
progress_thread.start()
with open(file_path, 'rb') as file:
api.upload_file(
path_or_fileobj=file,
path_in_repo=repo_path,
repo_id=full_repo_id,
token=api_key
)
progress_thread.join()
pbar.update(100 - pbar.n)
pbar.close()
print(f"{file_type} file uploaded successfully to {repo_path}")
elif file_path:
print(f"Error: {file_type} file not found at {file_path}")
def upload_zip(self, api, zip_data, repo_dir, full_repo_id, api_key, file_type):
repo_path = f"{repo_dir}/dataset.zip"
pbar = tqdm(total=100, unit='%', desc=f"Uploading {file_type} ZIP")
def update_progress():
progress = 0
while progress < 95:
time.sleep(0.5)
increment = min(5, 95 - progress)
progress += increment
pbar.update(increment)
progress_thread = threading.Thread(target=update_progress)
progress_thread.start()
api.upload_file(
path_or_fileobj=zip_data,
path_in_repo=repo_path,
repo_id=full_repo_id,
token=api_key
)
progress_thread.join()
pbar.update(100 - pbar.n)
pbar.close()
print(f"{file_type} ZIP uploaded successfully to {repo_path}")
def upload_image(self, api, image, repo_dir, full_repo_id, api_key, image_type):
img = Image.fromarray((image.squeeze().cpu().numpy() * 255).astype('uint8'))
img_byte_arr = io.BytesIO()
img.save(img_byte_arr, format='PNG')
img_byte_arr = img_byte_arr.getvalue()
repo_path = f"{repo_dir}/{image_type}.png"
api.upload_file(
path_or_fileobj=img_byte_arr,
path_in_repo=repo_path,
repo_id=full_repo_id,
token=api_key
)
print(f"{image_type} uploaded successfully")
def upload_csv(self, api, csv_data, repo_dir, full_repo_id, api_key):
repo_path = f"{repo_dir}/metadata.csv"
pbar = tqdm(total=100, unit='%', desc="Uploading CSV file")
def update_progress():
progress = 0
while progress < 95:
time.sleep(0.5)
increment = min(5, 95 - progress)
progress += increment
pbar.update(increment)
progress_thread = threading.Thread(target=update_progress)
progress_thread.start()
api.upload_file(
path_or_fileobj=csv_data,
path_in_repo=repo_path,
repo_id=full_repo_id,
token=api_key
)
progress_thread.join()
pbar.update(100 - pbar.n)
pbar.close()
print(f"CSV file uploaded successfully to {repo_path}")
@classmethod
def IS_CHANGED(cls, api_key, owner, repo_name, studio_name, project_name, character_name,
create_new_repo, repo_type, lora_file, dataset_zip, caption_layout, csv_file):
return float("NaN")
-68
View File
@@ -1,68 +0,0 @@
import torch
import numpy as np
from comfy.utils import ProgressBar
class FL_HalftonePattern:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"dot_size": ("INT", {"default": 5, "min": 1, "max": 20, "step": 1}),
"dot_spacing": ("INT", {"default": 10, "min": 5, "max": 50, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "halftone_pattern"
CATEGORY = "🏵️Fill Nodes/VFX"
def halftone_pattern(self, images, dot_size=5, dot_spacing=10):
out = []
total_images = len(images)
pbar = ProgressBar(total_images)
for i, img in enumerate(images, start=1):
img_np = img.cpu().numpy().squeeze()
grayscale_image = np.dot(img_np[..., :3], [0.299, 0.587, 0.114])
height, width = grayscale_image.shape
halftone_image = np.ones((height, width), dtype=np.float32)
for y in range(0, height, dot_spacing):
for x in range(0, width, dot_spacing):
box = (x, y, x + dot_spacing, y + dot_spacing)
region_mean = np.mean(grayscale_image[box[1]:box[3], box[0]:box[2]])
dot_radius = int((1 - region_mean) * dot_size / 2)
dot_position = (x + dot_spacing // 2, y + dot_spacing // 2)
# Create a circular mask for the dot
y_grid, x_grid = np.ogrid[-dot_radius:dot_radius + 1, -dot_radius:dot_radius + 1]
mask = x_grid ** 2 + y_grid ** 2 <= dot_radius ** 2
# Apply the dot mask to the halftone image
y_start = max(0, dot_position[1] - dot_radius)
y_end = min(height, dot_position[1] + dot_radius + 1)
x_start = max(0, dot_position[0] - dot_radius)
x_end = min(width, dot_position[0] + dot_radius + 1)
# Ensure the mask dimensions match the sliced halftone image dimensions
mask_height = y_end - y_start
mask_width = x_end - x_start
mask = mask[:mask_height, :mask_width]
try:
halftone_image[y_start:y_end, x_start:x_end][mask] = 0
except Exception:
pass
o = np.stack((halftone_image,) * 3, axis=-1)
o = torch.from_numpy(o).unsqueeze(0)
out.append(o)
pbar.update_absolute(i)
out = torch.cat(out, 0)
return (out,)
-106
View File
@@ -1,106 +0,0 @@
import torch
import numpy as np
from PIL import Image, ImageDraw
import math
from comfy.utils import ProgressBar
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 {
"required": {
"images": ("IMAGE",),
},
"optional": {
"hexagon_size": ("INT", {"default": 100, "min": 50, "max": 500, "step": 10}),
"shadow_offset": ("INT", {"default": 5, "min": 0, "max": 20, "step": 1}),
"shadow_color": ("STRING", {"default": "purple"}),
"background_color": ("STRING", {"default": "black"}),
"rotation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360.0, "step": 1.0}),
"spacing": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "hexagonal_pattern"
CATEGORY = "🏵️Fill Nodes/VFX"
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def create_hexagon_mask(self, size):
mask = Image.new("L", (size, size), 0)
draw = ImageDraw.Draw(mask)
draw.regular_polygon((size // 2, size // 2, size // 2), 6, fill=255)
return mask
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
if hasattr(current_value, 'values'):
current_value = float(current_value.values[0])
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):
out = []
total_images = len(images)
pbar = ProgressBar(total_images)
for i, img_tensor in enumerate(images, start=1):
p = self.t2p(img_tensor)
width, height = p.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)
hexagon_mask = self.create_hexagon_mask(current_hexagon_size)
output_image = Image.new("RGBA", (width, height), current_background_color)
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(current_shadow_color, (current_shadow_offset, current_shadow_offset), shadow_mask)
shadow.putalpha(int(255 * shadow_opacity))
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
o = torch.from_numpy(o).unsqueeze(0)
out.append(o)
pbar.update_absolute(i)
out = torch.cat(out, 0)
return (out,)
-89
View File
@@ -1,89 +0,0 @@
import os
import torch
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import textwrap
class FL_ImageCaptionLayout:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_directory": ("STRING", {"default": ""}),
"images_per_row": ("INT", {"default": 3, "min": 1, "max": 10}),
"image_size": ("INT", {"default": 256, "min": 64, "max": 1024}),
"caption_height": ("INT", {"default": 64, "min": 32, "max": 256}),
"font_size": ("INT", {"default": 12, "min": 8, "max": 32}),
"padding": ("INT", {"default": 10, "min": 0, "max": 100}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "create_layout"
CATEGORY = "🏵️Fill Nodes/Captioning"
OUTPUT_NODE = True
def create_layout(self, image_directory, images_per_row, image_size, caption_height, font_size, padding):
# Colors
background_color = (255, 255, 255) # White
text_color = (0, 0, 0) # Black
caption_background_color = (255, 255, 255) # White
# Get all image files and their corresponding caption files
image_files = [f for f in os.listdir(image_directory) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
image_files.sort() # Sort files to ensure consistent order
# Calculate layout dimensions with padding
total_width = images_per_row * (image_size + padding) + padding
rows = (len(image_files) + images_per_row - 1) // images_per_row
total_height = rows * (image_size + caption_height + padding) + padding
# Create the layout with padding
layout = Image.new('RGB', (total_width, total_height), color=background_color)
# Load font
try:
font = ImageFont.truetype("arial.ttf", font_size)
except IOError:
font = ImageFont.load_default()
for i, image_file in enumerate(image_files):
# Load and resize image
img_path = os.path.join(image_directory, image_file)
img = Image.open(img_path).convert('RGB')
img = img.resize((image_size, image_size), Image.LANCZOS)
# Load caption
caption_file = os.path.splitext(image_file)[0] + '.txt'
caption_path = os.path.join(image_directory, caption_file)
try:
with open(caption_path, 'r') as f:
caption = f.read().strip()
except FileNotFoundError:
caption = "No caption found"
# Calculate position with padding
row = i // images_per_row
col = i % images_per_row
x = padding + col * (image_size + padding)
y = padding + row * (image_size + caption_height + padding)
# Paste image
layout.paste(img, (x, y))
# Create caption box
caption_box = Image.new('RGB', (image_size, caption_height), color=caption_background_color)
draw = ImageDraw.Draw(caption_box)
# Wrap text
wrapped_text = textwrap.fill(caption, width=(image_size - 10) // (font_size // 2))
# Draw wrapped text
draw.text((5, 5), wrapped_text, font=font, fill=text_color)
# Paste caption box
layout.paste(caption_box, (x, y + image_size))
# Convert to tensor
layout_tensor = torch.from_numpy(np.array(layout).astype(np.float32) / 255.0).unsqueeze(0)
return (layout_tensor,)
-105
View File
@@ -1,105 +0,0 @@
import torch
import numpy as np
from PIL import Image
import sys
from comfy.utils import ProgressBar
class FL_ImageCollage:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base_image": ("IMAGE",),
"tile_image": ("IMAGE",),
"tile_size": ("INT", {"default": 32, "min": 8, "max": 256, "step": 8}),
"spacing": ("INT", {"default": 0, "min": 0, "max": 64, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "create_collage"
CATEGORY = "🏵️Fill Nodes/VFX"
def create_collage(self, base_image, tile_image, tile_size, spacing):
base_batch_size = len(base_image)
tile_batch_size = len(tile_image)
if tile_batch_size == 1:
# Duplicate the single tile image to match the base image batch size
tile_image = tile_image.repeat(base_batch_size, 1, 1, 1)
elif tile_batch_size != base_batch_size:
raise ValueError(f"The number of tile images ({tile_batch_size}) does not match the number of base images ({base_batch_size}).")
result = []
pbar = ProgressBar(total_images)
for i, (base_img, tile_img) in enumerate(zip(base_image, tile_image), start=1):
base_img = self.t2p(base_img)
tile_img = self.t2p(tile_img)
result_img = self.create_collage_image(base_img, tile_img, tile_size, spacing)
result_img = self.p2t(result_img)
result.append(result_img)
# Update the print log
progress = i / base_batch_size * 100
sys.stdout.write(f"\rProcessing images: {progress:.2f}%")
sys.stdout.flush()
# Print a new line after the progress log
print()
return (torch.cat(result, dim=0),)
def create_collage_image(self, base_image, tile_image, tile_size, spacing):
base_width, base_height = base_image.size
tile_width, tile_height = tile_image.size
# Calculate the aspect ratio of the tile image
aspect_ratio = tile_width / tile_height
# Calculate the new dimensions of the tile image while maintaining the aspect ratio
if tile_width > tile_height:
new_tile_width = tile_size
new_tile_height = int(tile_size / aspect_ratio)
else:
new_tile_width = int(tile_size * aspect_ratio)
new_tile_height = tile_size
# Resize the tile image to the new dimensions
tile_image = tile_image.resize((new_tile_width, new_tile_height), Image.Resampling.LANCZOS)
# Create a new blank image for the collage
collage_image = Image.new("RGB", base_image.size)
for y in range(0, base_height, new_tile_height + spacing):
for x in range(0, base_width, new_tile_width + spacing):
# Get the average color of the corresponding region in the base image
region = base_image.crop((x, y, x + new_tile_width, y + new_tile_height))
avg_color = tuple(np.array(region).mean(axis=(0, 1)).astype(int))
# Create a mask based on the brightness of the tile image
tile_mask = Image.new("L", (new_tile_width, new_tile_height), 0)
tile_mask_data = np.array(tile_image.convert("L"))
tile_mask_data = (tile_mask_data / 255.0) ** 2 # Adjust the brightness sensitivity
tile_mask.putdata(np.uint8(tile_mask_data.flatten() * 255))
# Colorize the tile image based on the average color of the base image region
colorized_tile = Image.new("RGB", (new_tile_width, new_tile_height), avg_color)
colorized_tile.putalpha(tile_mask)
# Paste the colorized tile onto the collage image
collage_image.paste(colorized_tile, (x, y), mask=tile_mask)
return collage_image
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-67
View File
@@ -1,67 +0,0 @@
import torch
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from comfy.utils import ProgressBar
class FL_ImageNotes:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"text": ("STRING", {"default": "Text Here", "multiline": False}),
"bar_height": ("INT", {"default": 50, "min": 10, "max": 200, "step": 2}),
"text_size": ("INT", {"default": 24, "min": 10, "max": 100, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "add_notes"
CATEGORY = "🏵️Fill Nodes/utility"
def add_notes(self, images, text, bar_height, text_size):
result = []
total_images = len(images)
pbar = ProgressBar(total_images)
for i, image in enumerate(images, start=1):
img = self.t2p(image)
result_img = self.add_text_bar(img, text, bar_height, text_size)
result_img = self.p2t(result_img)
result.append(result_img)
pbar.update_absolute(i)
return (torch.cat(result, dim=0),)
def add_text_bar(self, image, text, bar_height, text_size):
width, height = image.size
new_height = height + bar_height
new_image = Image.new("RGB", (width, new_height), color="black")
new_image.paste(image, (0, bar_height))
draw = ImageDraw.Draw(new_image)
font = ImageFont.truetype("arial.ttf", text_size)
text_width, text_height = self.get_text_size(text, font)
x = (width - text_width) // 2
y = (bar_height - text_height) // 2
draw.text((x, y), text, font=font, fill="white")
return new_image
def get_text_size(self, text, font):
ascent, descent = font.getmetrics()
text_width = font.getmask(text).getbbox()[2]
text_height = font.getmask(text).getbbox()[3] + descent
return text_width, text_height
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-74
View File
@@ -1,74 +0,0 @@
import os
import re
from PIL import Image
import numpy as np
from comfy.utils import ProgressBar
class FL_ImageCaptionSaver:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE", {}),
"folder_name": ("STRING", {"default": "output_folder"}),
"caption_text": ("STRING", {"default": "Your caption here"}),
"overwrite": ("BOOLEAN", {"default": True})
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "save_images_with_captions"
CATEGORY = "🏵️Fill Nodes/Captioning"
OUTPUT_NODE = True
def sanitize_text(self, text):
return re.sub(r'[^a-zA-Z0-9\s.,!?-]', '', text)
def save_images_with_captions(self, images, folder_name, caption_text, overwrite):
os.makedirs(folder_name, exist_ok=True)
sanitized_caption = self.sanitize_text(caption_text)
saved_files = []
pbar = ProgressBar(len(images))
for i, image_tensor in enumerate(images):
base_name = f"image_{i}"
image_file_name = f"{folder_name}/{base_name}.png"
text_file_name = f"{folder_name}/{base_name}.txt"
if not overwrite:
counter = 1
while os.path.exists(image_file_name) or os.path.exists(text_file_name):
image_file_name = f"{folder_name}/{base_name}_{counter}.png"
text_file_name = f"{folder_name}/{base_name}_{counter}.txt"
counter += 1
# Convert tensor to numpy array
image_np = image_tensor.cpu().numpy()
# Ensure the image is in the correct shape (height, width, channels)
if image_np.shape[0] == 1: # If the first dimension is 1, squeeze it
image_np = np.squeeze(image_np, axis=0)
# If the image is grayscale (2D), convert to RGB
if len(image_np.shape) == 2:
image_np = np.stack((image_np,) * 3, axis=-1)
elif image_np.shape[2] == 1: # If it's (height, width, 1)
image_np = np.repeat(image_np, 3, axis=2)
# Ensure values are in 0-255 range
image_np = (image_np * 255).clip(0, 255).astype(np.uint8)
# Convert to PIL Image
image = Image.fromarray(image_np)
# Save image
image.save(image_file_name)
saved_files.append(image_file_name)
with open(text_file_name, "w") as text_file:
text_file.write(sanitized_caption)
pbar.update_absolute(i)
return (f"Saved {len(images)} images and sanitized captions in '{folder_name}'",)
-35
View File
@@ -1,35 +0,0 @@
import torch
from PIL import Image
class FL_ImageDimensionDisplay:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "display_dimensions"
CATEGORY = "🏵️Fill Nodes/utility"
def display_dimensions(self, image):
if isinstance(image, torch.Tensor):
if image.dim() == 4: # Batch dimension is present
_, height, width, _ = image.shape
elif image.dim() == 3: # No batch dimension, single image
height, width, _ = image.shape
else:
return ("Unsupported tensor format",)
elif isinstance(image, Image.Image):
width, height = image.size
else:
return ("Unsupported image format",)
dimensions = f"Width: {width}, Height: {height}"
return (dimensions,)
@classmethod
def IS_CHANGED(cls, image):
return float("NaN") # This ensures the node always updates
-85
View File
@@ -1,85 +0,0 @@
import torch
from PIL import Image
from kornia.morphology import gradient
import comfy.model_management
from comfy.utils import ProgressBar
class FL_ImagePixelator:
def __init__(self):
self.modulation_index = 0
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"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}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "pixelate_image"
CATEGORY = "🏵️Fill Nodes/VFX"
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]
pbar = ProgressBar(total_frames)
for i, single_image in enumerate(image, start=1):
single_image = single_image.unsqueeze(0) # Add batch dimension
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)
pbar.update_absolute(i)
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
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
else:
return (None,)
elif isinstance(image, Image.Image):
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)
else:
return (None,)
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))
resized_image = input_image.resize(new_size, Image.NEAREST)
pixelated_image = resized_image.resize((width, height), Image.NEAREST)
return pixelated_image
def apply_pixelation_tensor(self, input_image, scale_factor):
_, num_channels, height, width = input_image.shape
new_height, new_width = max(1, int(height * scale_factor)), max(1, int(width * scale_factor))
resized_tensor = torch.nn.functional.interpolate(input_image, size=(new_height, new_width), mode='nearest')
output_tensor = torch.nn.functional.interpolate(resized_tensor, size=(height, width), mode='nearest')
return output_tensor
def process(self, image, kernel_size):
device = comfy.model_management.get_torch_device()
kernel = torch.ones(kernel_size, kernel_size, device=device)
image_k = image.to(device).movedim(-1, 1)
output = gradient(image_k, kernel)
img_out = output.to(comfy.model_management.intermediate_device()).movedim(1, -1)
return img_out
-44
View File
@@ -1,44 +0,0 @@
import os
import numpy as np
import torch
from PIL import Image, ImageOps
class FL_ImageRandomizer:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"directory_path": ("STRING", {"default": ""}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("IMAGE", "PATH")
FUNCTION = "select_image"
CATEGORY = "🏵️Fill Nodes/utility"
def select_image(self, directory_path, seed):
if not directory_path:
raise ValueError("Directory path is not provided.")
images = self.load_images(directory_path)
if not images:
raise ValueError("No images found in the specified directory.")
num_images = len(images)
selected_index = seed % num_images
selected_image_path = images[selected_index]
image = Image.open(selected_image_path)
image = ImageOps.exif_transpose(image)
image = image.convert("RGB")
image_np = np.array(image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_np)[None,]
return (image_tensor, selected_image_path)
def load_images(self, directory):
supported_formats = ["jpg", "jpeg", "png", "bmp", "gif"]
return sorted([os.path.join(directory, f) for f in os.listdir(directory)
if os.path.isfile(os.path.join(directory, f)) and f.split('.')[-1].lower() in supported_formats])
-191
View File
@@ -1,191 +0,0 @@
import torch
import numpy as np
from PIL import Image
import sys
import OpenGL.GL as gl
import glfw
import ctypes
from comfy.utils import ProgressBar
VERTEX_SHADER = """
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform vec2 iResolution;
void main()
{
vec2 scale = vec2(1.0, iResolution.y / iResolution.x);
gl_Position = vec4(aPos.xy * scale, aPos.z, 1.0);
TexCoord = aTexCoord;
}
"""
FRAGMENT_SHADER = """
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D iChannel0;
uniform vec3 iResolution;
uniform float iTime;
uniform float iScale;
uniform float iSwirl;
uniform float iSwirlStrength;
uniform float iIterations;
uniform float iTimeSpeed;
void main()
{
vec4 O = vec4(0.0);
vec2 U = TexCoord;
float s = 0.0, s2 = 0.0, t = iTime * iTimeSpeed;
U = U - 0.5;
U.x += 0.03 * sin(1.14 * t);
float sc = pow(iScale, -mod(t, 2.0) - 0.8);
U *= sc;
for (int i = 0; i < int(iIterations); i++) {
vec2 V = abs(U + U);
if (max(V.x, V.y) > 1.0) break;
V = smoothstep(1.0, 0.5, V);
float m = V.x * V.y;
O = mix(O, texture(iChannel0, U + 0.5), m);
s = mix(s, 1.0, m);
s2 = s2 * (1.0 - m) * (1.0 - m) + m * m;
U *= iScale;
if (iSwirl > 0.5) {
U.x = -U.x * iSwirlStrength;
}
}
vec4 mean = texture(iChannel0, U, 10.0);
O = mean + (O - s * mean) / sqrt(s2);
FragColor = O;
}
"""
class FL_InfiniteZoom:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"scale": ("FLOAT", {"default": 2.00, "min": 1.10, "max": 10.00, "step": 0.05}),
"mirror": (["on", "off"],),
"mirror_warp": ("FLOAT", {"default": 1.00, "min": 0.50, "max": 1.50, "step": 0.05}),
"iterations": ("INT", {"default": 10, "min": 1, "max": 100, "step": 1}),
"speed": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
"fps": ("INT", {"default": 30, "min": 1, "max": 120, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_shader"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_shader(self, images, scale, mirror, mirror_warp, iterations, speed, fps):
result = []
total_images = len(images)
pbar = ProgressBar(total_images)
frame_time = 1.0 / fps
for i, image in enumerate(images, start=1):
img = self.t2p(image)
result_img = self.process_image(img, scale, mirror, mirror_warp, iterations, speed, i * frame_time)
result_img = self.p2t(result_img)
result.append(result_img)
pbar.update_absolute(i)
return (torch.cat(result, dim=0),)
def process_image(self, image, scale, mirror, mirror_warp, iterations, speed, time):
img_array = np.array(image).astype(np.float32) / 255.0
if not glfw.init():
raise RuntimeError("Failed to initialize GLFW")
glfw.window_hint(glfw.VISIBLE, glfw.FALSE)
window = glfw.create_window(image.width, image.height, "Hidden Window", None, None)
if not window:
glfw.terminate()
raise RuntimeError("Failed to create GLFW window")
glfw.make_context_current(window)
# Set the viewport
gl.glViewport(0, 0, image.width, image.height)
vertex_shader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
gl.glShaderSource(vertex_shader, VERTEX_SHADER)
gl.glCompileShader(vertex_shader)
fragment_shader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
gl.glShaderSource(fragment_shader, FRAGMENT_SHADER)
gl.glCompileShader(fragment_shader)
shader_program = gl.glCreateProgram()
gl.glAttachShader(shader_program, vertex_shader)
gl.glAttachShader(shader_program, fragment_shader)
gl.glLinkProgram(shader_program)
gl.glUseProgram(shader_program)
vertices = np.array([
-1.0, -1.0, 0.0, 0.0, 0.0,
1.0, -1.0, 0.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 0.0, 1.0,
1.0, 1.0, 0.0, 1.0, 1.0
], dtype=np.float32)
vao = gl.glGenVertexArrays(1)
gl.glBindVertexArray(vao)
vbo = gl.glGenBuffers(1)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_STATIC_DRAW)
gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, 5 * vertices.itemsize, None)
gl.glEnableVertexAttribArray(0)
gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, gl.GL_FALSE, 5 * vertices.itemsize, ctypes.c_void_p(3 * vertices.itemsize))
gl.glEnableVertexAttribArray(1)
texture = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.width, image.height, 0, gl.GL_RGB, gl.GL_FLOAT, img_array)
gl.glUniform1i(gl.glGetUniformLocation(shader_program, "iChannel0"), 0)
gl.glUniform2f(gl.glGetUniformLocation(shader_program, "iResolution"), image.width, image.height)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iTime"), time)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iScale"), scale)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iSwirl"), 1.0 if mirror == "on" else 0.0)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iSwirlStrength"), mirror_warp)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iIterations"), iterations)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iTimeSpeed"), speed)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
img_data = gl.glReadPixels(0, 0, image.width, image.height, gl.GL_RGB, gl.GL_FLOAT)
img_array = np.frombuffer(img_data, dtype=np.float32).reshape((image.height, image.width, 3))
gl.glDeleteTextures(1, (texture,))
gl.glDeleteBuffers(1, [vbo])
gl.glDeleteVertexArrays(1, [vao])
gl.glDeleteProgram(shader_program)
gl.glDeleteShader(vertex_shader)
gl.glDeleteShader(fragment_shader)
glfw.destroy_window(window)
glfw.terminate()
processed_image = Image.fromarray((img_array * 255).astype(np.uint8))
return processed_image
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-336
View File
@@ -1,336 +0,0 @@
import comfy.utils
import math
import nodes
import numpy as np
import torch
from scipy.ndimage import gaussian_filter, grey_dilation, binary_fill_holes, binary_closing
class FL_InpaintCrop:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"context_expand_pixels": ("INT", {"default": 10, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}),
"context_expand_factor": ("FLOAT", {"default": 1.01, "min": 1.0, "max": 100.0, "step": 0.01}),
"invert_mask": ("BOOLEAN", {"default": False}),
"fill_mask_holes": ("BOOLEAN", {"default": True}),
"mode": (["free size", "forced size"], {"default": "free size"}),
"force_size": ([512, 768, 1024, 1344, 2048, 4096, 8192], {"default": 1024}),
"rescale_factor": ("FLOAT", {"default": 1.00, "min": 0.01, "max": 100.0, "step": 0.01}),
"padding": ([8, 16, 32, 64, 128, 256, 512], {"default": 32}),
},
"optional": {
"optional_context_mask": ("MASK",),
}
}
CATEGORY = "🏵️Fill Nodes/utility"
RETURN_TYPES = ("STITCH", "IMAGE", "MASK")
RETURN_NAMES = ("stitch", "cropped_image", "cropped_mask")
FUNCTION = "inpaint_crop"
def adjust_to_square(self, x_min, x_max, y_min, y_max, width, height, target_size = None):
if target_size is None:
x_size = x_max - x_min + 1
y_size = y_max - y_min + 1
target_size = max(x_size, y_size)
# Calculate the midpoint of the current x and y ranges
x_mid = (x_min + x_max) // 2
y_mid = (y_min + y_max) // 2
# Adjust x_min, x_max, y_min, y_max to make the range square centered around the midpoints
x_min = max(x_mid - target_size // 2, 0)
x_max = x_min + target_size - 1
y_min = max(y_mid - target_size // 2, 0)
y_max = y_min + target_size - 1
# Ensure the ranges do not exceed the image boundaries
if x_max >= width:
x_max = width - 1
x_min = x_max - target_size + 1
if y_max >= height:
y_max = height - 1
y_min = y_max - target_size + 1
# Additional checks to make sure all coordinates are within bounds
if x_min < 0:
x_min = 0
x_max = target_size - 1
if y_min < 0:
y_min = 0
y_max = target_size - 1
return x_min, x_max, y_min, y_max
def apply_padding(self, min_val, max_val, max_boundary, padding):
# Calculate the midpoint and the original range size
original_range_size = max_val - min_val + 1
midpoint = (min_val + max_val) // 2
# Determine the smallest multiple of padding that is >= original_range_size
if original_range_size % padding == 0:
new_range_size = original_range_size
else:
new_range_size = (original_range_size // padding + 1) * padding
# Calculate the new min and max values centered on the midpoint
new_min_val = max(midpoint - new_range_size // 2, 0)
new_max_val = new_min_val + new_range_size - 1
# Ensure the new max doesn't exceed the boundary
if new_max_val >= max_boundary:
new_max_val = max_boundary - 1
new_min_val = max(new_max_val - new_range_size + 1, 0)
# Ensure the range still ends on a multiple of padding
# Adjust if the calculated range isn't feasible within the given constraints
if (new_max_val - new_min_val + 1) != new_range_size:
new_min_val = max(new_max_val - new_range_size + 1, 0)
return new_min_val, new_max_val
# Parts of this function are from KJNodes: https://github.com/kijai/ComfyUI-KJNodes
def inpaint_crop(self, image, mask, context_expand_pixels, context_expand_factor, invert_mask, fill_mask_holes, mode, force_size, rescale_factor, padding, optional_context_mask = None):
original_image = image
original_mask = mask
original_width = image.shape[2]
original_height = image.shape[1]
#Validate or initialize mask
if mask.shape[1] != image.shape[1] or mask.shape[2] != image.shape[2]:
non_zero_indices = torch.nonzero(mask[0], as_tuple=True)
if not non_zero_indices[0].size(0):
mask = torch.zeros_like(image[:, :, :, 0])
else:
assert False, "mask size must match image size"
# Invert mask if requested
if invert_mask:
mask = 1.0 - mask
# Fill holes if requested
if fill_mask_holes:
holemask = mask.reshape((-1, mask.shape[-2], mask.shape[-1])).cpu()
out = []
for m in holemask:
mask_np = m.numpy()
binary_mask = mask_np > 0
struct = np.ones((5, 5))
closed_mask = binary_closing(binary_mask, structure=struct, border_value=1)
filled_mask = binary_fill_holes(closed_mask)
output = filled_mask.astype(np.float32) * 255
output = torch.from_numpy(output)
out.append(output)
mask = torch.stack(out, dim=0)
mask = torch.clamp(mask, 0.0, 1.0)
# Validate or initialize context mask
if optional_context_mask is None:
context_mask = mask
elif optional_context_mask.shape[1] != image.shape[1] or optional_context_mask.shape[2] != image.shape[2]:
non_zero_indices = torch.nonzero(optional_context_mask[0], as_tuple=True)
if not non_zero_indices[0].size(0):
context_mask = mask
else:
assert False, "context_mask size must match image size"
else:
context_mask = optional_context_mask + mask
context_mask = torch.clamp(context_mask, 0.0, 1.0)
# If there are no non-zero indices in the context_mask, return the original image and original mask
non_zero_indices = torch.nonzero(context_mask[0], as_tuple=True)
if not non_zero_indices[0].size(0):
stitch = {'x': 0, 'y': 0, 'original_image': original_image, 'cropped_mask': mask, 'rescale_x': 1.0, 'rescale_y': 1.0}
return (stitch, original_image, original_mask)
# Compute context area from context mask
y_min = torch.min(non_zero_indices[0]).item()
y_max = torch.max(non_zero_indices[0]).item()
x_min = torch.min(non_zero_indices[1]).item()
x_max = torch.max(non_zero_indices[1]).item()
height = context_mask.shape[1]
width = context_mask.shape[2]
# Grow context area if requested
y_size = y_max - y_min + 1
x_size = x_max - x_min + 1
y_grow = round(max(y_size*(context_expand_factor-1), context_expand_pixels))
x_grow = round(max(x_size*(context_expand_factor-1), context_expand_pixels))
y_min = max(y_min - y_grow // 2, 0)
y_max = min(y_max + y_grow // 2, height - 1)
x_min = max(x_min - x_grow // 2, 0)
x_max = min(x_max + x_grow // 2, width - 1)
effective_upscale_factor_x = 1.0
effective_upscale_factor_y = 1.0
# Adjust to preferred size
if mode == 'forced size':
# Turn into square
x_min, x_max, y_min, y_max = self.adjust_to_square(x_min, x_max, y_min, y_max, width, height)
current_size = x_max - x_min + 1 # Assuming x_max - x_min == y_max - y_min due to square adjustment
if current_size != force_size:
# Upscale to fit in the force_size square, will be downsized at stitch phase
upscale_factor = force_size / current_size
samples = image
samples = samples.movedim(-1, 1)
width = math.floor(samples.shape[3] * upscale_factor)
height = math.floor(samples.shape[2] * upscale_factor)
samples = comfy.utils.bislerp(samples, width, height)
effective_upscale_factor_x = float(width)/float(original_width)
effective_upscale_factor_y = float(height)/float(original_height)
samples = samples.movedim(1, -1)
image = samples
samples = mask
samples = samples.unsqueeze(1)
samples = comfy.utils.bislerp(samples, width, height)
samples = samples.squeeze(1)
mask = samples
x_min = math.floor(x_min * effective_upscale_factor_x)
x_max = math.floor(x_max * effective_upscale_factor_x)
y_min = math.floor(y_min * effective_upscale_factor_y)
y_max = math.floor(y_max * effective_upscale_factor_y)
# Readjust to force size because the upscale math may not round well
x_min, x_max, y_min, y_max = self.adjust_to_square(x_min, x_max, y_min, y_max, width, height, target_size=force_size)
elif mode == 'free size':
# Upscale image and masks if requested, they will be downsized at stitch phase
if rescale_factor < 0.999 or rescale_factor > 1.001:
samples = image
samples = samples.movedim(-1, 1)
width = math.floor(samples.shape[3] * rescale_factor)
height = math.floor(samples.shape[2] * rescale_factor)
samples = comfy.utils.bislerp(samples, width, height)
effective_upscale_factor_x = float(width)/float(original_width)
effective_upscale_factor_y = float(height)/float(original_height)
samples = samples.movedim(1, -1)
image = samples
samples = mask
samples = samples.unsqueeze(1)
samples = comfy.utils.bislerp(samples, width, height)
samples = samples.squeeze(1)
mask = samples
x_min = math.floor(x_min * effective_upscale_factor_x)
x_max = math.floor(x_max * effective_upscale_factor_x)
y_min = math.floor(y_min * effective_upscale_factor_y)
y_max = math.floor(y_max * effective_upscale_factor_y)
# Ensure that context area doesn't go outside of the image
x_min = max(x_min, 0)
x_max = min(x_max, width - 1)
y_min = max(y_min, 0)
y_max = min(y_max, height - 1)
# Pad area (if possible, i.e. if pad is smaller than width/height) to avoid the sampler returning smaller results
if padding > 1:
x_min, x_max = self.apply_padding(x_min, x_max, width, padding)
y_min, y_max = self.apply_padding(y_min, y_max, height, padding)
# Crop the image and the mask, sized context area
cropped_image = image[:, y_min:y_max+1, x_min:x_max+1]
cropped_mask = mask[:, y_min:y_max+1, x_min:x_max+1]
# Return stitch (to be consumed by the class below), image, and mask
stitch = {'x': x_min, 'y': y_min, 'original_image': original_image, 'cropped_mask': cropped_mask, 'rescale_x': effective_upscale_factor_x, 'rescale_y': effective_upscale_factor_y}
return (stitch, cropped_image, cropped_mask)
class FL_Inpaint_Stitch:
"""
ComfyUI-InpaintCropAndStitch
https://github.com/lquesada/ComfyUI-InpaintCropAndStitch
This node stitches the inpainted image without altering unmasked areas.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"stitch": ("STITCH",),
"inpainted_image": ("IMAGE",),
}
}
CATEGORY = "🏵️Fill Nodes/utility"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "inpaint_stitch"
# This function is from comfy_extras: https://github.com/comfyanonymous/ComfyUI
def composite(self, destination, source, x, y, mask = None, multiplier = 8, resize_source = False):
source = source.to(destination.device)
if resize_source:
source = torch.nn.functional.interpolate(source, size=(destination.shape[2], destination.shape[3]), mode="bilinear")
source = comfy.utils.repeat_to_batch_size(source, destination.shape[0])
x = max(-source.shape[3] * multiplier, min(x, destination.shape[3] * multiplier))
y = max(-source.shape[2] * multiplier, min(y, destination.shape[2] * multiplier))
left, top = (x // multiplier, y // multiplier)
right, bottom = (left + source.shape[3], top + source.shape[2],)
if mask is None:
mask = torch.ones_like(source)
else:
mask = mask.to(destination.device, copy=True)
mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(source.shape[2], source.shape[3]), mode="bilinear")
mask = comfy.utils.repeat_to_batch_size(mask, source.shape[0])
# calculate the bounds of the source that will be overlapping the destination
# this prevents the source trying to overwrite latent pixels that are out of bounds
# of the destination
visible_width, visible_height = (destination.shape[3] - left + min(0, x), destination.shape[2] - top + min(0, y),)
mask = mask[:, :, :visible_height, :visible_width]
inverse_mask = torch.ones_like(mask) - mask
source_portion = mask * source[:, :, :visible_height, :visible_width]
destination_portion = inverse_mask * destination[:, :, top:bottom, left:right]
destination[:, :, top:bottom, left:right] = source_portion + destination_portion
return destination
def inpaint_stitch(self, stitch, inpainted_image):
original_image = stitch['original_image']
cropped_mask = stitch['cropped_mask']
x = stitch['x']
y = stitch['y']
stitched_image = original_image.clone().movedim(-1, 1)
inpaint_width = inpainted_image.shape[2]
inpaint_height = inpainted_image.shape[1]
# Downscale inpainted before stitching if we upscaled it before
if stitch['rescale_x'] < 0.999 or stitch['rescale_x'] > 1.001 or stitch['rescale_y'] < 0.999 or stitch['rescale_y'] > 1.001:
samples = inpainted_image.movedim(-1, 1)
width = round(float(inpaint_width)/stitch['rescale_x'])
height = round(float(inpaint_height)/stitch['rescale_y'])
x = round(float(x)/stitch['rescale_x'])
y = round(float(y)/stitch['rescale_y'])
samples = comfy.utils.bislerp(samples, width, height)
inpainted_image = samples.movedim(1, -1)
samples = cropped_mask.movedim(-1, 1)
samples = samples.unsqueeze(0)
samples = comfy.utils.bislerp(samples, width, height)
samples = samples.squeeze(0)
cropped_mask = samples.movedim(1, -1)
output = self.composite(stitched_image, inpainted_image.movedim(-1, 1), x, y, cropped_mask, 1).movedim(1, -1)
return (output,)
-205
View File
@@ -1,205 +0,0 @@
import torch
import math
from nodes import common_ksampler, VAEDecode, VAEEncode
import comfy.samplers
import comfy.utils
import logging
import numpy as np
from PIL import Image
import torch.nn.functional as F
import latent_preview
class FL_KsamplerPlus:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"input_type": (["latent", "image"],),
"x_slices": ("INT", {"default": 2, "min": 1, "max": 8}),
"y_slices": ("INT", {"default": 2, "min": 1, "max": 8}),
"overlap": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 0.9, "step": 0.01}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
"use_sliced_conditioning": ("BOOLEAN", {"default": True}),
},
"optional": {
"latent_image": ("LATENT",),
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "IMAGE")
RETURN_NAMES = ("model", "positive", "negative", "latent", "vae", "image")
FUNCTION = "sample"
CATEGORY = "🏵️Fill Nodes/Ksamplers"
@staticmethod
def crop_tensor(tensor, region):
x1, y1, x2, y2 = region
return tensor[:, :, y1:y2, x1:x2]
@staticmethod
def resize_tensor(tensor, size, mode="nearest-exact"):
return F.interpolate(tensor, size=size, mode=mode)
@staticmethod
def resize_region(region, init_size, resize_size):
x1, y1, x2, y2 = region
init_width, init_height = init_size
resize_width, resize_height = resize_size
x1 = math.floor(x1 * resize_width / init_width)
x2 = math.ceil(x2 * resize_width / init_width)
y1 = math.floor(y1 * resize_height / init_height)
y2 = math.ceil(y2 * resize_height / init_height)
return (x1, y1, x2, y2)
@classmethod
def crop_controlnet(cls, cond_dict, region, init_size, canvas_size, tile_size):
if "control" not in cond_dict:
return
c = cond_dict["control"]
controlnet = c.copy()
cond_dict["control"] = controlnet
while c is not None:
hint = controlnet.cond_hint_original
resized_crop = cls.resize_region(region, canvas_size, hint.shape[2:])
hint = cls.crop_tensor(hint, resized_crop)
hint = cls.resize_tensor(hint, tile_size)
controlnet.cond_hint_original = hint
c = c.previous_controlnet
controlnet.set_previous_controlnet(c.copy() if c is not None else None)
controlnet = controlnet.previous_controlnet
@classmethod
def crop_cond(cls, cond, region, init_size, canvas_size, tile_size):
cropped = []
for emb, x in cond:
cond_dict = x.copy()
cls.crop_controlnet(cond_dict, region, init_size, canvas_size, tile_size)
cropped.append([emb, cond_dict])
return cropped
def sample(self, model, positive, negative, x_slices, y_slices, overlap, batch_size, seed, steps, cfg, sampler_name,
scheduler, denoise, input_type, use_sliced_conditioning, latent_image=None, image=None, vae=None):
try:
device = comfy.model_management.get_torch_device()
if input_type == "image" and image is not None and vae is not None:
latent_image = VAEEncode().encode(vae, image)[0]
elif input_type == "latent" and latent_image is None:
raise ValueError("Latent image is required when input type is set to latent")
elif input_type == "image" and (image is None or vae is None):
raise ValueError("Both image and VAE are required when input type is set to image")
b, c, h, w = latent_image["samples"].shape
base_slice_height = h // y_slices
base_slice_width = w // x_slices
overlap_height = int(base_slice_height * overlap)
overlap_width = int(base_slice_width * overlap)
samples = torch.zeros_like(latent_image["samples"], device=device)
def create_blend_mask(height, width, overlap_h, overlap_w, is_top, is_left, is_bottom, is_right):
mask = torch.ones((height, width), device=device)
if overlap_h > 0:
if not is_top:
mask[:overlap_h, :] *= torch.linspace(0, 1, overlap_h, device=device)[:, None]
if not is_bottom:
mask[-overlap_h:, :] *= torch.linspace(1, 0, overlap_h, device=device)[:, None]
if overlap_w > 0:
if not is_left:
mask[:, :overlap_w] *= torch.linspace(0, 1, overlap_w, device=device)[None, :]
if not is_right:
mask[:, -overlap_w:] *= torch.linspace(1, 0, overlap_w, device=device)[None, :]
return mask
def process_slice(y, x):
y_start = max(0, y * base_slice_height - overlap_height)
y_end = min(h, (y + 1) * base_slice_height + overlap_height)
x_start = max(0, x * base_slice_width - overlap_width)
x_end = min(w, (x + 1) * base_slice_width + overlap_width)
section = latent_image["samples"][:, :, y_start:y_end, x_start:x_end].to(device=device)
if use_sliced_conditioning:
region = (x_start * 8, y_start * 8, x_end * 8, y_end * 8)
init_size = (w * 8, h * 8)
canvas_size = init_size
tile_size = ((x_end - x_start) * 8, (y_end - y_start) * 8)
cropped_positive = self.crop_cond(positive, region, init_size, canvas_size, tile_size)
cropped_negative = self.crop_cond(negative, region, init_size, canvas_size, tile_size)
else:
cropped_positive = positive
cropped_negative = negative
return section, y_start, y_end, x_start, x_end, cropped_positive, cropped_negative
total_slices = x_slices * y_slices
for i in range(0, total_slices, batch_size):
batch_slices = [(y, x) for y in range(y_slices) for x in range(x_slices)][
i:min(i + batch_size, total_slices)]
batch_sections = [process_slice(y, x) for y, x in batch_slices]
batch_latents = torch.cat([section for section, _, _, _, _, _, _ in batch_sections], dim=0)
if use_sliced_conditioning:
batch_positive = [cond for _, _, _, _, _, cond, _ in batch_sections]
batch_negative = [cond for _, _, _, _, _, _, cond in batch_sections]
else:
batch_positive = [positive] * len(batch_sections)
batch_negative = [negative] * len(batch_sections)
# Process each slice in the batch individually
for j, (slice_latent, slice_positive, slice_negative) in enumerate(
zip(torch.split(batch_latents, 1), batch_positive, batch_negative)):
processed_slice = common_ksampler(model, seed + i + j, steps, cfg, sampler_name, scheduler,
slice_positive, slice_negative,
{"samples": slice_latent}, denoise=denoise)[0]
_, y_start, y_end, x_start, x_end, _, _ = batch_sections[j]
is_top = y_start == 0
is_left = x_start == 0
is_bottom = y_end == h
is_right = x_end == w
blend_mask = create_blend_mask(y_end - y_start, x_end - x_start,
overlap_height, overlap_width,
is_top, is_left, is_bottom, is_right)
blend_mask = blend_mask.unsqueeze(0).unsqueeze(0).expand_as(processed_slice["samples"])
processed_slice = processed_slice["samples"].to(device=device)
blend_mask = blend_mask.to(device=device)
samples[:, :, y_start:y_end, x_start:x_end] = (
samples[:, :, y_start:y_end, x_start:x_end] * (1 - blend_mask) +
processed_slice * blend_mask
)
if device.type == 'cuda':
torch.cuda.empty_cache()
output_image = None
if vae is not None:
vae_decoder = VAEDecode()
output_image = vae_decoder.decode(vae, {"samples": samples})[0]
return (model, positive, negative, {"samples": samples}, vae, output_image)
except Exception as e:
logging.error(f"Error in FL_UltimateUpscale: {str(e)}")
raise
@classmethod
def IS_CHANGED(s, model, positive, negative, x_slices, y_slices, overlap, batch_size, seed, steps, cfg,
sampler_name, scheduler, denoise, input_type, use_sliced_conditioning, latent_image=None, image=None,
vae=None):
return float("NaN")
-131
View File
@@ -1,131 +0,0 @@
import comfy.samplers
class FL_KsamplerSettings:
RATIO = [
("1:1___SD 512x512", 512, 512),
("4:3___SD 682x512", 682, 512),
("3:2___SD 768x512", 768, 512),
("16:9__SD 910x512", 910, 512),
("1:85:1 SD 952x512", 952, 512),
("2:1___SD 1024x512", 1024, 512),
("1:1_SV3D 576x576", 576, 576),
("16:9_SVD 576x1024", 1024, 576),
("1:1__SD2 768x768", 768, 768),
("1:1___XL 1024x1024", 1024, 1024),
("16:15_XL 1024x960", 1024, 960),
("17:15_XL 1088x960", 1088, 960),
("17:14_XL 1088x896", 1088, 896),
("4:3___XL 1152x896", 1152, 896),
("18:13_XL 1152x832", 1152, 832),
("3:2___XL 1216x832", 1216, 832),
("5:3___XL 1280x768", 1280, 768),
("7:4___XL 1344x768", 1344, 768),
("21:11_XL 1344x704", 1344, 704),
("2:1___XL 1408x704", 1408, 704),
("23:11_XL 1472x704", 1472, 704),
("21:9__XL 1536x640", 1536, 640),
("5:2___XL 1600x640", 1600, 640),
("26:9__XL 1664x576", 1664, 576),
("3:1___XL 1728x576", 1728, 576),
("28:9__XL 1792x576", 1792, 576),
("29:8__XL 1856x512", 1856, 512),
("15:4__XL 1920x512", 1920, 512),
("31:8__XL 1984x512", 1984, 512),
("4:1___XL 2048x512", 2048, 512),
]
@classmethod
def INPUT_TYPES(cls):
aspect_ratio_titles = [title for title, res1, res2 in cls.RATIO]
rotation = ("landscape", "portrait")
return {
"required": {
"Aspect_Ratio": (aspect_ratio_titles,
{"default": ("1:1___XL 1024x1024")}),
"rotation": (rotation,),
},
"optional": {
"batch": ("INT", {
"default": 1,
"min": 1,
"max": 10000,
}),
"Pass_1_steps": ("INT", {
"default": 25,
"min": 1,
"max": 10000,
}),
"Pass_2_steps": ("INT", {
"default": 25,
"min": 1,
"max": 10000,
}),
"Pass_1_CFG": ("FLOAT", {
"default": 6.0,
"min": -10.0,
"max": 100.0,
"step": 0.1,
"round": 0.1,
}),
"Pass_2_CFG": ("FLOAT", {
"default": 6.0,
"min": -10.0,
"max": 100.0,
"step": 0.1,
"round": 0.1,
}),
"Pass_2_denoise": ("FLOAT", {
"default": 0.500,
"min": -10.000,
"max": 100.000,
"step": 0.001,
"round": 0.01,
}),
"scale_factor": ("FLOAT", {
"default": 1.5,
"min": 1.0,
"max": 10.0,
"step": 0.1,
"round": 0.1,
}),
"sampler": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,)
}
}
RETURN_TYPES = (
"INT", "INT", "INT", "INT", "INT", "FLOAT",
"FLOAT", "FLOAT", "FLOAT", comfy.samplers.KSampler.SAMPLERS,
comfy.samplers.KSampler.SCHEDULERS,)
RETURN_NAMES = (
"WIDTH",
"HEIGHT",
"BATCH_SIZE",
"Pass_1_steps",
"Pass_2_steps",
"Pass_1_CFG",
"Pass_2_CFG",
"Pass_2_denoise",
"SCALE",
"SAMPLER",
"SCHEDULER",
)
FUNCTION = "settings"
CATEGORY = "🏵️Fill Nodes/utility"
def settings(self, Aspect_Ratio, rotation, batch, Pass_1_steps, Pass_2_steps, Pass_1_CFG, Pass_2_CFG,
Pass_2_denoise, scale_factor, sampler, scheduler):
for title, width, height in self.RATIO:
if title == Aspect_Ratio:
if rotation == "portrait":
width, height = height, width # Swap for portrait orientation
return (
width, height, batch, Pass_1_steps, Pass_2_steps, Pass_1_CFG, Pass_2_CFG, Pass_2_denoise, scale_factor,
sampler, scheduler)
return (
None, None, batch, Pass_1_steps, Pass_2_steps, Pass_1_CFG, Pass_2_CFG, Pass_2_denoise, scale_factor, sampler,
scheduler) # In case the Aspect Ratio is not found
-79
View File
@@ -1,79 +0,0 @@
import os
from PIL import Image, ImageOps
class FL_MirrorAndAppendCaptions:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_directory": ("STRING", {"default": "X://path/to/images"}),
"caption_extension": ([".caption", ".txt"], {"default": ".txt"}),
"additional_text": ("STRING", {"default": "Frame"}),
"text_position": (["append", "prepend"], {"default": "append"}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("directory",)
OUTPUT_NODE = True
FUNCTION = "start"
CATEGORY = "🏵️Fill Nodes/Captioning"
def start(self, image_directory, caption_extension, additional_text, text_position):
if not os.path.exists(image_directory):
raise Exception(f"Directory {image_directory} does not exist")
image_files = [f for f in os.listdir(image_directory) if f.lower().endswith((".png", ".jpg", ".webp", ".jpeg"))]
image_files.sort() # Ensure consistent order
new_images = []
new_captions = []
for index, image_file in enumerate(image_files):
image_path = os.path.join(image_directory, image_file)
caption_path = os.path.splitext(image_path)[0] + caption_extension
# Process original image
pil_image = Image.open(image_path)
new_images.append((pil_image, image_file))
# Process mirrored image
mirrored_image = ImageOps.mirror(pil_image)
mirrored_image_file = os.path.splitext(image_file)[0] + "_Mirror" + os.path.splitext(image_file)[1]
new_images.append((mirrored_image, mirrored_image_file))
# Process captions
if os.path.exists(caption_path):
with open(caption_path, 'r', encoding='utf-8') as f:
caption = f.read().strip()
frame_text = f"{additional_text}_{index * 2}"
mirrored_frame_text = f"{additional_text}_{index * 2 + 1}"
if text_position == "append":
new_caption = f"{caption}, {frame_text}"
new_mirrored_caption = f"{caption}, {mirrored_frame_text}"
else: # prepend
new_caption = f"{frame_text}, {caption}"
new_mirrored_caption = f"{mirrored_frame_text}, {caption}"
new_captions.append((new_caption, image_file))
new_captions.append((new_mirrored_caption, mirrored_image_file))
# Save new images and captions
for img, filename in new_images:
img.save(os.path.join(image_directory, filename))
for caption, filename in new_captions:
caption_filename = os.path.splitext(filename)[0] + caption_extension
with open(os.path.join(image_directory, caption_filename), "w", encoding="utf-8") as f:
f.write(caption)
return (image_directory,)
# Register the node in the ComfyUI system
def register_node():
return FL_MirrorAndAppendCaptions
-90
View File
@@ -1,90 +0,0 @@
import fnmatch
import os
import torch
import random
import numpy as np
from pathlib import Path
from PIL import Image
from .sup import ROOT
class FL_NFTGenerator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"folder_path": ("STRING", {"default": ""}),
"dummy_seed": ("INT", {"default": 0, "min": 0, "max": 1000000}),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE")
FUNCTION = "generate_nft"
CATEGORY = "🏵️Fill Nodes/experiments"
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def generate_nft(self, folder_path, dummy_seed):
if not (path := Path(ROOT / folder_path)).is_dir():
if not (path := Path(folder_path)).is_dir():
raise ValueError(f"Folder path does not exist: {folder_path}")
image_files = [str(f) for f in path.glob('*') if not fnmatch.fnmatch(f.name, '*-mask.*')]
if len(image_files) == 0:
raise ValueError(f"No image files found in the folder: {folder_path}")
# Extract rarity percentages from image filenames
rarities = []
for image_file in image_files:
if "-" in image_file:
# name-alexperval-was
rarity_str = image_file.split("-")[1].split("per")[0]
rarity = int(rarity_str)
rarities.append(rarity)
else:
raise ValueError(f"Invalid image filename format: {image_file}")
# Calculate cumulative probabilities
total_rarity = sum(rarities)
probabilities = [rarity / total_rarity for rarity in rarities]
cumulative_probabilities = [sum(probabilities[:i+1]) for i in range(len(probabilities))]
# Generate a random number between 0 and 1 using the dummy seed
random.seed(dummy_seed)
random_number = random.random()
# Find the index of the selected image based on the random number and cumulative probabilities
selected_index = None
for i, prob in enumerate(cumulative_probabilities):
if random_number <= prob:
selected_index = i
break
if selected_index is None:
raise ValueError("Failed to select an image based on rarity.")
# Get the selected image and its corresponding mask
selected_image_file = image_files[selected_index]
# selected_image_path = os.path.join(folder_path, selected_image_file)
selected_image = Image.open(selected_image_file)
# Get the file extension of the selected image
_, extension = os.path.splitext(selected_image_file)
# Generate the mask filename based on the selected image filename
mask_file = selected_image_file.rsplit(".", 1)[0] + "-mask" + extension
mask_path = os.path.join(folder_path, mask_file)
if os.path.exists(mask_path):
mask_image = Image.open(mask_path)
else:
# Create a blank mask image if the corresponding mask is not found
mask_image = Image.new("RGB", selected_image.size, (0, 0, 0))
selected_image_tensor = torch.from_numpy(np.array(selected_image).astype(np.float32) / 255.0).unsqueeze(0)
mask_image_tensor = torch.from_numpy(np.array(mask_image).astype(np.float32) / 255.0).unsqueeze(0)
return (selected_image_tensor, mask_image_tensor)
-291
View File
@@ -1,291 +0,0 @@
import glfw
import ctypes
import torch
import numpy as np
from PIL import Image
import OpenGL.GL as gl
from comfy.utils import ProgressBar
VERTEX_SHADER = """
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
TexCoord = aTexCoord;
}
"""
FRAGMENT_SHADER = """
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform vec3 iResolution;
uniform float iTime;
uniform float iAngleNum;
uniform float iSampNum;
uniform float iLineWidth;
uniform float iVignette;
#define Res0 textureSize(iChannel0, 0)
#define Res1 textureSize(iChannel1, 0)
#define Res iResolution.xy
#define randSamp iChannel1
#define colorSamp iChannel0
vec4 getRand(vec2 pos)
{
return textureLod(iChannel1, pos / Res1 / iResolution.y * 1080., 0.0);
}
vec4 getCol(vec2 pos)
{
vec2 uv = ((pos - Res.xy * .5) / Res.y * Res0.y) / Res0.xy + .5;
vec4 c1 = texture(iChannel0, uv);
vec4 e = smoothstep(vec4(-0.05), vec4(-0.0), vec4(uv, vec2(1) - uv));
c1 = mix(vec4(1, 1, 1, 0), c1, e.x * e.y * e.z * e.w);
float d = clamp(dot(c1.xyz, vec3(-.5, 1., -.5)), 0.0, 1.0);
vec4 c2 = vec4(.7);
return min(mix(c1, c2, 1.8 * d), .7);
}
vec4 getColHT(vec2 pos)
{
return smoothstep(.95, 1.05, getCol(pos) * .8 + .2 + getRand(pos * .7));
}
float getVal(vec2 pos)
{
vec4 c = getCol(pos);
return pow(dot(c.xyz, vec3(.333)), 1.) * 1.;
}
vec2 getGrad(vec2 pos, float eps)
{
vec2 d = vec2(eps, 0);
return vec2(
getVal(pos + d.xy) - getVal(pos - d.xy),
getVal(pos + d.yx) - getVal(pos - d.yx)
) / eps / 2.;
}
#define PI2 6.28318530717959
void main()
{
vec2 pos = TexCoord * iResolution.xy + 4.0 * sin(iTime * 1. * vec2(1, 1.7)) * iResolution.y / 400.;
vec3 col = vec3(0);
vec3 col2 = vec3(0);
float sum = 0.;
for (int i = 0; i < int(iAngleNum); i++)
{
float ang = PI2 / iAngleNum * (float(i) + .8);
vec2 v = vec2(cos(ang), sin(ang));
for (int j = 0; j < int(iSampNum); j++)
{
vec2 dpos = v.yx * vec2(1, -1) * float(j) * iLineWidth * iResolution.y / 400.;
vec2 dpos2 = v.xy * float(j * j) / iSampNum * .5 * iLineWidth * iResolution.y / 400.;
vec2 g;
float fact;
float fact2;
for (float s = -1.; s <= 1.; s += 2.)
{
vec2 pos2 = pos + s * dpos + dpos2;
vec2 pos3 = pos + (s * dpos + dpos2).yx * vec2(1, -1) * 2.;
g = getGrad(pos2, .4);
fact = dot(g, v) - .5 * abs(dot(g, v.yx * vec2(1, -1)));
fact2 = dot(normalize(g + vec2(.0001)), v.yx * vec2(1, -1));
fact = clamp(fact, 0., .05);
fact2 = abs(fact2);
fact *= 1. - float(j) / iSampNum;
col += fact;
col2 += fact2 * getColHT(pos3).xyz;
sum += fact2;
}
}
}
col /= iSampNum * iAngleNum * .75 / sqrt(iResolution.y);
col2 /= sum;
col.x *= (.6 + .8 * getRand(pos * .7).x);
col.x = 1. - col.x;
col.x *= col.x * col.x;
vec2 s = sin(pos.xy * .1 / sqrt(iResolution.y / 400.));
vec3 karo = vec3(1);
karo -= .5 * vec3(.25, .1, .1) * dot(exp(-s * s * 80.), vec2(1));
float r = length(pos - iResolution.xy * .5) / iResolution.x;
float vign = 1. - r * r * r * iVignette;
FragColor = vec4(vec3(col.x * col2 * karo * vign), 1);
}
"""
class FL_PaperDrawn:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
"optional": {
"angle_num": ("FLOAT", {"default": 3.0, "min": 1.0, "max": 10.0, "step": 1.0}),
"samp_num": ("FLOAT", {"default": 2.2, "min": 1.0, "max": 10.0, "step": 0.1}),
"line_width": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
"vignette": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.1}),
"fps": ("INT", {"default": 30, "min": 1, "max": 120, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_shader"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_shader(self, image, angle_num, samp_num, line_width, vignette, fps):
result = []
total_images = len(image)
frame_time = 1.0 / fps
pbar = ProgressBar(total_images)
for i, img in enumerate(image, start=1):
img = self.t2p(img)
result_img = self.process_image(img, angle_num, samp_num, line_width, vignette, i * frame_time)
result_img = self.p2t(result_img)
result.append(result_img)
pbar.update_absolute(i)
return (torch.cat(result, dim=0),)
def process_image(self, image, angle_num, samp_num, line_width, vignette, time):
# Convert the PIL image to a numpy array
img_array = np.array(image).astype(np.float32) / 255.0
# Create a white image for iChannel1
white_image = np.ones((image.height, image.width, 3), dtype=np.float32)
# Create a PyOpenGL context
if not glfw.init():
raise RuntimeError("Failed to initialize GLFW")
glfw.window_hint(glfw.VISIBLE, glfw.FALSE)
window = glfw.create_window(image.width, image.height, "Hidden Window", None, None)
if not window:
glfw.terminate()
raise RuntimeError("Failed to create GLFW window")
glfw.make_context_current(window)
# Compile the shader program
vertex_shader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
gl.glShaderSource(vertex_shader, VERTEX_SHADER)
gl.glCompileShader(vertex_shader)
fragment_shader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
gl.glShaderSource(fragment_shader, FRAGMENT_SHADER)
gl.glCompileShader(fragment_shader)
shader_program = gl.glCreateProgram()
gl.glAttachShader(shader_program, vertex_shader)
gl.glAttachShader(shader_program, fragment_shader)
gl.glLinkProgram(shader_program)
gl.glUseProgram(shader_program)
# Set up vertex buffer object (VBO) and vertex array object (VAO)
vertices = np.array([
-1.0, -1.0, 0.0, 0.0, 0.0,
1.0, -1.0, 0.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 0.0, 1.0,
1.0, 1.0, 0.0, 1.0, 1.0
], dtype=np.float32)
vao = gl.glGenVertexArrays(1)
gl.glBindVertexArray(vao)
vbo = gl.glGenBuffers(1)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_STATIC_DRAW)
gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, 5 * vertices.itemsize, None)
gl.glEnableVertexAttribArray(0)
gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, gl.GL_FALSE, 5 * vertices.itemsize, ctypes.c_void_p(3 * vertices.itemsize))
gl.glEnableVertexAttribArray(1)
# Set up textures
texture0 = gl.glGenTextures(1)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture0)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.width, image.height, 0, gl.GL_RGB, gl.GL_FLOAT, img_array)
texture1 = gl.glGenTextures(1)
gl.glActiveTexture(gl.GL_TEXTURE1)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture1)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.width, image.height, 0, gl.GL_RGB, gl.GL_FLOAT, white_image)
# Set shader uniforms
gl.glUniform1i(gl.glGetUniformLocation(shader_program, "iChannel0"), 0)
gl.glUniform1i(gl.glGetUniformLocation(shader_program, "iChannel1"), 1)
gl.glUniform3f(gl.glGetUniformLocation(shader_program, "iResolution"), image.width, image.height, 0.0)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iTime"), time)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iAngleNum"), angle_num)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iSampNum"), samp_num)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iLineWidth"), line_width)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iVignette"), vignette)
# Render the shader
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
# Read the rendered image from the framebuffer
img_data = gl.glReadPixels(0, 0, image.width, image.height, gl.GL_RGB, gl.GL_FLOAT)
img_array = np.frombuffer(img_data, dtype=np.float32).reshape((image.height, image.width, 3))
# Clean up OpenGL resources
gl.glDeleteTextures(2, [texture0, texture1])
gl.glDeleteBuffers(1, [vbo])
gl.glDeleteVertexArrays(1, [vao])
gl.glDeleteProgram(shader_program)
gl.glDeleteShader(vertex_shader)
gl.glDeleteShader(fragment_shader)
glfw.destroy_window(window)
glfw.terminate()
# Convert the processed image back to a PIL image
processed_image = Image.fromarray((img_array * 255).astype(np.uint8))
return processed_image
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-152
View File
@@ -1,152 +0,0 @@
import torch
import torch.nn.functional as F
from torchvision.ops import masks_to_boxes
from torchvision.transforms.functional import resize as tv_resize, InterpolationMode
import numpy as np
from PIL import Image
class FL_PasteOnCanvas:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("IMAGE",),
"canvas_width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 32}),
"canvas_height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 32}),
"background_red": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
"background_green": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
"background_blue": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
"padding": ("INT", {"default": 0, "min": 0, "max": 512, "step": 1}),
"resize_algorithm": (["bilinear", "nearest", "bicubic", "lanczos"],),
"include_alpha": ("BOOLEAN", {"default": False}),
},
"optional": {
"bg_image_optional": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "cut_and_paste"
CATEGORY = "🏵️Fill Nodes/utility"
def cut_and_paste(self, image, mask, canvas_width, canvas_height, background_red, background_green, background_blue,
padding, resize_algorithm, include_alpha, bg_image_optional=None):
# Ensure inputs are in the correct format
image = self.tensor_to_rgba(image)
mask = self.tensor_to_mask(mask)
B, H, W, C = image.shape
mask = F.interpolate(mask.unsqueeze(1), size=(H, W), mode='nearest')[:, 0, :, :]
MB, MH, MW = mask.shape
if MB < B:
assert B % MB == 0, "Batch size mismatch between image and mask"
mask = mask.repeat(B // MB, 1, 1)
# Prepare the background canvas
if bg_image_optional is not None:
canvas = self.prepare_background_image(bg_image_optional, canvas_width, canvas_height, B)
else:
background_color = torch.tensor([background_red, background_green, background_blue, 255],
dtype=torch.float32, device=image.device) / 255.0
canvas = background_color.expand(B, canvas_height, canvas_width, 4).clone()
# Handle empty masks
is_empty = ~torch.gt(mask.view(MB, -1).max(dim=1).values, 0)
mask[is_empty, 0, 0] = 1
boxes = masks_to_boxes(mask)
mask[is_empty, 0, 0] = 0
# Create alpha mask
alpha_mask = torch.ones((B, H, W, 4), device=image.device)
alpha_mask[..., 3] = mask
masked_image = image * alpha_mask
for i in range(B):
if not is_empty[i]:
box = boxes[i].long()
y1, x1, y2, x2 = box[1], box[0], box[3], box[2]
cropped = masked_image[i, y1:y2 + 1, x1:x2 + 1, :]
# Calculate scaling factor to fit within canvas, considering padding
available_width = canvas_width - 2 * padding
available_height = canvas_height - 2 * padding
scale = min(available_width / cropped.shape[1], available_height / cropped.shape[0])
new_h, new_w = int(cropped.shape[0] * scale), int(cropped.shape[1] * scale)
# Resize cropped image using the specified algorithm
resized = self.resize_image(cropped, (new_h, new_w), resize_algorithm)
# Calculate position to center the image on canvas, including padding
start_y = padding + (available_height - new_h) // 2
start_x = padding + (available_width - new_w) // 2
# Prepare the region of the canvas where we'll paste the image
canvas_region = canvas[i, start_y:start_y + new_h, start_x:start_x + new_w].clone()
# Blend the resized image with the canvas region
alpha = resized[..., 3:4]
blended = resized[..., :3] * alpha + canvas_region[..., :3] * (1 - alpha)
# Update the alpha channel
new_alpha = torch.maximum(canvas_region[..., 3:], resized[..., 3:])
# Combine the blended color channels with the new alpha
result = torch.cat([blended, new_alpha], dim=-1)
# Update the canvas with the result
canvas[i, start_y:start_y + new_h, start_x:start_x + new_w] = result
# Remove alpha channel if not included
if not include_alpha:
canvas = canvas[..., :3]
return (canvas,)
def prepare_background_image(self, bg_image_optional, canvas_width, canvas_height, batch_size):
bg_image_optional = self.tensor_to_rgba(bg_image_optional)
# Resize background image to match canvas size
resized_bg = F.interpolate(bg_image_optional.permute(0, 3, 1, 2),
size=(canvas_height, canvas_width),
mode='bilinear',
align_corners=False).permute(0, 2, 3, 1)
# If the background image batch size is 1, repeat it to match the main batch size
if resized_bg.shape[0] == 1 and batch_size > 1:
resized_bg = resized_bg.repeat(batch_size, 1, 1, 1)
return resized_bg
def resize_image(self, image, size, algorithm):
if algorithm == "lanczos":
# Convert to PIL Image for Lanczos resampling
pil_image = Image.fromarray((image.cpu().numpy() * 255).astype('uint8'))
resized_pil = pil_image.resize(size[::-1], Image.LANCZOS) # PIL uses (width, height)
return torch.from_numpy(np.array(resized_pil)).float().to(image.device) / 255.0
else:
# Use torchvision's resize for other algorithms
interpolation_mode = {
"bilinear": InterpolationMode.BILINEAR,
"nearest": InterpolationMode.NEAREST,
"bicubic": InterpolationMode.BICUBIC,
}[algorithm]
return tv_resize(image.permute(2, 0, 1), size, interpolation=interpolation_mode).permute(1, 2, 0)
@staticmethod
def tensor_to_rgba(tensor):
if len(tensor.shape) == 3:
return tensor.unsqueeze(-1).expand(-1, -1, -1, 4)
elif tensor.shape[-1] == 1:
return tensor.expand(-1, -1, -1, 4)
elif tensor.shape[-1] == 3:
return torch.cat([tensor, torch.ones_like(tensor[:, :, :, :1])], dim=-1)
return tensor
@staticmethod
def tensor_to_mask(tensor):
if len(tensor.shape) == 4:
return tensor.mean(dim=-1)
return tensor
-131
View File
@@ -1,131 +0,0 @@
import torch
import numpy as np
from PIL import Image
from sklearn.cluster import KMeans
from comfy.utils import ProgressBar
class FL_PixelArtShader:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"pixel_size": ("FLOAT", {"default": 100.0, "min": 1.0, "max": 1000.0, "step": 1.0}),
"color_depth": ("FLOAT", {"default": 50.0, "min": 1.0, "max": 255.0, "step": 1.0}),
"use_aspect_ratio": ("BOOLEAN", {"default": True}),
"palette_image": ("IMAGE", {"default": None}),
"palette_colors": ("INT", {"default": 16, "min": 2, "max": 15, "step": 1}),
"mask": ("IMAGE", {"default": None}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_pixel_art_shader"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_pixel_art_shader(self, images, use_aspect_ratio, pixel_size, color_depth, palette_image=None,
palette_colors=16, mask=None):
result = []
total_images = len(images)
pbar = ProgressBar(total_images)
if palette_image is not None:
palette = extract_palette(self.t2p(palette_image[0]), palette_colors)
else:
palette = None
mask_images = self.prepare_mask_batch(mask, total_images) if mask is not None else None
for idx, image in enumerate(images):
img = self.t2p(image)
mask_img = self.process_mask(mask_images[idx], img.size) if mask_images is not None else None
result_img = pixel_art_effect(img, pixel_size, color_depth, use_aspect_ratio, palette, mask_img)
result_img = self.p2t(result_img)
result.append(result_img)
pbar.update_absolute(idx + 1)
return (torch.cat(result, dim=0),)
def t2p(self, t):
i = 255.0 * t.cpu().numpy().squeeze()
return Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
def p2t(self, p):
i = np.array(p).astype(np.float32) / 255.0
return torch.from_numpy(i).unsqueeze(0)
def prepare_mask_batch(self, mask, total_images):
if mask is None:
return None
mask_images = [self.t2p(m) for m in mask]
if len(mask_images) < total_images:
mask_images = mask_images * (total_images // len(mask_images) + 1)
return mask_images[:total_images]
def process_mask(self, mask, target_size):
mask = mask.resize(target_size, Image.LANCZOS)
return mask.convert('L') if mask.mode != 'L' else mask
def extract_palette(image, n_colors):
image = image.convert('RGB')
pixels = np.array(image).reshape(-1, 3)
kmeans = KMeans(n_clusters=n_colors, random_state=42)
kmeans.fit(pixels)
colors = kmeans.cluster_centers_
return torch.from_numpy(colors.astype(np.float32) / 255.0).to("cuda")
def pixel_art_effect(image, pixel_size, color_depth, use_aspect_ratio, palette, mask=None):
image = torch.tensor(np.array(image)).float().to("cuda") / 255.0
height, width = image.shape[0], image.shape[1]
uv_x = torch.linspace(0, 1, width, device="cuda")
uv_y = torch.linspace(0, 1, height, device="cuda")
uv_grid = torch.stack(torch.meshgrid(uv_y, uv_x), dim=-1)
output_tensor = evaluate_shader(image, uv_grid, pixel_size, color_depth, use_aspect_ratio)
if palette is not None:
output_tensor = apply_palette(output_tensor, palette)
if mask is not None:
mask_tensor = torch.tensor(np.array(mask)).float().to("cuda") / 255.0
mask_tensor = mask_tensor.unsqueeze(-1).expand(-1, -1, 3)
output_tensor = output_tensor * mask_tensor + image * (1 - mask_tensor)
return Image.fromarray((output_tensor.cpu().numpy() * 255).astype(np.uint8))
def evaluate_shader(image, uv_grid, pixel_size, color_depth, use_aspect_ratio):
if use_aspect_ratio:
aspect_ratio = image.shape[1] / image.shape[0]
pixel_size_x, pixel_size_y = pixel_size, pixel_size * aspect_ratio
else:
pixel_size_x = pixel_size_y = pixel_size
pixelUV_x = torch.floor(uv_grid[..., 1] * pixel_size_x) / pixel_size_x
pixelUV_y = torch.floor(uv_grid[..., 0] * pixel_size_y) / pixel_size_y
pixelUV = torch.stack((pixelUV_y, pixelUV_x), dim=-1)
color = texture_lookup(image, pixelUV)
return adjust_color(color, color_depth)
def adjust_color(color, color_depth):
return torch.floor(color * color_depth) / color_depth
def texture_lookup(image, uv):
uv = torch.clamp(uv, 0.0, 1.0)
y = (uv[..., 0] * (image.shape[0] - 1)).long()
x = (uv[..., 1] * (image.shape[1] - 1)).long()
return image[y, x]
def apply_palette(image, palette):
original_shape = image.shape
pixels = image.reshape(-1, 3)
distances = torch.cdist(pixels, palette)
nearest_palette_indices = torch.argmin(distances, dim=1)
new_pixels = palette[nearest_palette_indices]
return new_pixels.reshape(original_shape)
-75
View File
@@ -1,75 +0,0 @@
import torch
import numpy as np
from PIL import Image
from colorsys import rgb_to_hsv
from comfy.utils import ProgressBar
class FL_PixelSort:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"direction": (["Horizontal", "Vertical"],),
"threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"smoothing": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
"rotation": ("INT", {"default": 0, "min": 0, "max": 3, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "pixel_sort_saturation"
CATEGORY = "🏵️Fill Nodes/VFX"
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def saturation(self, pixel):
r, g, b = pixel
_, s, _ = rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
return s
def pixel_sort_saturation(self, images, direction="Horizontal", threshold=0.5, smoothing=0.1, rotation=0):
out = []
total_images = len(images)
pbar = ProgressBar(total_images)
for i, img in enumerate(images, start=1):
p = self.t2p(img)
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)
pbar.update_absolute(i)
out = torch.cat(out, 0)
return (out,)
def sort_pixels(self, image, value, threshold, smoothing, rotation=0):
pixels = np.rot90(np.array(image), rotation)
values = np.apply_along_axis(value, 2, pixels)
edges = np.apply_along_axis(lambda row: np.convolve(row, [-1, 1], 'same'), 0, values > threshold)
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]
pbar = ProgressBar(len(values))
for row, key in enumerate(values):
order = np.split(key, intervals[row])
for index, interval in enumerate(order[1:]):
order[index + 1] = np.argsort(interval) + intervals[row][index]
order[0] = range(order[0].size)
order = np.concatenate(order)
for channel in range(3):
pixels[row, :, channel] = pixels[row, order.astype('uint32'), channel]
pbar.update_absolute(row)
return Image.fromarray(np.rot90(pixels, -rotation))
-36
View File
@@ -1,36 +0,0 @@
class FL_PromptSelector:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prepend_text": ("STRING", {"multiline": True, "default": ""}),
"prompts": ("STRING", {"multiline": True}),
"append_text": ("STRING", {"multiline": True, "default": ""}),
"index": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
},
"optional": {},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "select_prompt"
CATEGORY = "🏵️Fill Nodes/utility"
def select_prompt(self, prepend_text, prompts, append_text, index):
prepend_text = prepend_text.strip()
prompt_lines = prompts.split("\n")
append_text = append_text.strip()
num_prompts = len(prompt_lines)
if index < 0 or index >= num_prompts:
raise ValueError(f"Index {index} is out of range. Please provide an index between 0 and {num_prompts - 1}.")
selected_prompt = prompt_lines[index].strip()
if prepend_text:
selected_prompt = prepend_text + " " + selected_prompt
if append_text:
selected_prompt = selected_prompt + " " + append_text
return (selected_prompt,)
-38
View File
@@ -1,38 +0,0 @@
import random
import torch
class FL_RandomNumber:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"min_value": ("FLOAT", {"default": 0.0, "min": -1000000.0, "max": 1000000.0, "step": 0.1}),
"max_value": ("FLOAT", {"default": 1.0, "min": -1000000.0, "max": 1000000.0, "step": 0.1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 1000000}),
},
}
RETURN_TYPES = ("INT", "FLOAT")
FUNCTION = "generate_random_number"
CATEGORY = "🏵️Fill Nodes/utility"
def generate_random_number(self, min_value=0.0, max_value=1.0, seed=0):
if min_value > max_value:
raise ValueError("min_value should be less than or equal to max_value")
# Generate a random seed if seed is 0
if seed == 0:
seed = random.randint(1, 1000000)
# Set the random seed for reproducibility
random.seed(seed)
torch.manual_seed(seed)
# Generate a random float value within the specified range
random_float = min_value + (max_value - min_value) * random.random()
# Generate a random integer value within the specified range
random_int = int(min_value + (max_value - min_value) * random.random())
return (random_int, random_float)
-81
View File
@@ -1,81 +0,0 @@
import torch
import numpy as np
from PIL import Image, ImageEnhance, ImageOps, ImageFilter # Added ImageFilter import
import sys
class FL_RetroEffect:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"color_offset": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
"scanline_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"vignette_strength": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_strength": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.01}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_retro_effect"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_retro_effect(self, images, color_offset, scanline_strength, vignette_strength, noise_strength):
result = []
total_images = len(images)
for i, image in enumerate(images, start=1):
img = self.t2p(image)
result_img = self.process_image(img, color_offset, scanline_strength, vignette_strength, noise_strength)
result_img = self.p2t(result_img)
result.append(result_img)
# Update the print log
progress = i / total_images * 100
sys.stdout.write(f"\rProcessing images: {progress:.2f}%")
sys.stdout.flush()
# Print a new line after the progress log
print()
return (torch.cat(result, dim=0),)
def process_image(self, image, color_offset, scanline_strength, vignette_strength, noise_strength):
# Apply color offset
r, g, b = image.split()
r = ImageEnhance.Brightness(r).enhance(1 + color_offset)
b = ImageEnhance.Brightness(b).enhance(1 - color_offset)
image = Image.merge("RGB", (r, g, b))
# Apply scanlines
scanline_mask = Image.new("L", image.size, 0)
for y in range(0, image.size[1], 2):
scanline_mask.paste(int(255 * scanline_strength), (0, y, image.size[0], y + 1))
image.paste(image, mask=scanline_mask)
# Apply vignette
vignette_mask = Image.new("L", image.size, 0)
vignette_mask.paste(255, (0, 0, image.size[0], image.size[1]))
vignette_mask = ImageOps.invert(vignette_mask)
vignette_mask = vignette_mask.filter(ImageFilter.GaussianBlur(radius=image.size[0] * vignette_strength))
image.paste(image, mask=ImageOps.invert(vignette_mask))
# Apply noise
noise = Image.effect_noise(image.size, sigma=noise_strength * 255).convert("RGB")
image = Image.blend(image, noise, noise_strength)
return image
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-76
View File
@@ -1,76 +0,0 @@
import torch
import numpy as np
from PIL import Image
import math
from comfy.utils import ProgressBar
class FL_Ripple:
def __init__(self):
self.modulation_index = 0
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"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}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "ripple"
CATEGORY = "🏵️Fill Nodes/VFX"
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
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, center_x=50.0, center_y=50.0, modulation=0.0):
out = []
total_images = len(images)
pbar = ProgressBar(total_images)
for i, img in enumerate(images, start=1):
p = self.t2p(img)
width, height = p.size
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_pixel
dy = y - center_y_pixel
distance = np.sqrt(dx ** 2 + dy ** 2)
# 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)
p_array = np.array(p)
rippled_array = p_array[sample_y, sample_x]
o = rippled_array.astype(np.float32) / 255.0
o = torch.from_numpy(o).unsqueeze(0)
out.append(o)
self.modulation_index += 1
pbar.update_absolute(i)
out = torch.cat(out, 0)
return (out,)
-39
View File
@@ -1,39 +0,0 @@
import torch
import numpy as np
class FL_SDUltimate_Slices:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"slicing": (["1x1", "1x2", "1x3", "1x4",
"2x1", "2x2", "2x3", "2x4",
"3x1", "3x2", "3x3", "3x4",
"4x1", "4x2", "4x3", "4x4"],),
"multiplier": ("FLOAT", {
"default": 1.0,
"min": 1.0,
"max": 4.0,
"step": 0.25
}),
},
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "FLOAT")
RETURN_NAMES = ("image", "slice_width", "slice_height", "multiplier")
FUNCTION = "slice_image"
CATEGORY = "🏵️Fill Nodes/utility"
def slice_image(self, image: torch.Tensor, slicing: str, multiplier: float):
_, height, width, _ = image.shape
slices_x, slices_y = map(int, slicing.split('x'))
slice_width = int((width // slices_x) * multiplier)
slice_height = int((height // slices_y) * multiplier)
return (image, slice_width, slice_height, multiplier)
@classmethod
def IS_CHANGED(cls, image, slicing, multiplier):
return float("NaN")
-57
View File
@@ -1,57 +0,0 @@
import torch
import cv2
import numpy as np
class FL_SeparateMaskComponents:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE", "MASK_MAPPING")
FUNCTION = "separate"
CATEGORY = "🏵️Fill Nodes/utility"
def separate(self, mask):
device = mask.device
# Ensure mask is in the correct format (B, H, W, C)
if mask.dim() == 3:
mask = mask.unsqueeze(-1)
B, H, W, C = mask.shape
all_component_masks = []
all_mappings = []
for b in range(B):
# Convert to numpy and ensure it's a single-channel image
mask_np = mask[b].squeeze().cpu().numpy()
if mask_np.ndim == 3:
mask_np = mask_np.mean(axis=-1) # Average across channels if multi-channel
# Threshold the mask
mask_np = (mask_np > 0).astype(np.uint8)
# Use OpenCV for connected component labeling
num_labels, labels = cv2.connectedComponents(mask_np)
for i in range(1, num_labels): # Skip background (label 0)
component_mask = (labels == i)
component_tensor = torch.from_numpy(component_mask).to(device).unsqueeze(-1).expand(-1, -1, C)
all_component_masks.append(component_tensor * mask[b])
all_mappings.append(b)
if all_component_masks:
result = torch.stack(all_component_masks)
mappings = torch.tensor(all_mappings, device=device)
else:
# Handle case where no components were found
result = torch.zeros((0, H, W, C), device=device)
mappings = torch.zeros(0, dtype=torch.long, device=device)
return (result, mappings)
-221
View File
@@ -1,221 +0,0 @@
import numpy as np
import torch
import OpenGL.GL as gl
import glfw
from comfy.utils import ProgressBar
SHADERTOY_HEADER = """
#version 440
precision highp float;
uniform vec3 iResolution;
uniform vec4 iMouse;
uniform float iTime;
uniform float iTimeDelta;
uniform float iFrameRate;
uniform int iFrame;
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform sampler2D iChannel2;
uniform sampler2D iChannel3;
#define texture2D texture
"""
SHADERTOY_FOOTER = """
layout(location = 0) out vec4 _fragColor;
void main()
{
mainImage(_fragColor, gl_FragCoord.xy);
}
"""
SHADERTOY_DEFAULT = """
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;
// Time varying pixel color
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
// Output to screen
fragColor = vec4(col,1.0);
}
"""
def render_surface_and_context_init(width, height):
if not glfw.init():
raise RuntimeError("GLFW did not init")
glfw.window_hint(glfw.VISIBLE, glfw.FALSE) # hidden
window = glfw.create_window(width, height, "hidden", None, None)
if not window:
raise RuntimeError("GLFW did not init window")
glfw.make_context_current(window)
return {}
def render_surface_and_context_deinit(**kwargs):
glfw.terminate()
def compile_shader(source, shader_type):
shader = gl.glCreateShader(shader_type)
gl.glShaderSource(shader, source)
gl.glCompileShader(shader)
if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
raise RuntimeError(gl.glGetShaderInfoLog(shader))
return shader
def compile_program(vertex_source, fragment_source):
vertex_shader = compile_shader(vertex_source, gl.GL_VERTEX_SHADER)
fragment_shader = compile_shader(fragment_source, gl.GL_FRAGMENT_SHADER)
program = gl.glCreateProgram()
gl.glAttachShader(program, vertex_shader)
gl.glAttachShader(program, fragment_shader)
gl.glLinkProgram(program)
if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE:
raise RuntimeError(gl.glGetProgramInfoLog(program))
return program
def setup_framebuffer(width, height):
texture = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, width, height, 0, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, None)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
fbo = gl.glGenFramebuffers(1)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, texture, 0)
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
raise RuntimeError("Framebuffer is not complete")
return fbo, texture
def setup_render_resources(width, height, fragment_source: str):
ctx = render_surface_and_context_init(width, height)
vertex_source = """
#version 330 core
void main()
{
vec2 verts[3] = vec2[](vec2(-1, -1), vec2(3, -1), vec2(-1, 3));
gl_Position = vec4(verts[gl_VertexID], 0, 1);
}
"""
shader = compile_program(vertex_source, fragment_source)
fbo, texture = setup_framebuffer(width, height)
textures = gl.glGenTextures(4)
return (ctx, fbo, shader, textures)
def render_resources_cleanup(ctx):
# assume all other resources get cleaned up with the context
render_surface_and_context_deinit(**ctx)
def render(width, height, fbo, shader):
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
gl.glClearColor(0.0, 0.0, 0.0, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glUseProgram(shader)
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
data = gl.glReadPixels(0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE)
image = np.frombuffer(data, dtype=np.uint8).reshape(height, width, 3)
image = image[::-1, :, :]
image = np.array(image).astype(np.float32) / 255.0
return image
def shadertoy_vars_update(shader, width, height, time, time_delta, frame_rate, frame):
gl.glUseProgram(shader)
iResolution_location = gl.glGetUniformLocation(shader, "iResolution")
gl.glUniform3f(iResolution_location, width, height, 0)
iMouse_location = gl.glGetUniformLocation(shader, "iMouse")
gl.glUniform4f(iMouse_location, 0, 0, 0, 0)
iTime_location = gl.glGetUniformLocation(shader, "iTime")
gl.glUniform1f(iTime_location, time)
iTimeDelta_location = gl.glGetUniformLocation(shader, "iTimeDelta")
gl.glUniform1f(iTimeDelta_location, time_delta)
iFrameRate_location = gl.glGetUniformLocation(shader, "iFrameRate")
gl.glUniform1f(iFrameRate_location, frame_rate)
iFrame_location = gl.glGetUniformLocation(shader, "iFrame")
gl.glUniform1i(iFrame_location, frame)
def shadertoy_texture_update(texture, image, frame):
if len(image.shape) == 4:
image = image[frame]
image = image.cpu().numpy()
image = image[::-1, :, :]
gl.glBindTexture(gl.GL_TEXTURE_2D, texture)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.shape[1], image.shape[0], 0, gl.GL_RGB, gl.GL_FLOAT, image)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
def shadertoy_texture_bind(shader, textures):
gl.glUseProgram(shader)
for i in range(4):
gl.glActiveTexture(gl.GL_TEXTURE0 + i) # type: ignore
gl.glBindTexture(gl.GL_TEXTURE_2D, textures[i])
iChannel_location = gl.glGetUniformLocation(shader, f"iChannel{i}")
gl.glUniform1i(iChannel_location, i)
class FL_Shadertoy:
@classmethod
def INPUT_TYPES(s):
return {"required": {"width": ("INT", {"default": 512, "min": 64, "max": 15360, "step": 8}),
"height": ("INT", {"default": 512, "min": 64, "max": 15360, "step": 8}),
"frame_count": ("INT", {"default": 1, "min": 1, "max": 262144}),
"fps": ("INT", {"default": 1, "min": 1, "max": 120}),
"source": (
"STRING", {"default": SHADERTOY_DEFAULT, "multiline": True, "dynamicPrompts": False})},
"optional": {"channel_0": ("IMAGE",),
"channel_1": ("IMAGE",),
"channel_2": ("IMAGE",),
"channel_3": ("IMAGE",)}}
RETURN_TYPES = ("IMAGE",)
CATEGORY = "🏵️Fill Nodes/VFX"
FUNCTION = "render"
def render(self, width: int, height: int, frame_count: int, fps: int, source: str,
channel_0: torch.Tensor | None = None, channel_1: torch.Tensor | None = None,
channel_2: torch.Tensor | None = None, channel_3: torch.Tensor | None = None):
fragment_source = SHADERTOY_HEADER
fragment_source += source
fragment_source += SHADERTOY_FOOTER
ctx, fbo, shader, textures = setup_render_resources(width, height, fragment_source)
images = []
frame = 0
pbar = ProgressBar(frame_count)
for idx in range(frame_count):
shadertoy_vars_update(shader, width, height, frame * (1.0 / fps), (1.0 / fps), fps, frame)
if channel_0 is not None: shadertoy_texture_update(textures[0], channel_0, frame)
if channel_1 is not None: shadertoy_texture_update(textures[1], channel_1, frame)
if channel_2 is not None: shadertoy_texture_update(textures[2], channel_2, frame)
if channel_3 is not None: shadertoy_texture_update(textures[3], channel_3, frame)
shadertoy_texture_bind(shader, textures)
image = render(width, height, fbo, shader)
image = torch.from_numpy(image)[None,]
images.append(image)
frame += 1
pbar.update_absolute(idx)
render_resources_cleanup(ctx)
return (torch.cat(images, dim=0),)
-113
View File
@@ -1,113 +0,0 @@
import torch
import aiohttp
import asyncio
from PIL import Image
import io
import base64
import time
import random
class FL_SimpleGPTVision:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"api_key": ("STRING", {"default": "", "multiline": False, "hidden": True}),
"model": (["gpt-4o-mini", "gpt-4o", "gpt-4-vision-preview"],),
"system_prompt": ("STRING", {
"default": "You are a helpful assistant that describes images accurately and concisely.",
"multiline": True}),
"request_prompt": ("STRING", {"default": "Describe this image in detail.", "multiline": True}),
"max_tokens": ("INT", {"default": 300, "min": 1, "max": 4096}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.1}),
"detail": (["auto", "low", "high"],),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_caption"
CATEGORY = "🏵️Fill Nodes/GPT"
async def process_image(self, session, img, model, system_prompt, request_prompt, max_tokens, temperature, detail):
# Encode image to base64
buffered = io.BytesIO()
img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
payload = {
"model": model,
"messages": [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": [
{
"type": "text",
"text": request_prompt
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{img_str}",
"detail": detail
}
}
]
}
],
"max_tokens": max_tokens,
"temperature": temperature
}
max_retries = 5
base_delay = 1
for attempt in range(max_retries):
try:
async with session.post("https://api.openai.com/v1/chat/completions", json=payload) as response:
if response.status == 429:
retry_after = int(response.headers.get('Retry-After', base_delay * (2 ** attempt)))
print(f"Rate limited. Retrying after {retry_after} seconds.")
await asyncio.sleep(retry_after)
continue
response.raise_for_status()
data = await response.json()
return data['choices'][0]['message']['content']
except aiohttp.ClientResponseError as e:
if e.status == 429:
retry_after = int(e.headers.get('Retry-After', base_delay * (2 ** attempt)))
print(f"Rate limited. Retrying after {retry_after} seconds.")
await asyncio.sleep(retry_after)
else:
return f"Error processing image: {str(e)}"
except Exception as e:
return f"Unexpected error: {str(e)}"
return "Failed to process image after multiple retries due to rate limiting."
def generate_caption(self, image, api_key, model, system_prompt, request_prompt, max_tokens, temperature, detail):
if not api_key:
return ("API key is required",)
# Convert tensor to PIL Image
pil_img = Image.fromarray((image.squeeze().cpu().numpy() * 255).astype('uint8'))
async def main():
async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {api_key}"}) as session:
result = await self.process_image(session, pil_img, model, system_prompt, request_prompt, max_tokens,
temperature, detail)
return result
try:
result = asyncio.run(main())
return (result,)
except Exception as e:
error_message = f"Error in API request: {str(e)}"
print(error_message)
return (error_message,)
-73
View File
@@ -1,73 +0,0 @@
# custom_nodes/FL_SystemCheck.py
import sys
import os
import platform
import psutil
import importlib
import json
from server import PromptServer
from aiohttp import web
class FL_SystemCheck:
@classmethod
def INPUT_TYPES(s):
return {"required": {}}
RETURN_TYPES = ()
FUNCTION = "run_check"
OUTPUT_NODE = True
CATEGORY = "🏵️Fill Nodes/utility"
def run_check(self):
return (True,)
def gather_system_info():
def get_gpu_info():
try:
import torch
return f"CUDA available: {torch.cuda.get_device_name(0)}" if torch.cuda.is_available() else "CUDA not available"
except ImportError:
return "PyTorch not installed"
def check_library_version(library):
try:
module = importlib.import_module(library)
return module.__version__
except ImportError:
return "Not installed"
def get_env_var(var):
return os.environ.get(var, 'Not set')
info = {
"Python version": sys.version.split()[0],
"Operating System": f"{platform.system()} {platform.release()}",
"CPU": platform.processor() or "Unable to determine",
"RAM": f"{psutil.virtual_memory().total / (1024 ** 3):.2f} GB",
"GPU": get_gpu_info(),
"PyTorch": check_library_version('torch'),
"torchvision": check_library_version('torchvision'),
"xformers": check_library_version('xformers'),
"numpy": check_library_version('numpy'),
"Pillow": check_library_version('pillow'),
"OpenCV": check_library_version('cv2'),
"transformers": check_library_version('transformers'),
"diffusers": check_library_version('diffusers'),
}
try:
import torch
if torch.cuda.is_available():
info["CUDA version"] = torch.version.cuda
except:
info["CUDA version"] = "Unable to determine"
for var in ['PYTHONPATH', 'CUDA_HOME', 'LD_LIBRARY_PATH']:
info[f"Env: {var}"] = get_env_var(var)
return info
@PromptServer.instance.routes.get("/fl_system_info")
async def system_info(request):
return web.json_response(gather_system_info())
-70
View File
@@ -1,70 +0,0 @@
import torch
import comfy.sd
import comfy.model_base
import comfy.samplers
import comfy.sample
import comfy.k_diffusion.sampling
class FL_TD_KSampler:
@classmethod
def INPUT_TYPES(s):
return {"required": {"model": ("MODEL",),
"conditioning_positive": ("CONDITIONING",),
"conditioning_negative": ("CONDITIONING",),
"latent_image": ("LATENT",),
"steps": ("INT", {"default": 20, "min": 1, "max": 1000, "step": 1}),
"seed": ("INT", {"default": 42, "min": 0, "max": 2 ** 32 - 1}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01})}}
RETURN_TYPES = ("LATENT",)
FUNCTION = "sample"
CATEGORY = "🏵️Fill Nodes/experiments"
def sample(self, model, conditioning_positive, conditioning_negative, latent_image, steps, seed, cfg, sampler_name,
scheduler, denoise):
device = comfy.model_management.get_torch_device()
latent = latent_image["samples"]
original_shape = latent.shape
# Set the seed for reproducibility
torch.manual_seed(seed)
# Setup noise
noise = torch.randn_like(latent, device=device)
# Setup sampler
sampler = comfy.samplers.KSampler(model, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=denoise, model_options=model.model_options)
# Setup progress bar
pbar = comfy.utils.ProgressBar(steps)
def callback(step, x0, x, total_steps):
pbar.update_absolute(step + 1, total_steps)
disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
try:
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler,
conditioning_positive, conditioning_negative, latent,
denoise=denoise, disable_noise=False, start_step=0, last_step=steps,
force_full_denoise=True, noise_mask=None, callback=callback,
disable_pbar=disable_pbar, seed=seed)
except Exception as e:
print('Custom KSampler error encountered:', e)
raise e
finally:
if pbar:
pbar.update_absolute(steps, steps)
# Prepare the output in the expected format
out = {
"samples": samples,
"original_shape": original_shape,
"noise_seed": seed,
"steps": steps
}
return out
-11
View File
@@ -1,11 +0,0 @@
class FL_TetrisGame:
@classmethod
def INPUT_TYPES(s):
return {"required": {}}
RETURN_TYPES = ()
FUNCTION = "execute"
CATEGORY = "🏵️Fill Nodes/games"
def execute(self):
return ()
-47
View File
@@ -1,47 +0,0 @@
import json
import torch
from PIL import Image
import numpy as np
from server import PromptServer
from aiohttp import web
class FL_TimeLine:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"timeline_data": ("STRING", {"multiline": True}),
},
"optional": {
"ipadapter_preset": (["LIGHT - SD1.5 only (low strength)", "STANDARD (medium strength)", "VIT-G (medium strength)", "PLUS (high strength)", "PLUS FACE (portraits)", "FULL FACE - SD1.5 only (portraits stronger)"], {"default": "LIGHT - SD1.5 only (low strength)"}),
"video_width": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 8}),
"video_height": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 8}),
"interpolation_mode": (["Linear", "Ease_in", "Ease_out", "Ease_in_out"], {"default": "Linear"}),
"number_animation_frames": ("INT", {"default": 96, "min": 1, "max": 1000, "step": 1}),
"frames_per_second": ("INT", {"default": 12, "min": 1, "max": 60, "step": 1}),
}
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "process_timeline"
CATEGORY = "animation"
def process_timeline(self, model, timeline_data, ipadapter_preset, video_width, video_height, interpolation_mode, number_animation_frames, frames_per_second):
# Parse the timeline data
timeline = json.loads(timeline_data)
# Process timeline data here
# For now, we'll just return the model as-is
return (model,)
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# API route for handling timeline data
@PromptServer.instance.routes.post("/fl_timeline/data")
async def handle_timeline_data(request):
data = await request.json()
print("Received timeline data:", data)
return web.json_response({"status": "success"})
-181
View File
@@ -1,181 +0,0 @@
import torch
import torch.nn.functional as F
import numpy as np
class FL_VideoCropMask:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video": ("IMAGE",),
"mask": ("IMAGE",),
"output_width": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 64}),
"output_height": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 64}),
"padding": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
"smoothing_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}),
},
}
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "CROP_DATA")
RETURN_NAMES = ("cropped_video", "mask", "original_video", "crop_data")
FUNCTION = "crop_video"
CATEGORY = "🏵️Fill Nodes/utility"
def crop_video(self, video: torch.Tensor, mask: torch.Tensor, output_width: int, output_height: int, padding: int,
smoothing_factor: float):
batch_size, height, width, channels = video.shape
cropped_video = []
cropped_masks = []
crop_data_list = []
prev_center_x, prev_center_y = None, None
prev_crop_width, prev_crop_height = None, None
for i in range(batch_size):
frame = video[i]
frame_mask = mask[i]
# Find the bounding box of the mask
mask_binary = (frame_mask.sum(dim=-1) > 0).float()
y_indices, x_indices = torch.where(mask_binary > 0)
if len(y_indices) == 0 or len(x_indices) == 0:
# If no mask is found, use the previous crop or the center of the frame
if prev_center_x is None:
center_y, center_x = height // 2, width // 2
crop_width, crop_height = width, height
else:
center_y, center_x = prev_center_y, prev_center_x
crop_width, crop_height = prev_crop_width, prev_crop_height
else:
top, bottom = y_indices.min().item(), y_indices.max().item()
left, right = x_indices.min().item(), x_indices.max().item()
center_y = (top + bottom) // 2
center_x = (left + right) // 2
crop_width = right - left + 2 * padding
crop_height = bottom - top + 2 * padding
# Apply smoothing to the center position and crop size
if prev_center_x is not None:
center_x = int(smoothing_factor * center_x + (1 - smoothing_factor) * prev_center_x)
center_y = int(smoothing_factor * center_y + (1 - smoothing_factor) * prev_center_y)
crop_width = int(smoothing_factor * crop_width + (1 - smoothing_factor) * prev_crop_width)
crop_height = int(smoothing_factor * crop_height + (1 - smoothing_factor) * prev_crop_height)
prev_center_x, prev_center_y = center_x, center_y
prev_crop_width, prev_crop_height = crop_width, crop_height
# Calculate the aspect ratio of the output and the crop
output_aspect_ratio = output_width / output_height
crop_aspect_ratio = crop_width / crop_height
# Adjust crop size to fit the output aspect ratio without distortion
if crop_aspect_ratio > output_aspect_ratio:
# Crop is wider, adjust height
crop_height = int(crop_width / output_aspect_ratio)
else:
# Crop is taller, adjust width
crop_width = int(crop_height * output_aspect_ratio)
# Ensure the crop stays within the frame
top = max(0, center_y - crop_height // 2)
bottom = min(height, top + crop_height)
left = max(0, center_x - crop_width // 2)
right = min(width, left + crop_width)
# Adjust if the crop goes out of bounds
if top == 0:
bottom = crop_height
if bottom == height:
top = height - crop_height
if left == 0:
right = crop_width
if right == width:
left = width - crop_width
# Crop the video and mask
cropped_frame = frame[top:bottom, left:right, :]
cropped_frame_mask = frame_mask[top:bottom, left:right, :]
# Resize the cropped video and mask to the desired output size
cropped_frame = F.interpolate(cropped_frame.unsqueeze(0).permute(0, 3, 1, 2),
size=(output_height, output_width), mode='bilinear',
align_corners=False).squeeze(0).permute(1, 2, 0)
cropped_frame_mask = F.interpolate(cropped_frame_mask.unsqueeze(0).permute(0, 3, 1, 2),
size=(output_height, output_width), mode='nearest').squeeze(0).permute(1,
2,
0)
cropped_video.append(cropped_frame)
cropped_masks.append(cropped_frame_mask)
# Create crop data
crop_data = {
"top": top,
"bottom": bottom,
"left": left,
"right": right,
"original_height": height,
"original_width": width,
"output_height": output_height,
"output_width": output_width,
}
crop_data_list.append(crop_data)
cropped_video = torch.stack(cropped_video)
cropped_masks = torch.stack(cropped_masks)
return (cropped_video, cropped_masks, video, crop_data_list)
class FL_VideoRecompose:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"original_video": ("IMAGE",),
"cropped_video": ("IMAGE",),
"crop_data": ("CROP_DATA",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_video",)
FUNCTION = "replace_crop"
CATEGORY = "🏵️Fill Nodes/experiments"
def replace_crop(self, original_video: torch.Tensor, cropped_video: torch.Tensor, crop_data: list):
batch_size, height, width, channels = original_video.shape
output_video = []
for i in range(batch_size):
frame = original_video[i]
cropped_frame = cropped_video[i]
frame_crop_data = crop_data[i]
# Resize the cropped video back to its original size
resized_crop = F.interpolate(
cropped_frame.unsqueeze(0).permute(0, 3, 1, 2),
size=(
frame_crop_data["bottom"] - frame_crop_data["top"], frame_crop_data["right"] - frame_crop_data["left"]),
mode='bilinear',
align_corners=False
).squeeze(0).permute(1, 2, 0)
# Create a copy of the original frame
output_frame = frame.clone()
# Replace the cropped area in the original frame
output_frame[frame_crop_data["top"]:frame_crop_data["bottom"],
frame_crop_data["left"]:frame_crop_data["right"], :] = resized_crop
output_video.append(output_frame)
output_video = torch.stack(output_video)
return (output_video,)
-43
View File
@@ -1,43 +0,0 @@
import os
import zipfile
import tempfile
class FL_ZipDirectory:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"directory_path": ("STRING", {"default": ""}),
"zip_filename": ("STRING", {"default": "archive.zip"}),
},
}
RETURN_TYPES = ("ZIP",)
FUNCTION = "zip_directory"
CATEGORY = "🏵️Fill Nodes/File Operations"
def zip_directory(self, directory_path: str, zip_filename: str) -> tuple[str]:
if not os.path.exists(directory_path):
raise ValueError(f"Directory not found: {directory_path}")
# Create a temporary directory to store the zip file
with tempfile.TemporaryDirectory() as temp_dir:
zip_path = os.path.join(temp_dir, zip_filename)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(directory_path):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, directory_path)
zipf.write(file_path, arcname)
# Read the zip file into memory
with open(zip_path, 'rb') as f:
zip_data = f.read()
return (zip_data,)
@classmethod
def IS_CHANGED(cls, directory_path, zip_filename):
return float("NaN")
-47
View File
@@ -1,47 +0,0 @@
import os
import zipfile
import tempfile
class FL_ZipSave:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_directory": ("STRING", {"default": ""}),
"output_directory": ("STRING", {"default": ""}),
"zip_filename": ("STRING", {"default": "archive.zip"}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("zip_path",)
FUNCTION = "zip_and_save"
CATEGORY = "🏵️Fill Nodes/File Operations"
OUTPUT_NODE = True
def zip_and_save(self, input_directory: str, output_directory: str, zip_filename: str) -> tuple[str]:
if not os.path.exists(input_directory):
raise ValueError(f"Input directory not found: {input_directory}")
if not os.path.exists(output_directory):
os.makedirs(output_directory)
# Ensure the zip filename ends with .zip
if not zip_filename.lower().endswith('.zip'):
zip_filename += '.zip'
zip_path = os.path.join(output_directory, zip_filename)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(input_directory):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, input_directory)
zipf.write(file_path, arcname)
print(f"Zip file created: {zip_path}")
return (zip_path,)
@classmethod
def IS_CHANGED(cls, input_directory, output_directory, zip_filename):
return float("NaN")
View File
-35
View File
@@ -1,35 +0,0 @@
import torch
from PIL import Image
class FL_ImageDimensionDisplay:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "display_dimensions"
CATEGORY = "🏵️Fill Nodes/utility"
def display_dimensions(self, image):
if isinstance(image, torch.Tensor):
if image.dim() == 4: # Batch dimension is present
_, height, width, _ = image.shape
elif image.dim() == 3: # No batch dimension, single image
height, width, _ = image.shape
else:
return ("Unsupported tensor format",)
elif isinstance(image, Image.Image):
width, height = image.size
else:
return ("Unsupported image format",)
dimensions = f"Width: {width}, Height: {height}"
return (dimensions,)
@classmethod
def IS_CHANGED(cls, image):
return float("NaN") # This ensures the node always updates
-85
View File
@@ -1,85 +0,0 @@
import torch
from PIL import Image
from kornia.morphology import gradient
import comfy.model_management
from comfy.utils import ProgressBar
class FL_ImagePixelator:
def __init__(self):
self.modulation_index = 0
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"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}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "pixelate_image"
CATEGORY = "🏵️Fill Nodes/VFX"
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]
pbar = ProgressBar(total_frames)
for i, single_image in enumerate(image, start=1):
single_image = single_image.unsqueeze(0) # Add batch dimension
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)
pbar.update_absolute(i)
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
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
else:
return (None,)
elif isinstance(image, Image.Image):
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)
else:
return (None,)
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))
resized_image = input_image.resize(new_size, Image.NEAREST)
pixelated_image = resized_image.resize((width, height), Image.NEAREST)
return pixelated_image
def apply_pixelation_tensor(self, input_image, scale_factor):
_, num_channels, height, width = input_image.shape
new_height, new_width = max(1, int(height * scale_factor)), max(1, int(width * scale_factor))
resized_tensor = torch.nn.functional.interpolate(input_image, size=(new_height, new_width), mode='nearest')
output_tensor = torch.nn.functional.interpolate(resized_tensor, size=(height, width), mode='nearest')
return output_tensor
def process(self, image, kernel_size):
device = comfy.model_management.get_torch_device()
kernel = torch.ones(kernel_size, kernel_size, device=device)
image_k = image.to(device).movedim(-1, 1)
output = gradient(image_k, kernel)
img_out = output.to(comfy.model_management.intermediate_device()).movedim(1, -1)
return img_out
-44
View File
@@ -1,44 +0,0 @@
import os
import numpy as np
import torch
from PIL import Image, ImageOps
class FL_ImageRandomizer:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"directory_path": ("STRING", {"default": ""}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("IMAGE", "PATH")
FUNCTION = "select_image"
CATEGORY = "🏵️Fill Nodes/utility"
def select_image(self, directory_path, seed):
if not directory_path:
raise ValueError("Directory path is not provided.")
images = self.load_images(directory_path)
if not images:
raise ValueError("No images found in the specified directory.")
num_images = len(images)
selected_index = seed % num_images
selected_image_path = images[selected_index]
image = Image.open(selected_image_path)
image = ImageOps.exif_transpose(image)
image = image.convert("RGB")
image_np = np.array(image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_np)[None,]
return (image_tensor, selected_image_path)
def load_images(self, directory):
supported_formats = ["jpg", "jpeg", "png", "bmp", "gif"]
return sorted([os.path.join(directory, f) for f in os.listdir(directory)
if os.path.isfile(os.path.join(directory, f)) and f.split('.')[-1].lower() in supported_formats])
-105
View File
@@ -1,105 +0,0 @@
import torch
import numpy as np
from PIL import Image
import sys
from comfy.utils import ProgressBar
class FL_ImageCollage:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base_image": ("IMAGE",),
"tile_image": ("IMAGE",),
"tile_size": ("INT", {"default": 32, "min": 8, "max": 256, "step": 8}),
"spacing": ("INT", {"default": 0, "min": 0, "max": 64, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "create_collage"
CATEGORY = "🏵️Fill Nodes/VFX"
def create_collage(self, base_image, tile_image, tile_size, spacing):
base_batch_size = len(base_image)
tile_batch_size = len(tile_image)
if tile_batch_size == 1:
# Duplicate the single tile image to match the base image batch size
tile_image = tile_image.repeat(base_batch_size, 1, 1, 1)
elif tile_batch_size != base_batch_size:
raise ValueError(f"The number of tile images ({tile_batch_size}) does not match the number of base images ({base_batch_size}).")
result = []
pbar = ProgressBar(total_images)
for i, (base_img, tile_img) in enumerate(zip(base_image, tile_image), start=1):
base_img = self.t2p(base_img)
tile_img = self.t2p(tile_img)
result_img = self.create_collage_image(base_img, tile_img, tile_size, spacing)
result_img = self.p2t(result_img)
result.append(result_img)
# Update the print log
progress = i / base_batch_size * 100
sys.stdout.write(f"\rProcessing images: {progress:.2f}%")
sys.stdout.flush()
# Print a new line after the progress log
print()
return (torch.cat(result, dim=0),)
def create_collage_image(self, base_image, tile_image, tile_size, spacing):
base_width, base_height = base_image.size
tile_width, tile_height = tile_image.size
# Calculate the aspect ratio of the tile image
aspect_ratio = tile_width / tile_height
# Calculate the new dimensions of the tile image while maintaining the aspect ratio
if tile_width > tile_height:
new_tile_width = tile_size
new_tile_height = int(tile_size / aspect_ratio)
else:
new_tile_width = int(tile_size * aspect_ratio)
new_tile_height = tile_size
# Resize the tile image to the new dimensions
tile_image = tile_image.resize((new_tile_width, new_tile_height), Image.Resampling.LANCZOS)
# Create a new blank image for the collage
collage_image = Image.new("RGB", base_image.size)
for y in range(0, base_height, new_tile_height + spacing):
for x in range(0, base_width, new_tile_width + spacing):
# Get the average color of the corresponding region in the base image
region = base_image.crop((x, y, x + new_tile_width, y + new_tile_height))
avg_color = tuple(np.array(region).mean(axis=(0, 1)).astype(int))
# Create a mask based on the brightness of the tile image
tile_mask = Image.new("L", (new_tile_width, new_tile_height), 0)
tile_mask_data = np.array(tile_image.convert("L"))
tile_mask_data = (tile_mask_data / 255.0) ** 2 # Adjust the brightness sensitivity
tile_mask.putdata(np.uint8(tile_mask_data.flatten() * 255))
# Colorize the tile image based on the average color of the base image region
colorized_tile = Image.new("RGB", (new_tile_width, new_tile_height), avg_color)
colorized_tile.putalpha(tile_mask)
# Paste the colorized tile onto the collage image
collage_image.paste(colorized_tile, (x, y), mask=tile_mask)
return collage_image
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-67
View File
@@ -1,67 +0,0 @@
import torch
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from comfy.utils import ProgressBar
class FL_ImageNotes:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"text": ("STRING", {"default": "Text Here", "multiline": False}),
"bar_height": ("INT", {"default": 50, "min": 10, "max": 200, "step": 2}),
"text_size": ("INT", {"default": 24, "min": 10, "max": 100, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "add_notes"
CATEGORY = "🏵️Fill Nodes/utility"
def add_notes(self, images, text, bar_height, text_size):
result = []
total_images = len(images)
pbar = ProgressBar(total_images)
for i, image in enumerate(images, start=1):
img = self.t2p(image)
result_img = self.add_text_bar(img, text, bar_height, text_size)
result_img = self.p2t(result_img)
result.append(result_img)
pbar.update_absolute(i)
return (torch.cat(result, dim=0),)
def add_text_bar(self, image, text, bar_height, text_size):
width, height = image.size
new_height = height + bar_height
new_image = Image.new("RGB", (width, new_height), color="black")
new_image.paste(image, (0, bar_height))
draw = ImageDraw.Draw(new_image)
font = ImageFont.truetype("arial.ttf", text_size)
text_width, text_height = self.get_text_size(text, font)
x = (width - text_width) // 2
y = (bar_height - text_height) // 2
draw.text((x, y), text, font=font, fill="white")
return new_image
def get_text_size(self, text, font):
ascent, descent = font.getmetrics()
text_width = font.getmask(text).getbbox()[2]
text_height = font.getmask(text).getbbox()[3] + descent
return text_width, text_height
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-191
View File
@@ -1,191 +0,0 @@
import torch
import numpy as np
from PIL import Image
import sys
import OpenGL.GL as gl
import glfw
import ctypes
from comfy.utils import ProgressBar
VERTEX_SHADER = """
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform vec2 iResolution;
void main()
{
vec2 scale = vec2(1.0, iResolution.y / iResolution.x);
gl_Position = vec4(aPos.xy * scale, aPos.z, 1.0);
TexCoord = aTexCoord;
}
"""
FRAGMENT_SHADER = """
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D iChannel0;
uniform vec3 iResolution;
uniform float iTime;
uniform float iScale;
uniform float iSwirl;
uniform float iSwirlStrength;
uniform float iIterations;
uniform float iTimeSpeed;
void main()
{
vec4 O = vec4(0.0);
vec2 U = TexCoord;
float s = 0.0, s2 = 0.0, t = iTime * iTimeSpeed;
U = U - 0.5;
U.x += 0.03 * sin(1.14 * t);
float sc = pow(iScale, -mod(t, 2.0) - 0.8);
U *= sc;
for (int i = 0; i < int(iIterations); i++) {
vec2 V = abs(U + U);
if (max(V.x, V.y) > 1.0) break;
V = smoothstep(1.0, 0.5, V);
float m = V.x * V.y;
O = mix(O, texture(iChannel0, U + 0.5), m);
s = mix(s, 1.0, m);
s2 = s2 * (1.0 - m) * (1.0 - m) + m * m;
U *= iScale;
if (iSwirl > 0.5) {
U.x = -U.x * iSwirlStrength;
}
}
vec4 mean = texture(iChannel0, U, 10.0);
O = mean + (O - s * mean) / sqrt(s2);
FragColor = O;
}
"""
class FL_InfiniteZoom:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"scale": ("FLOAT", {"default": 2.00, "min": 1.10, "max": 10.00, "step": 0.05}),
"mirror": (["on", "off"],),
"mirror_warp": ("FLOAT", {"default": 1.00, "min": 0.50, "max": 1.50, "step": 0.05}),
"iterations": ("INT", {"default": 10, "min": 1, "max": 100, "step": 1}),
"speed": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
"fps": ("INT", {"default": 30, "min": 1, "max": 120, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_shader"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_shader(self, images, scale, mirror, mirror_warp, iterations, speed, fps):
result = []
total_images = len(images)
pbar = ProgressBar(total_images)
frame_time = 1.0 / fps
for i, image in enumerate(images, start=1):
img = self.t2p(image)
result_img = self.process_image(img, scale, mirror, mirror_warp, iterations, speed, i * frame_time)
result_img = self.p2t(result_img)
result.append(result_img)
pbar.update_absolute(i)
return (torch.cat(result, dim=0),)
def process_image(self, image, scale, mirror, mirror_warp, iterations, speed, time):
img_array = np.array(image).astype(np.float32) / 255.0
if not glfw.init():
raise RuntimeError("Failed to initialize GLFW")
glfw.window_hint(glfw.VISIBLE, glfw.FALSE)
window = glfw.create_window(image.width, image.height, "Hidden Window", None, None)
if not window:
glfw.terminate()
raise RuntimeError("Failed to create GLFW window")
glfw.make_context_current(window)
# Set the viewport
gl.glViewport(0, 0, image.width, image.height)
vertex_shader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
gl.glShaderSource(vertex_shader, VERTEX_SHADER)
gl.glCompileShader(vertex_shader)
fragment_shader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
gl.glShaderSource(fragment_shader, FRAGMENT_SHADER)
gl.glCompileShader(fragment_shader)
shader_program = gl.glCreateProgram()
gl.glAttachShader(shader_program, vertex_shader)
gl.glAttachShader(shader_program, fragment_shader)
gl.glLinkProgram(shader_program)
gl.glUseProgram(shader_program)
vertices = np.array([
-1.0, -1.0, 0.0, 0.0, 0.0,
1.0, -1.0, 0.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 0.0, 1.0,
1.0, 1.0, 0.0, 1.0, 1.0
], dtype=np.float32)
vao = gl.glGenVertexArrays(1)
gl.glBindVertexArray(vao)
vbo = gl.glGenBuffers(1)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_STATIC_DRAW)
gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, 5 * vertices.itemsize, None)
gl.glEnableVertexAttribArray(0)
gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, gl.GL_FALSE, 5 * vertices.itemsize, ctypes.c_void_p(3 * vertices.itemsize))
gl.glEnableVertexAttribArray(1)
texture = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.width, image.height, 0, gl.GL_RGB, gl.GL_FLOAT, img_array)
gl.glUniform1i(gl.glGetUniformLocation(shader_program, "iChannel0"), 0)
gl.glUniform2f(gl.glGetUniformLocation(shader_program, "iResolution"), image.width, image.height)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iTime"), time)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iScale"), scale)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iSwirl"), 1.0 if mirror == "on" else 0.0)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iSwirlStrength"), mirror_warp)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iIterations"), iterations)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iTimeSpeed"), speed)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
img_data = gl.glReadPixels(0, 0, image.width, image.height, gl.GL_RGB, gl.GL_FLOAT)
img_array = np.frombuffer(img_data, dtype=np.float32).reshape((image.height, image.width, 3))
gl.glDeleteTextures(1, (texture,))
gl.glDeleteBuffers(1, [vbo])
gl.glDeleteVertexArrays(1, [vao])
gl.glDeleteProgram(shader_program)
gl.glDeleteShader(vertex_shader)
gl.glDeleteShader(fragment_shader)
glfw.destroy_window(window)
glfw.terminate()
processed_image = Image.fromarray((img_array * 255).astype(np.uint8))
return processed_image
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-336
View File
@@ -1,336 +0,0 @@
import comfy.utils
import math
import nodes
import numpy as np
import torch
from scipy.ndimage import gaussian_filter, grey_dilation, binary_fill_holes, binary_closing
class FL_InpaintCrop:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"context_expand_pixels": ("INT", {"default": 10, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}),
"context_expand_factor": ("FLOAT", {"default": 1.01, "min": 1.0, "max": 100.0, "step": 0.01}),
"invert_mask": ("BOOLEAN", {"default": False}),
"fill_mask_holes": ("BOOLEAN", {"default": True}),
"mode": (["free size", "forced size"], {"default": "free size"}),
"force_size": ([512, 768, 1024, 1344, 2048, 4096, 8192], {"default": 1024}),
"rescale_factor": ("FLOAT", {"default": 1.00, "min": 0.01, "max": 100.0, "step": 0.01}),
"padding": ([8, 16, 32, 64, 128, 256, 512], {"default": 32}),
},
"optional": {
"optional_context_mask": ("MASK",),
}
}
CATEGORY = "🏵️Fill Nodes/utility"
RETURN_TYPES = ("STITCH", "IMAGE", "MASK")
RETURN_NAMES = ("stitch", "cropped_image", "cropped_mask")
FUNCTION = "inpaint_crop"
def adjust_to_square(self, x_min, x_max, y_min, y_max, width, height, target_size = None):
if target_size is None:
x_size = x_max - x_min + 1
y_size = y_max - y_min + 1
target_size = max(x_size, y_size)
# Calculate the midpoint of the current x and y ranges
x_mid = (x_min + x_max) // 2
y_mid = (y_min + y_max) // 2
# Adjust x_min, x_max, y_min, y_max to make the range square centered around the midpoints
x_min = max(x_mid - target_size // 2, 0)
x_max = x_min + target_size - 1
y_min = max(y_mid - target_size // 2, 0)
y_max = y_min + target_size - 1
# Ensure the ranges do not exceed the image boundaries
if x_max >= width:
x_max = width - 1
x_min = x_max - target_size + 1
if y_max >= height:
y_max = height - 1
y_min = y_max - target_size + 1
# Additional checks to make sure all coordinates are within bounds
if x_min < 0:
x_min = 0
x_max = target_size - 1
if y_min < 0:
y_min = 0
y_max = target_size - 1
return x_min, x_max, y_min, y_max
def apply_padding(self, min_val, max_val, max_boundary, padding):
# Calculate the midpoint and the original range size
original_range_size = max_val - min_val + 1
midpoint = (min_val + max_val) // 2
# Determine the smallest multiple of padding that is >= original_range_size
if original_range_size % padding == 0:
new_range_size = original_range_size
else:
new_range_size = (original_range_size // padding + 1) * padding
# Calculate the new min and max values centered on the midpoint
new_min_val = max(midpoint - new_range_size // 2, 0)
new_max_val = new_min_val + new_range_size - 1
# Ensure the new max doesn't exceed the boundary
if new_max_val >= max_boundary:
new_max_val = max_boundary - 1
new_min_val = max(new_max_val - new_range_size + 1, 0)
# Ensure the range still ends on a multiple of padding
# Adjust if the calculated range isn't feasible within the given constraints
if (new_max_val - new_min_val + 1) != new_range_size:
new_min_val = max(new_max_val - new_range_size + 1, 0)
return new_min_val, new_max_val
# Parts of this function are from KJNodes: https://github.com/kijai/ComfyUI-KJNodes
def inpaint_crop(self, image, mask, context_expand_pixels, context_expand_factor, invert_mask, fill_mask_holes, mode, force_size, rescale_factor, padding, optional_context_mask = None):
original_image = image
original_mask = mask
original_width = image.shape[2]
original_height = image.shape[1]
#Validate or initialize mask
if mask.shape[1] != image.shape[1] or mask.shape[2] != image.shape[2]:
non_zero_indices = torch.nonzero(mask[0], as_tuple=True)
if not non_zero_indices[0].size(0):
mask = torch.zeros_like(image[:, :, :, 0])
else:
assert False, "mask size must match image size"
# Invert mask if requested
if invert_mask:
mask = 1.0 - mask
# Fill holes if requested
if fill_mask_holes:
holemask = mask.reshape((-1, mask.shape[-2], mask.shape[-1])).cpu()
out = []
for m in holemask:
mask_np = m.numpy()
binary_mask = mask_np > 0
struct = np.ones((5, 5))
closed_mask = binary_closing(binary_mask, structure=struct, border_value=1)
filled_mask = binary_fill_holes(closed_mask)
output = filled_mask.astype(np.float32) * 255
output = torch.from_numpy(output)
out.append(output)
mask = torch.stack(out, dim=0)
mask = torch.clamp(mask, 0.0, 1.0)
# Validate or initialize context mask
if optional_context_mask is None:
context_mask = mask
elif optional_context_mask.shape[1] != image.shape[1] or optional_context_mask.shape[2] != image.shape[2]:
non_zero_indices = torch.nonzero(optional_context_mask[0], as_tuple=True)
if not non_zero_indices[0].size(0):
context_mask = mask
else:
assert False, "context_mask size must match image size"
else:
context_mask = optional_context_mask + mask
context_mask = torch.clamp(context_mask, 0.0, 1.0)
# If there are no non-zero indices in the context_mask, return the original image and original mask
non_zero_indices = torch.nonzero(context_mask[0], as_tuple=True)
if not non_zero_indices[0].size(0):
stitch = {'x': 0, 'y': 0, 'original_image': original_image, 'cropped_mask': mask, 'rescale_x': 1.0, 'rescale_y': 1.0}
return (stitch, original_image, original_mask)
# Compute context area from context mask
y_min = torch.min(non_zero_indices[0]).item()
y_max = torch.max(non_zero_indices[0]).item()
x_min = torch.min(non_zero_indices[1]).item()
x_max = torch.max(non_zero_indices[1]).item()
height = context_mask.shape[1]
width = context_mask.shape[2]
# Grow context area if requested
y_size = y_max - y_min + 1
x_size = x_max - x_min + 1
y_grow = round(max(y_size*(context_expand_factor-1), context_expand_pixels))
x_grow = round(max(x_size*(context_expand_factor-1), context_expand_pixels))
y_min = max(y_min - y_grow // 2, 0)
y_max = min(y_max + y_grow // 2, height - 1)
x_min = max(x_min - x_grow // 2, 0)
x_max = min(x_max + x_grow // 2, width - 1)
effective_upscale_factor_x = 1.0
effective_upscale_factor_y = 1.0
# Adjust to preferred size
if mode == 'forced size':
# Turn into square
x_min, x_max, y_min, y_max = self.adjust_to_square(x_min, x_max, y_min, y_max, width, height)
current_size = x_max - x_min + 1 # Assuming x_max - x_min == y_max - y_min due to square adjustment
if current_size != force_size:
# Upscale to fit in the force_size square, will be downsized at stitch phase
upscale_factor = force_size / current_size
samples = image
samples = samples.movedim(-1, 1)
width = math.floor(samples.shape[3] * upscale_factor)
height = math.floor(samples.shape[2] * upscale_factor)
samples = comfy.utils.bislerp(samples, width, height)
effective_upscale_factor_x = float(width)/float(original_width)
effective_upscale_factor_y = float(height)/float(original_height)
samples = samples.movedim(1, -1)
image = samples
samples = mask
samples = samples.unsqueeze(1)
samples = comfy.utils.bislerp(samples, width, height)
samples = samples.squeeze(1)
mask = samples
x_min = math.floor(x_min * effective_upscale_factor_x)
x_max = math.floor(x_max * effective_upscale_factor_x)
y_min = math.floor(y_min * effective_upscale_factor_y)
y_max = math.floor(y_max * effective_upscale_factor_y)
# Readjust to force size because the upscale math may not round well
x_min, x_max, y_min, y_max = self.adjust_to_square(x_min, x_max, y_min, y_max, width, height, target_size=force_size)
elif mode == 'free size':
# Upscale image and masks if requested, they will be downsized at stitch phase
if rescale_factor < 0.999 or rescale_factor > 1.001:
samples = image
samples = samples.movedim(-1, 1)
width = math.floor(samples.shape[3] * rescale_factor)
height = math.floor(samples.shape[2] * rescale_factor)
samples = comfy.utils.bislerp(samples, width, height)
effective_upscale_factor_x = float(width)/float(original_width)
effective_upscale_factor_y = float(height)/float(original_height)
samples = samples.movedim(1, -1)
image = samples
samples = mask
samples = samples.unsqueeze(1)
samples = comfy.utils.bislerp(samples, width, height)
samples = samples.squeeze(1)
mask = samples
x_min = math.floor(x_min * effective_upscale_factor_x)
x_max = math.floor(x_max * effective_upscale_factor_x)
y_min = math.floor(y_min * effective_upscale_factor_y)
y_max = math.floor(y_max * effective_upscale_factor_y)
# Ensure that context area doesn't go outside of the image
x_min = max(x_min, 0)
x_max = min(x_max, width - 1)
y_min = max(y_min, 0)
y_max = min(y_max, height - 1)
# Pad area (if possible, i.e. if pad is smaller than width/height) to avoid the sampler returning smaller results
if padding > 1:
x_min, x_max = self.apply_padding(x_min, x_max, width, padding)
y_min, y_max = self.apply_padding(y_min, y_max, height, padding)
# Crop the image and the mask, sized context area
cropped_image = image[:, y_min:y_max+1, x_min:x_max+1]
cropped_mask = mask[:, y_min:y_max+1, x_min:x_max+1]
# Return stitch (to be consumed by the class below), image, and mask
stitch = {'x': x_min, 'y': y_min, 'original_image': original_image, 'cropped_mask': cropped_mask, 'rescale_x': effective_upscale_factor_x, 'rescale_y': effective_upscale_factor_y}
return (stitch, cropped_image, cropped_mask)
class FL_Inpaint_Stitch:
"""
ComfyUI-InpaintCropAndStitch
https://github.com/lquesada/ComfyUI-InpaintCropAndStitch
This node stitches the inpainted image without altering unmasked areas.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"stitch": ("STITCH",),
"inpainted_image": ("IMAGE",),
}
}
CATEGORY = "🏵️Fill Nodes/utility"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "inpaint_stitch"
# This function is from comfy_extras: https://github.com/comfyanonymous/ComfyUI
def composite(self, destination, source, x, y, mask = None, multiplier = 8, resize_source = False):
source = source.to(destination.device)
if resize_source:
source = torch.nn.functional.interpolate(source, size=(destination.shape[2], destination.shape[3]), mode="bilinear")
source = comfy.utils.repeat_to_batch_size(source, destination.shape[0])
x = max(-source.shape[3] * multiplier, min(x, destination.shape[3] * multiplier))
y = max(-source.shape[2] * multiplier, min(y, destination.shape[2] * multiplier))
left, top = (x // multiplier, y // multiplier)
right, bottom = (left + source.shape[3], top + source.shape[2],)
if mask is None:
mask = torch.ones_like(source)
else:
mask = mask.to(destination.device, copy=True)
mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(source.shape[2], source.shape[3]), mode="bilinear")
mask = comfy.utils.repeat_to_batch_size(mask, source.shape[0])
# calculate the bounds of the source that will be overlapping the destination
# this prevents the source trying to overwrite latent pixels that are out of bounds
# of the destination
visible_width, visible_height = (destination.shape[3] - left + min(0, x), destination.shape[2] - top + min(0, y),)
mask = mask[:, :, :visible_height, :visible_width]
inverse_mask = torch.ones_like(mask) - mask
source_portion = mask * source[:, :, :visible_height, :visible_width]
destination_portion = inverse_mask * destination[:, :, top:bottom, left:right]
destination[:, :, top:bottom, left:right] = source_portion + destination_portion
return destination
def inpaint_stitch(self, stitch, inpainted_image):
original_image = stitch['original_image']
cropped_mask = stitch['cropped_mask']
x = stitch['x']
y = stitch['y']
stitched_image = original_image.clone().movedim(-1, 1)
inpaint_width = inpainted_image.shape[2]
inpaint_height = inpainted_image.shape[1]
# Downscale inpainted before stitching if we upscaled it before
if stitch['rescale_x'] < 0.999 or stitch['rescale_x'] > 1.001 or stitch['rescale_y'] < 0.999 or stitch['rescale_y'] > 1.001:
samples = inpainted_image.movedim(-1, 1)
width = round(float(inpaint_width)/stitch['rescale_x'])
height = round(float(inpaint_height)/stitch['rescale_y'])
x = round(float(x)/stitch['rescale_x'])
y = round(float(y)/stitch['rescale_y'])
samples = comfy.utils.bislerp(samples, width, height)
inpainted_image = samples.movedim(1, -1)
samples = cropped_mask.movedim(-1, 1)
samples = samples.unsqueeze(0)
samples = comfy.utils.bislerp(samples, width, height)
samples = samples.squeeze(0)
cropped_mask = samples.movedim(1, -1)
output = self.composite(stitched_image, inpainted_image.movedim(-1, 1), x, y, cropped_mask, 1).movedim(1, -1)
return (output,)
-131
View File
@@ -1,131 +0,0 @@
import comfy.samplers
class FL_KsamplerSettings:
RATIO = [
("1:1___SD 512x512", 512, 512),
("4:3___SD 682x512", 682, 512),
("3:2___SD 768x512", 768, 512),
("16:9__SD 910x512", 910, 512),
("1:85:1 SD 952x512", 952, 512),
("2:1___SD 1024x512", 1024, 512),
("1:1_SV3D 576x576", 576, 576),
("16:9_SVD 576x1024", 1024, 576),
("1:1__SD2 768x768", 768, 768),
("1:1___XL 1024x1024", 1024, 1024),
("16:15_XL 1024x960", 1024, 960),
("17:15_XL 1088x960", 1088, 960),
("17:14_XL 1088x896", 1088, 896),
("4:3___XL 1152x896", 1152, 896),
("18:13_XL 1152x832", 1152, 832),
("3:2___XL 1216x832", 1216, 832),
("5:3___XL 1280x768", 1280, 768),
("7:4___XL 1344x768", 1344, 768),
("21:11_XL 1344x704", 1344, 704),
("2:1___XL 1408x704", 1408, 704),
("23:11_XL 1472x704", 1472, 704),
("21:9__XL 1536x640", 1536, 640),
("5:2___XL 1600x640", 1600, 640),
("26:9__XL 1664x576", 1664, 576),
("3:1___XL 1728x576", 1728, 576),
("28:9__XL 1792x576", 1792, 576),
("29:8__XL 1856x512", 1856, 512),
("15:4__XL 1920x512", 1920, 512),
("31:8__XL 1984x512", 1984, 512),
("4:1___XL 2048x512", 2048, 512),
]
@classmethod
def INPUT_TYPES(cls):
aspect_ratio_titles = [title for title, res1, res2 in cls.RATIO]
rotation = ("landscape", "portrait")
return {
"required": {
"Aspect_Ratio": (aspect_ratio_titles,
{"default": ("1:1___XL 1024x1024")}),
"rotation": (rotation,),
},
"optional": {
"batch": ("INT", {
"default": 1,
"min": 1,
"max": 10000,
}),
"Pass_1_steps": ("INT", {
"default": 25,
"min": 1,
"max": 10000,
}),
"Pass_2_steps": ("INT", {
"default": 25,
"min": 1,
"max": 10000,
}),
"Pass_1_CFG": ("FLOAT", {
"default": 6.0,
"min": -10.0,
"max": 100.0,
"step": 0.1,
"round": 0.1,
}),
"Pass_2_CFG": ("FLOAT", {
"default": 6.0,
"min": -10.0,
"max": 100.0,
"step": 0.1,
"round": 0.1,
}),
"Pass_2_denoise": ("FLOAT", {
"default": 0.500,
"min": -10.000,
"max": 100.000,
"step": 0.001,
"round": 0.01,
}),
"scale_factor": ("FLOAT", {
"default": 1.5,
"min": 1.0,
"max": 10.0,
"step": 0.1,
"round": 0.1,
}),
"sampler": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,)
}
}
RETURN_TYPES = (
"INT", "INT", "INT", "INT", "INT", "FLOAT",
"FLOAT", "FLOAT", "FLOAT", comfy.samplers.KSampler.SAMPLERS,
comfy.samplers.KSampler.SCHEDULERS,)
RETURN_NAMES = (
"WIDTH",
"HEIGHT",
"BATCH_SIZE",
"Pass_1_steps",
"Pass_2_steps",
"Pass_1_CFG",
"Pass_2_CFG",
"Pass_2_denoise",
"SCALE",
"SAMPLER",
"SCHEDULER",
)
FUNCTION = "settings"
CATEGORY = "🏵️Fill Nodes/utility"
def settings(self, Aspect_Ratio, rotation, batch, Pass_1_steps, Pass_2_steps, Pass_1_CFG, Pass_2_CFG,
Pass_2_denoise, scale_factor, sampler, scheduler):
for title, width, height in self.RATIO:
if title == Aspect_Ratio:
if rotation == "portrait":
width, height = height, width # Swap for portrait orientation
return (
width, height, batch, Pass_1_steps, Pass_2_steps, Pass_1_CFG, Pass_2_CFG, Pass_2_denoise, scale_factor,
sampler, scheduler)
return (
None, None, batch, Pass_1_steps, Pass_2_steps, Pass_1_CFG, Pass_2_CFG, Pass_2_denoise, scale_factor, sampler,
scheduler) # In case the Aspect Ratio is not found
-90
View File
@@ -1,90 +0,0 @@
import fnmatch
import os
import torch
import random
import numpy as np
from pathlib import Path
from PIL import Image
from .sup import ROOT
class FL_NFTGenerator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"folder_path": ("STRING", {"default": ""}),
"dummy_seed": ("INT", {"default": 0, "min": 0, "max": 1000000}),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE")
FUNCTION = "generate_nft"
CATEGORY = "🏵️Fill Nodes/experiments"
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def generate_nft(self, folder_path, dummy_seed):
if not (path := Path(ROOT / folder_path)).is_dir():
if not (path := Path(folder_path)).is_dir():
raise ValueError(f"Folder path does not exist: {folder_path}")
image_files = [str(f) for f in path.glob('*') if not fnmatch.fnmatch(f.name, '*-mask.*')]
if len(image_files) == 0:
raise ValueError(f"No image files found in the folder: {folder_path}")
# Extract rarity percentages from image filenames
rarities = []
for image_file in image_files:
if "-" in image_file:
# name-alexperval-was
rarity_str = image_file.split("-")[1].split("per")[0]
rarity = int(rarity_str)
rarities.append(rarity)
else:
raise ValueError(f"Invalid image filename format: {image_file}")
# Calculate cumulative probabilities
total_rarity = sum(rarities)
probabilities = [rarity / total_rarity for rarity in rarities]
cumulative_probabilities = [sum(probabilities[:i+1]) for i in range(len(probabilities))]
# Generate a random number between 0 and 1 using the dummy seed
random.seed(dummy_seed)
random_number = random.random()
# Find the index of the selected image based on the random number and cumulative probabilities
selected_index = None
for i, prob in enumerate(cumulative_probabilities):
if random_number <= prob:
selected_index = i
break
if selected_index is None:
raise ValueError("Failed to select an image based on rarity.")
# Get the selected image and its corresponding mask
selected_image_file = image_files[selected_index]
# selected_image_path = os.path.join(folder_path, selected_image_file)
selected_image = Image.open(selected_image_file)
# Get the file extension of the selected image
_, extension = os.path.splitext(selected_image_file)
# Generate the mask filename based on the selected image filename
mask_file = selected_image_file.rsplit(".", 1)[0] + "-mask" + extension
mask_path = os.path.join(folder_path, mask_file)
if os.path.exists(mask_path):
mask_image = Image.open(mask_path)
else:
# Create a blank mask image if the corresponding mask is not found
mask_image = Image.new("RGB", selected_image.size, (0, 0, 0))
selected_image_tensor = torch.from_numpy(np.array(selected_image).astype(np.float32) / 255.0).unsqueeze(0)
mask_image_tensor = torch.from_numpy(np.array(mask_image).astype(np.float32) / 255.0).unsqueeze(0)
return (selected_image_tensor, mask_image_tensor)
-291
View File
@@ -1,291 +0,0 @@
import glfw
import ctypes
import torch
import numpy as np
from PIL import Image
import OpenGL.GL as gl
from comfy.utils import ProgressBar
VERTEX_SHADER = """
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
TexCoord = aTexCoord;
}
"""
FRAGMENT_SHADER = """
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform vec3 iResolution;
uniform float iTime;
uniform float iAngleNum;
uniform float iSampNum;
uniform float iLineWidth;
uniform float iVignette;
#define Res0 textureSize(iChannel0, 0)
#define Res1 textureSize(iChannel1, 0)
#define Res iResolution.xy
#define randSamp iChannel1
#define colorSamp iChannel0
vec4 getRand(vec2 pos)
{
return textureLod(iChannel1, pos / Res1 / iResolution.y * 1080., 0.0);
}
vec4 getCol(vec2 pos)
{
vec2 uv = ((pos - Res.xy * .5) / Res.y * Res0.y) / Res0.xy + .5;
vec4 c1 = texture(iChannel0, uv);
vec4 e = smoothstep(vec4(-0.05), vec4(-0.0), vec4(uv, vec2(1) - uv));
c1 = mix(vec4(1, 1, 1, 0), c1, e.x * e.y * e.z * e.w);
float d = clamp(dot(c1.xyz, vec3(-.5, 1., -.5)), 0.0, 1.0);
vec4 c2 = vec4(.7);
return min(mix(c1, c2, 1.8 * d), .7);
}
vec4 getColHT(vec2 pos)
{
return smoothstep(.95, 1.05, getCol(pos) * .8 + .2 + getRand(pos * .7));
}
float getVal(vec2 pos)
{
vec4 c = getCol(pos);
return pow(dot(c.xyz, vec3(.333)), 1.) * 1.;
}
vec2 getGrad(vec2 pos, float eps)
{
vec2 d = vec2(eps, 0);
return vec2(
getVal(pos + d.xy) - getVal(pos - d.xy),
getVal(pos + d.yx) - getVal(pos - d.yx)
) / eps / 2.;
}
#define PI2 6.28318530717959
void main()
{
vec2 pos = TexCoord * iResolution.xy + 4.0 * sin(iTime * 1. * vec2(1, 1.7)) * iResolution.y / 400.;
vec3 col = vec3(0);
vec3 col2 = vec3(0);
float sum = 0.;
for (int i = 0; i < int(iAngleNum); i++)
{
float ang = PI2 / iAngleNum * (float(i) + .8);
vec2 v = vec2(cos(ang), sin(ang));
for (int j = 0; j < int(iSampNum); j++)
{
vec2 dpos = v.yx * vec2(1, -1) * float(j) * iLineWidth * iResolution.y / 400.;
vec2 dpos2 = v.xy * float(j * j) / iSampNum * .5 * iLineWidth * iResolution.y / 400.;
vec2 g;
float fact;
float fact2;
for (float s = -1.; s <= 1.; s += 2.)
{
vec2 pos2 = pos + s * dpos + dpos2;
vec2 pos3 = pos + (s * dpos + dpos2).yx * vec2(1, -1) * 2.;
g = getGrad(pos2, .4);
fact = dot(g, v) - .5 * abs(dot(g, v.yx * vec2(1, -1)));
fact2 = dot(normalize(g + vec2(.0001)), v.yx * vec2(1, -1));
fact = clamp(fact, 0., .05);
fact2 = abs(fact2);
fact *= 1. - float(j) / iSampNum;
col += fact;
col2 += fact2 * getColHT(pos3).xyz;
sum += fact2;
}
}
}
col /= iSampNum * iAngleNum * .75 / sqrt(iResolution.y);
col2 /= sum;
col.x *= (.6 + .8 * getRand(pos * .7).x);
col.x = 1. - col.x;
col.x *= col.x * col.x;
vec2 s = sin(pos.xy * .1 / sqrt(iResolution.y / 400.));
vec3 karo = vec3(1);
karo -= .5 * vec3(.25, .1, .1) * dot(exp(-s * s * 80.), vec2(1));
float r = length(pos - iResolution.xy * .5) / iResolution.x;
float vign = 1. - r * r * r * iVignette;
FragColor = vec4(vec3(col.x * col2 * karo * vign), 1);
}
"""
class FL_PaperDrawn:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
"optional": {
"angle_num": ("FLOAT", {"default": 3.0, "min": 1.0, "max": 10.0, "step": 1.0}),
"samp_num": ("FLOAT", {"default": 2.2, "min": 1.0, "max": 10.0, "step": 0.1}),
"line_width": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
"vignette": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.1}),
"fps": ("INT", {"default": 30, "min": 1, "max": 120, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_shader"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_shader(self, image, angle_num, samp_num, line_width, vignette, fps):
result = []
total_images = len(image)
frame_time = 1.0 / fps
pbar = ProgressBar(total_images)
for i, img in enumerate(image, start=1):
img = self.t2p(img)
result_img = self.process_image(img, angle_num, samp_num, line_width, vignette, i * frame_time)
result_img = self.p2t(result_img)
result.append(result_img)
pbar.update_absolute(i)
return (torch.cat(result, dim=0),)
def process_image(self, image, angle_num, samp_num, line_width, vignette, time):
# Convert the PIL image to a numpy array
img_array = np.array(image).astype(np.float32) / 255.0
# Create a white image for iChannel1
white_image = np.ones((image.height, image.width, 3), dtype=np.float32)
# Create a PyOpenGL context
if not glfw.init():
raise RuntimeError("Failed to initialize GLFW")
glfw.window_hint(glfw.VISIBLE, glfw.FALSE)
window = glfw.create_window(image.width, image.height, "Hidden Window", None, None)
if not window:
glfw.terminate()
raise RuntimeError("Failed to create GLFW window")
glfw.make_context_current(window)
# Compile the shader program
vertex_shader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
gl.glShaderSource(vertex_shader, VERTEX_SHADER)
gl.glCompileShader(vertex_shader)
fragment_shader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
gl.glShaderSource(fragment_shader, FRAGMENT_SHADER)
gl.glCompileShader(fragment_shader)
shader_program = gl.glCreateProgram()
gl.glAttachShader(shader_program, vertex_shader)
gl.glAttachShader(shader_program, fragment_shader)
gl.glLinkProgram(shader_program)
gl.glUseProgram(shader_program)
# Set up vertex buffer object (VBO) and vertex array object (VAO)
vertices = np.array([
-1.0, -1.0, 0.0, 0.0, 0.0,
1.0, -1.0, 0.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 0.0, 1.0,
1.0, 1.0, 0.0, 1.0, 1.0
], dtype=np.float32)
vao = gl.glGenVertexArrays(1)
gl.glBindVertexArray(vao)
vbo = gl.glGenBuffers(1)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_STATIC_DRAW)
gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, 5 * vertices.itemsize, None)
gl.glEnableVertexAttribArray(0)
gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, gl.GL_FALSE, 5 * vertices.itemsize, ctypes.c_void_p(3 * vertices.itemsize))
gl.glEnableVertexAttribArray(1)
# Set up textures
texture0 = gl.glGenTextures(1)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture0)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.width, image.height, 0, gl.GL_RGB, gl.GL_FLOAT, img_array)
texture1 = gl.glGenTextures(1)
gl.glActiveTexture(gl.GL_TEXTURE1)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture1)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.width, image.height, 0, gl.GL_RGB, gl.GL_FLOAT, white_image)
# Set shader uniforms
gl.glUniform1i(gl.glGetUniformLocation(shader_program, "iChannel0"), 0)
gl.glUniform1i(gl.glGetUniformLocation(shader_program, "iChannel1"), 1)
gl.glUniform3f(gl.glGetUniformLocation(shader_program, "iResolution"), image.width, image.height, 0.0)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iTime"), time)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iAngleNum"), angle_num)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iSampNum"), samp_num)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iLineWidth"), line_width)
gl.glUniform1f(gl.glGetUniformLocation(shader_program, "iVignette"), vignette)
# Render the shader
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
# Read the rendered image from the framebuffer
img_data = gl.glReadPixels(0, 0, image.width, image.height, gl.GL_RGB, gl.GL_FLOAT)
img_array = np.frombuffer(img_data, dtype=np.float32).reshape((image.height, image.width, 3))
# Clean up OpenGL resources
gl.glDeleteTextures(2, [texture0, texture1])
gl.glDeleteBuffers(1, [vbo])
gl.glDeleteVertexArrays(1, [vao])
gl.glDeleteProgram(shader_program)
gl.glDeleteShader(vertex_shader)
gl.glDeleteShader(fragment_shader)
glfw.destroy_window(window)
glfw.terminate()
# Convert the processed image back to a PIL image
processed_image = Image.fromarray((img_array * 255).astype(np.uint8))
return processed_image
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-152
View File
@@ -1,152 +0,0 @@
import torch
import torch.nn.functional as F
from torchvision.ops import masks_to_boxes
from torchvision.transforms.functional import resize as tv_resize, InterpolationMode
import numpy as np
from PIL import Image
class FL_PasteOnCanvas:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("IMAGE",),
"canvas_width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 32}),
"canvas_height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 32}),
"background_red": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
"background_green": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
"background_blue": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
"padding": ("INT", {"default": 0, "min": 0, "max": 512, "step": 1}),
"resize_algorithm": (["bilinear", "nearest", "bicubic", "lanczos"],),
"include_alpha": ("BOOLEAN", {"default": False}),
},
"optional": {
"bg_image_optional": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "cut_and_paste"
CATEGORY = "🏵️Fill Nodes/utility"
def cut_and_paste(self, image, mask, canvas_width, canvas_height, background_red, background_green, background_blue,
padding, resize_algorithm, include_alpha, bg_image_optional=None):
# Ensure inputs are in the correct format
image = self.tensor_to_rgba(image)
mask = self.tensor_to_mask(mask)
B, H, W, C = image.shape
mask = F.interpolate(mask.unsqueeze(1), size=(H, W), mode='nearest')[:, 0, :, :]
MB, MH, MW = mask.shape
if MB < B:
assert B % MB == 0, "Batch size mismatch between image and mask"
mask = mask.repeat(B // MB, 1, 1)
# Prepare the background canvas
if bg_image_optional is not None:
canvas = self.prepare_background_image(bg_image_optional, canvas_width, canvas_height, B)
else:
background_color = torch.tensor([background_red, background_green, background_blue, 255],
dtype=torch.float32, device=image.device) / 255.0
canvas = background_color.expand(B, canvas_height, canvas_width, 4).clone()
# Handle empty masks
is_empty = ~torch.gt(mask.view(MB, -1).max(dim=1).values, 0)
mask[is_empty, 0, 0] = 1
boxes = masks_to_boxes(mask)
mask[is_empty, 0, 0] = 0
# Create alpha mask
alpha_mask = torch.ones((B, H, W, 4), device=image.device)
alpha_mask[..., 3] = mask
masked_image = image * alpha_mask
for i in range(B):
if not is_empty[i]:
box = boxes[i].long()
y1, x1, y2, x2 = box[1], box[0], box[3], box[2]
cropped = masked_image[i, y1:y2 + 1, x1:x2 + 1, :]
# Calculate scaling factor to fit within canvas, considering padding
available_width = canvas_width - 2 * padding
available_height = canvas_height - 2 * padding
scale = min(available_width / cropped.shape[1], available_height / cropped.shape[0])
new_h, new_w = int(cropped.shape[0] * scale), int(cropped.shape[1] * scale)
# Resize cropped image using the specified algorithm
resized = self.resize_image(cropped, (new_h, new_w), resize_algorithm)
# Calculate position to center the image on canvas, including padding
start_y = padding + (available_height - new_h) // 2
start_x = padding + (available_width - new_w) // 2
# Prepare the region of the canvas where we'll paste the image
canvas_region = canvas[i, start_y:start_y + new_h, start_x:start_x + new_w].clone()
# Blend the resized image with the canvas region
alpha = resized[..., 3:4]
blended = resized[..., :3] * alpha + canvas_region[..., :3] * (1 - alpha)
# Update the alpha channel
new_alpha = torch.maximum(canvas_region[..., 3:], resized[..., 3:])
# Combine the blended color channels with the new alpha
result = torch.cat([blended, new_alpha], dim=-1)
# Update the canvas with the result
canvas[i, start_y:start_y + new_h, start_x:start_x + new_w] = result
# Remove alpha channel if not included
if not include_alpha:
canvas = canvas[..., :3]
return (canvas,)
def prepare_background_image(self, bg_image_optional, canvas_width, canvas_height, batch_size):
bg_image_optional = self.tensor_to_rgba(bg_image_optional)
# Resize background image to match canvas size
resized_bg = F.interpolate(bg_image_optional.permute(0, 3, 1, 2),
size=(canvas_height, canvas_width),
mode='bilinear',
align_corners=False).permute(0, 2, 3, 1)
# If the background image batch size is 1, repeat it to match the main batch size
if resized_bg.shape[0] == 1 and batch_size > 1:
resized_bg = resized_bg.repeat(batch_size, 1, 1, 1)
return resized_bg
def resize_image(self, image, size, algorithm):
if algorithm == "lanczos":
# Convert to PIL Image for Lanczos resampling
pil_image = Image.fromarray((image.cpu().numpy() * 255).astype('uint8'))
resized_pil = pil_image.resize(size[::-1], Image.LANCZOS) # PIL uses (width, height)
return torch.from_numpy(np.array(resized_pil)).float().to(image.device) / 255.0
else:
# Use torchvision's resize for other algorithms
interpolation_mode = {
"bilinear": InterpolationMode.BILINEAR,
"nearest": InterpolationMode.NEAREST,
"bicubic": InterpolationMode.BICUBIC,
}[algorithm]
return tv_resize(image.permute(2, 0, 1), size, interpolation=interpolation_mode).permute(1, 2, 0)
@staticmethod
def tensor_to_rgba(tensor):
if len(tensor.shape) == 3:
return tensor.unsqueeze(-1).expand(-1, -1, -1, 4)
elif tensor.shape[-1] == 1:
return tensor.expand(-1, -1, -1, 4)
elif tensor.shape[-1] == 3:
return torch.cat([tensor, torch.ones_like(tensor[:, :, :, :1])], dim=-1)
return tensor
@staticmethod
def tensor_to_mask(tensor):
if len(tensor.shape) == 4:
return tensor.mean(dim=-1)
return tensor
-131
View File
@@ -1,131 +0,0 @@
import torch
import numpy as np
from PIL import Image
from sklearn.cluster import KMeans
from comfy.utils import ProgressBar
class FL_PixelArtShader:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"pixel_size": ("FLOAT", {"default": 100.0, "min": 1.0, "max": 1000.0, "step": 1.0}),
"color_depth": ("FLOAT", {"default": 50.0, "min": 1.0, "max": 255.0, "step": 1.0}),
"use_aspect_ratio": ("BOOLEAN", {"default": True}),
"palette_image": ("IMAGE", {"default": None}),
"palette_colors": ("INT", {"default": 16, "min": 2, "max": 15, "step": 1}),
"mask": ("IMAGE", {"default": None}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_pixel_art_shader"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_pixel_art_shader(self, images, use_aspect_ratio, pixel_size, color_depth, palette_image=None,
palette_colors=16, mask=None):
result = []
total_images = len(images)
pbar = ProgressBar(total_images)
if palette_image is not None:
palette = extract_palette(self.t2p(palette_image[0]), palette_colors)
else:
palette = None
mask_images = self.prepare_mask_batch(mask, total_images) if mask is not None else None
for idx, image in enumerate(images):
img = self.t2p(image)
mask_img = self.process_mask(mask_images[idx], img.size) if mask_images is not None else None
result_img = pixel_art_effect(img, pixel_size, color_depth, use_aspect_ratio, palette, mask_img)
result_img = self.p2t(result_img)
result.append(result_img)
pbar.update_absolute(idx + 1)
return (torch.cat(result, dim=0),)
def t2p(self, t):
i = 255.0 * t.cpu().numpy().squeeze()
return Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
def p2t(self, p):
i = np.array(p).astype(np.float32) / 255.0
return torch.from_numpy(i).unsqueeze(0)
def prepare_mask_batch(self, mask, total_images):
if mask is None:
return None
mask_images = [self.t2p(m) for m in mask]
if len(mask_images) < total_images:
mask_images = mask_images * (total_images // len(mask_images) + 1)
return mask_images[:total_images]
def process_mask(self, mask, target_size):
mask = mask.resize(target_size, Image.LANCZOS)
return mask.convert('L') if mask.mode != 'L' else mask
def extract_palette(image, n_colors):
image = image.convert('RGB')
pixels = np.array(image).reshape(-1, 3)
kmeans = KMeans(n_clusters=n_colors, random_state=42)
kmeans.fit(pixels)
colors = kmeans.cluster_centers_
return torch.from_numpy(colors.astype(np.float32) / 255.0).to("cuda")
def pixel_art_effect(image, pixel_size, color_depth, use_aspect_ratio, palette, mask=None):
image = torch.tensor(np.array(image)).float().to("cuda") / 255.0
height, width = image.shape[0], image.shape[1]
uv_x = torch.linspace(0, 1, width, device="cuda")
uv_y = torch.linspace(0, 1, height, device="cuda")
uv_grid = torch.stack(torch.meshgrid(uv_y, uv_x), dim=-1)
output_tensor = evaluate_shader(image, uv_grid, pixel_size, color_depth, use_aspect_ratio)
if palette is not None:
output_tensor = apply_palette(output_tensor, palette)
if mask is not None:
mask_tensor = torch.tensor(np.array(mask)).float().to("cuda") / 255.0
mask_tensor = mask_tensor.unsqueeze(-1).expand(-1, -1, 3)
output_tensor = output_tensor * mask_tensor + image * (1 - mask_tensor)
return Image.fromarray((output_tensor.cpu().numpy() * 255).astype(np.uint8))
def evaluate_shader(image, uv_grid, pixel_size, color_depth, use_aspect_ratio):
if use_aspect_ratio:
aspect_ratio = image.shape[1] / image.shape[0]
pixel_size_x, pixel_size_y = pixel_size, pixel_size * aspect_ratio
else:
pixel_size_x = pixel_size_y = pixel_size
pixelUV_x = torch.floor(uv_grid[..., 1] * pixel_size_x) / pixel_size_x
pixelUV_y = torch.floor(uv_grid[..., 0] * pixel_size_y) / pixel_size_y
pixelUV = torch.stack((pixelUV_y, pixelUV_x), dim=-1)
color = texture_lookup(image, pixelUV)
return adjust_color(color, color_depth)
def adjust_color(color, color_depth):
return torch.floor(color * color_depth) / color_depth
def texture_lookup(image, uv):
uv = torch.clamp(uv, 0.0, 1.0)
y = (uv[..., 0] * (image.shape[0] - 1)).long()
x = (uv[..., 1] * (image.shape[1] - 1)).long()
return image[y, x]
def apply_palette(image, palette):
original_shape = image.shape
pixels = image.reshape(-1, 3)
distances = torch.cdist(pixels, palette)
nearest_palette_indices = torch.argmin(distances, dim=1)
new_pixels = palette[nearest_palette_indices]
return new_pixels.reshape(original_shape)
-75
View File
@@ -1,75 +0,0 @@
import torch
import numpy as np
from PIL import Image
from colorsys import rgb_to_hsv
from comfy.utils import ProgressBar
class FL_PixelSort:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"direction": (["Horizontal", "Vertical"],),
"threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"smoothing": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
"rotation": ("INT", {"default": 0, "min": 0, "max": 3, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "pixel_sort_saturation"
CATEGORY = "🏵️Fill Nodes/VFX"
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def saturation(self, pixel):
r, g, b = pixel
_, s, _ = rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
return s
def pixel_sort_saturation(self, images, direction="Horizontal", threshold=0.5, smoothing=0.1, rotation=0):
out = []
total_images = len(images)
pbar = ProgressBar(total_images)
for i, img in enumerate(images, start=1):
p = self.t2p(img)
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)
pbar.update_absolute(i)
out = torch.cat(out, 0)
return (out,)
def sort_pixels(self, image, value, threshold, smoothing, rotation=0):
pixels = np.rot90(np.array(image), rotation)
values = np.apply_along_axis(value, 2, pixels)
edges = np.apply_along_axis(lambda row: np.convolve(row, [-1, 1], 'same'), 0, values > threshold)
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]
pbar = ProgressBar(len(values))
for row, key in enumerate(values):
order = np.split(key, intervals[row])
for index, interval in enumerate(order[1:]):
order[index + 1] = np.argsort(interval) + intervals[row][index]
order[0] = range(order[0].size)
order = np.concatenate(order)
for channel in range(3):
pixels[row, :, channel] = pixels[row, order.astype('uint32'), channel]
pbar.update_absolute(row)
return Image.fromarray(np.rot90(pixels, -rotation))
-36
View File
@@ -1,36 +0,0 @@
class FL_PromptSelector:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prepend_text": ("STRING", {"multiline": True, "default": ""}),
"prompts": ("STRING", {"multiline": True}),
"append_text": ("STRING", {"multiline": True, "default": ""}),
"index": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
},
"optional": {},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "select_prompt"
CATEGORY = "🏵️Fill Nodes/utility"
def select_prompt(self, prepend_text, prompts, append_text, index):
prepend_text = prepend_text.strip()
prompt_lines = prompts.split("\n")
append_text = append_text.strip()
num_prompts = len(prompt_lines)
if index < 0 or index >= num_prompts:
raise ValueError(f"Index {index} is out of range. Please provide an index between 0 and {num_prompts - 1}.")
selected_prompt = prompt_lines[index].strip()
if prepend_text:
selected_prompt = prepend_text + " " + selected_prompt
if append_text:
selected_prompt = selected_prompt + " " + append_text
return (selected_prompt,)
-38
View File
@@ -1,38 +0,0 @@
import random
import torch
class FL_RandomNumber:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"min_value": ("FLOAT", {"default": 0.0, "min": -1000000.0, "max": 1000000.0, "step": 0.1}),
"max_value": ("FLOAT", {"default": 1.0, "min": -1000000.0, "max": 1000000.0, "step": 0.1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 1000000}),
},
}
RETURN_TYPES = ("INT", "FLOAT")
FUNCTION = "generate_random_number"
CATEGORY = "🏵️Fill Nodes/utility"
def generate_random_number(self, min_value=0.0, max_value=1.0, seed=0):
if min_value > max_value:
raise ValueError("min_value should be less than or equal to max_value")
# Generate a random seed if seed is 0
if seed == 0:
seed = random.randint(1, 1000000)
# Set the random seed for reproducibility
random.seed(seed)
torch.manual_seed(seed)
# Generate a random float value within the specified range
random_float = min_value + (max_value - min_value) * random.random()
# Generate a random integer value within the specified range
random_int = int(min_value + (max_value - min_value) * random.random())
return (random_int, random_float)
-81
View File
@@ -1,81 +0,0 @@
import torch
import numpy as np
from PIL import Image, ImageEnhance, ImageOps, ImageFilter # Added ImageFilter import
import sys
class FL_RetroEffect:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"color_offset": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
"scanline_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"vignette_strength": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_strength": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.01}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_retro_effect"
CATEGORY = "🏵️Fill Nodes/VFX"
def apply_retro_effect(self, images, color_offset, scanline_strength, vignette_strength, noise_strength):
result = []
total_images = len(images)
for i, image in enumerate(images, start=1):
img = self.t2p(image)
result_img = self.process_image(img, color_offset, scanline_strength, vignette_strength, noise_strength)
result_img = self.p2t(result_img)
result.append(result_img)
# Update the print log
progress = i / total_images * 100
sys.stdout.write(f"\rProcessing images: {progress:.2f}%")
sys.stdout.flush()
# Print a new line after the progress log
print()
return (torch.cat(result, dim=0),)
def process_image(self, image, color_offset, scanline_strength, vignette_strength, noise_strength):
# Apply color offset
r, g, b = image.split()
r = ImageEnhance.Brightness(r).enhance(1 + color_offset)
b = ImageEnhance.Brightness(b).enhance(1 - color_offset)
image = Image.merge("RGB", (r, g, b))
# Apply scanlines
scanline_mask = Image.new("L", image.size, 0)
for y in range(0, image.size[1], 2):
scanline_mask.paste(int(255 * scanline_strength), (0, y, image.size[0], y + 1))
image.paste(image, mask=scanline_mask)
# Apply vignette
vignette_mask = Image.new("L", image.size, 0)
vignette_mask.paste(255, (0, 0, image.size[0], image.size[1]))
vignette_mask = ImageOps.invert(vignette_mask)
vignette_mask = vignette_mask.filter(ImageFilter.GaussianBlur(radius=image.size[0] * vignette_strength))
image.paste(image, mask=ImageOps.invert(vignette_mask))
# Apply noise
noise = Image.effect_noise(image.size, sigma=noise_strength * 255).convert("RGB")
image = Image.blend(image, noise, noise_strength)
return image
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
p = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return p
def p2t(self, p):
if p is not None:
i = np.array(p).astype(np.float32) / 255.0
t = torch.from_numpy(i).unsqueeze(0)
return t
-76
View File
@@ -1,76 +0,0 @@
import torch
import numpy as np
from PIL import Image
import math
from comfy.utils import ProgressBar
class FL_Ripple:
def __init__(self):
self.modulation_index = 0
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"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}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "ripple"
CATEGORY = "🏵️Fill Nodes/VFX"
def t2p(self, t):
if t is not None:
i = 255.0 * t.cpu().numpy().squeeze()
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, center_x=50.0, center_y=50.0, modulation=0.0):
out = []
total_images = len(images)
pbar = ProgressBar(total_images)
for i, img in enumerate(images, start=1):
p = self.t2p(img)
width, height = p.size
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_pixel
dy = y - center_y_pixel
distance = np.sqrt(dx ** 2 + dy ** 2)
# 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)
p_array = np.array(p)
rippled_array = p_array[sample_y, sample_x]
o = rippled_array.astype(np.float32) / 255.0
o = torch.from_numpy(o).unsqueeze(0)
out.append(o)
self.modulation_index += 1
pbar.update_absolute(i)
out = torch.cat(out, 0)
return (out,)
-57
View File
@@ -1,57 +0,0 @@
import torch
import cv2
import numpy as np
class FL_SeparateMaskComponents:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE", "MASK_MAPPING")
FUNCTION = "separate"
CATEGORY = "🏵️Fill Nodes/utility"
def separate(self, mask):
device = mask.device
# Ensure mask is in the correct format (B, H, W, C)
if mask.dim() == 3:
mask = mask.unsqueeze(-1)
B, H, W, C = mask.shape
all_component_masks = []
all_mappings = []
for b in range(B):
# Convert to numpy and ensure it's a single-channel image
mask_np = mask[b].squeeze().cpu().numpy()
if mask_np.ndim == 3:
mask_np = mask_np.mean(axis=-1) # Average across channels if multi-channel
# Threshold the mask
mask_np = (mask_np > 0).astype(np.uint8)
# Use OpenCV for connected component labeling
num_labels, labels = cv2.connectedComponents(mask_np)
for i in range(1, num_labels): # Skip background (label 0)
component_mask = (labels == i)
component_tensor = torch.from_numpy(component_mask).to(device).unsqueeze(-1).expand(-1, -1, C)
all_component_masks.append(component_tensor * mask[b])
all_mappings.append(b)
if all_component_masks:
result = torch.stack(all_component_masks)
mappings = torch.tensor(all_mappings, device=device)
else:
# Handle case where no components were found
result = torch.zeros((0, H, W, C), device=device)
mappings = torch.zeros(0, dtype=torch.long, device=device)
return (result, mappings)
-221
View File
@@ -1,221 +0,0 @@
import numpy as np
import torch
import OpenGL.GL as gl
import glfw
from comfy.utils import ProgressBar
SHADERTOY_HEADER = """
#version 440
precision highp float;
uniform vec3 iResolution;
uniform vec4 iMouse;
uniform float iTime;
uniform float iTimeDelta;
uniform float iFrameRate;
uniform int iFrame;
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform sampler2D iChannel2;
uniform sampler2D iChannel3;
#define texture2D texture
"""
SHADERTOY_FOOTER = """
layout(location = 0) out vec4 _fragColor;
void main()
{
mainImage(_fragColor, gl_FragCoord.xy);
}
"""
SHADERTOY_DEFAULT = """
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;
// Time varying pixel color
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
// Output to screen
fragColor = vec4(col,1.0);
}
"""
def render_surface_and_context_init(width, height):
if not glfw.init():
raise RuntimeError("GLFW did not init")
glfw.window_hint(glfw.VISIBLE, glfw.FALSE) # hidden
window = glfw.create_window(width, height, "hidden", None, None)
if not window:
raise RuntimeError("GLFW did not init window")
glfw.make_context_current(window)
return {}
def render_surface_and_context_deinit(**kwargs):
glfw.terminate()
def compile_shader(source, shader_type):
shader = gl.glCreateShader(shader_type)
gl.glShaderSource(shader, source)
gl.glCompileShader(shader)
if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
raise RuntimeError(gl.glGetShaderInfoLog(shader))
return shader
def compile_program(vertex_source, fragment_source):
vertex_shader = compile_shader(vertex_source, gl.GL_VERTEX_SHADER)
fragment_shader = compile_shader(fragment_source, gl.GL_FRAGMENT_SHADER)
program = gl.glCreateProgram()
gl.glAttachShader(program, vertex_shader)
gl.glAttachShader(program, fragment_shader)
gl.glLinkProgram(program)
if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE:
raise RuntimeError(gl.glGetProgramInfoLog(program))
return program
def setup_framebuffer(width, height):
texture = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, width, height, 0, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, None)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
fbo = gl.glGenFramebuffers(1)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, texture, 0)
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
raise RuntimeError("Framebuffer is not complete")
return fbo, texture
def setup_render_resources(width, height, fragment_source: str):
ctx = render_surface_and_context_init(width, height)
vertex_source = """
#version 330 core
void main()
{
vec2 verts[3] = vec2[](vec2(-1, -1), vec2(3, -1), vec2(-1, 3));
gl_Position = vec4(verts[gl_VertexID], 0, 1);
}
"""
shader = compile_program(vertex_source, fragment_source)
fbo, texture = setup_framebuffer(width, height)
textures = gl.glGenTextures(4)
return (ctx, fbo, shader, textures)
def render_resources_cleanup(ctx):
# assume all other resources get cleaned up with the context
render_surface_and_context_deinit(**ctx)
def render(width, height, fbo, shader):
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
gl.glClearColor(0.0, 0.0, 0.0, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glUseProgram(shader)
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
data = gl.glReadPixels(0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE)
image = np.frombuffer(data, dtype=np.uint8).reshape(height, width, 3)
image = image[::-1, :, :]
image = np.array(image).astype(np.float32) / 255.0
return image
def shadertoy_vars_update(shader, width, height, time, time_delta, frame_rate, frame):
gl.glUseProgram(shader)
iResolution_location = gl.glGetUniformLocation(shader, "iResolution")
gl.glUniform3f(iResolution_location, width, height, 0)
iMouse_location = gl.glGetUniformLocation(shader, "iMouse")
gl.glUniform4f(iMouse_location, 0, 0, 0, 0)
iTime_location = gl.glGetUniformLocation(shader, "iTime")
gl.glUniform1f(iTime_location, time)
iTimeDelta_location = gl.glGetUniformLocation(shader, "iTimeDelta")
gl.glUniform1f(iTimeDelta_location, time_delta)
iFrameRate_location = gl.glGetUniformLocation(shader, "iFrameRate")
gl.glUniform1f(iFrameRate_location, frame_rate)
iFrame_location = gl.glGetUniformLocation(shader, "iFrame")
gl.glUniform1i(iFrame_location, frame)
def shadertoy_texture_update(texture, image, frame):
if len(image.shape) == 4:
image = image[frame]
image = image.cpu().numpy()
image = image[::-1, :, :]
gl.glBindTexture(gl.GL_TEXTURE_2D, texture)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.shape[1], image.shape[0], 0, gl.GL_RGB, gl.GL_FLOAT, image)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
def shadertoy_texture_bind(shader, textures):
gl.glUseProgram(shader)
for i in range(4):
gl.glActiveTexture(gl.GL_TEXTURE0 + i) # type: ignore
gl.glBindTexture(gl.GL_TEXTURE_2D, textures[i])
iChannel_location = gl.glGetUniformLocation(shader, f"iChannel{i}")
gl.glUniform1i(iChannel_location, i)
class FL_Shadertoy:
@classmethod
def INPUT_TYPES(s):
return {"required": {"width": ("INT", {"default": 512, "min": 64, "max": 15360, "step": 8}),
"height": ("INT", {"default": 512, "min": 64, "max": 15360, "step": 8}),
"frame_count": ("INT", {"default": 1, "min": 1, "max": 262144}),
"fps": ("INT", {"default": 1, "min": 1, "max": 120}),
"source": (
"STRING", {"default": SHADERTOY_DEFAULT, "multiline": True, "dynamicPrompts": False})},
"optional": {"channel_0": ("IMAGE",),
"channel_1": ("IMAGE",),
"channel_2": ("IMAGE",),
"channel_3": ("IMAGE",)}}
RETURN_TYPES = ("IMAGE",)
CATEGORY = "🏵️Fill Nodes/VFX"
FUNCTION = "render"
def render(self, width: int, height: int, frame_count: int, fps: int, source: str,
channel_0: torch.Tensor | None = None, channel_1: torch.Tensor | None = None,
channel_2: torch.Tensor | None = None, channel_3: torch.Tensor | None = None):
fragment_source = SHADERTOY_HEADER
fragment_source += source
fragment_source += SHADERTOY_FOOTER
ctx, fbo, shader, textures = setup_render_resources(width, height, fragment_source)
images = []
frame = 0
pbar = ProgressBar(frame_count)
for idx in range(frame_count):
shadertoy_vars_update(shader, width, height, frame * (1.0 / fps), (1.0 / fps), fps, frame)
if channel_0 is not None: shadertoy_texture_update(textures[0], channel_0, frame)
if channel_1 is not None: shadertoy_texture_update(textures[1], channel_1, frame)
if channel_2 is not None: shadertoy_texture_update(textures[2], channel_2, frame)
if channel_3 is not None: shadertoy_texture_update(textures[3], channel_3, frame)
shadertoy_texture_bind(shader, textures)
image = render(width, height, fbo, shader)
image = torch.from_numpy(image)[None,]
images.append(image)
frame += 1
pbar.update_absolute(idx)
render_resources_cleanup(ctx)
return (torch.cat(images, dim=0),)
-70
View File
@@ -1,70 +0,0 @@
import torch
import comfy.sd
import comfy.model_base
import comfy.samplers
import comfy.sample
import comfy.k_diffusion.sampling
class FL_TD_KSampler:
@classmethod
def INPUT_TYPES(s):
return {"required": {"model": ("MODEL",),
"conditioning_positive": ("CONDITIONING",),
"conditioning_negative": ("CONDITIONING",),
"latent_image": ("LATENT",),
"steps": ("INT", {"default": 20, "min": 1, "max": 1000, "step": 1}),
"seed": ("INT", {"default": 42, "min": 0, "max": 2 ** 32 - 1}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01})}}
RETURN_TYPES = ("LATENT",)
FUNCTION = "sample"
CATEGORY = "🏵️Fill Nodes/experiments"
def sample(self, model, conditioning_positive, conditioning_negative, latent_image, steps, seed, cfg, sampler_name,
scheduler, denoise):
device = comfy.model_management.get_torch_device()
latent = latent_image["samples"]
original_shape = latent.shape
# Set the seed for reproducibility
torch.manual_seed(seed)
# Setup noise
noise = torch.randn_like(latent, device=device)
# Setup sampler
sampler = comfy.samplers.KSampler(model, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=denoise, model_options=model.model_options)
# Setup progress bar
pbar = comfy.utils.ProgressBar(steps)
def callback(step, x0, x, total_steps):
pbar.update_absolute(step + 1, total_steps)
disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
try:
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler,
conditioning_positive, conditioning_negative, latent,
denoise=denoise, disable_noise=False, start_step=0, last_step=steps,
force_full_denoise=True, noise_mask=None, callback=callback,
disable_pbar=disable_pbar, seed=seed)
except Exception as e:
print('Custom KSampler error encountered:', e)
raise e
finally:
if pbar:
pbar.update_absolute(steps, steps)
# Prepare the output in the expected format
out = {
"samples": samples,
"original_shape": original_shape,
"noise_seed": seed,
"steps": steps
}
return out
-11
View File
@@ -1,11 +0,0 @@
class FL_TetrisGame:
@classmethod
def INPUT_TYPES(s):
return {"required": {}}
RETURN_TYPES = ()
FUNCTION = "execute"
CATEGORY = "🏵️Fill Nodes/games"
def execute(self):
return ()
-181
View File
@@ -1,181 +0,0 @@
import torch
import torch.nn.functional as F
import numpy as np
class FL_VideoCropMask:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video": ("IMAGE",),
"mask": ("IMAGE",),
"output_width": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 64}),
"output_height": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 64}),
"padding": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
"smoothing_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}),
},
}
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "CROP_DATA")
RETURN_NAMES = ("cropped_video", "mask", "original_video", "crop_data")
FUNCTION = "crop_video"
CATEGORY = "🏵️Fill Nodes/utility"
def crop_video(self, video: torch.Tensor, mask: torch.Tensor, output_width: int, output_height: int, padding: int,
smoothing_factor: float):
batch_size, height, width, channels = video.shape
cropped_video = []
cropped_masks = []
crop_data_list = []
prev_center_x, prev_center_y = None, None
prev_crop_width, prev_crop_height = None, None
for i in range(batch_size):
frame = video[i]
frame_mask = mask[i]
# Find the bounding box of the mask
mask_binary = (frame_mask.sum(dim=-1) > 0).float()
y_indices, x_indices = torch.where(mask_binary > 0)
if len(y_indices) == 0 or len(x_indices) == 0:
# If no mask is found, use the previous crop or the center of the frame
if prev_center_x is None:
center_y, center_x = height // 2, width // 2
crop_width, crop_height = width, height
else:
center_y, center_x = prev_center_y, prev_center_x
crop_width, crop_height = prev_crop_width, prev_crop_height
else:
top, bottom = y_indices.min().item(), y_indices.max().item()
left, right = x_indices.min().item(), x_indices.max().item()
center_y = (top + bottom) // 2
center_x = (left + right) // 2
crop_width = right - left + 2 * padding
crop_height = bottom - top + 2 * padding
# Apply smoothing to the center position and crop size
if prev_center_x is not None:
center_x = int(smoothing_factor * center_x + (1 - smoothing_factor) * prev_center_x)
center_y = int(smoothing_factor * center_y + (1 - smoothing_factor) * prev_center_y)
crop_width = int(smoothing_factor * crop_width + (1 - smoothing_factor) * prev_crop_width)
crop_height = int(smoothing_factor * crop_height + (1 - smoothing_factor) * prev_crop_height)
prev_center_x, prev_center_y = center_x, center_y
prev_crop_width, prev_crop_height = crop_width, crop_height
# Calculate the aspect ratio of the output and the crop
output_aspect_ratio = output_width / output_height
crop_aspect_ratio = crop_width / crop_height
# Adjust crop size to fit the output aspect ratio without distortion
if crop_aspect_ratio > output_aspect_ratio:
# Crop is wider, adjust height
crop_height = int(crop_width / output_aspect_ratio)
else:
# Crop is taller, adjust width
crop_width = int(crop_height * output_aspect_ratio)
# Ensure the crop stays within the frame
top = max(0, center_y - crop_height // 2)
bottom = min(height, top + crop_height)
left = max(0, center_x - crop_width // 2)
right = min(width, left + crop_width)
# Adjust if the crop goes out of bounds
if top == 0:
bottom = crop_height
if bottom == height:
top = height - crop_height
if left == 0:
right = crop_width
if right == width:
left = width - crop_width
# Crop the video and mask
cropped_frame = frame[top:bottom, left:right, :]
cropped_frame_mask = frame_mask[top:bottom, left:right, :]
# Resize the cropped video and mask to the desired output size
cropped_frame = F.interpolate(cropped_frame.unsqueeze(0).permute(0, 3, 1, 2),
size=(output_height, output_width), mode='bilinear',
align_corners=False).squeeze(0).permute(1, 2, 0)
cropped_frame_mask = F.interpolate(cropped_frame_mask.unsqueeze(0).permute(0, 3, 1, 2),
size=(output_height, output_width), mode='nearest').squeeze(0).permute(1,
2,
0)
cropped_video.append(cropped_frame)
cropped_masks.append(cropped_frame_mask)
# Create crop data
crop_data = {
"top": top,
"bottom": bottom,
"left": left,
"right": right,
"original_height": height,
"original_width": width,
"output_height": output_height,
"output_width": output_width,
}
crop_data_list.append(crop_data)
cropped_video = torch.stack(cropped_video)
cropped_masks = torch.stack(cropped_masks)
return (cropped_video, cropped_masks, video, crop_data_list)
class FL_VideoRecompose:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"original_video": ("IMAGE",),
"cropped_video": ("IMAGE",),
"crop_data": ("CROP_DATA",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_video",)
FUNCTION = "replace_crop"
CATEGORY = "🏵️Fill Nodes/experiments"
def replace_crop(self, original_video: torch.Tensor, cropped_video: torch.Tensor, crop_data: list):
batch_size, height, width, channels = original_video.shape
output_video = []
for i in range(batch_size):
frame = original_video[i]
cropped_frame = cropped_video[i]
frame_crop_data = crop_data[i]
# Resize the cropped video back to its original size
resized_crop = F.interpolate(
cropped_frame.unsqueeze(0).permute(0, 3, 1, 2),
size=(
frame_crop_data["bottom"] - frame_crop_data["top"], frame_crop_data["right"] - frame_crop_data["left"]),
mode='bilinear',
align_corners=False
).squeeze(0).permute(1, 2, 0)
# Create a copy of the original frame
output_frame = frame.clone()
# Replace the cropped area in the original frame
output_frame[frame_crop_data["top"]:frame_crop_data["bottom"],
frame_crop_data["left"]:frame_crop_data["right"], :] = resized_crop
output_video.append(output_frame)
output_video = torch.stack(output_video)
return (output_video,)
-28
View File
@@ -1,28 +0,0 @@
from pathlib import Path
### GLOBALS
ROOT = Path(__file__).resolve().parent.parent
ROOT_COMFY = ROOT.parent.parent
ROOT_FONTS = ROOT / "fonts"
### SUPPORT CLASSES
class AlwaysEqualProxy(str):
def __eq__(self, other):
return True
def __ne__(self, other):
return False
### SUPPORT FUNCTIONS
def parse_dynamic(data:dict, key:str) -> list:
vals = []
count = 1
while data.get((who := f"{key}_{count}"), None) is not None:
vals.append(who)
count += 1
if len(vals) == 0:
vals.append([])
return vals
-460
View File
@@ -1,460 +0,0 @@
import numpy as np
from PIL import Image, ImageFilter
import torch
import torch.nn.functional as F
from torchvision.transforms import GaussianBlur
import math
if (not hasattr(Image, 'Resampling')): # For older versions of Pillow
Image.Resampling = Image
BLUR_KERNEL_SIZE = 15
def tensor_to_pil(img_tensor, batch_index=0):
# Takes an image in a batch in the form of a tensor of shape [batch_size, channels, height, width]
# and returns an PIL Image with the corresponding mode deduced by the number of channels
# Take the image in the batch given by batch_index
img_tensor = img_tensor[batch_index].unsqueeze(0)
i = 255. * img_tensor.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8).squeeze())
return img
def pil_to_tensor(image):
# Takes a PIL image and returns a tensor of shape [1, height, width, channels]
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image).unsqueeze(0)
if len(image.shape) == 3: # If the image is grayscale, add a channel dimension
image = image.unsqueeze(-1)
return image
def controlnet_hint_to_pil(tensor, batch_index=0):
return tensor_to_pil(tensor.movedim(1, -1), batch_index)
def pil_to_controlnet_hint(img):
return pil_to_tensor(img).movedim(-1, 1)
def crop_tensor(tensor, region):
# Takes a tensor of shape [batch_size, height, width, channels] and crops it to the given region
x1, y1, x2, y2 = region
return tensor[:, y1:y2, x1:x2, :]
def resize_tensor(tensor, size, mode="nearest-exact"):
# Takes a tensor of shape [B, C, H, W] and resizes
# it to a shape of [B, C, size[0], size[1]] using the given mode
return torch.nn.functional.interpolate(tensor, size=size, mode=mode)
def get_crop_region(mask, pad=0):
# Takes a black and white PIL image in 'L' mode and returns the coordinates of the white rectangular mask region
# Should be equivalent to the get_crop_region function from https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/modules/masking.py
coordinates = mask.getbbox()
if coordinates is not None:
x1, y1, x2, y2 = coordinates
else:
x1, y1, x2, y2 = mask.width, mask.height, 0, 0
# Apply padding
x1 = max(x1 - pad, 0)
y1 = max(y1 - pad, 0)
x2 = min(x2 + pad, mask.width)
y2 = min(y2 + pad, mask.height)
return fix_crop_region((x1, y1, x2, y2), (mask.width, mask.height))
def fix_crop_region(region, image_size):
# Remove the extra pixel added by the get_crop_region function
image_width, image_height = image_size
x1, y1, x2, y2 = region
if x2 < image_width:
x2 -= 1
if y2 < image_height:
y2 -= 1
return x1, y1, x2, y2
def expand_crop(region, width, height, target_width, target_height):
'''
Expands a crop region to a specified target size.
:param region: A tuple of the form (x1, y1, x2, y2) denoting the upper left and the lower right points
of the rectangular region. Expected to have x2 > x1 and y2 > y1.
:param width: The width of the image the crop region is from.
:param height: The height of the image the crop region is from.
:param target_width: The desired width of the crop region.
:param target_height: The desired height of the crop region.
'''
x1, y1, x2, y2 = region
actual_width = x2 - x1
actual_height = y2 - y1
# target_width = math.ceil(actual_width / 8) * 8
# target_height = math.ceil(actual_height / 8) * 8
# Try to expand region to the right of half the difference
width_diff = target_width - actual_width
x2 = min(x2 + width_diff // 2, width)
# Expand region to the left of the difference including the pixels that could not be expanded to the right
width_diff = target_width - (x2 - x1)
x1 = max(x1 - width_diff, 0)
# Try the right again
width_diff = target_width - (x2 - x1)
x2 = min(x2 + width_diff, width)
# Try to expand region to the bottom of half the difference
height_diff = target_height - actual_height
y2 = min(y2 + height_diff // 2, height)
# Expand region to the top of the difference including the pixels that could not be expanded to the bottom
height_diff = target_height - (y2 - y1)
y1 = max(y1 - height_diff, 0)
# Try the bottom again
height_diff = target_height - (y2 - y1)
y2 = min(y2 + height_diff, height)
return (x1, y1, x2, y2), (target_width, target_height)
def resize_region(region, init_size, resize_size):
# Resize a crop so that it fits an image that was resized to the given width and height
x1, y1, x2, y2 = region
init_width, init_height = init_size
resize_width, resize_height = resize_size
x1 = math.floor(x1 * resize_width / init_width)
x2 = math.ceil(x2 * resize_width / init_width)
y1 = math.floor(y1 * resize_height / init_height)
y2 = math.ceil(y2 * resize_height / init_height)
return (x1, y1, x2, y2)
def pad_image(image, left_pad, right_pad, top_pad, bottom_pad, fill=False, blur=False):
'''
Pads an image with the given number of pixels on each side and fills the padding with data from the edges.
:param image: A PIL image
:param left_pad: The number of pixels to pad on the left side
:param right_pad: The number of pixels to pad on the right side
:param top_pad: The number of pixels to pad on the top side
:param bottom_pad: The number of pixels to pad on the bottom side
:param blur: Whether to blur the padded edges
:return: A PIL image with size (image.width + left_pad + right_pad, image.height + top_pad + bottom_pad)
'''
left_edge = image.crop((0, 1, 1, image.height - 1))
right_edge = image.crop((image.width - 1, 1, image.width, image.height - 1))
top_edge = image.crop((1, 0, image.width - 1, 1))
bottom_edge = image.crop((1, image.height - 1, image.width - 1, image.height))
new_width = image.width + left_pad + right_pad
new_height = image.height + top_pad + bottom_pad
padded_image = Image.new(image.mode, (new_width, new_height))
padded_image.paste(image, (left_pad, top_pad))
if fill:
for i in range(left_pad):
edge = left_edge.resize(
(1, new_height - i * (top_pad + bottom_pad) // left_pad), resample=Image.Resampling.NEAREST)
padded_image.paste(edge, (i, i * top_pad // left_pad))
for i in range(right_pad):
edge = right_edge.resize(
(1, new_height - i * (top_pad + bottom_pad) // right_pad), resample=Image.Resampling.NEAREST)
padded_image.paste(edge, (new_width - 1 - i, i * top_pad // right_pad))
for i in range(top_pad):
edge = top_edge.resize(
(new_width - i * (left_pad + right_pad) // top_pad, 1), resample=Image.Resampling.NEAREST)
padded_image.paste(edge, (i * left_pad // top_pad, i))
for i in range(bottom_pad):
edge = bottom_edge.resize(
(new_width - i * (left_pad + right_pad) // bottom_pad, 1), resample=Image.Resampling.NEAREST)
padded_image.paste(edge, (i * left_pad // bottom_pad, new_height - 1 - i))
if blur and not (left_pad == right_pad == top_pad == bottom_pad == 0):
padded_image = padded_image.filter(ImageFilter.GaussianBlur(BLUR_KERNEL_SIZE))
padded_image.paste(image, (left_pad, top_pad))
return padded_image
def pad_image2(image, left_pad, right_pad, top_pad, bottom_pad, fill=False, blur=False):
'''
Pads an image with the given number of pixels on each side and fills the padding with data from the edges.
Faster than pad_image, but only pads with edge data in straight lines.
:param image: A PIL image
:param left_pad: The number of pixels to pad on the left side
:param right_pad: The number of pixels to pad on the right side
:param top_pad: The number of pixels to pad on the top side
:param bottom_pad: The number of pixels to pad on the bottom side
:param blur: Whether to blur the padded edges
:return: A PIL image with size (image.width + left_pad + right_pad, image.height + top_pad + bottom_pad)
'''
left_edge = image.crop((0, 1, 1, image.height - 1))
right_edge = image.crop((image.width - 1, 1, image.width, image.height - 1))
top_edge = image.crop((1, 0, image.width - 1, 1))
bottom_edge = image.crop((1, image.height - 1, image.width - 1, image.height))
new_width = image.width + left_pad + right_pad
new_height = image.height + top_pad + bottom_pad
padded_image = Image.new(image.mode, (new_width, new_height))
padded_image.paste(image, (left_pad, top_pad))
if fill:
if left_pad > 0:
padded_image.paste(left_edge.resize((left_pad, new_height), resample=Image.Resampling.NEAREST), (0, 0))
if right_pad > 0:
padded_image.paste(right_edge.resize((right_pad, new_height),
resample=Image.Resampling.NEAREST), (new_width - right_pad, 0))
if top_pad > 0:
padded_image.paste(top_edge.resize((new_width, top_pad), resample=Image.Resampling.NEAREST), (0, 0))
if bottom_pad > 0:
padded_image.paste(bottom_edge.resize((new_width, bottom_pad),
resample=Image.Resampling.NEAREST), (0, new_height - bottom_pad))
if blur and not (left_pad == right_pad == top_pad == bottom_pad == 0):
padded_image = padded_image.filter(ImageFilter.GaussianBlur(BLUR_KERNEL_SIZE))
padded_image.paste(image, (left_pad, top_pad))
return padded_image
def pad_tensor(tensor, left_pad, right_pad, top_pad, bottom_pad, fill=False, blur=False):
'''
Pads an image tensor with the given number of pixels on each side and fills the padding with data from the edges.
:param tensor: A tensor of shape [B, H, W, C]
:param left_pad: The number of pixels to pad on the left side
:param right_pad: The number of pixels to pad on the right side
:param top_pad: The number of pixels to pad on the top side
:param bottom_pad: The number of pixels to pad on the bottom side
:param blur: Whether to blur the padded edges
:return: A tensor of shape [B, H + top_pad + bottom_pad, W + left_pad + right_pad, C]
'''
batch_size, channels, height, width = tensor.shape
h_pad = left_pad + right_pad
v_pad = top_pad + bottom_pad
new_width = width + h_pad
new_height = height + v_pad
# Create empty image
padded = torch.zeros((batch_size, channels, new_height, new_width), dtype=tensor.dtype)
# Copy the original image into the centor of the padded tensor
padded[:, :, top_pad:top_pad + height, left_pad:left_pad + width] = tensor
# Duplicate the edges of the original image into the padding
if top_pad > 0:
padded[:, :, :top_pad, :] = padded[:, :, top_pad:top_pad + 1, :] # Top edge
if bottom_pad > 0:
padded[:, :, -bottom_pad:, :] = padded[:, :, -bottom_pad - 1:-bottom_pad, :] # Bottom edge
if left_pad > 0:
padded[:, :, :, :left_pad] = padded[:, :, :, left_pad:left_pad + 1] # Left edge
if right_pad > 0:
padded[:, :, :, -right_pad:] = padded[:, :, :, -right_pad - 1:-right_pad] # Right edge
return padded
def resize_and_pad_image(image, width, height, fill=False, blur=False):
'''
Resizes an image to the given width and height and pads it to the given width and height.
:param image: A PIL image
:param width: The width of the resized image
:param height: The height of the resized image
:param fill: Whether to fill the padding with data from the edges
:param blur: Whether to blur the padded edges
:return: A PIL image of size (width, height)
'''
width_ratio = width / image.width
height_ratio = height / image.height
if height_ratio > width_ratio:
resize_ratio = width_ratio
else:
resize_ratio = height_ratio
resize_width = round(image.width * resize_ratio)
resize_height = round(image.height * resize_ratio)
resized = image.resize((resize_width, resize_height), resample=Image.Resampling.LANCZOS)
# Pad the sides of the image to get the image to the desired size that wasn't covered by the resize
horizontal_pad = (width - resize_width) // 2
vertical_pad = (height - resize_height) // 2
result = pad_image2(resized, horizontal_pad, horizontal_pad, vertical_pad, vertical_pad, fill, blur)
result = result.resize((width, height), resample=Image.Resampling.LANCZOS)
return result, (horizontal_pad, vertical_pad)
def resize_and_pad_tensor(tensor, width, height, fill=False, blur=False):
'''
Resizes an image tensor to the given width and height and pads it to the given width and height.
:param tensor: A tensor of shape [B, H, W, C]
:param width: The width of the resized image
:param height: The height of the resized image
:param fill: Whether to fill the padding with data from the edges
:param blur: Whether to blur the padded edges
:return: A tensor of shape [B, height, width, C]
'''
# Resize the image to the closest size that maintains the aspect ratio
width_ratio = width / tensor.shape[3]
height_ratio = height / tensor.shape[2]
if height_ratio > width_ratio:
resize_ratio = width_ratio
else:
resize_ratio = height_ratio
resize_width = round(tensor.shape[3] * resize_ratio)
resize_height = round(tensor.shape[2] * resize_ratio)
resized = F.interpolate(tensor, size=(resize_height, resize_width), mode='nearest-exact')
# Pad the sides of the image to get the image to the desired size that wasn't covered by the resize
horizontal_pad = (width - resize_width) // 2
vertical_pad = (height - resize_height) // 2
result = pad_tensor(resized, horizontal_pad, horizontal_pad, vertical_pad, vertical_pad, fill, blur)
result = F.interpolate(result, size=(height, width), mode='nearest-exact')
return result
def crop_controlnet(cond_dict, region, init_size, canvas_size, tile_size, w_pad, h_pad):
if "control" not in cond_dict:
return
c = cond_dict["control"]
controlnet = c.copy()
cond_dict["control"] = controlnet
while c is not None:
# hint is shape (B, C, H, W)
hint = controlnet.cond_hint_original
resized_crop = resize_region(region, canvas_size, hint.shape[:-3:-1])
hint = crop_tensor(hint.movedim(1, -1), resized_crop).movedim(-1, 1)
hint = resize_tensor(hint, tile_size[::-1])
controlnet.cond_hint_original = hint
c = c.previous_controlnet
controlnet.set_previous_controlnet(c.copy() if c is not None else None)
controlnet = controlnet.previous_controlnet
def region_intersection(region1, region2):
"""
Returns the coordinates of the intersection of two rectangular regions.
:param region1: A tuple of the form (x1, y1, x2, y2) denoting the upper left and the lower right points
of the first rectangular region. Expected to have x2 > x1 and y2 > y1.
:param region2: The second rectangular region with the same format as the first.
:return: A tuple of the form (x1, y1, x2, y2) denoting the rectangular intersection.
None if there is no intersection.
"""
x1, y1, x2, y2 = region1
x1_, y1_, x2_, y2_ = region2
x1 = max(x1, x1_)
y1 = max(y1, y1_)
x2 = min(x2, x2_)
y2 = min(y2, y2_)
if x1 >= x2 or y1 >= y2:
return None
return (x1, y1, x2, y2)
def crop_gligen(cond_dict, region, init_size, canvas_size, tile_size, w_pad, h_pad):
if "gligen" not in cond_dict:
return
type, model, cond = cond_dict["gligen"]
if type != "position":
from warnings import warn
warn(f"Unknown gligen type {type}")
return
cropped = []
for c in cond:
emb, h, w, y, x = c
# Get the coordinates of the box in the upscaled image
x1 = x * 8
y1 = y * 8
x2 = x1 + w * 8
y2 = y1 + h * 8
gligen_upscaled_box = resize_region((x1, y1, x2, y2), init_size, canvas_size)
# Calculate the intersection of the gligen box and the region
intersection = region_intersection(gligen_upscaled_box, region)
if intersection is None:
continue
x1, y1, x2, y2 = intersection
# Offset the gligen box so that the origin is at the top left of the tile region
x1 -= region[0]
y1 -= region[1]
x2 -= region[0]
y2 -= region[1]
# Add the padding
x1 += w_pad
y1 += h_pad
x2 += w_pad
y2 += h_pad
# Set the new position params
h = (y2 - y1) // 8
w = (x2 - x1) // 8
x = x1 // 8
y = y1 // 8
cropped.append((emb, h, w, y, x))
cond_dict["gligen"] = (type, model, cropped)
def crop_area(cond_dict, region, init_size, canvas_size, tile_size, w_pad, h_pad):
if "area" not in cond_dict:
return
# Resize the area conditioning to the canvas size and confine it to the tile region
h, w, y, x = cond_dict["area"]
w, h, x, y = 8 * w, 8 * h, 8 * x, 8 * y
x1, y1, x2, y2 = resize_region((x, y, x + w, y + h), init_size, canvas_size)
intersection = region_intersection((x1, y1, x2, y2), region)
if intersection is None:
del cond_dict["area"]
del cond_dict["strength"]
return
x1, y1, x2, y2 = intersection
# Offset origin to the top left of the tile
x1 -= region[0]
y1 -= region[1]
x2 -= region[0]
y2 -= region[1]
# Add the padding
x1 += w_pad
y1 += h_pad
x2 += w_pad
y2 += h_pad
# Set the params for tile
w, h = (x2 - x1) // 8, (y2 - y1) // 8
x, y = x1 // 8, y1 // 8
cond_dict["area"] = (h, w, y, x)
def crop_mask(cond_dict, region, init_size, canvas_size, tile_size, w_pad, h_pad):
if "mask" not in cond_dict:
return
mask_tensor = cond_dict["mask"] # (B, H, W)
masks = []
for i in range(mask_tensor.shape[0]):
# Convert to PIL image
mask = tensor_to_pil(mask_tensor, i) # W x H
# Resize the mask to the canvas size
mask = mask.resize(canvas_size, Image.Resampling.BICUBIC)
# Crop the mask to the region
mask = mask.crop(region)
# Add padding
mask, _ = resize_and_pad_image(mask, tile_size[0], tile_size[1], fill=True)
# Resize the mask to the tile size
if tile_size != mask.size:
mask = mask.resize(tile_size, Image.Resampling.BICUBIC)
# Convert back to tensor
mask = pil_to_tensor(mask) # (1, H, W, 1)
mask = mask.squeeze(-1) # (1, H, W)
masks.append(mask)
cond_dict["mask"] = torch.cat(masks, dim=0) # (B, H, W)
def crop_cond(cond, region, init_size, canvas_size, tile_size, w_pad=0, h_pad=0):
cropped = []
for emb, x in cond:
cond_dict = x.copy()
n = [emb, cond_dict]
crop_controlnet(cond_dict, region, init_size, canvas_size, tile_size, w_pad, h_pad)
crop_gligen(cond_dict, region, init_size, canvas_size, tile_size, w_pad, h_pad)
crop_area(cond_dict, region, init_size, canvas_size, tile_size, w_pad, h_pad)
crop_mask(cond_dict, region, init_size, canvas_size, tile_size, w_pad, h_pad)
cropped.append(n)
return cropped