From dbf2c63c9035ed692ccdc5c9871772686325a6ec Mon Sep 17 00:00:00 2001 From: Travco Date: Mon, 12 Sep 2022 15:37:26 -0400 Subject: [PATCH] Add Embiggen automation to upscale-cut-img2img-stitch and achieve high res without extra VRAM (#437) * Add Embiggen automation * Make embiggen_tiles masking more intelligent and count from one (at least for the user), rewrite sections of Embiggen README, fix various typos throughout README * drop duplicate log message --- README.md | 3 +- ldm/dream/generator/embiggen.py | 403 ++++++++++++++++++++++++++++++++ ldm/dream/generator/img2img.py | 2 +- ldm/dream/pngwriter.py | 4 + ldm/generate.py | 19 ++ scripts/dream.py | 16 +- 6 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 ldm/dream/generator/embiggen.py diff --git a/README.md b/README.md index 1737e6515b..49d57811fe 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ report bugs and make feature requests. Be sure to use the provided templates. They will help aid diagnose issues faster._ # **Table of Contents** - 1. [Installation](#installation) 2. [Major Features](#features) 3. [Changelog](#latest-changes) @@ -134,7 +133,7 @@ To run in full-precision mode, start `dream.py` with the - Works on M1 Apple hardware. - Multiple bug fixes. -For older changelogs, please visit **[CHANGELOGS](docs/CHANGELOG.md)**. +For older changelogs, please visit **[CHANGELOGS](docs/CHANGELOG.md)**. # Troubleshooting diff --git a/ldm/dream/generator/embiggen.py b/ldm/dream/generator/embiggen.py new file mode 100644 index 0000000000..cb9c029a66 --- /dev/null +++ b/ldm/dream/generator/embiggen.py @@ -0,0 +1,403 @@ +''' +ldm.dream.generator.embiggen descends from ldm.dream.generator +and generates with ldm.dream.generator.img2img +''' + +import torch +import numpy as np +from PIL import Image +from ldm.dream.generator.base import Generator +from ldm.models.diffusion.ddim import DDIMSampler +from ldm.dream.generator.img2img import Img2Img + +class Embiggen(Generator): + def __init__(self,model): + super().__init__(model) + self.init_latent = None + + @torch.no_grad() + def get_make_image( + self, + prompt, + sampler, + steps, + cfg_scale, + ddim_eta, + conditioning, + init_img, + strength, + width, + height, + embiggen, + embiggen_tiles, + step_callback=None, + **kwargs + ): + """ + Returns a function returning an image derived from the prompt and multi-stage twice-baked potato layering over the img2img on the initial image + Return value depends on the seed at the time you call it + """ + # Construct embiggen arg array, and sanity check arguments + if embiggen == None: # embiggen can also be called with just embiggen_tiles + embiggen = [1.0] # If not specified, assume no scaling + elif embiggen[0] < 0 : + embiggen[0] = 1.0 + print('>> Embiggen scaling factor cannot be negative, fell back to the default of 1.0 !') + if len(embiggen) < 2: + embiggen.append(0.75) + elif embiggen[1] > 1.0 or embiggen[1] < 0 : + embiggen[1] = 0.75 + print('>> Embiggen upscaling strength for ESRGAN must be between 0 and 1, fell back to the default of 0.75 !') + if len(embiggen) < 3: + embiggen.append(0.25) + elif embiggen[2] < 0 : + embiggen[2] = 0.25 + print('>> Overlap size for Embiggen must be a positive ratio between 0 and 1 OR a number of pixels, fell back to the default of 0.25 !') + + # Convert tiles from their user-freindly count-from-one to count-from-zero, because we need to do modulo math + # and then sort them, because... people. + if embiggen_tiles: + embiggen_tiles = list(map(lambda n: n-1, embiggen_tiles)) + embiggen_tiles.sort() + + # Prep img2img generator, since we wrap over it + gen_img2img = Img2Img(self.model) + + # Open original init image (not a tensor) to manipulate + initsuperimage = Image.open(init_img) + + with Image.open(init_img) as img: + initsuperimage = img.convert('RGB') + + # Size of the target super init image in pixels + initsuperwidth, initsuperheight = initsuperimage.size + + # Increase by scaling factor if not already resized, using ESRGAN as able + if embiggen[0] != 1.0: + initsuperwidth = round(initsuperwidth*embiggen[0]) + initsuperheight = round(initsuperheight*embiggen[0]) + if embiggen[1] > 0: # No point in ESRGAN upscaling if strength is set zero + from ldm.gfpgan.gfpgan_tools import ( + real_esrgan_upscale, + ) + print(f'>> ESRGAN upscaling init image prior to cutting with Embiggen with strength {embiggen[1]}') + if embiggen[0] > 2: + initsuperimage = real_esrgan_upscale( + initsuperimage, + embiggen[1], # upscale strength + 4, # upscale scale + self.seed, + ) + else: + initsuperimage = real_esrgan_upscale( + initsuperimage, + embiggen[1], # upscale strength + 2, # upscale scale + self.seed, + ) + # We could keep recursively re-running ESRGAN for a requested embiggen[0] larger than 4x + # but from personal experiance it doesn't greatly improve anything after 4x + # Resize to target scaling factor resolution + initsuperimage = initsuperimage.resize((initsuperwidth, initsuperheight), Image.Resampling.LANCZOS) + + # Use width and height as tile widths and height + # Determine buffer size in pixels + if embiggen[2] < 1: + if embiggen[2] < 0: + embiggen[2] = 0 + overlap_size_x = round(embiggen[2] * width) + overlap_size_y = round(embiggen[2] * height) + else: + overlap_size_x = round(embiggen[2]) + overlap_size_y = round(embiggen[2]) + + # With overall image width and height known, determine how many tiles we need + def ceildiv(a, b): + return -1 * (-a // b) + + # X and Y needs to be determined independantly (we may have savings on one based on the buffer pixel count) + # (initsuperwidth - width) is the area remaining to the right that we need to layers tiles to fill + # (width - overlap_size_x) is how much new we can fill with a single tile + emb_tiles_x = 1 + emb_tiles_y = 1 + if (initsuperwidth - width) > 0: + emb_tiles_x = ceildiv(initsuperwidth - width, width - overlap_size_x) + 1 + if (initsuperheight - height) > 0: + emb_tiles_y = ceildiv(initsuperheight - height, height - overlap_size_y) + 1 + # Sanity + assert emb_tiles_x > 1 or emb_tiles_y > 1, f'ERROR: Based on the requested dimensions of {initsuperwidth}x{initsuperheight} and tiles of {width}x{height} you don\'t need to Embiggen! Check your arguments.' + + # Prep alpha layers -------------- + # https://stackoverflow.com/questions/69321734/how-to-create-different-transparency-like-gradient-with-python-pil + # agradientL is Left-side transparent + agradientL = Image.linear_gradient('L').rotate(90).resize((overlap_size_x, height)) + # agradientT is Top-side transparent + agradientT = Image.linear_gradient('L').resize((width, overlap_size_y)) + # radial corner is the left-top corner, made full circle then cut to just the left-top quadrant + agradientC = Image.new('L', (256, 256)) + for y in range(256): + for x in range(256): + #Find distance to lower right corner (numpy takes arrays) + distanceToLR = np.sqrt([(255 - x) ** 2 + (255 - y) ** 2])[0] + #Clamp values to max 255 + if distanceToLR > 255: + distanceToLR = 255 + #Place the pixel as invert of distance + agradientC.putpixel((x, y), int(255 - distanceToLR)) + + # Create alpha layers default fully white + alphaLayerL = Image.new("L", (width, height), 255) + alphaLayerT = Image.new("L", (width, height), 255) + alphaLayerLTC = Image.new("L", (width, height), 255) + # Paste gradients into alpha layers + alphaLayerL.paste(agradientL, (0, 0)) + alphaLayerT.paste(agradientT, (0, 0)) + alphaLayerLTC.paste(agradientL, (0, 0)) + alphaLayerLTC.paste(agradientT, (0, 0)) + alphaLayerLTC.paste(agradientC.resize((overlap_size_x, overlap_size_y)), (0, 0)) + + if embiggen_tiles: + # Individual unconnected sides + alphaLayerR = Image.new("L", (width, height), 255) + alphaLayerR.paste(agradientL.rotate(180), (width - overlap_size_x, 0)) + alphaLayerB = Image.new("L", (width, height), 255) + alphaLayerB.paste(agradientT.rotate(180), (0, height - overlap_size_y)) + alphaLayerTB = Image.new("L", (width, height), 255) + alphaLayerTB.paste(agradientT, (0, 0)) + alphaLayerTB.paste(agradientT.rotate(180), (0, height - overlap_size_y)) + alphaLayerLR = Image.new("L", (width, height), 255) + alphaLayerLR.paste(agradientL, (0, 0)) + alphaLayerLR.paste(agradientL.rotate(180), (width - overlap_size_x, 0)) + + # Sides and corner Layers + alphaLayerRBC = Image.new("L", (width, height), 255) + alphaLayerRBC.paste(agradientL.rotate(180), (width - overlap_size_x, 0)) + alphaLayerRBC.paste(agradientT.rotate(180), (0, height - overlap_size_y)) + alphaLayerRBC.paste(agradientC.rotate(180).resize((overlap_size_x, overlap_size_y)), (width - overlap_size_x, height - overlap_size_y)) + alphaLayerLBC = Image.new("L", (width, height), 255) + alphaLayerLBC.paste(agradientL, (0, 0)) + alphaLayerLBC.paste(agradientT.rotate(180), (0, height - overlap_size_y)) + alphaLayerLBC.paste(agradientC.rotate(90).resize((overlap_size_x, overlap_size_y)), (0, height - overlap_size_y)) + alphaLayerRTC = Image.new("L", (width, height), 255) + alphaLayerRTC.paste(agradientL.rotate(180), (width - overlap_size_x, 0)) + alphaLayerRTC.paste(agradientT, (0, 0)) + alphaLayerRTC.paste(agradientC.rotate(270).resize((overlap_size_x, overlap_size_y)), (width - overlap_size_x, 0)) + + # All but X layers + alphaLayerABT = Image.new("L", (width, height), 255) + alphaLayerABT.paste(alphaLayerLBC, (0, 0)) + alphaLayerABT.paste(agradientL.rotate(180), (width - overlap_size_x, 0)) + alphaLayerABT.paste(agradientC.rotate(180).resize((overlap_size_x, overlap_size_y)), (width - overlap_size_x, height - overlap_size_y)) + alphaLayerABL = Image.new("L", (width, height), 255) + alphaLayerABL.paste(alphaLayerRTC, (0, 0)) + alphaLayerABL.paste(agradientT.rotate(180), (0, height - overlap_size_y)) + alphaLayerABL.paste(agradientC.rotate(180).resize((overlap_size_x, overlap_size_y)), (width - overlap_size_x, height - overlap_size_y)) + alphaLayerABR = Image.new("L", (width, height), 255) + alphaLayerABR.paste(alphaLayerLBC, (0, 0)) + alphaLayerABR.paste(agradientT, (0, 0)) + alphaLayerABR.paste(agradientC.resize((overlap_size_x, overlap_size_y)), (0, 0)) + alphaLayerABB = Image.new("L", (width, height), 255) + alphaLayerABB.paste(alphaLayerRTC, (0, 0)) + alphaLayerABB.paste(agradientL, (0, 0)) + alphaLayerABB.paste(agradientC.resize((overlap_size_x, overlap_size_y)), (0, 0)) + + # All-around layer + alphaLayerAA = Image.new("L", (width, height), 255) + alphaLayerAA.paste(alphaLayerABT, (0, 0)) + alphaLayerAA.paste(agradientT, (0, 0)) + alphaLayerAA.paste(agradientC.resize((overlap_size_x, overlap_size_y)), (0, 0)) + alphaLayerAA.paste(agradientC.rotate(270).resize((overlap_size_x, overlap_size_y)), (width - overlap_size_x, 0)) + + # Clean up temporary gradients + del agradientL + del agradientT + del agradientC + + def make_image(x_T): + # Make main tiles ------------------------------------------------- + if embiggen_tiles: + print(f'>> Making {len(embiggen_tiles)} Embiggen tiles...') + else: + print(f'>> Making {(emb_tiles_x * emb_tiles_y)} Embiggen tiles ({emb_tiles_x}x{emb_tiles_y})...') + + emb_tile_store = [] + for tile in range(emb_tiles_x * emb_tiles_y): + # Determine if this is a re-run and replace + if embiggen_tiles and not tile in embiggen_tiles: + continue + # Get row and column entries + emb_row_i = tile // emb_tiles_x + emb_column_i = tile % emb_tiles_x + # Determine bounds to cut up the init image + # Determine upper-left point + if emb_column_i + 1 == emb_tiles_x: + left = initsuperwidth - width + else: + left = round(emb_column_i * (width - overlap_size_x)) + if emb_row_i + 1 == emb_tiles_y: + top = initsuperheight - height + else: + top = round(emb_row_i * (height - overlap_size_y)) + right = left + width + bottom = top + height + + # Cropped image of above dimension (does not modify the original) + newinitimage = initsuperimage.crop((left, top, right, bottom)) + # DEBUG: + # newinitimagepath = init_img[0:-4] + f'_emb_Ti{tile}.png' + # newinitimage.save(newinitimagepath) + + if embiggen_tiles: + print(f'Making tile #{tile + 1} ({embiggen_tiles.index(tile) + 1} of {len(embiggen_tiles)} requested)') + else: + print(f'Starting {tile + 1} of {(emb_tiles_x * emb_tiles_y)} tiles') + + # create a torch tensor from an Image + newinitimage = np.array(newinitimage).astype(np.float32) / 255.0 + newinitimage = newinitimage[None].transpose(0, 3, 1, 2) + newinitimage = torch.from_numpy(newinitimage) + newinitimage = 2.0 * newinitimage - 1.0 + newinitimage = newinitimage.to(self.model.device) + + tile_results = gen_img2img.generate( + prompt, + iterations = 1, + seed = self.seed, + sampler = sampler, + steps = steps, + cfg_scale = cfg_scale, + conditioning = conditioning, + ddim_eta = ddim_eta, + image_callback = None, # called only after the final image is generated + step_callback = step_callback, # called after each intermediate image is generated + width = width, + height = height, + init_img = init_img, # img2img doesn't need this, but it might in the future + init_image = newinitimage, # notice that init_image is different from init_img + mask_image = None, + strength = strength, + ) + + emb_tile_store.append(tile_results[0][0]) + # DEBUG (but, also has other uses), worth saving if you want tiles without a transparency overlap to manually composite + # emb_tile_store[-1].save(init_img[0:-4] + f'_emb_To{tile}.png') + del newinitimage + + # Sanity check we have them all + if len(emb_tile_store) == (emb_tiles_x * emb_tiles_y) or (embiggen_tiles != [] and len(emb_tile_store) == len(embiggen_tiles)): + outputsuperimage = Image.new("RGBA", (initsuperwidth, initsuperheight)) + if embiggen_tiles: + outputsuperimage.alpha_composite(initsuperimage.convert('RGBA'), (0, 0)) + for tile in range(emb_tiles_x * emb_tiles_y): + if embiggen_tiles: + if tile in embiggen_tiles: + intileimage = emb_tile_store.pop(0) + else: + continue + else: + intileimage = emb_tile_store[tile] + intileimage = intileimage.convert('RGBA') + # Get row and column entries + emb_row_i = tile // emb_tiles_x + emb_column_i = tile % emb_tiles_x + if emb_row_i == 0 and emb_column_i == 0 and not embiggen_tiles: + left = 0 + top = 0 + else: + # Determine upper-left point + if emb_column_i + 1 == emb_tiles_x: + left = initsuperwidth - width + else: + left = round(emb_column_i * (width - overlap_size_x)) + if emb_row_i + 1 == emb_tiles_y: + top = initsuperheight - height + else: + top = round(emb_row_i * (height - overlap_size_y)) + # Handle gradients for various conditions + # Handle emb_rerun case + if embiggen_tiles: + # top of image + if emb_row_i == 0: + if emb_column_i == 0: + if (tile+1) in embiggen_tiles: # Look-ahead right + if (tile+emb_tiles_x) not in embiggen_tiles: # Look-ahead down + intileimage.putalpha(alphaLayerB) + # Otherwise do nothing on this tile + elif (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down only + intileimage.putalpha(alphaLayerR) + else: + intileimage.putalpha(alphaLayerRBC) + elif emb_column_i == emb_tiles_x - 1: + if (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down + intileimage.putalpha(alphaLayerL) + else: + intileimage.putalpha(alphaLayerLBC) + else: + if (tile+1) in embiggen_tiles: # Look-ahead right + if (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down + intileimage.putalpha(alphaLayerL) + else: + intileimage.putalpha(alphaLayerLBC) + elif (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down only + intileimage.putalpha(alphaLayerLR) + else: + intileimage.putalpha(alphaLayerABT) + # bottom of image + elif emb_row_i == emb_tiles_y - 1: + if emb_column_i == 0: + if (tile+1) in embiggen_tiles: # Look-ahead right + intileimage.putalpha(alphaLayerT) + else: + intileimage.putalpha(alphaLayerRTC) + elif emb_column_i == emb_tiles_x - 1: + # No tiles to look ahead to + intileimage.putalpha(alphaLayerLTC) + else: + if (tile+1) in embiggen_tiles: # Look-ahead right + intileimage.putalpha(alphaLayerLTC) + else: + intileimage.putalpha(alphaLayerABB) + # vertical middle of image + else: + if emb_column_i == 0: + if (tile+1) in embiggen_tiles: # Look-ahead right + if (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down + intileimage.putalpha(alphaLayerT) + else: + intileimage.putalpha(alphaLayerTB) + elif (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down only + intileimage.putalpha(alphaLayerRTC) + else: + intileimage.putalpha(alphaLayerABL) + elif emb_column_i == emb_tiles_x - 1: + if (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down + intileimage.putalpha(alphaLayerLTC) + else: + intileimage.putalpha(alphaLayerABR) + else: + if (tile+1) in embiggen_tiles: # Look-ahead right + if (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down + intileimage.putalpha(alphaLayerLTC) + else: + intileimage.putalpha(alphaLayerABR) + elif (tile+emb_tiles_x) in embiggen_tiles: # Look-ahead down only + intileimage.putalpha(alphaLayerABB) + else: + intileimage.putalpha(alphaLayerAA) + # Handle normal tiling case (much simpler - since we tile left to right, top to bottom) + else: + if emb_row_i == 0 and emb_column_i >= 1: + intileimage.putalpha(alphaLayerL) + elif emb_row_i >= 1 and emb_column_i == 0: + intileimage.putalpha(alphaLayerT) + else: + intileimage.putalpha(alphaLayerLTC) + # Layer tile onto final image + outputsuperimage.alpha_composite(intileimage, (left, top)) + else: + print(f'Error: could not find all Embiggen output tiles in memory? Something must have gone wrong with img2img generation.') + + # after internal loops and patching up return Embiggen image + return outputsuperimage + # end of function declaration + return make_image \ No newline at end of file diff --git a/ldm/dream/generator/img2img.py b/ldm/dream/generator/img2img.py index 242912d0eb..6a1561db6f 100644 --- a/ldm/dream/generator/img2img.py +++ b/ldm/dream/generator/img2img.py @@ -1,5 +1,5 @@ ''' -ldm.dream.generator.txt2img descends from ldm.dream.generator +ldm.dream.generator.img2img descends from ldm.dream.generator ''' import torch diff --git a/ldm/dream/pngwriter.py b/ldm/dream/pngwriter.py index 3f3a15891b..4e8bca2366 100644 --- a/ldm/dream/pngwriter.py +++ b/ldm/dream/pngwriter.py @@ -73,6 +73,10 @@ class PromptFormatter: switches.append(f'-G{opt.gfpgan_strength}') if opt.upscale: switches.append(f'-U {" ".join([str(u) for u in opt.upscale])}') + if opt.embiggen: + switches.append(f'-embiggen {" ".join([str(u) for u in opt.embiggen])}') + if opt.embiggen_tiles: + switches.append(f'-embiggen_tiles {" ".join([str(u) for u in opt.embiggen_tiles])}') if opt.variation_amount > 0: switches.append(f'-v{opt.variation_amount}') if opt.with_variations: diff --git a/ldm/generate.py b/ldm/generate.py index 8f67403633..89bbb470f5 100644 --- a/ldm/generate.py +++ b/ldm/generate.py @@ -205,6 +205,9 @@ class Generate: init_mask = None, fit = False, strength = None, + # these are specific to embiggen (which also relies on img2img args) + embiggen = None, + embiggen_tiles = None, # these are specific to GFPGAN/ESRGAN gfpgan_strength= 0, save_original = False, @@ -230,6 +233,8 @@ class Generate: image_callback // a function or method that will be called each time an image is generated with_variations // a weighted list [(seed_1, weight_1), (seed_2, weight_2), ...] of variations which should be applied before doing any generation variation_amount // optional 0-1 value to slerp from -S noise to random noise (allows variations on an image) + embiggen // scale factor relative to the size of the --init_img (-I), followed by ESRGAN upscaling strength (0-1.0), followed by minimum amount of overlap between tiles as a decimal ratio (0 - 1.0) or number of pixels + embiggen_tiles // list of tiles by number in order to process and replace onto the image e.g. `0 2 4` To use the step callback, define a function that receives two arguments: - Image GPU data @@ -274,6 +279,9 @@ class Generate: assert ( 0.0 <= variation_amount <= 1.0 ), '-v --variation_amount must be in [0.0, 1.0]' + assert ( + (embiggen == None and embiggen_tiles == None) or ((embiggen != None or embiggen_tiles != None) and init_img != None) + ), 'Embiggen requires an init/input image to be specified' # check this logic - doesn't look right if len(with_variations) > 0 or variation_amount > 1.0: @@ -310,6 +318,8 @@ class Generate: if (init_image is not None) and (mask_image is not None): generator = self._make_inpaint() + elif (embiggen != None or embiggen_tiles != None): + generator = self._make_embiggen() elif init_image is not None: generator = self._make_img2img() else: @@ -329,9 +339,12 @@ class Generate: step_callback = step_callback, # called after each intermediate image is generated width = width, height = height, + init_img = init_img, # embiggen needs to manipulate from the unmodified init_img init_image = init_image, # notice that init_image is different from init_img mask_image = mask_image, strength = strength, + embiggen = embiggen, + embiggen_tiles = embiggen_tiles, ) if upscale is not None or gfpgan_strength > 0: @@ -404,6 +417,12 @@ class Generate: from ldm.dream.generator.img2img import Img2Img self.generators['img2img'] = Img2Img(self.model) return self.generators['img2img'] + + def _make_embiggen(self): + if not self.generators.get('embiggen'): + from ldm.dream.generator.embiggen import Embiggen + self.generators['embiggen'] = Embiggen(self.model) + return self.generators['embiggen'] def _make_txt2img(self): if not self.generators.get('txt2img'): diff --git a/scripts/dream.py b/scripts/dream.py index 891a448bf2..8559c1b083 100755 --- a/scripts/dream.py +++ b/scripts/dream.py @@ -631,7 +631,7 @@ def create_cmd_parser(): nargs='+', default=None, type=float, - help='Scale factor (2, 4) for upscaling followed by upscaling strength (0-1.0). If strength not specified, defaults to 0.75' + help='Scale factor (2, 4) for upscaling final output followed by upscaling strength (0-1.0). If strength not specified, defaults to 0.75' ) parser.add_argument( '-save_orig', @@ -639,6 +639,20 @@ def create_cmd_parser(): action='store_true', help='Save original. Use it when upscaling to save both versions.', ) + parser.add_argument( + '-embiggen', + nargs='+', + default=None, + type=float, + help='Embiggen tiled img2img for higher resolution and detail without extra VRAM usage. Takes scale factor relative to the size of the --init_img (-I), followed by ESRGAN upscaling strength (0-1.0), followed by minimum amount of overlap between tiles as a decimal ratio (0 - 1.0) or number of pixels. ESRGAN strength defaults to 0.75, and overlap defaults to 0.25 . ESRGAN is used to upscale the init prior to cutting it into tiles/pieces to run through img2img and then stitch back togeather.', + ) + parser.add_argument( + '-embiggen_tiles', + nargs='+', + default=None, + type=int, + help='If while doing Embiggen we are altering only parts of the image, takes a list of tiles by number to process and replace onto the image e.g. `1 3 5`, useful for redoing problematic spots from a prior Embiggen run', + ) # variants is going to be superseded by a generalized "prompt-morph" function # parser.add_argument('-v','--variants',type=int,help="in img2img mode, the first generated image will get passed back to img2img to generate the requested number of variants") parser.add_argument(