pisa-sr-diffusers / README.md
ndtran0101's picture
PiSA-SR repacked to diffusers format: fused UNets, adapter-only LoRA, example inference
5761977 verified
|
Raw
History Blame Contribute Delete
6.95 kB
---
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}
}
```