diff --git a/CHANGELOG.md b/CHANGELOG.md index 212a0402d..8adadb3bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log for SD.Next +## Update for 2025-02-06 + +- **Other**: + - asymmetric tiling + allows for configurable image tiling for x/y axis separately + enable in *scripts -> asymmetric tiling* + *note*: traditional symmetric tiling is achieved by setting circular mode for both x and y + ## Update for 2025-02-05 - refresh dev/master branches diff --git a/scripts/tiling.py b/scripts/tiling.py new file mode 100644 index 000000000..60c234dcf --- /dev/null +++ b/scripts/tiling.py @@ -0,0 +1,104 @@ +from typing import Optional +import torch +import gradio as gr +from PIL import Image +from diffusers.models.lora import LoRACompatibleConv +from torch import Tensor +from torch.nn import functional as F +from torch.nn.modules.utils import _pair +from modules import scripts, processing, shared + + +modex = 'constant' +modey = 'constant' + + +def asymmetricConv2DConvForward(self, input: Tensor, weight: Tensor, bias: Optional[Tensor]): # pylint: disable=redefined-builtin + self.paddingX = (self._reversed_padding_repeated_twice[0], self._reversed_padding_repeated_twice[1], 0, 0) # pylint: disable=protected-access + self.paddingY = (0, 0, self._reversed_padding_repeated_twice[2], self._reversed_padding_repeated_twice[3]) # pylint: disable=protected-access + working = F.pad(input, self.paddingX, mode=modex) + working = F.pad(working, self.paddingY, mode=modex) + return F.conv2d(working, weight, bias, self.stride, _pair(0), self.dilation, self.groups) + + +class Script(scripts.Script): + def __init__(self): + super().__init__() + self.orig_pipe = None + self.conv_layers = [] + self.modes = ['constant', 'circular', 'reflect', 'replicate'] + + def title(self): + return 'Asymmetric Tiling' + + def show(self, is_img2img): + return shared.native + + def ui(self, _is_img2img): # ui elements + with gr.Row(): + gr.HTML('Asymmetric Tiling
') + with gr.Row(): + tilex = gr.Dropdown(label="Mode x-axis", choices=self.modes, value='constant') + numx = gr.Slider(label="Repeat x-axis", value=1, minimum=1, maximum=10, step=1) + with gr.Row(): + tiley = gr.Dropdown(label="Mode y-axis", choices=self.modes, value='constant') + numy = gr.Slider(label="Repeat y-axis", value=1, minimum=1, maximum=10, step=1) + return [tilex, numx, tiley, numy] + + def run(self, p: processing.StableDiffusionProcessing, tilex:bool=False, numx:int=1, tiley:bool=False, numy:int=1): # pylint: disable=arguments-differ, unused-argument + global modex, modey # pylint: disable=global-statement + supported_model_list = ['sd', 'sdxl'] + if shared.sd_model_type not in supported_model_list: + shared.log.warning(f'Tiling: class={shared.sd_model.__class__.__name__} model={shared.sd_model_type} required={supported_model_list}') + return None + if not tilex and not tiley: + return None + self.orig_pipe = shared.sd_model + + modex = tilex + modey = tiley + self.conv_layers.clear() + targets = [shared.sd_model.vae, shared.sd_model.text_encoder, shared.sd_model.unet] + for target in targets: + for module in target.modules(): + if isinstance(module, torch.nn.Conv2d): + self.conv_layers.append(module) + + for cl in self.conv_layers: + if isinstance(cl, LoRACompatibleConv) and cl.lora_layer is None: + cl.lora_layer = lambda *x: 0 + if hasattr(cl, '_conv_forward'): + cl._orig_conv_forward = cl._conv_forward # pylint: disable=protected-access + cl._conv_forward = asymmetricConv2DConvForward.__get__(cl, torch.nn.Conv2d) # pylint: disable=protected-access, no-value-for-parameter + shared.log.info(f'Tiling: x={tilex}:{numx} y={tiley}:{numy}') + + + def after(self, p: processing.StableDiffusionProcessing, processed: processing.Processed, tilex:bool=False, numx:int=1, tiley:bool=False, numy:int=1): # pylint: disable=arguments-differ, unused-argument + if len(self.conv_layers) == 0: + return processed + for cl in self.conv_layers: + if hasattr(cl, '_orig_conv_forward'): + cl._conv_forward = cl._orig_conv_forward # pylint: disable=protected-access + if self.orig_pipe is None: + return processed + if shared.sd_model_type == "sdxl": + shared.sd_model = self.orig_pipe + self.orig_pipe = None + self.conv_layers.clear() + if not hasattr(processed, 'images') or processed.images is None: + return processed + images = [] + for image in processed.images: + if tilex and isinstance(image, Image.Image): + tiled = Image.new('RGB', (image.width * numx, image.height), (0, 0, 0)) + for i in range(numx): + tiled.paste(image, (i * image.width, 0)) + image = tiled + if tiley and isinstance(image, Image.Image): + tiled = Image.new('RGB', (image.width, image.height * numy), (0, 0, 0)) + for i in range(numy): + tiled.paste(image, (0, i * image.height)) + image = tiled + images.append(image) + processed.images = images + return processed