Add files via upload
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
import folder_paths
|
||||
|
||||
RATIOS = ["1:1", "5:4", "4:3", "3:2", "16:9", "2:1", "21:9", "32:9"]
|
||||
|
||||
|
||||
def ensure_multiple_of_32(img):
|
||||
"""Ajuste les dimensions d'une image PIL pour qu'elles soient multiples de 32."""
|
||||
w, h = img.size
|
||||
if w % 32 != 0 or h % 32 != 0:
|
||||
w_new = max(32, int(round(w / 32.0) * 32))
|
||||
h_new = max(32, int(round(h / 32.0) * 32))
|
||||
return img.resize((w_new, h_new), Image.Resampling.LANCZOS)
|
||||
return img
|
||||
|
||||
|
||||
def calculate_dimensions(ratio_str, portrait, target_megapixel):
|
||||
"""Calcule les dimensions (largeur, hauteur) arrondies à un multiple de 32."""
|
||||
try:
|
||||
w_ratio, h_ratio = map(float, ratio_str.split(":"))
|
||||
except ValueError:
|
||||
w_ratio, h_ratio = 1.0, 1.0
|
||||
|
||||
if portrait:
|
||||
w_ratio, h_ratio = h_ratio, w_ratio
|
||||
|
||||
aspect_ratio = w_ratio / h_ratio
|
||||
target_pixels = target_megapixel * 1_000_000
|
||||
|
||||
h = np.sqrt(target_pixels / aspect_ratio)
|
||||
w = aspect_ratio * h
|
||||
|
||||
w = max(32, int(round(w / 32.0) * 32))
|
||||
h = max(32, int(round(h / 32.0) * 32))
|
||||
return w, h
|
||||
|
||||
|
||||
def scale_to_megapixel(img, target_megapixel):
|
||||
"""Redimensionne une image PIL selon un nombre de mégapixels cible, multiple de 32."""
|
||||
w_orig, h_orig = img.size
|
||||
aspect_ratio = w_orig / h_orig
|
||||
target_pixels = target_megapixel * 1_000_000
|
||||
|
||||
h = np.sqrt(target_pixels / aspect_ratio)
|
||||
w = aspect_ratio * h
|
||||
|
||||
w = max(32, int(round(w / 32.0) * 32))
|
||||
h = max(32, int(round(h / 32.0) * 32))
|
||||
return img.resize((w, h), Image.Resampling.LANCZOS)
|
||||
|
||||
|
||||
class Load_Image_Advanced:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
files = [
|
||||
f
|
||||
for f in os.listdir(input_dir)
|
||||
if os.path.isfile(os.path.join(input_dir, f))
|
||||
]
|
||||
return {
|
||||
"required": {
|
||||
"image_file": (sorted(files), {"image_upload": True}),
|
||||
"enable": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": True,
|
||||
"label_on": "Enabled 🟢",
|
||||
"label_off": "Disabled 🔴",
|
||||
},
|
||||
),
|
||||
"scale_image": ("BOOLEAN", {"default": False}),
|
||||
"megapixel": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": 0.1, "max": 16.0, "step": 0.1},
|
||||
),
|
||||
"ratio": (RATIOS, {"default": "1:1"}),
|
||||
"orientation": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": False,
|
||||
"label_on": "Portrait ▯",
|
||||
"label_off": "Landscape ▭",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE_PARAMS",)
|
||||
RETURN_NAMES = ("_",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Fictiverse/Image"
|
||||
|
||||
def run(self, image_file, enable, scale_image, megapixel, ratio, orientation):
|
||||
if enable:
|
||||
image_path = folder_paths.get_annotated_filepath(image_file)
|
||||
img = Image.open(image_path)
|
||||
img = img.convert("RGB")
|
||||
|
||||
if scale_image:
|
||||
img = scale_to_megapixel(img, megapixel)
|
||||
else:
|
||||
img = ensure_multiple_of_32(img)
|
||||
|
||||
w, h = img.size
|
||||
else:
|
||||
w, h = calculate_dimensions(ratio, orientation, megapixel)
|
||||
img = Image.new("RGB", (w, h), (0, 0, 0))
|
||||
|
||||
image_tensor = torch.from_numpy(
|
||||
np.array(img).astype(np.float32) / 255.0
|
||||
)[None,]
|
||||
|
||||
# Compactage de tous les paramètres et de l'image
|
||||
params = {
|
||||
"image": image_tensor,
|
||||
"width": w,
|
||||
"height": h,
|
||||
"megapixel": megapixel,
|
||||
"enabled": enable,
|
||||
}
|
||||
|
||||
return (params,)
|
||||
|
||||
|
||||
|
||||
class Load_Image_Bypass:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
|
||||
# Extensions d'images et de GIFs supportées par PIL
|
||||
valid_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.gif', '.apng'}
|
||||
|
||||
files = [
|
||||
f for f in os.listdir(input_dir)
|
||||
if os.path.isfile(os.path.join(input_dir, f))
|
||||
and os.path.splitext(f)[1].lower() in valid_extensions
|
||||
]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image_file": (sorted(files), {"image_upload": True}),
|
||||
"enable": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": True,
|
||||
"label_on": "Active 🟢",
|
||||
"label_off": "Disabled 🔴",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Fictiverse/Image"
|
||||
|
||||
def run(self, image_file, enable):
|
||||
# Si désactivé, on retourne (None,) pour interrompre la branche dans ComfyUI
|
||||
if not enable:
|
||||
return (None,)
|
||||
|
||||
image_path = folder_paths.get_annotated_filepath(image_file)
|
||||
img = Image.open(image_path)
|
||||
img = img.convert("RGB")
|
||||
img = ensure_multiple_of_32(img)
|
||||
|
||||
image_tensor = torch.from_numpy(
|
||||
np.array(img).astype(np.float32) / 255.0
|
||||
)[None,]
|
||||
|
||||
return (image_tensor,)
|
||||
|
||||
|
||||
class Unpack_Image_Params:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"params": ("IMAGE_PARAMS",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT", "FLOAT", "BOOLEAN")
|
||||
RETURN_NAMES = ("image", "width", "height", "megapixel", "enabled")
|
||||
FUNCTION = "unpack"
|
||||
CATEGORY = "Fictiverse/Image"
|
||||
|
||||
def unpack(self, params):
|
||||
image = params.get("image")
|
||||
if image is None:
|
||||
image = torch.zeros((1, 512, 512, 3), dtype=torch.float32)
|
||||
|
||||
return (
|
||||
image,
|
||||
params.get("width", 512),
|
||||
params.get("height", 512),
|
||||
params.get("megapixel", 1.0),
|
||||
params.get("enabled", True),
|
||||
)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# MAPPINGS COMFYUI
|
||||
# ==========================================
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LoadImageAdvanced": Load_Image_Advanced,
|
||||
"LoadImageBypass": Load_Image_Bypass,
|
||||
"UnpackImageParams": Unpack_Image_Params,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LoadImageAdvanced": "Load Image Advanced",
|
||||
"LoadImageBypass": "Load Image Bypass",
|
||||
"UnpackImageParams": "Unpack Image Params",
|
||||
}
|
||||
+91
-47
@@ -1,78 +1,122 @@
|
||||
#Code inspired by sv-nodes, thanks to him.
|
||||
|
||||
import math
|
||||
|
||||
# ==========================================
|
||||
# CLASSSES PARAMS VIDÉO
|
||||
# ==========================================
|
||||
|
||||
|
||||
class VideoParams:
|
||||
RATIOS = ["1:1", "5:4", "4:3", "3:2", "16:9", "21:9"]
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"base": ("INT", {"default": 768, "min": 128, "max": 4096, "step": 128}),
|
||||
"ratio": (VideoParams.RATIOS,),
|
||||
"base": (
|
||||
"INT",
|
||||
{"default": 768, "min": 128, "max": 4096, "step": 128},
|
||||
),
|
||||
"ratio": (cls.RATIOS,),
|
||||
"orientation": ("BOOLEAN", {"default": False, "label_on": "Portrait ▯", "label_off": "Landscape ▭"}),
|
||||
"Frames": ("INT", {"min": 1, "max": 600, "step": 1, "default": 81}),
|
||||
"FPS": ("INT", {"min": 1, "max": 120, "step": 1, "default": 16})
|
||||
"duration": (
|
||||
"FLOAT",
|
||||
{"default": 10.0, "min": 1.0, "max": 60.0, "step": 1.0},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RETURN_TYPES = ("VParams",)
|
||||
RETURN_NAMES = ("_",)
|
||||
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Fictiverse/Params"
|
||||
|
||||
def run(self, base, ratio, orientation, Frames, FPS,):
|
||||
|
||||
ratio = ratio.split(":")
|
||||
if len(ratio) != 2:
|
||||
raise ValueError("Invalid ratio")
|
||||
|
||||
ratio = math.sqrt(float(ratio[0]) / float(ratio[1]))
|
||||
|
||||
width = math.floor(base * ratio / 64) * 64
|
||||
height = math.floor(base / ratio / 64) * 64
|
||||
|
||||
#Frames = max(7, (math.floor(Frames / 6) * 6) + 1)
|
||||
megapixels = (base*base)/1000000
|
||||
|
||||
def run(self, base, ratio, orientation, duration):
|
||||
ratio_parts = ratio.split(":")
|
||||
if len(ratio_parts) != 2:
|
||||
raise ValueError("Invalid ratio format")
|
||||
|
||||
ratio_val = math.sqrt(float(ratio_parts[0]) / float(ratio_parts[1]))
|
||||
|
||||
# Calcul des dimensions multiples de 32
|
||||
width = math.floor(base * ratio_val / 32) * 32
|
||||
height = math.floor(base / ratio_val / 32) * 32
|
||||
|
||||
if orientation:
|
||||
width, height = height, width
|
||||
return ((width, height, Frames, float(FPS), megapixels),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Video Params": VideoParams,
|
||||
}
|
||||
# Calcul plus précis des mégapixels réels en sortie
|
||||
megapixels = (width * height) / 1_000_000.0
|
||||
|
||||
return ((width, height, duration, megapixels),)
|
||||
|
||||
|
||||
class VideoParamsExpand:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"_": ("VParams",)}}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT", "FLOAT", "FLOAT")
|
||||
RETURN_NAMES = ("width", "height", "duration", "Megapixels")
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Fictiverse/Params"
|
||||
|
||||
def run(self, _):
|
||||
if not isinstance(_, (tuple, list)):
|
||||
raise TypeError("Invalid packet input type")
|
||||
if len(_) != 4:
|
||||
raise ValueError(
|
||||
f"Invalid packet length (expected 4, got {len(_)})"
|
||||
)
|
||||
return _
|
||||
|
||||
|
||||
# ==========================================
|
||||
# NOUVEAU NODE : DURATION TO FRAMES
|
||||
# ==========================================
|
||||
|
||||
|
||||
class DurationToFrames:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"_": ("VParams",)
|
||||
"duration": (
|
||||
"FLOAT",
|
||||
{"default": 10.0, "min": 0.1, "max": 1000.0, "step": 0.1},
|
||||
),
|
||||
"fps": (
|
||||
"INT",
|
||||
{"default": 24, "min": 1, "max": 240, "step": 1},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT", "INT", "FLOAT", "FLOAT")
|
||||
RETURN_NAMES = ("width", "height", "frames", "fps", "Megapixels")
|
||||
|
||||
RETURN_TYPES = ("INT", "FLOAT")
|
||||
RETURN_NAMES = ("int", "float")
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Fictiverse/Params"
|
||||
|
||||
def run(self, _):
|
||||
if not isinstance(_, tuple):
|
||||
raise TypeError("Invalid packet input type")
|
||||
if len(_) != 5:
|
||||
raise ValueError("Invalid packet length")
|
||||
return _
|
||||
|
||||
NODE_CLASS_MAPPINGS["Video Params Expand"] = VideoParamsExpand
|
||||
def run(self, duration, fps):
|
||||
# Traduction de l'expression :
|
||||
# a -> duration
|
||||
# 24 -> fps
|
||||
raw_frames = max(5, round(duration * fps))
|
||||
frame_count = raw_frames + (5 - (raw_frames % 17)) % 17
|
||||
|
||||
return (frame_count, float(frame_count))
|
||||
|
||||
|
||||
# ==========================================
|
||||
# MAPPINGS COMFYUI
|
||||
# ==========================================
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Video Params": VideoParams,
|
||||
"Video Params Expand": VideoParamsExpand,
|
||||
"Duration To Frames": DurationToFrames,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Video Params": "Video Params",
|
||||
"Video Params Expand": "Video Params Expand",
|
||||
"Duration To Frames": "Duration To Frames",
|
||||
}
|
||||
Reference in New Issue
Block a user