194 lines
7.5 KiB
Python
194 lines
7.5 KiB
Python
import datetime
|
|
from fractions import Fraction
|
|
import json
|
|
from math import ceil
|
|
import os
|
|
import re
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
from PIL.PngImagePlugin import PngInfo
|
|
import torch
|
|
|
|
from comfy.model_management import unload_model, soft_empty_cache
|
|
import comfy.utils
|
|
import folder_paths
|
|
|
|
def sdxl_size(width: int, height: int) -> (int, int):
|
|
# solver
|
|
w = 0
|
|
h = 0
|
|
for i in range(1, 256):
|
|
for j in range(1, 256):
|
|
if Fraction(8 * i, 8 * j) > Fraction(width, height) * 0.98 and Fraction(8 * i, 8 * j) < Fraction(width, height) and 8 * i * 8 * j <= 1024 * 1024:
|
|
if (ceil(8 * i / 64) * 64) * (ceil(8 * j / 64) * 64) <= 1024 * 1024:
|
|
w = ceil(8 * i / 64) * 64
|
|
h = ceil(8 * j / 64) * 64
|
|
elif (8 * i // 64 * 64) * (ceil(8 * j / 64) * 64) <= 1024 * 1024:
|
|
w = 8 * i // 64 * 64
|
|
h = ceil(8 * j / 64) * 64
|
|
elif (ceil(8 * i / 64) * 64) * (8 * j // 64 * 64) <= 1024 * 1024:
|
|
w = ceil(8 * i / 64) * 64
|
|
h = 8 * j // 64 * 64
|
|
else:
|
|
w = 8 * i // 64 * 64
|
|
h = 8 * j // 64 * 64
|
|
return w, h
|
|
|
|
class EmptyLatentRatioSelector:
|
|
ratio_sizes = ['1:1','5:4','4:3','3:2','16:9','21:9','4:5','3:4','2:3','9:16','9:21','5:7','7:5']
|
|
ratio_dict = {'1:1': (1024, 1024),
|
|
'5:4': (1152, 896),
|
|
'4:3': (1152, 832),
|
|
'3:2': (1216, 832),
|
|
'16:9': (1344, 768),
|
|
'21:9': (1536, 640),
|
|
'4:5': (896, 1152),
|
|
'3:4': (832, 1152),
|
|
'2:3': (832, 1216),
|
|
'9:16': (768, 1344),
|
|
'9:21': (640, 1536),
|
|
'5:7': (840, 1176),
|
|
'7:5': (1176, 840),}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {'required': {'ratio_selected': (s.ratio_sizes,),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64})}}
|
|
|
|
RETURN_TYPES = ('LATENT',)
|
|
FUNCTION = 'generate'
|
|
CATEGORY = 'sdxl'
|
|
|
|
def generate(self, ratio_selected, batch_size=1):
|
|
width = self.ratio_dict[ratio_selected][0]
|
|
height = self.ratio_dict[ratio_selected][1]
|
|
latent = torch.zeros([batch_size, 4, height // 8, width // 8])
|
|
return ({"samples":latent}, )
|
|
|
|
class EmptyLatentRatioCustom:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "width": ("INT", {"default": 1024, "min": 1, "max": 8192, "step": 1}),
|
|
"height": ("INT", {"default": 1024, "min": 1, "max": 8192, "step": 1}),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64})}}
|
|
|
|
RETURN_TYPES = ('LATENT',)
|
|
FUNCTION = 'generate'
|
|
CATEGORY = 'sdxl'
|
|
|
|
def generate(self, width, height, batch_size=1):
|
|
# solver
|
|
if width == 1 and height == 1:
|
|
w, h = 1024, 1024
|
|
if f'{width}:{height}' in EmptyLatentRatioSelector.ratio_dict:
|
|
w, h = EmptyLatentRatioSelector.ratio_dict[f'{width}:{height}']
|
|
else:
|
|
w, h = sdxl_size(width, height)
|
|
latent = torch.zeros([batch_size, 4, h // 8, w // 8])
|
|
return ({"samples":latent}, )
|
|
|
|
class ResizeImageSDXL:
|
|
crop_methods = ["disabled", "center"]
|
|
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "image": ("IMAGE",), "upscale_method": (s.upscale_methods,),
|
|
"crop": (s.crop_methods,)}}
|
|
|
|
RETURN_TYPES = ('IMAGE',)
|
|
FUNCTION = 'resize'
|
|
CATEGORY = 'sdxl'
|
|
|
|
def upscale(self, image, upscale_method, width, height, crop):
|
|
samples = image.movedim(-1,1)
|
|
s = comfy.utils.common_upscale(samples, width, height, upscale_method, crop)
|
|
s = s.movedim(1,-1)
|
|
return (s,)
|
|
|
|
def resize(self, image, upscale_method, crop):
|
|
w, h = sdxl_size(image.shape[2], image.shape[1])
|
|
print('Resizing image from {}x{} to {}x{}'.format(image.shape[2], image.shape[1], w, h))
|
|
img = self.upscale(image, upscale_method, w, h, crop)[0]
|
|
return (img, )
|
|
|
|
class SaveImagesMikey:
|
|
def __init__(self):
|
|
self.output_dir = folder_paths.get_output_directory()
|
|
self.type = "output"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required":
|
|
{"images": ("IMAGE", ),
|
|
"positive_prompt": ("STRING", {'default': 'Positive Prompt'}),
|
|
"negative_prompt": ("STRING", {'default': 'Negative Prompt'}),},
|
|
"filename_prefix": ("STRING", {"default": ""}),
|
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_images"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "sdxl"
|
|
|
|
def save_images(self, images, filename_prefix='', prompt=None, extra_pnginfo=None, positive_prompt='', negative_prompt=''):
|
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
|
results = list()
|
|
for image in images:
|
|
i = 255. * image.cpu().numpy()
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
metadata = PngInfo()
|
|
pos_trunc = ''
|
|
if prompt is not None:
|
|
metadata.add_text("prompt", json.dumps(prompt))
|
|
if extra_pnginfo is not None:
|
|
for x in extra_pnginfo:
|
|
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
|
if positive_prompt:
|
|
metadata.add_text("positive_prompt", json.dumps(positive_prompt))
|
|
# replace any special characters with nothing and spaces with _
|
|
clean_pos = re.sub(r'[^a-zA-Z0-9 ]', '', positive_prompt)
|
|
pos_trunc = clean_pos.replace(' ', '_')[0:40]
|
|
if negative_prompt:
|
|
metadata.add_text("negative_prompt", json.dumps(negative_prompt))
|
|
ts_str = datetime.datetime.now().strftime("%y%m%d%H%M%S")
|
|
file = f"{ts_str}_{pos_trunc}_{filename}_{counter:05}_.png"
|
|
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4)
|
|
results.append({
|
|
"filename": file,
|
|
"subfolder": subfolder,
|
|
"type": self.type
|
|
})
|
|
counter += 1
|
|
|
|
return { "ui": { "images": results } }
|
|
|
|
class VAEDecode6GB:
|
|
""" deprecated. update comfy to fix issue. """
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {'required': {'vae': ('VAE',),
|
|
'samples': ('LATENT',)}}
|
|
RETURN_TYPES = ('IMAGE',)
|
|
FUNCTION = 'decode'
|
|
CATEGORY = 'sdxl'
|
|
|
|
def decode(self, vae, samples):
|
|
unload_model()
|
|
soft_empty_cache()
|
|
return (vae.decode(samples['samples']), )
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
'Empty Latent Ratio Select SDXL': EmptyLatentRatioSelector,
|
|
'Empty Latent Ratio Custom SDXL': EmptyLatentRatioCustom,
|
|
'Save Image With Prompt Data': SaveImagesMikey,
|
|
'Resize Image for SDXL': ResizeImageSDXL,
|
|
'VAE Decode 6GB SDXL (deprecated)': VAEDecode6GB,
|
|
}
|
|
## TODO
|
|
# Resize Image and return the new width and height
|
|
# SDXL Ultimate Upscaler? |