Instructions to use ndtran0101/pisa-sr-diffusers with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use ndtran0101/pisa-sr-diffusers with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline from diffusers.utils import load_image # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("ndtran0101/pisa-sr-diffusers", dtype=torch.bfloat16, device_map="cuda") prompt = "Turn this cat into a dog" input_image = load_image("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/cat.png") image = pipe(image=input_image, prompt=prompt).images[0] - Notebooks
- Google Colab
- Kaggle
PiSA-SR repacked to diffusers format: fused UNets, adapter-only LoRA, example inference
5761977 verified | license: other | |
| license_name: creativeml-openrail-m-plus-plus | |
| license_link: https://huggingface.co/stabilityai/stable-diffusion-2-1-base/blob/main/LICENSE | |
| base_model: stabilityai/stable-diffusion-2-1-base | |
| tags: | |
| - super-resolution | |
| - image-to-image | |
| - diffusers | |
| - one-step-diffusion | |
| - real-isr | |
| library_name: diffusers | |
| pipeline_tag: image-to-image | |
| # PiSA-SR — diffusers-native repack | |
| A format conversion of the official [PiSA-SR](https://github.com/csslc/PiSA-SR) (CVPR 2025) | |
| weights into standard 🤗 diffusers format, so inference needs no vendored model code and | |
| no PEFT/LoRA plumbing. **No new training** — all credit belongs to the original authors. | |
| Upstream ships a raw PEFT state dict with six sub-adapters whose keys embed adapter names, | |
| so `load_lora_weights()` doesn't work on it. Here the deltas are pre-fused into two | |
| ready-to-load UNets. | |
| ## Contents | |
| | Path | What | Size | | |
| |---|---|---| | |
| | `unet_full/` | pix + sem fused — default 1-step mode | 1.7 GB | | |
| | `unet_pix/` | pix only — second model for adjustable mode | 1.7 GB | | |
| | `lora/pisa_{pix,sem}.safetensors` | adapter-only deltas (+ config json) | 16 MB ea | | |
| | `example_inference.py` | runnable default / adjustable inference | — | | |
| VAE, text encoder and tokenizer are **unmodified SD 2.1-base** — load them from the base | |
| model. The upstream checkpoint contains no VAE or text-encoder weights. | |
| ## Usage | |
| Not a standard diffusion pipeline: no scheduler, no noise, no sampling loop. The LQ image | |
| is upsampled first, encoded, and the UNet runs a single pass at `t=1` on an empty prompt. | |
| Its output is a **residual subtracted** from the input latent. | |
| `example_inference.py` in this repo is a runnable version of everything below. | |
| ```python | |
| import torch, PIL.Image as Image | |
| import torchvision.transforms.functional as TF | |
| from torchvision import transforms | |
| from diffusers import AutoencoderKL, UNet2DConditionModel | |
| from transformers import AutoTokenizer, CLIPTextModel | |
| BASE, REPO = "stabilityai/stable-diffusion-2-1-base", "ndtran0101/pisa-sr-diffusers" | |
| dev, dt = "cuda", torch.float16 | |
| tok = AutoTokenizer.from_pretrained(BASE, subfolder="tokenizer") | |
| te = CLIPTextModel.from_pretrained(BASE, subfolder="text_encoder").to(dev, dt).eval() | |
| vae = AutoencoderKL.from_pretrained(BASE, subfolder="vae").to(dev, dt).eval() | |
| unet = UNet2DConditionModel.from_pretrained(REPO, subfolder="unet_full").to(dev, dt).eval() | |
| img = Image.open("lq.png").convert("RGB") | |
| img = img.resize((img.width * 4, img.height * 4)) | |
| img = img.resize((img.width - img.width % 8, img.height - img.height % 8), Image.LANCZOS) | |
| with torch.no_grad(): | |
| x = TF.to_tensor(img).unsqueeze(0).to(dev, dt) * 2 - 1 | |
| ids = tok("", max_length=tok.model_max_length, padding="max_length", | |
| truncation=True, return_tensors="pt").input_ids.to(dev) | |
| emb = te(ids)[0].to(dt) | |
| t = torch.tensor([1], device=dev).long() | |
| z = vae.encode(x).latent_dist.sample() * vae.config.scaling_factor | |
| pred = unet(z, t, encoder_hidden_states=emb).sample | |
| out = vae.decode((z - pred) / vae.config.scaling_factor).sample.clamp(-1, 1) | |
| transforms.ToPILImage()((out * 0.5 + 0.5).clamp(0, 1)[0].float().cpu()).save("sr.png") | |
| ``` | |
| ### Adjustable mode | |
| Load `unet_pix` as well and combine the two predictions. Higher `lambda_pix` removes | |
| noise and compression artifacts (too high → over-smoothed); higher `lambda_sem` adds | |
| semantic detail (too high → artifacts). Both are 1.0 in the default mode above. | |
| ```python | |
| unet_pix = UNet2DConditionModel.from_pretrained(REPO, subfolder="unet_pix").to(dev, dt).eval() | |
| with torch.no_grad(): | |
| pred_sem = unet(z, t, encoder_hidden_states=emb).sample | |
| pred_pix = unet_pix(z, t, encoder_hidden_states=emb).sample | |
| pred = lambda_pix * pred_pix + lambda_sem * (pred_sem - pred_pix) | |
| out = vae.decode((z - pred) / vae.config.scaling_factor).sample.clamp(-1, 1) | |
| ``` | |
| ### Colour fix | |
| The upstream pipeline applies an AdaIN colour transfer from the upsampled input to the | |
| decoded output. It sits outside the network and skipping it shifts colour noticeably. | |
| ```python | |
| def adain(target, source): | |
| t = TF.to_tensor(target).unsqueeze(0) | |
| s = TF.to_tensor(source).unsqueeze(0) | |
| t_mean, t_std = t.mean([2, 3], keepdim=True), t.std([2, 3], keepdim=True) | |
| s_mean, s_std = s.mean([2, 3], keepdim=True), s.std([2, 3], keepdim=True) | |
| return transforms.ToPILImage()( | |
| (((t - t_mean) / (t_std + 1e-5)) * s_std + s_mean).clamp(0, 1)[0]) | |
| ``` | |
| ### Also easy to get wrong | |
| - **Pre-upsampling** — ×4 happens *before* the UNet, so compute scales with **output** | |
| pixels, not input. A 128² input at ×4 costs the same as a 512² input at ×1. | |
| - **Large outputs** — no tiling is shipped here. Use `vae.enable_tiling()` and tile the | |
| image yourself above ~768². | |
| - **Seeds** — inference is deterministic apart from `latent_dist.sample()`; use `.mode()` | |
| for bit-reproducible output. | |
| ## Verification | |
| Checked against the official implementation (RealSR crops, RTX 4090, fp16): | |
| | | PSNR | mean abs err (0–255) | | |
| |---|---|---| | |
| | Reference vs itself (noise floor) | 63.1 dB | 0.03 | | |
| | This repack vs reference | **56.7 dB** | 0.13 | | |
| Visually and metrically indistinguishable. Upstream metrics reproduce to within 0.39% | |
| relative across 27 values (StableSR protocol, ×4, 1 step): | |
| | Dataset | PSNR(Y) | SSIM(Y) | LPIPS | DISTS | FID | MUSIQ | CLIPIQA | | |
| |---|---|---|---|---|---|---|---| | |
| | RealSR (100) | 25.50 | 0.7418 | 0.2672 | 0.2044 | 124.13 | 70.15 | 0.6697 | | |
| | DRealSR (93) | 28.32 | 0.7804 | 0.2960 | 0.2169 | 130.45 | 66.11 | 0.6971 | | |
| | DIV2K-Val (3000) | 23.87 | 0.6058 | 0.2823 | 0.1934 | 25.09 | 69.68 | 0.6928 | | |
| PSNR/SSIM are **Y-channel (YCbCr)**; MANIQA (not shown) needs the **PIPAL** weights — | |
| RGB PSNR or KonIQ MANIQA will not reproduce the paper. | |
| Speed, single RTX 4090 fp16, 1 step: 512² 0.07 s / 5.0 GB · 1024² 0.47 s / 7.4 GB · | |
| 2048² 17.9 s / 15.4 GB. Larger outputs work with a reduced VAE-decoder tile (8192² in | |
| 8.3 GB) — the limit is time, not VRAM. | |
| ## License | |
| Two licenses apply: | |
| - `unet_pix/`, `unet_full/` are derivatives of Stable Diffusion 2.1-base → | |
| [CreativeML Open RAIL++-M](https://huggingface.co/stabilityai/stable-diffusion-2-1-base/blob/main/LICENSE), | |
| including its use-based restrictions, which you must pass on downstream. | |
| - `lora/*.safetensors` contain only PiSA-SR-trained parameters → | |
| [Apache 2.0](https://github.com/csslc/PiSA-SR/blob/main/LICENSE). | |
| ## Citation | |
| ```bibtex | |
| @inproceedings{sun2025pisasr, | |
| title = {Pixel-level and Semantic-level Adjustable Super-resolution: A Dual-LoRA Approach}, | |
| author = {Sun, Lingchen and Wu, Rongyuan and Ma, Zhiyuan and Liu, Shuaizheng and Yi, Qiaosi and Zhang, Lei}, | |
| booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)}, | |
| year = {2025}, | |
| eprint = {2412.03017}, | |
| archivePrefix = {arXiv}, | |
| url = {https://arxiv.org/abs/2412.03017} | |
| } | |
| ``` | |