From e17efc19f76c30d9e02ab1ee83af388e50a49249 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 22 Aug 2026 20:10:00 +0200 Subject: [PATCH] update upscaler workflow Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + data/upscalers.json | 6 +-- extensions-builtin/sd-extension-chainner | 2 +- extensions-builtin/sdnq | 2 +- modules/postprocess/esrgan_model.py | 2 +- modules/postprocess/realesrgan_model.py | 41 ++++++++++---------- modules/postprocess/realesrgan_model_arch.py | 2 +- modules/postprocess/scunet_model.py | 9 +++-- modules/postprocess/swinir_model.py | 4 +- modules/upscaler.py | 27 +++++++------ modules/upscaler_spandrel.py | 8 ++-- modules/upscaler_vae.py | 6 +-- 12 files changed, 57 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e20993b..323d996e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ Plus quite a lot more, see full [changelog](https://github.com/vladmandic/automa - api: auth via remote-ip - krea2: fallback to base pipeline/transformer for nunchaku-lite - torchsde: handle obsolete dependency + - upscaler: avoid unnecessary multi-pass ## Update for 2026-08-07 diff --git a/data/upscalers.json b/data/upscalers.json index 373ff3c75..6c10d2f24 100644 --- a/data/upscalers.json +++ b/data/upscalers.json @@ -20,13 +20,13 @@ "RealESRGAN": [ ["4x General V3", "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-general-x4v3.pth"], ["4x General WDN V3", "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-general-wdn-x4v3.pth"], - ["AnimeVideo V3", "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-animevideov3.pth"], + ["4x AnimeVideo V3", "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-animevideov3.pth"], ["4x+ Anime6B", "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth"], ["4x+", "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth"], ["2x+", "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth"] ], "SCUNet": [ - ["GAN", "https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_gan.pth"], - ["PSNR", "https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_psnr.pth"] + ["GAN 1x", "https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_gan.pth"], + ["PSNR 1x", "https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_psnr.pth"] ] } diff --git a/extensions-builtin/sd-extension-chainner b/extensions-builtin/sd-extension-chainner index 78f29b45f..ef38d4783 160000 --- a/extensions-builtin/sd-extension-chainner +++ b/extensions-builtin/sd-extension-chainner @@ -1 +1 @@ -Subproject commit 78f29b45f248e6029c0924862de8a3f06819c053 +Subproject commit ef38d47839d6670a05eddd9e7f3ccb6c62bce68e diff --git a/extensions-builtin/sdnq b/extensions-builtin/sdnq index c5d505a96..21d60c8c3 160000 --- a/extensions-builtin/sdnq +++ b/extensions-builtin/sdnq @@ -1 +1 @@ -Subproject commit c5d505a96d8b38b411db78abbbe564c0dd8d42d3 +Subproject commit 21d60c8c3fbd66501adfc8b8129f0b254cedfbac diff --git a/modules/postprocess/esrgan_model.py b/modules/postprocess/esrgan_model.py index 9ca7d7c69..92e20f71e 100644 --- a/modules/postprocess/esrgan_model.py +++ b/modules/postprocess/esrgan_model.py @@ -145,7 +145,7 @@ class UpscalerESRGAN(Upscaler): log.debug(f"Upscaler cached: type={self.name} model={info.local_data_path}") return self.models[info.local_data_path] state_dict = torch.load(info.local_data_path, map_location='cpu' if devices.device.type in {'mps', 'cpu'} else None) - log.info(f"Upscaler loaded: type={self.name} model={info.local_data_path}") + log.info(f'Upscaler loaded: type="{self.name}" model="{info.local_data_path}"') if "params_ema" in state_dict: state_dict = state_dict["params_ema"] diff --git a/modules/postprocess/realesrgan_model.py b/modules/postprocess/realesrgan_model.py index 51a51bae9..66ba9b4e4 100644 --- a/modules/postprocess/realesrgan_model.py +++ b/modules/postprocess/realesrgan_model.py @@ -29,7 +29,7 @@ class UpscalerRealESRGAN(Upscaler): scaler.model = lambda: SRVGGNetCompact(num_in_ch=3, num_out_ch=3, num_feat=64, num_conv=32, upscale=4, act_type='prelu') elif scaler.name == 'RealESRGAN 4x General WDN V3': scaler.model = lambda: SRVGGNetCompact(num_in_ch=3, num_out_ch=3, num_feat=64, num_conv=32, upscale=4, act_type='prelu') - elif scaler.name == 'RealESRGAN AnimeVideo V3': + elif scaler.name == 'RealESRGAN 4x AnimeVideo V3': scaler.model=lambda: SRVGGNetCompact(num_in_ch=3, num_out_ch=3, num_feat=64, num_conv=16, upscale=4, act_type='prelu') elif scaler.name == 'RealESRGAN 4x+': scaler.model = lambda: RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4) @@ -47,27 +47,28 @@ class UpscalerRealESRGAN(Upscaler): except Exception: log.error("Error importing Real-ESRGAN:") return img - info = self.find_model(selected_model) - if info is None or not os.path.exists(info.local_data_path): + model = self.find_model(selected_model) + if model is None or not os.path.exists(model.local_data_path): return img - if self.models.get(info.local_data_path, None) is not None: - log.debug(f"Upscaler cached: type={self.name} model={info.local_data_path}") - upsampler=self.models[info.local_data_path] + if self.models.get(model.local_data_path, None) is not None: + log.debug(f"Upscaler cached: type={self.name} model={model.local_data_path}") + upsampler=self.models[model.local_data_path] else: - upsampler = RealESRGANer( - name=info.name, - scale=info.scale, - model_path=info.local_data_path, - model=info.model(), - half=not opts.no_half and not opts.upcast_sampling, - tile=opts.upscaler_tile_size, - tile_pad=opts.upscaler_tile_overlap, - device=device, - ) - self.models[info.local_data_path] = upsampler - upsampled = upsampler.enhance(np.array(img), outscale=info.scale)[0] - if opts.upscaler_unload and info.local_data_path in self.models: - del self.models[info.local_data_path] + kwargs = { + 'name': model.name, + 'scale': model.scale, + 'model_path': model.local_data_path, + 'model': model.model(), + 'half': not opts.no_half and not opts.upcast_sampling, + 'tile': opts.upscaler_tile_size, + 'tile_pad': opts.upscaler_tile_overlap, + 'device': device, + } + upsampler = RealESRGANer(**kwargs) + self.models[model.local_data_path] = upsampler + upsampled = upsampler.enhance(np.array(img), outscale=model.scale)[0] + if opts.upscaler_unload and model.local_data_path in self.models: + del self.models[model.local_data_path] log.debug(f"Upscaler unloaded: type={self.name} model={selected_model}") devices.torch_gc(force=True) diff --git a/modules/postprocess/realesrgan_model_arch.py b/modules/postprocess/realesrgan_model_arch.py index b53d4adc0..89081367e 100644 --- a/modules/postprocess/realesrgan_model_arch.py +++ b/modules/postprocess/realesrgan_model_arch.py @@ -67,7 +67,7 @@ class RealESRGANer: from modules.modelloader import load_file_from_url model_path = load_file_from_url(url=model_path, model_dir=os.path.join(ROOT_DIR, 'weights'), progress=True, file_name=None) loadnet = torch.load(model_path, map_location=torch.device('cpu')) - log.info(f"Upscaler loaded: type={self.name} model={model_path}") + log.info(f'Upscaler loaded: type="{self.name}" model="{model_path}"') # prefer to use params_ema if 'params_ema' in loadnet: diff --git a/modules/postprocess/scunet_model.py b/modules/postprocess/scunet_model.py index 9be3c3be8..bb7e63db4 100644 --- a/modules/postprocess/scunet_model.py +++ b/modules/postprocess/scunet_model.py @@ -27,7 +27,7 @@ class UpscalerSCUNet(Upscaler): model = net(in_nc=3, config=[4, 4, 4, 4, 4, 4, 4], dim=64) model.load_state_dict(torch.load(info.local_data_path), strict=True) model.eval() - log.info(f"Upscaler loaded: type={self.name} model={info.local_data_path}") + log.info(f'Upscaler loaded: type="{self.name}" model="{info.local_data_path}"') for _, v in model.named_parameters(): v.requires_grad = False model = model.to(devices.device) @@ -41,9 +41,11 @@ class UpscalerSCUNet(Upscaler): # test the image tile by tile h, w = img.shape[2:] tile = opts.upscaler_tile_size + tile = 0 tile_overlap = opts.upscaler_tile_overlap if tile == 0: - return model(img) + output = model(img) + return output assert tile % 8 == 0, "tile size should be a multiple of window_size" sf = 1 stride = tile - tile_overlap @@ -73,6 +75,7 @@ class UpscalerSCUNet(Upscaler): model = self.load_model(selected_file) if model is None: return img + sf = 1 tile = opts.upscaler_tile_size h, w = img.height, img.width np_img = np.array(img) @@ -84,7 +87,7 @@ class UpscalerSCUNet(Upscaler): _img[:, :, :h, :w] = torch_img # pad image torch_img = _img torch_output = self.tiled_inference(torch_img, model).squeeze(0) - torch_output = torch_output[:, :h * 1, :w * 1] # remove padding, if any + torch_output = torch_output[:, :h * sf, :w * sf] # remove padding, if any np_output: np.ndarray = torch_output.float().cpu().clamp_(0, 1).numpy() del torch_img, torch_output devices.torch_gc() diff --git a/modules/postprocess/swinir_model.py b/modules/postprocess/swinir_model.py index 6f1f10476..44a87cbc3 100644 --- a/modules/postprocess/swinir_model.py +++ b/modules/postprocess/swinir_model.py @@ -58,12 +58,12 @@ class UpscalerSwinIR(Upscaler): model.load_state_dict(pretrained_model[param], strict=True) else: model.load_state_dict(pretrained_model, strict=True) - log.info(f"Upscaler loaded: type={self.name} model={info.local_data_path} param={param}") + log.info(f'Upscaler loaded: type="{self.name}" model="{info.local_data_path}" param="{param}"') model = compile_upscaler(model) self.models[info.local_data_path] = model return model except Exception as e: - log.error(f'Upscaler invalid parameters: type={self.name} model={info.local_data_path} {e}') + log.error(f'Upscaler invalid parameters: type="{self.name}" model="{info.local_data_path}" error="{e}"') return model def do_upscale(self, img, selected_model): # pylint: disable=arguments-differ diff --git a/modules/upscaler.py b/modules/upscaler.py index 5d1ef81f5..6756e0b51 100644 --- a/modules/upscaler.py +++ b/modules/upscaler.py @@ -38,7 +38,7 @@ class Upscaler: self.device = shared.device self.img = None self.output = None - self.scale = 1 + self.scale = 4 self.half = not shared.cmd_opts.no_half self.pre_pad = 0 self.mod_scale = None @@ -116,11 +116,11 @@ class Upscaler: for _ in range(3): shape = (img.width, img.height) img = self.do_upscale(img, selected_model) - if shape == (img.width, img.height): + if shape == (img.width, img.height): # no change, no point of running another iteration break - if img.width >= (dest_w - 8) and img.height >= (dest_h - 8): + if (abs(img.width - dest_w) <= 12 or img.width >= dest_w) or (abs(img.height - dest_h) <= 12 or img.height >= dest_h): # close enough, do not run one more iteration break - if img.width != dest_w or img.height != dest_h: + if abs(img.width - dest_w) > 8 or abs(img.height - dest_h) > 8: from modules.image import sharpfin img = sharpfin.resize(img, (int(dest_w), int(dest_h))) shared.state.end(jobid) @@ -158,30 +158,29 @@ class UpscalerData: custom: bool = False name = None data_path = None - scale: int = 1 + scale: int = 2 scaler: Upscaler | None = None model: None - def __init__(self, name: str, path: str | None = None, upscaler: Upscaler | None = None, scale: int = 1, model=None): + def __init__(self, name: str, path: str | None = None, upscaler: Upscaler | None = None, scale: int = 0, model=None): self.name = name self.data_path = path self.local_data_path = path self.scaler = upscaler if scale > 0: self.scale = scale - elif '2x' in name.lower(): + elif '1x' in name.lower() or 'x1' in name.lower(): + self.scale = 1 + elif '2x' in name.lower() or 'x2' in name.lower(): self.scale = 2 - elif '3x' in name.lower(): + elif '3x' in name.lower() or 'x3' in name.lower(): self.scale = 3 - elif '4x' in name.lower(): + elif '4x' in name.lower() or 'x4' in name.lower(): self.scale = 4 - elif '4x' in name.lower(): - self.scale = 4 - elif '8x' in name.lower(): + elif '8x' in name.lower() or 'x8' in name.lower(): self.scale = 8 else: - self.scale = 1 - self.scale = scale + self.scale = 2 # default scale to 2 if not specified self.model = model def __str__(self): diff --git a/modules/upscaler_spandrel.py b/modules/upscaler_spandrel.py index 0585f3394..a06221062 100644 --- a/modules/upscaler_spandrel.py +++ b/modules/upscaler_spandrel.py @@ -13,7 +13,7 @@ MODELS = { "Spandrel 2x RealPLKSR AnimeSharpV2": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/2x-AnimeSharpV2_RPLKSR_Sharp.pth", "Spandrel 2x RealESRGAN Compact": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/RealESRGAN-2x-Compact.pth", "Spandrel 2x RealESRGAN UltraCompact": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/RealESRGAN-2x-UltraCompact.pth", - "Spandrel 2x RealSAFMN++": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/Real-SAFMN++.pth", + "Spandrel 4x RealSAFMN++": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/Real-SAFMN++.pth", "Spandrel 2x RealSAFMN": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/Real-SAFMN-x2.pth", "Spandrel 4x RealSAFMN": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/Real-SAFMN-x4-v2.pth", "Spandrel 2x SAFMN PureScale": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/2x_SAFMN_PureScale.pth", @@ -37,9 +37,9 @@ class UpscalerSpandrel(Upscaler): scaler = UpscalerData(name=k, path=v, upscaler=self) self.scalers.append(scaler) else: - for s in self.scalers: # update name of existing scaler if it was found - if os.path.basename(s.data_path) == fn: - s.name = k + for i, _s in enumerate(self.scalers): # update name of existing scaler if it was found + if os.path.basename(self.scalers[i].data_path) == fn: + self.scalers[i].name = k break def process(self, img: Image.Image, output_type='pil', quiet=False): diff --git a/modules/upscaler_vae.py b/modules/upscaler_vae.py index dca3ef9b1..8272db62a 100644 --- a/modules/upscaler_vae.py +++ b/modules/upscaler_vae.py @@ -11,8 +11,8 @@ class UpscalerAsymmetricVAE(Upscaler): self.vae = None self.selected = None self.scalers = [ - UpscalerData("Asymmetric VAE v1", None, self), - UpscalerData("Asymmetric VAE v2", None, self), + UpscalerData("Asymmetric VAE 2x v1", None, self), + UpscalerData("Asymmetric VAE 2x v2", None, self), ] def do_upscale(self, img: Image.Image, selected_model=None): # pylint: disable=arguments-differ @@ -52,7 +52,7 @@ class UpscalerWanUpscale(Upscaler): self.vae_decode = None self.selected = None self.scalers = [ - UpscalerData("WAN Asymmetric Upscale", None, self), + UpscalerData("WAN Asymmetric Upscale 2x", None, self), ] def do_upscale(self, img: Image.Image, selected_model=None): # pylint: disable=arguments-differ