mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
add outcrop postprocessor
This commit is contained in:
@ -213,6 +213,9 @@ class Args(object):
|
||||
if a['gfpgan_strength']:
|
||||
switches.append(f'-G {a["gfpgan_strength"]}')
|
||||
|
||||
if a['outcrop']:
|
||||
switches.append(f'-c {" ".join([str(u) for u in a["outcrop"]])}')
|
||||
|
||||
# esrgan-specific parameters
|
||||
if a['upscale']:
|
||||
switches.append(f'-U {" ".join([str(u) for u in a["upscale"]])}')
|
||||
@ -639,6 +642,14 @@ class Args(object):
|
||||
metavar=('direction', 'pixels'),
|
||||
help='Direction to extend the given image (left|right|top|bottom). If a distance pixel value is not specified it defaults to half the image size'
|
||||
)
|
||||
img2img_group.add_argument(
|
||||
'-c',
|
||||
'--outcrop',
|
||||
nargs='+',
|
||||
type=str,
|
||||
metavar=('direction:pixels'),
|
||||
help='Outcrop the image "direction:pixels direction:pixels..." where direction is (top|left|bottom|right)'
|
||||
)
|
||||
postprocessing_group.add_argument(
|
||||
'-ft',
|
||||
'--facetool',
|
||||
@ -736,24 +747,12 @@ def metadata_dumps(opt,
|
||||
'app_version' : APP_VERSION,
|
||||
}
|
||||
|
||||
# add some RFC266 fields that are generated internally, and not as
|
||||
# user args
|
||||
# # add some RFC266 fields that are generated internally, and not as
|
||||
# # user args
|
||||
image_dict = opt.to_dict(
|
||||
postprocessing=postprocessing
|
||||
postprocessing=postprocessing
|
||||
)
|
||||
|
||||
# 'postprocessing' is either null or an array of postprocessing metadatal
|
||||
if postprocessing:
|
||||
# TODO: This is just a hack until postprocessing pipeline work completed
|
||||
image_dict['postprocessing'] = []
|
||||
|
||||
if image_dict['gfpgan_strength'] and image_dict['gfpgan_strength'] > 0:
|
||||
image_dict['postprocessing'].append('GFPGAN (not RFC compliant)')
|
||||
if image_dict['upscale'] and image_dict['upscale'][0] > 0:
|
||||
image_dict['postprocessing'].append('ESRGAN (not RFC compliant)')
|
||||
else:
|
||||
image_dict['postprocessing'] = None
|
||||
|
||||
# remove any image keys not mentioned in RFC #266
|
||||
rfc266_img_fields = ['type','postprocessing','sampler','prompt','seed','variations','steps',
|
||||
'cfg_scale','threshold','perlin','step_number','width','height','extra','strength']
|
||||
|
@ -21,6 +21,7 @@ class Img2Img(Generator):
|
||||
"""
|
||||
self.perlin = perlin
|
||||
|
||||
print(f'DEBUG: init_image = {init_image}')
|
||||
sampler.make_schedule(
|
||||
ddim_num_steps=steps, ddim_eta=ddim_eta, verbose=False
|
||||
)
|
||||
|
@ -37,7 +37,7 @@ class PngWriter:
|
||||
path = os.path.join(self.outdir, name)
|
||||
info = PngImagePlugin.PngInfo()
|
||||
info.add_text('Dream', dream_prompt)
|
||||
if metadata: # TODO: merge command line app's method of writing metadata and always just write metadata
|
||||
if metadata:
|
||||
info.add_text('sd-metadata', json.dumps(metadata))
|
||||
image.save(path, 'PNG', pnginfo=info)
|
||||
return path
|
||||
@ -61,3 +61,8 @@ def retrieve_metadata(img_path):
|
||||
dream_prompt = im.text.get('Dream', '')
|
||||
return {'sd-metadata': json.loads(md), 'Dream': dream_prompt}
|
||||
|
||||
def write_metadata(img_path:str, meta:dict):
|
||||
im = Image.open(img_path)
|
||||
info = PngImagePlugin.PngInfo()
|
||||
info.add_text('sd-metadata', json.dumps(meta))
|
||||
im.save(img_path,'PNG',pnginfo=info)
|
||||
|
109
ldm/dream/restoration/outcrop.py
Normal file
109
ldm/dream/restoration/outcrop.py
Normal file
@ -0,0 +1,109 @@
|
||||
import warnings
|
||||
import math
|
||||
from ldm.dream.conditioning import get_uc_and_c
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
class Outcrop():
|
||||
def __init__(
|
||||
self,
|
||||
image,
|
||||
generator, # current generator object
|
||||
):
|
||||
self.image = image
|
||||
self.generator = generator
|
||||
|
||||
def extend(
|
||||
self,
|
||||
extents:dict,
|
||||
opt,
|
||||
image_callback = None,
|
||||
prefix = None
|
||||
):
|
||||
extended_image = self._extend_all(extents)
|
||||
|
||||
# switch samplers temporarily
|
||||
curr_sampler = self.generator.sampler
|
||||
self.generator.sampler_name = opt.sampler_name
|
||||
self.generator._set_sampler()
|
||||
|
||||
def wrapped_callback(img,seed,**kwargs):
|
||||
image_callback(img,opt.seed,use_prefix=prefix,**kwargs)
|
||||
|
||||
result= self.generator.prompt2image(
|
||||
opt.prompt,
|
||||
sampler = self.generator.sampler,
|
||||
steps = opt.steps,
|
||||
cfg_scale = opt.cfg_scale,
|
||||
ddim_eta = self.generator.ddim_eta,
|
||||
width = extended_image.width,
|
||||
height = extended_image.height,
|
||||
init_img = extended_image,
|
||||
strength = opt.strength,
|
||||
image_callback = wrapped_callback,
|
||||
)
|
||||
|
||||
# swap sampler back
|
||||
self.generator.sampler = curr_sampler
|
||||
return result
|
||||
|
||||
def _extend_all(
|
||||
self,
|
||||
extents:dict,
|
||||
) -> Image:
|
||||
'''
|
||||
Extend the image in direction ('top','bottom','left','right') by
|
||||
the indicated value. The image canvas is extended, and the empty
|
||||
rectangular section will be filled with a blurred copy of the
|
||||
adjacent image.
|
||||
'''
|
||||
image = self.image
|
||||
for direction in extents:
|
||||
assert direction in ['top', 'left', 'bottom', 'right'],'Direction must be one of "top", "left", "bottom", "right"'
|
||||
pixels = extents[direction]
|
||||
# round pixels up to the nearest 64
|
||||
pixels = math.ceil(pixels/64) * 64
|
||||
print(f'>> extending image {direction}ward by {pixels} pixels')
|
||||
image = self._rotate(image,direction)
|
||||
image = self._extend(image,pixels)
|
||||
image = self._rotate(image,direction,reverse=True)
|
||||
return image
|
||||
|
||||
def _rotate(self,image:Image,direction:str,reverse=False) -> Image:
|
||||
'''
|
||||
Rotates image so that the area to extend is always at the top top.
|
||||
Simplifies logic later. The reverse argument, if true, will undo the
|
||||
previous transpose.
|
||||
'''
|
||||
transposes = {
|
||||
'right': ['ROTATE_90','ROTATE_270'],
|
||||
'bottom': ['ROTATE_180','ROTATE_180'],
|
||||
'left': ['ROTATE_270','ROTATE_90']
|
||||
}
|
||||
if direction not in transposes:
|
||||
return image
|
||||
transpose = transposes[direction][1 if reverse else 0]
|
||||
return image.transpose(Image.Transpose.__dict__[transpose])
|
||||
|
||||
def _extend(self,image:Image,pixels:int)-> Image:
|
||||
extended_img = Image.new('RGBA',(image.width,image.height+pixels))
|
||||
|
||||
# first paste places old image at top of extended image, stretch
|
||||
# it, and applies a gaussian blur to it
|
||||
# take the top half region, stretch and paste it
|
||||
top_slice = image.crop(box=(0,0,image.width,pixels//2))
|
||||
top_slice = top_slice.resize((image.width,pixels))
|
||||
extended_img.paste(top_slice,box=(0,0))
|
||||
|
||||
# second paste creates a copy of the image displaced pixels downward;
|
||||
# The overall effect is to create a blurred duplicate of the top portion of
|
||||
# the image.
|
||||
extended_img.paste(image,box=(0,pixels))
|
||||
extended_img = extended_img.filter(filter=ImageFilter.GaussianBlur(radius=pixels//2))
|
||||
extended_img.paste(image,box=(0,pixels))
|
||||
|
||||
# now make the top part transparent to use as a mask
|
||||
alpha = extended_img.getchannel('A')
|
||||
alpha.paste(0,(0,0,extended_img.width,pixels*2))
|
||||
extended_img.putalpha(alpha)
|
||||
|
||||
return extended_img
|
@ -499,6 +499,7 @@ class Generate:
|
||||
codeformer_fidelity = 0.75,
|
||||
upscale = None,
|
||||
out_direction = None,
|
||||
outcrop = [],
|
||||
save_original = True, # to get new name
|
||||
callback = None,
|
||||
opt = None,
|
||||
@ -527,8 +528,13 @@ class Generate:
|
||||
# face fixers and esrgan take an Image, but embiggen takes a path
|
||||
image = Image.open(image_path)
|
||||
|
||||
# Note that we need to adopt a uniform API for the postprocessors.
|
||||
# This is completely ad hoc ATCM
|
||||
# used by multiple postfixers
|
||||
uc, c = get_uc_and_c(
|
||||
prompt, model =self.model,
|
||||
skip_normalize=opt.skip_normalize,
|
||||
log_tokens =opt.log_tokenization
|
||||
)
|
||||
|
||||
if tool in ('gfpgan','codeformer','upscale'):
|
||||
if tool == 'gfpgan':
|
||||
facetool = 'gfpgan'
|
||||
@ -548,14 +554,25 @@ class Generate:
|
||||
prefix = prefix,
|
||||
)
|
||||
|
||||
elif tool == 'outcrop':
|
||||
from ldm.dream.restoration.outcrop import Outcrop
|
||||
extend_instructions = {}
|
||||
for direction,pixels in _pairwise(opt.outcrop):
|
||||
extend_instructions[direction]=int(pixels)
|
||||
generator = Outcrop(
|
||||
image,
|
||||
self,
|
||||
)
|
||||
return generator.extend(
|
||||
extend_instructions,
|
||||
args,
|
||||
image_callback = callback,
|
||||
prefix = prefix,
|
||||
)
|
||||
|
||||
elif tool == 'embiggen':
|
||||
# fetch the metadata from the image
|
||||
generator = self._make_embiggen()
|
||||
uc, c = get_uc_and_c(
|
||||
prompt, model =self.model,
|
||||
skip_normalize=opt.skip_normalize,
|
||||
log_tokens =opt.log_tokenization
|
||||
)
|
||||
opt.strength = 0.40
|
||||
print(f'>> Setting img2img strength to {opt.strength} for happy embiggening')
|
||||
# embiggen takes a image path (sigh)
|
||||
@ -586,16 +603,13 @@ class Generate:
|
||||
steps = opt.steps,
|
||||
cfg_scale = opt.cfg_scale,
|
||||
ddim_eta = self.ddim_eta,
|
||||
conditioning= get_uc_and_c(
|
||||
oldargs.prompt, model =self.model,
|
||||
skip_normalize=opt.skip_normalize,
|
||||
log_tokens =opt.log_tokenization
|
||||
),
|
||||
conditioning= (uc,c),
|
||||
width = opt.width,
|
||||
height = opt.height,
|
||||
init_img = image_path, # not the Image! (sigh)
|
||||
strength = opt.strength,
|
||||
image_callback = callback,
|
||||
prefix = prefix,
|
||||
)
|
||||
elif tool is None:
|
||||
print(f'* please provide at least one postprocessing option, such as -G or -U')
|
||||
@ -968,7 +982,6 @@ class Generate:
|
||||
|
||||
image = image.resize((image.width//downsampling, image.height //
|
||||
downsampling), resample=Image.Resampling.NEAREST)
|
||||
|
||||
image = np.array(image)
|
||||
image = image.astype(np.float32) / 255.0
|
||||
image = image[None].transpose(0, 3, 1, 2)
|
||||
@ -1088,3 +1101,8 @@ class Generate:
|
||||
image = self.sample_to_image(img)
|
||||
image.save(os.path.join(path,f'{counter:03}.png'),'PNG')
|
||||
return callback
|
||||
|
||||
def _pairwise(iterable):
|
||||
"s -> (s0, s1), (s2, s3), (s4, s5), ..."
|
||||
a = iter(iterable)
|
||||
return zip(a, a)
|
||||
|
Reference in New Issue
Block a user