mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
This commit is contained in:
commit
8a2b849620
@ -1,3 +1,4 @@
|
|||||||
|
from math import sqrt, floor, ceil
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
class InitImageResizer():
|
class InitImageResizer():
|
||||||
@ -51,4 +52,22 @@ class InitImageResizer():
|
|||||||
|
|
||||||
return new_image
|
return new_image
|
||||||
|
|
||||||
|
def make_grid(image_list, rows=None, cols=None):
|
||||||
|
image_cnt = len(image_list)
|
||||||
|
if None in (rows, cols):
|
||||||
|
rows = floor(sqrt(image_cnt)) # try to make it square
|
||||||
|
cols = ceil(image_cnt / rows)
|
||||||
|
width = image_list[0].width
|
||||||
|
height = image_list[0].height
|
||||||
|
|
||||||
|
grid_img = Image.new('RGB', (width * cols, height * rows))
|
||||||
|
i = 0
|
||||||
|
for r in range(0, rows):
|
||||||
|
for c in range(0, cols):
|
||||||
|
if i >= len(image_list):
|
||||||
|
break
|
||||||
|
grid_img.paste(image_list[i], (c * width, r * height))
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
return grid_img
|
||||||
|
|
||||||
|
@ -2,97 +2,42 @@
|
|||||||
Two helper classes for dealing with PNG images and their path names.
|
Two helper classes for dealing with PNG images and their path names.
|
||||||
PngWriter -- Converts Images generated by T2I into PNGs, finds
|
PngWriter -- Converts Images generated by T2I into PNGs, finds
|
||||||
appropriate names for them, and writes prompt metadata
|
appropriate names for them, and writes prompt metadata
|
||||||
into the PNG. Intended to be subclassable in order to
|
into the PNG.
|
||||||
create more complex naming schemes, including using the
|
|
||||||
prompt for file/directory names.
|
|
||||||
PromptFormatter -- Utility for converting a Namespace of prompt parameters
|
PromptFormatter -- Utility for converting a Namespace of prompt parameters
|
||||||
back into a formatted prompt string with command-line switches.
|
back into a formatted prompt string with command-line switches.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from math import sqrt, floor, ceil
|
from PIL import PngImagePlugin
|
||||||
from PIL import Image, PngImagePlugin
|
|
||||||
|
|
||||||
# -------------------image generation utils-----
|
# -------------------image generation utils-----
|
||||||
|
|
||||||
|
|
||||||
class PngWriter:
|
class PngWriter:
|
||||||
def __init__(self, outdir, prompt=None):
|
def __init__(self, outdir):
|
||||||
self.outdir = outdir
|
self.outdir = outdir
|
||||||
self.prompt = prompt
|
|
||||||
self.filepath = None
|
|
||||||
self.files_written = []
|
|
||||||
os.makedirs(outdir, exist_ok=True)
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
|
||||||
def write_image(self, image, seed, upscaled=False):
|
# gives the next unique prefix in outdir
|
||||||
self.filepath = self.unique_filename(
|
def unique_prefix(self):
|
||||||
seed, upscaled, self.filepath
|
# sort reverse alphabetically until we find max+1
|
||||||
) # will increment name in some sensible way
|
dirlist = sorted(os.listdir(self.outdir), reverse=True)
|
||||||
try:
|
# find the first filename that matches our pattern or return 000000.0.png
|
||||||
prompt = f'{self.prompt} -S{seed}'
|
existing_name = next(
|
||||||
self.save_image_and_prompt_to_png(image, prompt, self.filepath)
|
(f for f in dirlist if re.match('^(\d+)\..*\.png', f)),
|
||||||
except IOError as e:
|
'0000000.0.png',
|
||||||
print(e)
|
)
|
||||||
if not upscaled:
|
basecount = int(existing_name.split('.', 1)[0]) + 1
|
||||||
self.files_written.append([self.filepath, seed])
|
return f'{basecount:06}'
|
||||||
|
|
||||||
def unique_filename(self, seed, upscaled=False, previouspath=None):
|
# saves image named _image_ to outdir/name, writing metadata from prompt
|
||||||
revision = 1
|
# returns full path of output
|
||||||
|
def save_image_and_prompt_to_png(self, image, prompt, name):
|
||||||
if previouspath is None:
|
path = os.path.join(self.outdir, name)
|
||||||
# sort reverse alphabetically until we find max+1
|
|
||||||
dirlist = sorted(os.listdir(self.outdir), reverse=True)
|
|
||||||
# find the first filename that matches our pattern or return 000000.0.png
|
|
||||||
filename = next(
|
|
||||||
(f for f in dirlist if re.match('^(\d+)\..*\.png', f)),
|
|
||||||
'0000000.0.png',
|
|
||||||
)
|
|
||||||
basecount = int(filename.split('.', 1)[0])
|
|
||||||
basecount += 1
|
|
||||||
filename = f'{basecount:06}.{seed}.png'
|
|
||||||
return os.path.join(self.outdir, filename)
|
|
||||||
|
|
||||||
else:
|
|
||||||
basename = os.path.basename(previouspath)
|
|
||||||
x = re.match('^(\d+)\..*\.png', basename)
|
|
||||||
if not x:
|
|
||||||
return self.unique_filename(seed, upscaled, previouspath)
|
|
||||||
|
|
||||||
basecount = int(x.groups()[0])
|
|
||||||
series = 0
|
|
||||||
finished = False
|
|
||||||
while not finished:
|
|
||||||
series += 1
|
|
||||||
filename = f'{basecount:06}.{seed}.png'
|
|
||||||
path = os.path.join(self.outdir, filename)
|
|
||||||
if os.path.exists(path) and upscaled:
|
|
||||||
break
|
|
||||||
finished = not os.path.exists(path)
|
|
||||||
return os.path.join(self.outdir, filename)
|
|
||||||
|
|
||||||
def save_image_and_prompt_to_png(self, image, prompt, path):
|
|
||||||
info = PngImagePlugin.PngInfo()
|
info = PngImagePlugin.PngInfo()
|
||||||
info.add_text('Dream', prompt)
|
info.add_text('Dream', prompt)
|
||||||
image.save(path, 'PNG', pnginfo=info)
|
image.save(path, 'PNG', pnginfo=info)
|
||||||
|
return path
|
||||||
def make_grid(self, image_list, rows=None, cols=None):
|
|
||||||
image_cnt = len(image_list)
|
|
||||||
if None in (rows, cols):
|
|
||||||
rows = floor(sqrt(image_cnt)) # try to make it square
|
|
||||||
cols = ceil(image_cnt / rows)
|
|
||||||
width = image_list[0].width
|
|
||||||
height = image_list[0].height
|
|
||||||
|
|
||||||
grid_img = Image.new('RGB', (width * cols, height * rows))
|
|
||||||
i = 0
|
|
||||||
for r in range(0, rows):
|
|
||||||
for c in range(0, cols):
|
|
||||||
if i>=len(image_list):
|
|
||||||
break
|
|
||||||
grid_img.paste(image_list[i], (c * width, r * height))
|
|
||||||
i = i + 1
|
|
||||||
|
|
||||||
return grid_img
|
|
||||||
|
|
||||||
|
|
||||||
class PromptFormatter:
|
class PromptFormatter:
|
||||||
|
@ -88,24 +88,24 @@ class DreamServer(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
images_generated = 0 # helps keep track of when upscaling is started
|
images_generated = 0 # helps keep track of when upscaling is started
|
||||||
images_upscaled = 0 # helps keep track of when upscaling is completed
|
images_upscaled = 0 # helps keep track of when upscaling is completed
|
||||||
pngwriter = PngWriter(
|
pngwriter = PngWriter("./outputs/img-samples/")
|
||||||
"./outputs/img-samples/", config['prompt'], 1
|
|
||||||
)
|
|
||||||
|
|
||||||
|
prefix = pngwriter.unique_prefix()
|
||||||
# if upscaling is requested, then this will be called twice, once when
|
# if upscaling is requested, then this will be called twice, once when
|
||||||
# the images are first generated, and then again when after upscaling
|
# the images are first generated, and then again when after upscaling
|
||||||
# is complete. The upscaling replaces the original file, so the second
|
# is complete. The upscaling replaces the original file, so the second
|
||||||
# entry should not be inserted into the image list.
|
# entry should not be inserted into the image list.
|
||||||
def image_done(image, seed, upscaled=False):
|
def image_done(image, seed, upscaled=False):
|
||||||
pngwriter.write_image(image, seed, upscaled)
|
name = f'{prefix}.{seed}.png'
|
||||||
|
path = pngwriter.save_image_and_prompt_to_png(image, f'{prompt} -S{seed}', name)
|
||||||
|
|
||||||
# Append post_data to log, but only once!
|
# Append post_data to log, but only once!
|
||||||
if not upscaled:
|
if not upscaled:
|
||||||
current_image = pngwriter.files_written[-1]
|
|
||||||
with open("./outputs/img-samples/dream_web_log.txt", "a") as log:
|
with open("./outputs/img-samples/dream_web_log.txt", "a") as log:
|
||||||
log.write(f"{current_image[0]}: {json.dumps(config)}\n")
|
log.write(f"{path}: {json.dumps(config)}\n")
|
||||||
|
|
||||||
self.wfile.write(bytes(json.dumps(
|
self.wfile.write(bytes(json.dumps(
|
||||||
{'event':'result', 'files':current_image, 'config':config}
|
{'event': 'result', 'url': path, 'seed': seed, 'config': config}
|
||||||
) + '\n',"utf-8"))
|
) + '\n',"utf-8"))
|
||||||
|
|
||||||
# control state of the "postprocessing..." message
|
# control state of the "postprocessing..." message
|
||||||
@ -129,22 +129,24 @@ class DreamServer(BaseHTTPRequestHandler):
|
|||||||
{'event':action,'processed_file_cnt':f'{x}/{iterations}'}
|
{'event':action,'processed_file_cnt':f'{x}/{iterations}'}
|
||||||
) + '\n',"utf-8"))
|
) + '\n',"utf-8"))
|
||||||
|
|
||||||
# TODO: refactor PngWriter:
|
step_writer = PngWriter('./outputs/intermediates/')
|
||||||
# it doesn't need to know if batch_size > 1, just if this is _part of a batch_
|
step_index = 1
|
||||||
step_writer = PngWriter('./outputs/intermediates/', prompt, 2)
|
|
||||||
def image_progress(sample, step):
|
def image_progress(sample, step):
|
||||||
if self.canceled.is_set():
|
if self.canceled.is_set():
|
||||||
self.wfile.write(bytes(json.dumps({'event':'canceled'}) + '\n', 'utf-8'))
|
self.wfile.write(bytes(json.dumps({'event':'canceled'}) + '\n', 'utf-8'))
|
||||||
raise CanceledException
|
raise CanceledException
|
||||||
url = None
|
path = None
|
||||||
# since rendering images is moderately expensive, only render every 5th image
|
# since rendering images is moderately expensive, only render every 5th image
|
||||||
# and don't bother with the last one, since it'll render anyway
|
# and don't bother with the last one, since it'll render anyway
|
||||||
|
nonlocal step_index
|
||||||
if progress_images and step % 5 == 0 and step < steps - 1:
|
if progress_images and step % 5 == 0 and step < steps - 1:
|
||||||
image = self.model._sample_to_image(sample)
|
image = self.model._sample_to_image(sample)
|
||||||
step_writer.write_image(image, seed) # TODO PngWriter to return path
|
name = f'{prefix}.{seed}.{step_index}.png'
|
||||||
url = step_writer.filepath
|
metadata = f'{prompt} -S{seed} [intermediate]'
|
||||||
|
path = step_writer.save_image_and_prompt_to_png(image, metadata, name)
|
||||||
|
step_index += 1
|
||||||
self.wfile.write(bytes(json.dumps(
|
self.wfile.write(bytes(json.dumps(
|
||||||
{'event':'step', 'step':step + 1, 'url': url}
|
{'event': 'step', 'step': step + 1, 'url': path}
|
||||||
) + '\n',"utf-8"))
|
) + '\n',"utf-8"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -171,10 +171,14 @@ class T2I:
|
|||||||
Optional named arguments are the same as those passed to T2I and prompt2image()
|
Optional named arguments are the same as those passed to T2I and prompt2image()
|
||||||
"""
|
"""
|
||||||
results = self.prompt2image(prompt, **kwargs)
|
results = self.prompt2image(prompt, **kwargs)
|
||||||
pngwriter = PngWriter(outdir, prompt)
|
pngwriter = PngWriter(outdir)
|
||||||
for r in results:
|
prefix = pngwriter.unique_prefix()
|
||||||
pngwriter.write_image(r[0], r[1])
|
outputs = []
|
||||||
return pngwriter.files_written
|
for image, seed in results:
|
||||||
|
name = f'{prefix}.{seed}.png'
|
||||||
|
path = pngwriter.save_image_and_prompt_to_png(image, f'{prompt} -S{seed}', name)
|
||||||
|
outputs.append([path, seed])
|
||||||
|
return outputs
|
||||||
|
|
||||||
def txt2img(self, prompt, **kwargs):
|
def txt2img(self, prompt, **kwargs):
|
||||||
outdir = kwargs.pop('outdir', 'outputs/img-samples')
|
outdir = kwargs.pop('outdir', 'outputs/img-samples')
|
||||||
@ -349,10 +353,7 @@ class T2I:
|
|||||||
f'Error running RealESRGAN - Your image was not upscaled.\n{e}'
|
f'Error running RealESRGAN - Your image was not upscaled.\n{e}'
|
||||||
)
|
)
|
||||||
if image_callback is not None:
|
if image_callback is not None:
|
||||||
if save_original:
|
image_callback(image, seed, upscaled=True)
|
||||||
image_callback(image, seed)
|
|
||||||
else:
|
|
||||||
image_callback(image, seed, upscaled=True)
|
|
||||||
else: # no callback passed, so we simply replace old image with rescaled one
|
else: # no callback passed, so we simply replace old image with rescaled one
|
||||||
result[0] = image
|
result[0] = image
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import time
|
|||||||
import ldm.dream.readline
|
import ldm.dream.readline
|
||||||
from ldm.dream.pngwriter import PngWriter, PromptFormatter
|
from ldm.dream.pngwriter import PngWriter, PromptFormatter
|
||||||
from ldm.dream.server import DreamServer, ThreadingDreamServer
|
from ldm.dream.server import DreamServer, ThreadingDreamServer
|
||||||
|
from ldm.dream.image_util import make_grid
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Initialize command-line parsers and the diffusion model"""
|
"""Initialize command-line parsers and the diffusion model"""
|
||||||
@ -203,24 +204,40 @@ def main_loop(t2i, outdir, prompt_as_dir, parser, infile):
|
|||||||
|
|
||||||
# Here is where the images are actually generated!
|
# Here is where the images are actually generated!
|
||||||
try:
|
try:
|
||||||
file_writer = PngWriter(current_outdir, normalized_prompt)
|
file_writer = PngWriter(current_outdir)
|
||||||
callback = file_writer.write_image if individual_images else None
|
prefix = file_writer.unique_prefix()
|
||||||
image_list = t2i.prompt2image(image_callback=callback, **vars(opt))
|
seeds = set()
|
||||||
results = (
|
results = []
|
||||||
file_writer.files_written if individual_images else image_list
|
grid_images = dict() # seed -> Image, only used if `do_grid`
|
||||||
)
|
def image_writer(image, seed, upscaled=False):
|
||||||
|
if do_grid:
|
||||||
|
grid_images[seed] = image
|
||||||
|
else:
|
||||||
|
if upscaled and opt.save_original:
|
||||||
|
filename = f'{prefix}.{seed}.postprocessed.png'
|
||||||
|
else:
|
||||||
|
filename = f'{prefix}.{seed}.png'
|
||||||
|
path = file_writer.save_image_and_prompt_to_png(image, f'{normalized_prompt} -S{seed}', filename)
|
||||||
|
if (not upscaled) or opt.save_original:
|
||||||
|
# only append to results if we didn't overwrite an earlier output
|
||||||
|
results.append([path, seed])
|
||||||
|
|
||||||
if do_grid and len(results) > 0:
|
seeds.add(seed)
|
||||||
grid_img = file_writer.make_grid([r[0] for r in results])
|
|
||||||
filename = file_writer.unique_filename(results[0][1])
|
t2i.prompt2image(image_callback=image_writer, **vars(opt))
|
||||||
seeds = [a[1] for a in results]
|
|
||||||
results = [[filename, seeds]]
|
if do_grid and len(grid_images) > 0:
|
||||||
metadata_prompt = f'{normalized_prompt} -S{results[0][1]}'
|
grid_img = make_grid(list(grid_images.values()))
|
||||||
file_writer.save_image_and_prompt_to_png(
|
first_seed = next(iter(seeds))
|
||||||
|
filename = f'{prefix}.{first_seed}.png'
|
||||||
|
# TODO better metadata for grid images
|
||||||
|
metadata_prompt = f'{normalized_prompt} -S{first_seed}'
|
||||||
|
path = file_writer.save_image_and_prompt_to_png(
|
||||||
grid_img, metadata_prompt, filename
|
grid_img, metadata_prompt, filename
|
||||||
)
|
)
|
||||||
|
results = [[path, seeds]]
|
||||||
|
|
||||||
last_seeds = [r[1] for r in results]
|
last_seeds = list(seeds)
|
||||||
|
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
@ -95,7 +95,7 @@ async function generateSubmit(form) {
|
|||||||
if (data.event === 'result') {
|
if (data.event === 'result') {
|
||||||
noOutputs = false;
|
noOutputs = false;
|
||||||
document.querySelector("#no-results-message")?.remove();
|
document.querySelector("#no-results-message")?.remove();
|
||||||
appendOutput(data.files[0],data.files[1],data.config);
|
appendOutput(data.url, data.seed, data.config);
|
||||||
progressEle.setAttribute('value', 0);
|
progressEle.setAttribute('value', 0);
|
||||||
progressEle.setAttribute('max', totalSteps);
|
progressEle.setAttribute('max', totalSteps);
|
||||||
progressImageEle.src = BLANK_IMAGE_URL;
|
progressImageEle.src = BLANK_IMAGE_URL;
|
||||||
|
Loading…
Reference in New Issue
Block a user