File size: 6,947 Bytes
735d0c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5761977
 
 
735d0c0
5761977
 
 
735d0c0
 
 
 
 
5761977
 
 
 
735d0c0
5761977
 
735d0c0
 
 
5761977
 
 
 
 
735d0c0
 
 
 
 
 
 
 
5761977
735d0c0
 
 
 
 
5761977
735d0c0
 
5761977
735d0c0
 
 
 
 
 
 
 
 
 
5761977
735d0c0
 
5761977
735d0c0
 
5761977
735d0c0
5761977
 
 
735d0c0
 
5761977
735d0c0
5761977
 
 
 
 
735d0c0
 
5761977
735d0c0
5761977
 
735d0c0
5761977
 
 
 
 
 
 
 
 
735d0c0
5761977
 
 
 
 
 
 
 
735d0c0
 
 
5761977
735d0c0
5761977
735d0c0
5761977
 
735d0c0
5761977
 
735d0c0
5761977
 
 
 
 
735d0c0
5761977
 
735d0c0
5761977
 
 
735d0c0
5761977
735d0c0
5761977
735d0c0
5761977
 
 
 
 
735d0c0
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
---
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}
}
```