超分辨率技术这几年发展很快,从最早的SRCNN到现在的Real-ESRGAN,放大效果越来越好。但真正上手跑过模型的人都会遇到一个尴尬的问题:图片确实变清晰了,可是画面里多出了不少奇怪的东西——边缘一圈一圈的波纹、人脸皮肤上莫名出现的色块、砖墙上整齐排列的网格状图案。这些就是超分伪影。本文围绕两个核心方向展开:一是用降噪模型在超分前或超分后压制伪影,二是通过纹理恢复手段让被涂抹掉的细节重新自然地长回来。

超分伪影到底是怎么产生的
要解决问题,先得搞清楚伪影从哪来。超分模型本质上是在"猜"高频细节,而这个猜测过程会引入几类典型的错误。第一类是振铃伪影,也叫吉布斯效应。模型在重建锐利边缘时,会在边缘两侧产生明暗交替的波纹。这是因为模型学到的核函数在频域上无法完美截断高频信息,于是产生了过冲和震荡。放大倍率越高,这种效应越明显,四倍放大时几乎必然出现。
第二类是噪声放大。原始低分辨率图像里本来就有传感器噪声、JPEG压缩块效应,模型分不清哪些是真实纹理、哪些是噪声,经常把噪声也当成"细节"一起放大。你会在放大结果里看到颗粒被放大成明显的斑点,或者在暗部区域出现彩色噪点拖尾。
第三类是幻觉细节。基于GAN的生成式模型尤其擅长"编造"细节:人脸牙齿被生成成整齐的假牙缝,砖墙被补全成规则排列的砖块,草地上出现不存在的条纹。这些细节看起来很清晰,但和真实场景对不上,在监控取证、医疗影像这类对真实性敏感的场景里是致命的。
降噪模型压制伪影的两种接入方式
对付噪声放大类的伪影,最直接的思路是把降噪模型和超分模型串起来。接入位置有两种选择:前置降噪和后置降噪,各有适用场景。
前置降噪是指在超分之前先做一次去噪。低分辨率图像里的噪声尺度小,去除起来相对容易,而且能给超分模型一个干净的输入。缺点是降噪过程难免抹掉一部分真实高频纹理,超分模型拿到的输入信息变少了,重建的细节会偏软。适合噪声污染严重的老照片或低照度监控画面。
后置降噪则是在超分输出之后处理。此时伪影已经被放大,网格状波纹和色斑都比较明显,降噪模型可以针对性地清除,同时保留超分生成的主要结构。缺点是伪影和真实细节混在一起,降噪力度不好拿捏,压重了会回到涂抹感。实际工程中,对视频做超分时常用"前置轻降噪加后置轻平滑"的组合策略,两边力度都不大,整体效果反而最好。
下面给出一段PyTorch代码,演示如何把预训练的降噪模型和超分模型串联起来,并加入一个可调节强度的融合环节,避免过度降噪:
import torch
import cv2
import numpy as np
class DenoiseSRPipeline:
def __init__(self, sr_model, denoise_model, device="cuda"):
self.sr_model = sr_model.eval().to(device)
self.denoise_model = denoise_model.eval().to(device)
self.device = device
@torch.no_grad()
def run(self, img_bgr, denoise_strength=0.5):
# 转换为张量,范围归一化到 0~1
img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
tensor = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0)
tensor = tensor.float().div(255.0).to(self.device)
# 第一步:超分重建
sr_out = self.sr_model(tensor)
# 第二步:降噪处理
dn_out = self.denoise_model(sr_out)
# 第三步:按强度线性融合,避免过度平滑
blend = denoise_strength * dn_out + (1 - denoise_strength) * sr_out
blend = torch.clamp(blend, 0.0, 1.0)
out = blend.squeeze(0).permute(1, 2, 0).cpu().numpy()
out = (out * 255.0).astype(np.uint8)
return cv2.cvtColor(out, cv2.COLOR_RGB2BGR)
# 使用示例:
# pipeline = DenoiseSRPipeline(sr_model, denoise_model)
# result = pipeline.run(cv2.imread("input.jpg"), denoise_strength=0.4)
代码里的融合系数denoise_strength很关键。设成1.0等于完全采用降噪结果,细节损失最大;设成0.3到0.5之间通常能在伪影抑制和细节保留之间取得平衡。如果降噪模型输出的是噪声残差(如DnCNN的结构),融合方式要改成在超分结果上减去残差乘以强度系数,逻辑是一样的。
纹理恢复:让细节自然地长回来
压制伪影之后,新的问题出现了:画面干净了但也变"塑料"了。纹理恢复解决的就是这个补偿问题,目前主流思路有三条。
第一条是损失函数层面的约束。训练超分模型时,如果只用L1或L2损失,模型倾向于输出所有位置的平均值,结果就是细节模糊;如果只用GAN损失,又容易产生幻觉伪影。实践中的有效做法是组合多种损失:L1损失保底,感知损失(基于VGG特征)保证结构相似,GAN损失提供高频真实感,再加一个频域损失显式约束振铃。频域损失把预测图和真值图做FFT后计算幅度谱差异,对抑制边缘波纹特别有效,因为振铃本质上是频域上的能量泄漏。权重大致取L1占1.0、感知占0.1、GAN占0.005、频域占0.01,可作为调参起点。
第二条是参考图引导的纹理迁移,也就是Reference-based SR。如果手头有同一场景的高清参考图(比如同一地点的另一张高清照片),可以让模型从参考图中检索相似纹理块,迁移到超分结果的对应位置。这条路线对老电影修复、游戏材质提升效果突出,因为纹理来源真实,不会凭空编造。SRNTT和TTSR是这类方法的代表工作。
第三条是后处理级的纹理增强,成本最低。思路是先分离图像的结构层和纹理层,对纹理层单独做增强后再合成回去。用导向滤波或双边滤波可以快速完成分层:
import torch
import torch.nn.functional as F
def decompose_and_enhance(sr_img, texture_gain=1.4, radius_ratio=0.02):
"""
sr_img: 超分输出,形状 [1,3,H,W],范围 0~1
先用高斯模糊近似提取结构层,
残差即纹理层,放大后再合成
"""
_, _, h, w = sr_img.shape
k = max(3, int(min(h, w) * radius_ratio) | 1) # 保证奇数
sigma = k / 3.0
# 生成一维高斯核再扩展为二维
x = torch.arange(k, dtype=torch.float32, device=sr_img.device) - k // 2
g1d = torch.exp(-(x ** 2) / (2 * sigma ** 2))
g1d = g1d / g1d.sum()
g2d = torch.outer(g1d, g1d).expand(3, 1, k, k)
structure = F.conv2d(sr_img, g2d, padding=k // 2, groups=3)
texture = sr_img - structure
# 纹理层增益放大,轻微裁剪防止过冲
enhanced_texture = torch.clamp(texture * texture_gain, -0.15, 0.15)
return torch.clamp(structure + enhanced_texture, 0.0, 1.0)
这段代码把超分结果分解为结构层和纹理层,只对纹理层做增益放大。由于结构层完全不变,边缘不会引入新的振铃,安全性比直接做锐化高得多。纹理增益建议控制在1.2到1.6之间,超过2.0就可能把残余噪声也放大回来。
实战调优建议与常见坑
最后总结几条实战经验。首先,处理JPEG压缩图像时,先做一遍去块效应处理(deblocking),再送入超分模型,否则压缩块边界会被模型当作真实边缘放大,形成规则的网格伪影。Real-ESRGAN的训练集里混入了JPEG压缩退化,对这类输入有额外抗性,如果条件允许优先选这类考虑了真实退化的模型。
其次,人脸场景建议使用专门的面向人脸的超分模型,例如GFPGAN或CodeFormer。通用模型在人脸上生成的高频细节错误非常显眼,而人脸专用模型内置了先验约束,能有效避免牙齿、眼睛等关键部位的畸变。
再次,评估效果时不要只看PSNR和SSIM。这两个指标天然偏向平滑输出,一张涂抹严重但干净的图可能得分很高。建议结合LPIPS感知指标和主观目视综合判断,特别是要放大到像素级别检查边缘两侧有没有波纹、平坦区域有没有斑点。
最后是性能层面的取舍。降噪加超分两阶段串联的计算开销接近翻倍,如果目标是视频流实时处理,可以考虑单阶段联合模型,或者对视频做时域一致性约束:相邻帧的超分结果做光流对齐后取时域均值,既能压制闪烁伪影,也能等效降低噪声。超分伪影的治理没有一劳永逸的银弹,核心思路永远是——搞清楚伪影的具体成因,再针对性选择降噪、损失约束或纹理恢复的组合拳。