OMGSR: You Only Need One Mid-timestep Guidance for Real-World Image Super-Resolution

SD2.1 20 步推理过程中各时刻解码出的图,低清图与中间时刻最接近
Fig. 1(论文 Fig. 1) — 在 SD2.1-base 上跑 20 步推理,把每一步的 latent 解码成图。可以看到:低清图(右下 LQ Image)和 t=999(纯噪声)对不上、和 t=1(干净图)也对不上,反而和 t≈401~201 的中间带噪图最接近。这就是全文的出发点——低清图 latent 该注入到中间时刻,而不是两端。

📌 先回答一个常见误解:OMGSR(以及建立在它之上的 op4ksr-2026不是师生蒸馏。它没有 teacher、没有 score distillation、没有轨迹一致性。它是一套 GAN:生成器就是扩散模型本身(LoRA 微调),判别器是 DINOv3-ConvNeXt + 多级头,监督来自真值高清图 + 判别器。和 SinSR(一致性蒸馏)、OSEDiff(VSD 分数蒸馏)是两条不同的路。本文末尾 §5 有交叉对比。

1. 出发点 (Motivation)

多步扩散超分质量好但慢(N 步约等于 N 倍耗时)。一步方法快,但要解决一个问题:低清图 latent 该从扩散流的哪个时刻注入?

以往一步方法(OSEDiff 注入 t=999,PiSA-SR 注入 t=1)默认 \(z_{999}=z_L\)\(z_1=z_L\)。但按扩散的加噪公式 \(z_t=\sqrt{\bar\alpha_t}\,z_H+\sqrt{1-\bar\alpha_t}\,\epsilon\)\(z_{999}\approx\epsilon\)(纯噪声)、\(z_1\approx z_H\)(干净图)。而真实世界的低清图 \(z_L\) 既不是纯噪声、也不是干净图——硬塞到两端就制造了一个「潜空间表征鸿沟」(latent representation gap),预训练的生成先验被打乱。

Fig. 1 用肉眼证明了这点。最近的 InvSR 也注意到了,但它凭经验手选一串中间时刻(250/200/150/100/50),既没量化、也没从训练角度进一步缩小鸿沟。OMGSR 的两个贡献正是补这两个洞:

  1. 用 SNR 把最佳中间时刻 \(t^*\) 算出来(不再拍脑袋)。
  2. 用 LRR 损失把 \(z_L\) 进一步训练得更贴近 \(t^*\) 的带噪 latent(从参数层面缩小鸿沟)。

Real-ISR

真实世界图像超分:输入退化未知(非简单 bicubic),用 Real-ESRGAN 合成 LQ-HQ 训练对。

潜空间鸿沟

低清图 latent z_L 与「预训练扩散在某时刻该有的带噪 latent z_t」之间的距离。注入点选错 → 鸿沟大 → 先验失效。

中间时刻 t*

让 z_L 与 z_t 的信噪比最接近的那个扩散时刻。SD2.1-base 上算出来是 273。

SNR(信噪比)

latent 里「信号能量 / 噪声能量」。把 z_t 和 z_L 都分解成信号+噪声,比 SNR 就能定位 t*。

LRR 损失

Latent Representation Refinement:LoRA 微调 VAE 编码器,把 z_L 进一步拉向 z_t* 的带噪 latent。

Dv3CD 损失

把 DISTS 结构感知损失架在 DINOv3-ConvNeXt 上(替代 LPIPS),原生支持 512/1K/4K 高分辨率。

生成器 = 扩散模型

OMGSR 是 GAN:把 LoRA 微调后的 SD 当生成器,而非从噪声生成。LQ→HQ 的映射比 Noise→HQ 好学、训练更稳。

多级判别器

从 DINOv3-ConvNeXt 抽 3 个层级特征,各接一个判别头出 logits,兼顾细节与结构。

2. 方法 (Method)

OMGSR 训练与推理流程:生成器(VAE编码+UNet+VAE解码) + 判别器(DINOv3-ConvNeXt多级头)
Fig. 2(论文 Fig. 2)(a) 训练:低清图 x_L 经(LoRA)VAE 编码器得 z_L → UNet 在 t* 处一步去噪得 z_P → VAE 解码得 x_P。三路监督:L_LRR(把 z_L 拉向下方 DDPM 加噪得到的 z_t*)、L_Dv3CD + L1(x_P 对 x_H)、以及判别器给的 GAN 损失 L_g/L_fake/L_real。火苗=LoRA 可训,雪花=冻结。(b) 推理:x_L → VAE 编码 → UNet 一步(t*)→ VAE 解码 → x_H,整条只过一次。

2.1 用 SNR 算出最佳中间时刻 t*(核心一)

把预训练带噪 latent \(z_t\)(由干净 latent \(z_H\) 和噪声 \(\epsilon\) 线性组合)和低清 latent \(z_L\)(看成 \(z_H\) 与退化噪声 \(z_L-z_H\) 的组合)都算信噪比:

\[\mathrm{SNR}(z_t) = \frac{\bar\alpha_t \cdot \mathbb{E}[z_H^2]}{(1-\bar\alpha_t)\cdot \mathbb{E}[\epsilon^2]} = \frac{\bar\alpha_t \cdot \mathbb{E}[z_H^2]}{1-\bar\alpha_t}, \qquad \mathrm{SNR}(z_L) = \frac{\mathbb{E}[z_H^2]}{\mathbb{E}[(z_L-z_H)^2]}\]

—— 翻译: 左边是扩散在时刻 t 的「信号/噪声」比(t 越大噪声越多、SNR 越小);右边是低清图相对干净图的「信号/退化」比。两者相等的那个 t,就是 z_L 在扩散流上「物理位置」最匹配的时刻。

然后遍历所有 \(t\),取让两者 SNR 差最小的平均最优 \(t^*\)

\[t^* = \arg\min_t \frac{1}{N}\sum_{i=1}^{N}\left| \mathrm{SNR}(z_t^{(i)}) - \mathrm{SNR}(z_L^{(i)}) \right|\]

—— 翻译: 在整个数据集上,挑那个让「扩散时刻 t 的信噪比」与「低清图信噪比」平均最接近的 t,作为统一的注入时刻。SD2.1-base 上算出来是 273。

代码里就是逐 \(t\) 算两个 SNR、取绝对差、在数据集上累加求最小:

repo/mid_timestep/mid_timestep_sd.py:L59-L75 — 逐时刻算 SNR1(z_t) 与 SNR2(z_L) 的绝对差,对应 Eq.3–5

for t in select_timestep:
    sqrt_alphas_cumprod_t = math.sqrt(noise_scheduler.alphas_cumprod[t])
    sqrt_one_minus_alphas_cumprod_t = math.sqrt(1 - noise_scheduler.alphas_cumprod[t])

    signal_power = torch.mean((hq_latent) ** 2) * (sqrt_alphas_cumprod_t**2)
    noise_power  = sqrt_one_minus_alphas_cumprod_t ** 2
    SNR1 = signal_power / noise_power               # SNR(z_t)

    signal_power2 = torch.mean(hq_latent ** 2)
    noise_power2 = torch.mean((lq_latent - hq_latent) ** 2)
    SNR2 = signal_power2 / noise_power2             # SNR(z_L)

    loss = torch.abs(SNR1 - SNR2)                   # |SNR(z_t) - SNR(z_L)|
    loss_accumulators[t] += loss.item() * batch_size

高中生版直觉:你有一张糊掉的照片,想把它放回"扩散电影"的某一帧。扩散电影从清晰(t=1)一路加噪到雪花屏(t=999)。这张糊照片的"糊"程度对应电影里的某一帧——SNR 就是用来量"糊到什么程度"的尺子,量完直接对号入座。

2.2 LRR 损失:把 z_L 进一步训得更贴 t*(核心二)

算出 \(t^*\) 只是「平均」最优。OMGSR 还想做样本级的细化:用 LoRA 微调 VAE 编码器 \(E_\varpi\),让它编出来的 \(z_L\) 主动逼近 \(t^*\) 处的带噪 latent \(z_{t^*}\)

\[\mathcal{L}_{\text{LRR}}(x_L, x_H, t^*) = \mathbb{E}\left[\lVert z_L - z_{t^*}\rVert_2^2\right],\quad z_L=E_\varpi(x_L),\ z_{t^*}=\sqrt{\bar\alpha_{t^*}}E(x_H)+\sqrt{1-\bar\alpha_{t^*}}\epsilon\]

—— 翻译: 让"可训练编码器编出的低清 latent"和"由高清图在 t* 处加噪得到的带噪 latent"尽量相等。用 L2 是因为它和 DDPM 预训练目标一致(KL 在 DDPM 里等价于 L2),梯度更稳。

注意一个对照实验设计:作者还试过把 \(z_L\) 拉向干净的 \(z_H\)(记 \(\mathcal{L}_{\text{TOHQ}}\)),结果 LRR(拉向带噪 \(z_{t^*}\))更好——因为贴合"模型预训练时见过的带噪分布"才能真正激活生成先验。

repo/train/train_omgsr_s.py:L446-L453 — 由 HQ 加噪造 z_t*,再与可训练编码器的 z_L 算 MSE,对应 Eq.6

hq_latent = encode_images(hq_img, fixed_vae)
noise = torch.randn_like(hq_latent)
pretrained_noisy_latent = sqrt_alphas_cumprod_t * hq_latent + sqrt_one_minus_alphas_cumprod_t * noise

lq_latent = encode_images(lq_img, unwrap_model(lora_vae))   # 可训练 LoRA 编码器

# LRR Loss: Latent Representation Refinement Loss
loss_LRR = F.mse_loss(pretrained_noisy_latent, lq_latent, reduction="mean") * args.lambda_LRR

2.3 一步去噪(在 t* 处)

UNet 也用 LoRA 微调。把 \(z_L\) 当作 \(t^*\) 处的带噪 latent,预测噪声 \(\epsilon_\theta\),一步反解出干净 latent \(z_P\),再 VAE 解码出图:

\[z_P = \frac{z_L - \sqrt{1-\bar\alpha_{t^*}}\cdot \epsilon_\theta(z_L, t^*, c)}{\sqrt{\bar\alpha_{t^*}}},\qquad x_P = D(z_P)\]

—— 翻译: 这就是把加噪公式 z_t=√ᾱ·z_H+√(1-ᾱ)·ε 反过来解 z_H——只不过 t 固定在 t*、只算一次,所以叫"一步"。

repo/train/train_omgsr_s.py:L432-L436 — 一步预测:UNet 出噪声 → 反解 latent → VAE 解码,对应 Eq.7

def one_mid_timestep_pred(lq_latent):
    model_pred = unet(lq_latent, mid_timestep, encoder_hidden_states=prompt_embeds).sample
    denoised_latent = (lq_latent - sqrt_one_minus_alphas_cumprod_t * model_pred) / sqrt_alphas_cumprod_t
    pred_img = (fixed_vae.decode(denoised_latent / fixed_vae.config.scaling_factor).sample).clamp(-1, 1)
    return pred_img

2.4 GAN 架构与 Dv3CD 损失

为什么是 GAN 而不是从头训判别器? 生成器(扩散模型)自带强先验、太能打;判别器从零学会被瞬间碾压。所以判别器也建在带先验的 DINOv3-ConvNeXt(冻结)上抽多级特征,只训判别头(Fig. 3)。选 DINOv3 是看中它靠 RoPE 的外推能力,能原生适配 512/1K/4K 等不同分辨率。

DINOv3-ConvNeXt 抽 3 级特征送入多级判别头
Fig. 3(论文 Fig. 3) — (a) 冻结的 DINOv3-ConvNeXt 抽 3 个层级特征 F¹/F²/F³(前三级偏细节,末级偏全局语义;因为 z_P 已含全局语义、缺细节,所以只取前三级)。(b) 每级接一个可训判别头出 logits。BlurPool 是抗锯齿低通滤波,防伪影。

结构感知损失不用 LPIPS(VGG 只在 224×224 上训,高分辨率会出伪影),改用 Dv3CD:在 DINOv3-ConvNeXt 的多级 L2-pooling 特征上算 DISTS(结构+纹理相似度)。

repo/dinov3_gan/dinov3_convnext_dists.py:L86-L108 — Dv3CD:逐层算均值相似度 S1 + 协方差相似度 S2,对应 Eq.11

def forward(self, x, y):
    feats0 = self.l2pooled_dinov3_convnext(x)
    feats1 = self.l2pooled_dinov3_convnext(y)
    dist1 = dist2 = 0
    c1 = c2 = 1e-6
    for k in range(len(self.channels)):
        x_mean = feats0[k].mean([2, 3], keepdim=True); y_mean = feats1[k].mean([2, 3], keepdim=True)
        S1 = (2 * x_mean * y_mean + c1) / (x_mean**2 + y_mean**2 + c1)          # 纹理(均值)相似
        dist1 = dist1 + (self.init_value * S1).sum(1, keepdim=True)
        x_var = ((feats0[k]-x_mean)**2).mean([2,3],keepdim=True); y_var = ((feats1[k]-y_mean)**2).mean([2,3],keepdim=True)
        xy_cov = (feats0[k]*feats1[k]).mean([2,3],keepdim=True) - x_mean*y_mean
        S2 = (2 * xy_cov + c2) / (x_var + y_var + c2)                           # 结构(协方差)相似
        dist2 = dist2 + (self.init_value * S2).sum(1, keepdim=True)
    return (1 - (dist1 + dist2)).mean()

完整损失。生成器 \(\mathcal{L}_G=\lambda_1\mathcal{L}_{\text{LRR}}+\lambda_2\mathcal{L}_g+\lambda_3\mathcal{L}_{\text{Dv3CD}}+\lambda_4\mathcal{L}_1\);判别器 \(\mathcal{L}_D=\lambda_2(\mathcal{L}_{\text{fake}}+\mathcal{L}_{\text{real}})\)(标准 GAN,BCE,real 软标签 0.8、fake 0)。训练循环里 G 和 D 交替更新:

repo/train/train_omgsr_s.py:L464-L483 — 生成器 4 项损失合并更新;判别器对 fake(detach)/real 分别更新

loss_G = net_disc(pred_img, for_G=True) * args.lambda_GAN
total_G_loss = loss_LRR + loss_Dv3D + loss_L1 + loss_G       # 生成器:LRR + Dv3CD + L1 + GAN
accelerator.backward(total_G_loss); optimizer_sr.step()

fake_img = pred_img.detach()
loss_D_fake = net_disc(fake_img, for_real=False) * args.lambda_GAN   # 判别器:假图 → 0
loss_D_real = net_disc(hq_img,   for_real=True)  * args.lambda_GAN   # 判别器:真图 → 1
total_D_loss = loss_D_real + loss_D_fake
accelerator.backward(total_D_loss); optimizer_disc.step()

3. 结论 (Key findings)

数字(论文 Tab. 1–4):

不同注入时刻的归一化综合得分曲线,峰值在 273
Fig. 4(论文 Fig. 4) — 注入时刻消融(用不含 LRR 的 OMGSR-S,避免 LRR 干扰 latent)。横轴时刻 999→1,纵轴四数据集 9 指标归一化综合分。早期(999–673)注入分数极低,中后段平稳走高,**Best Point 落在 t=273**,与 SNR 理论推导一致。

4. 实现细节 (Implementation notes)

  1. 生成器 = 扩散模型本身,无 teachertrain_omgsr_s.py:L432-L467)。监督来自 GT 高清图(L1 + Dv3CD)+ 判别器(GAN),不是某个老师模型的输出。这是它和 SinSR/OSEDiff 蒸馏路线的根本区别。
  2. LoRA 秩:VAE 编码器 rank=16,UNet rank=32(train_omgsr_s.py:L224 及配置)。VAE 解码器冻结,只动编码器 + UNet(one_mid_timestep_pred 里用的是 fixed_vae.decode)。
  3. 训练规模很小:SD2.1-base,AdamW,lr=5e-5,batch=1 + 4 步梯度累积,4×RTX 4090、5100 steps(§4.1.1)。损失权重 λ1=5 (LRR)、λ2=0.5 (GAN)、λ3=5 (Dv3CD)、λ4=0.5 (L1)。
  4. 判别器细节:DINOv3-ConvNeXt 取前 3 级特征(末级偏全局语义、舍去),每级 L2-pooling 用 Hanning 窗做低通(dinov3_convnext_dists.py:L7-L29);判别头里 BlurPool 抗锯齿防伪影(Fig. 3 + §3.6)。real 用软标签 0.8、fake 用 0(§3.7.2 / §3.8.1)。
  5. t* 是数据集统计量、预计算一次mid_timestep_sd.py)。注入时刻不是逐样本变化,而是全数据集平均的单个 t*=273,推理时固定。
  6. 测试大图要 tiled diffusion(§4.1.2):RealLQ250 / RealLR200 这类大/变尺寸图测试时仍需分块——⚠️ 这正是 op4ksr-2026 要干掉的"分块原罪",OMGSR 在 4K 上仍未解决(OP4KSR 用 F16 VAE 全图一步替代)。

paper↔code 一致性:核验过 SNR(Eq.3–5)、LRR(Eq.6)、一步预测(Eq.7)、Dv3CD(Eq.11)、GAN 损失(Eq.12–15)五处,代码与公式完全对齐,无明显出入。

OMGSR-F(Flux 版):结论里提到 OMGSR 框架也兼容 flow matching,作者在补充材料给了基于 FLUX.1-dev 的 OMGSR-F(仓库里有 mid_timestep_flux.py / train_omgsr_f.py)。这就是 op4ksr-2026 的真正起点——OP4KSR = OMGSR-F + F16 极压缩 VAE(上 4K)+ RFR/L_AP(治周期伪影)。

5. 批判性总结 (Critical assessment)

Strengths

Limitations / open questions

When to use / not use

延伸阅读 (Further reading)

交叉验证比较 (Cross-validation against similar work) — 三条"一步扩散超分"路线,看它们对同一问题(怎么把多步压成一步、低清图怎么注入)给出的结论:

工作 核心结论 关键观察 与本文的异 / 同
本文 OMGSR 一步 SR 的关键是把 z_L 注入到 SNR 匹配的中间时刻 t*,再 GAN 微调 z_L 既非噪声也非干净图,硬塞两端 → 潜空间鸿沟;t*=273
op4ksr-2026 把 OMGSR-F 抬到 4K + 无分块,但一步化触发 32px 周期伪影需 RFR/L_AP 极压缩 + 一步 + RoPE 相位坍缩 = 周期格子 同: 都用中间时刻对齐 + GAN/Flux; 异: OP4KSR 解决的是 OMGSR 没碰的"4K 显存 + 分块 + 周期伪影"
oftsr-2024 一步流超分可调"保真↔真实感"整条曲线(调 t 滑动) 噪声增广 + PF-ODE 轨迹约束解锁整条权衡曲线 异: OFTSR 是两阶段蒸馏(先训 teacher rectified flow 再蒸 student),OMGSR 是单阶段 GAN 无 teacher——同问题、完全不同刀法
OSEDiff(基线,无独立页) 一步 SR 用 VSD 分数蒸馏做 KL 正则 注入 t=999、靠冻结 SD 当 score teacher 异: OSEDiff 有 teacher(VSD)、注入点拍在 999;OMGSR 无 teacher、注入点算出来的 273——Tab.1 显示把 OSEDiff 改到 273 也能涨

分歧的可能成因:OMGSR vs OSEDiff/OFTSR 的最大分野在于**「靠什么把一步训出来」——OSEDiff/SinSR/OFTSR 用蒸馏**(teacher 提供 score/轨迹/一致性目标),OMGSR 用 GAN + 中间时刻对齐(判别器 + GT 直接监督)。前者把"多步知识"压进学生,后者干脆绕开多步、直接在最优注入点上一步回归 + 对抗。两条路都能到 SOTA,说明"一步"本身不挑实现范式,挑的是注入点选得准不准——这正是 OMGSR 的核心论点。

相关工作 (Related work)

6. 研究启发 (Transferable takeaways)

1. 用物理量把超参从"经验"升级成"算出来"

InvSR 拍脑袋选中间时刻,OMGSR 用 SNR 给闭式判据。当你在为某个超参做网格搜索时,先问:有没有一个能直接算出它的物理量?把搜索变成测量,结论才可迁移、可复现。

2. 对齐"模型见过的分布",而非"看起来对的目标"

LRR 把 z_L 拉向带噪 z_t* 比拉向干净 z_H 更好——因为前者贴合预训练分布、能激活先验。微调预训练模型时,监督目标要落在它训练时的数据流形上。

3. 判别器/感知损失也要"带先验 + 配分辨率"

从零训判别器会被强生成器碾压 → 架在 DINOv3 上;LPIPS@224 在高分辨率出伪影 → 换原生支持高分辨率的骨干。辅助网络的预训练域必须匹配任务的数据尺度。

4. 反直觉点:一步 SR 不一定要蒸馏

主流一步方案(VSD/一致性/ADD)都绕不开 teacher。OMGSR 证明:只要把低清图注入到正确的中间时刻,一步回归 + GAN 就够了,省掉整个 teacher 流程。"加速=蒸馏"是个该被质疑的默认。

讨论 / Comments

评论托管在本仓库的 GitHub Discussions, 需 GitHub 账号。