├── README.md ├── LICENSE └── Scripts └── keyframer.py /README.md: -------------------------------------------------------------------------------- 1 | # sd-webui-keyframer 2 | Automatic1111 Stable Diffusion WebUI extension, increase consistency between images by generating in same latent space. 3 | 4 | Script accepts multiple images, plots them on a grid, generates against them, then splits them up again. Good for keyframes or small/short animations. 5 | 6 | - Requires some sort of control, such as ControlNet or InstructPix2Pix, to keep content in-frame. 7 | - Adjust rows and columns until image is most square and within size limits your GPU can handle. 8 | - Set max dimensions to constrain generated sheet within your known GPU limits. 9 | - Press "generate sheet." Image will populate. 10 | - Empty spaces in grid will be populated with repeats (black space ruins generation). 11 | - Press "generate" in img2img. Images will be sent to default img2img directory. 12 | 13 | ![image](https://user-images.githubusercontent.com/93007558/224556800-47ac4610-7603-4c36-a7c2-66b3e6b3091d.png) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LonicaMewinsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Scripts/keyframer.py: -------------------------------------------------------------------------------- 1 | import modules.scripts as scripts 2 | import modules.images 3 | import gradio as gr 4 | from PIL import Image 5 | from modules.processing import Processed, process_images 6 | 7 | #Get num closest to 8 8 | def cl8(num): 9 | rem = num % 8 10 | if rem <= 4: 11 | return round(num - rem) 12 | else: 13 | return round(num + (8 - rem)) 14 | 15 | def normalize_size(images): 16 | refimage = images[0] 17 | refimage = refimage.resize((cl8(refimage.width), cl8(refimage.height)), Image.Resampling.LANCZOS) 18 | return_images = [] 19 | for i in range(len(images)): 20 | if images[i].size != refimage.size: 21 | images[i] = images[i].resize(refimage.size, Image.Resampling.LANCZOS) 22 | return_images.append(images[i]) 23 | return return_images 24 | 25 | def constrain_image(image, max_width, max_height): 26 | width, height = image.size 27 | aspect_ratio = width / float(height) 28 | 29 | if width > max_width or height > max_height: 30 | if width / float(max_width) > height / float(max_height): 31 | new_width = max_width 32 | new_height = int(new_width / aspect_ratio) 33 | else: 34 | new_height = max_height 35 | new_width = int(new_height * aspect_ratio) 36 | image = image.resize((cl8(new_width), cl8(new_height)), Image.Resampling.LANCZOS) 37 | 38 | return image 39 | 40 | def padlist(lst, targetsize): 41 | if targetsize <= len(lst): 42 | return lst[:targetsize] 43 | 44 | last_elem = lst[-1] 45 | num_repeats = targetsize - len(lst) 46 | 47 | return lst + [last_elem] * num_repeats 48 | 49 | def MakeGrid(images, rows, cols): 50 | widths, heights = zip(*(i.size for i in images)) 51 | 52 | grid_width = max(widths) * cols 53 | grid_height = max(heights) * rows 54 | cell_width = grid_width // cols 55 | cell_height = grid_height // rows 56 | final_image = Image.new('RGB', (grid_width, grid_height)) 57 | x_offset = 0 58 | y_offset = 0 59 | for i in range(len(images)): 60 | final_image.paste(images[i], (x_offset, y_offset)) 61 | x_offset += cell_width 62 | if x_offset == grid_width: 63 | x_offset = 0 64 | y_offset += cell_height 65 | 66 | # Save the final image 67 | return final_image 68 | 69 | def BreakGrid(grid, rows, cols): 70 | width = grid.width // cols 71 | height = grid.height // rows 72 | outimages = [] 73 | for row in range(rows): 74 | for col in range(cols): 75 | left = col * width 76 | top = row * height 77 | right = left + width 78 | bottom = top + height 79 | current_img = grid.crop((left, top, right, bottom)) 80 | outimages.append(current_img) 81 | return outimages 82 | 83 | class Script(scripts.Script): 84 | def __init__(self): 85 | #self.frame2frame_dir = tempfile.TemporaryDirectory() 86 | self.img2img_component = gr.Image() 87 | self.img2img_inpaint_component = gr.Image() 88 | self.img2img_w_slider = gr.Slider() 89 | self.img2img_h_slider = gr.Slider() 90 | return None 91 | 92 | def title(self): 93 | return "keyframer" 94 | 95 | def show(self, is_img2img): 96 | return is_img2img 97 | 98 | def ui(self, is_img2img): 99 | #Controls 100 | with gr.Column(): 101 | with gr.Row(): 102 | with gr.Box(): 103 | with gr.Column(): 104 | input_upload = gr.Files(label = "Drop or select keyframe files") 105 | with gr.Column(): 106 | with gr.Row(): 107 | with gr.Column(): 108 | gen_rows = gr.Slider(2, 20, 8, step=2, label="Grid rows", interactive=True) 109 | gen_cols = gr.Slider(2, 20, 8, step=2, label="Grid columns", interactive=True) 110 | gen_maxwidth = gr.Slider(64, 3992, 2048, step=8, label="Maximum generation width", interactive=True, elem_id="maxwidth") 111 | gen_maxheight = gr.Slider(64, 3992, 2048, step=8, label="Maximum generation height", interactive=True, elem_id="maxheight") 112 | with gr.Column(): 113 | info_width = gr.Number(label="width", interactive=False) 114 | info_height = gr.Number(label="height", interactive=False) 115 | gen_button = gr.Button("Generate sheet") 116 | gen_image = gr.Image(Source="Upload", label = "Preview", type= "filepath", interactive=False) 117 | 118 | def submit_images(files, rows, cols, maxwidth, maxheight): 119 | if files == None: 120 | return gr.Image.update(), gr.Image.update(), gr.Image.update(), gr.Slider.update(), gr.Slider.update(), gr.Number.update(), gr.Number.update() 121 | else: 122 | imageslist = [] 123 | for file in files: 124 | try: 125 | imageslist.append(Image.open(file.name)) 126 | except: pass 127 | if len(imageslist) < 2: 128 | print("keyframer: Not enough valid images found in input (need at least two)") 129 | return gr.Image.update(), gr.Image.update(), gr.Image.update(), gr.Slider.update(), gr.Slider.update(), gr.Number.update(), gr.Number.update() 130 | else: 131 | imageslist = normalize_size(imageslist) 132 | imageslist = padlist(imageslist, (rows*cols)) 133 | grid = MakeGrid(imageslist, rows, cols) 134 | grid = constrain_image(grid, maxwidth, maxheight) 135 | return grid, grid, grid, grid.width, grid.height, grid.width, grid.height 136 | 137 | gen_button.click(fn=submit_images, inputs=[input_upload, gen_rows, gen_cols, gen_maxwidth, gen_maxheight], outputs=[gen_image, self.img2img_component, self.img2img_inpaint_component, self.img2img_w_slider, self.img2img_h_slider, info_width, info_height]) 138 | 139 | return [gen_rows, gen_cols, info_width, info_height] 140 | 141 | #Grab the img2img image components for update later 142 | #Maybe there's a better way to do this? 143 | def after_component(self, component, **kwargs): 144 | if component.elem_id == "img2img_image": 145 | self.img2img_component = component 146 | return self.img2img_component 147 | if component.elem_id == "img2maskimg": 148 | self.img2img_inpaint_component = component 149 | return self.img2img_inpaint_component 150 | if component.elem_id == "img2img_width": 151 | self.img2img_w_slider = component 152 | return self.img2img_w_slider 153 | if component.elem_id == "img2img_height": 154 | self.img2img_h_slider = component 155 | return self.img2img_h_slider 156 | 157 | #Main run 158 | def run(self, p, gen_rows, gen_cols, info_width, info_height): 159 | 160 | p.do_not_save_grid = True 161 | p.width = int(info_width) 162 | p.height = int(info_height) 163 | p.control_net_lowvram = True 164 | p.control_net_resize_mode = "Just Resize" 165 | if p.init_images[0].height > p.init_images[0].width: 166 | p.control_net_pres = p.init_images[0].height 167 | else: 168 | p.control_net_pres = p.init_images[0].width 169 | proc = process_images(p) #process 170 | image_frames = [] 171 | for grid in proc.images: 172 | image_frames.extend(BreakGrid(grid, gen_rows, gen_cols)) 173 | return_images = [] 174 | for frame in image_frames: 175 | out_filename = (modules.images.save_image(frame, p.outpath_samples, "keyframe", extension = 'png')[0]) 176 | return_images.append(out_filename) 177 | 178 | return Processed(p, return_images, p.seed, "", all_prompts=proc.all_prompts, infotexts=proc.infotexts) --------------------------------------------------------------------------------