diff --git a/docs/features/OUTPAINTING.md b/docs/features/OUTPAINTING.md index 122c732605..e27bbcaf00 100644 --- a/docs/features/OUTPAINTING.md +++ b/docs/features/OUTPAINTING.md @@ -92,6 +92,21 @@ The new image is larger than the original (576x704) because 64 pixels were added to the top and right sides. You will need enough VRAM to process an image of this size. +#### Outcropping non-InvokeAI images + +You can outcrop an arbitrary image that was not generated by InvokeAI, +but your results will vary. The `inpainting-1.5` model is highly +recommended, but if not feasible, then you may be able to improve the +output by conditioning the outcropping with a text prompt that +describes the scene using the `--new_prompt` argument: + +```bash +invoke> !fix images/vacation.png --outcrop top 128 --new_prompt "family vacation" +``` + +You may also provide a different seed for outcropping to use by passing +`-S`. A negative seed will generate a new random seed. + A number of caveats: 1. Although you can specify any pixel values, they will be rounded up to the diff --git a/ldm/generate.py b/ldm/generate.py index d172e97a66..121ccc4e16 100644 --- a/ldm/generate.py +++ b/ldm/generate.py @@ -561,18 +561,22 @@ class Generate: ): # retrieve the seed from the image; seed = None - image_metadata = None prompt = None args = metadata_from_png(image_path) - seed = args.seed - prompt = args.prompt or '' - if seed == 0: - seed = random.randrange(0, np.iinfo(np.uint32).max) - opt.seed = seed - print(f'>> generated new seed {seed} and prompt "{prompt}" for {image_path}') + if opt.seed is not None: + seed = opt.seed + elif args.seed >= 0: + seed = args.seed else: - print(f'>> retrieved seed {seed} and prompt "{prompt}" from {image_path}') + seed = random.randrange(0, np.iinfo(np.uint32).max) + + if opt.prompt is not None: + prompt = opt.prompt + else: + prompt = args.prompt + + print(f'>> using seed {seed} and prompt "{prompt}" for {image_path}') # try to reuse the same filename prefix as the original file. # we take everything up to the first period @@ -619,6 +623,10 @@ class Generate: extend_instructions[direction]=int(pixels) except ValueError: print(f'** invalid extension instruction. Use ..., as in "top 64 left 128 right 64 bottom 64"') + + opt.seed = seed + opt.prompt = prompt + if len(extend_instructions)>0: restorer = Outcrop(image,self,) return restorer.process ( diff --git a/ldm/invoke/args.py b/ldm/invoke/args.py index 3c3b2059d5..0392418d5d 100644 --- a/ldm/invoke/args.py +++ b/ldm/invoke/args.py @@ -866,6 +866,11 @@ class Args(object): default=32, help='When outpainting, the tile size to use for filling outpaint areas', ) + postprocessing_group.add_argument( + '--new_prompt', + type=str, + help='Change the text prompt applied during postprocessing (default, use original generation prompt)', + ) postprocessing_group.add_argument( '-ft', '--facetool', diff --git a/ldm/invoke/generator/base.py b/ldm/invoke/generator/base.py index 719d08c7c0..003ac1b466 100644 --- a/ldm/invoke/generator/base.py +++ b/ldm/invoke/generator/base.py @@ -63,7 +63,7 @@ class Generator(): **kwargs ) results = [] - seed = seed if seed is not None and seed > 0 else self.new_seed() + seed = seed if seed is not None and seed >= 0 else self.new_seed() first_seed = seed seed, initial_noise = self.generate_initial_noise(seed, width, height) diff --git a/ldm/invoke/generator/inpaint.py b/ldm/invoke/generator/inpaint.py index d8a471350f..792a01b3b1 100644 --- a/ldm/invoke/generator/inpaint.py +++ b/ldm/invoke/generator/inpaint.py @@ -169,7 +169,7 @@ class Inpaint(Img2Img): # Fill missing areas of original image init_filled = self.tile_fill_missing( self.pil_image.copy(), - seed = self.seed, + seed = self.seed if self.seed >= 0 else self.new_seed(), tile_size = tile_size ) init_filled.paste(init_image, (0,0), init_image.split()[-1]) diff --git a/ldm/invoke/restoration/outcrop.py b/ldm/invoke/restoration/outcrop.py index 1a0aaf2c8f..2587bb5776 100644 --- a/ldm/invoke/restoration/outcrop.py +++ b/ldm/invoke/restoration/outcrop.py @@ -28,12 +28,12 @@ class Outcrop(object): self.generate._set_sampler() def wrapped_callback(img,seed,**kwargs): - preferred_seed = orig_opt.seed if orig_opt.seed> 0 else seed + preferred_seed = orig_opt.seed if orig_opt.seed >= 0 else seed image_callback(img,preferred_seed,use_prefix=prefix,**kwargs) result= self.generate.prompt2image( - orig_opt.prompt, - seed = orig_opt.seed if orig_opt.seed>0 else opt.seed, + opt.prompt, + seed = opt.seed or orig_opt.seed, sampler = self.generate.sampler, steps = opt.steps, cfg_scale = opt.cfg_scale, diff --git a/scripts/invoke.py b/scripts/invoke.py index 325a3ca6c4..5a474a5463 100755 --- a/scripts/invoke.py +++ b/scripts/invoke.py @@ -208,7 +208,10 @@ def main_loop(gen, opt): setattr(opt,attr,path) # retrieve previous value of seed if requested - if opt.seed is not None and opt.seed < 0: + # Exception: for postprocess operations negative seed values + # mean "discard the original seed and generate a new one" + # (this is a non-obvious hack and needs to be reworked) + if opt.seed is not None and opt.seed < 0 and operation != 'postprocess': try: opt.seed = last_results[opt.seed][1] print(f'>> Reusing previous seed {opt.seed}') @@ -277,7 +280,7 @@ def main_loop(gen, opt): filename = f'{prefix}.{use_prefix}.{seed}.png' tm = opt.text_mask[0] th = opt.text_mask[1] if len(opt.text_mask)>1 else 0.5 - formatted_dream_prompt = f'!mask {opt.prompt} -tm {tm} {th}' + formatted_dream_prompt = f'!mask {opt.input_file_path} -tm {tm} {th}' path = file_writer.save_image_and_prompt_to_png( image = image, dream_prompt = formatted_dream_prompt, @@ -317,7 +320,7 @@ def main_loop(gen, opt): tool = re.match('postprocess:(\w+)',opt.last_operation).groups()[0] add_postprocessing_to_metadata( opt, - opt.prompt, + opt.input_file_path, filename, tool, formatted_dream_prompt, @@ -614,10 +617,16 @@ def do_textmask(gen, opt, callback): ) def do_postprocess (gen, opt, callback): - file_path = opt.prompt # treat the prompt as the file pathname + file_path = opt.prompt # treat the prompt as the file pathname + if opt.new_prompt is not None: + opt.prompt = opt.new_prompt + else: + opt.prompt = None + if os.path.dirname(file_path) == '': #basename given file_path = os.path.join(opt.outdir,file_path) + opt.input_file_path = file_path tool=None if opt.facetool_strength > 0: tool = opt.facetool @@ -659,7 +668,11 @@ def add_postprocessing_to_metadata(opt,original_file,new_file,tool,command): try: meta = retrieve_metadata(original_file)['sd-metadata'] except AttributeError: - meta = retrieve_metadata(new_file)['sd-metadata'] + try: + meta = retrieve_metadata(new_file)['sd-metadata'] + except AttributeError: + meta = {} + if 'image' not in meta: meta = metadata_dumps(opt,seeds=[opt.seed])['image'] meta['image'] = {} @@ -707,7 +720,7 @@ def prepare_image_metadata( elif len(prior_variations) > 0: formatted_dream_prompt = opt.dream_prompt_str(seed=first_seed) elif operation == 'postprocess': - formatted_dream_prompt = '!fix '+opt.dream_prompt_str(seed=seed) + formatted_dream_prompt = '!fix '+opt.dream_prompt_str(seed=seed,prompt=opt.input_file_path) else: formatted_dream_prompt = opt.dream_prompt_str(seed=seed) return filename,formatted_dream_prompt