Delete nodes directory
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 ()
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
@@ -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,)
|
||||
@@ -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
|
||||
@@ -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,)
|
||||
@@ -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)
|
||||
@@ -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, "")
|
||||
@@ -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,)
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"\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")
|
||||
@@ -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")
|
||||
@@ -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,)
|
||||
@@ -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,)
|
||||
@@ -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,)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}'",)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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,)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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,)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,)
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -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),)
|
||||
@@ -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,)
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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 ()
|
||||
@@ -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"})
|
||||
@@ -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,)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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,)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,)
|
||||
@@ -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)
|
||||
@@ -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),)
|
||||
@@ -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
|
||||
@@ -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 ()
|
||||
@@ -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,)
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user