做图像超分辨率或者生成任务的读者大概率遇到过这样的现象:模型输出的图片整体结构没问题,颜色也还原得不错,但放大看就是一片糊,皮肤像磨过皮,树叶糊成一团绿,布料的纹理几乎完全消失。这种纹理模糊问题的根源,往往不在网络结构不够大,而在损失函数的设计。本文围绕纹理空间对抗训练与细节注入两个方向展开,给出完整的分析思路和可落地的代码实现。

一、纹理为什么会糊:损失函数的高频压制机制
要解决模糊,先得理解模糊是怎么产生的。目前绝大多数重建类模型都以L1或L2损失作为主要监督信号。这两种损失本质上逐像素比较两张图,追求的是像素级的平均值最优。当训练数据中同一个局部区域存在多种可能的纹理走向时(比如草地中草叶的朝向、头发丝的走向),L1损失的最优解就是所有可能纹理的均值,而多个不同纹理求平均,结果就是模糊。这是数学上的必然,不是模型容量问题。
从频域角度看更直观。对大量训练结果做傅里叶变换统计会发现,L1监督下模型在高频段的能量明显低于真实图像,也就是高频细节被系统性抹掉了。而人眼对高频信息极其敏感,我们判断一张图是否真实,很大程度上依赖的就是纹理颗粒、边缘锐度这些高频成分。
另一个不可忽视的因素是数据层面的:如果训练数据本身就是压缩严重、噪声被预先抹除的图像,模型学到的纹理分布天然就偏平滑。因此在动手改损失函数之前,建议先检查训练对的制作流程,确认高分辨率图像没有经过过度的高斯滤波或过强的JPEG压缩。数据没问题之后,再引入下面两个技术手段。
二、纹理空间对抗训练:让判别器盯住纹理而不是整体
标准GAN思路是把整个图像喂给判别器,判断真假。但全局判别器容易被低频的结构信息主导,对纹理是否真实反而不敏感。纹理空间对抗训练的核心改进是:先把图像从像素空间转换到纹理空间,再在纹理空间里做对抗。常用的纹理空间表示有三种:局部图像块(patch)、Gram矩阵、以及经过VGG等特征提取器后的中层特征图。其中VGG特征配合patch判别器是最实用的组合。
具体做法是取真实图和生成图各自经过VGG提取的conv3_4或conv4_4层特征,将特征图切分成若干小块,比如每个块大小为8乘8,然后训练一个轻量的纹理判别器去区分这些块来自真实图还是生成图。由于每个块只覆盖很小的感受野,判别器无法依赖全局结构作弊,只能从局部统计特性(也就是纹理)上找差异,这就迫使生成器学会生成统计上真实的纹理。实验中这种做法对草丛、皮肤毛孔、布料这类重复性纹理的提升尤其明显。
需要注意训练稳定性。纹理判别器的学习率建议设为主生成器的十分之一左右,并且使用谱归一化(Spectral Norm)约束判别器权重,否则很容易出现判别器过强导致训练崩溃的情况。下面给出一个简化但可直接运行的纹理判别器与纹理对抗损失的PyTorch实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
class TextureDiscriminator(nn.Module):
def __init__(self, in_channels=256, base_channels=64):
super().__init__()
# 谱归一化约束,稳定对抗训练
self.net = nn.Sequential(
nn.utils.spectral_norm(nn.Conv2d(in_channels, base_channels, 3, 1, 1)),
nn.LeakyReLU(0.2, inplace=True),
nn.utils.spectral_norm(nn.Conv2d(base_channels, base_channels * 2, 3, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
nn.utils.spectral_norm(nn.Conv2d(base_channels * 2, 1, 3, 1, 1))
)
def forward(self, feat):
# feat: VGG中层特征 [B, C, H, W]
return self.net(feat)
def texture_adversarial_loss(vgg, tex_d, fake_img, real_img):
# 提取VGG中层特征作为纹理空间表示
fake_feat = vgg(fake_img)
real_feat = vgg(real_img).detach()
# 纹理对抗损失(生成器侧,采用LSGAN形式)
g_loss = ((tex_d(fake_feat) + 1) ** 2).mean()
return g_loss这个判别器作用在VGG特征上,特征通道数为256对应conv3_4输出。生成器损失用最小二乘形式(LSGAN)比原始BCE更稳定。实际训练中纹理对抗损失权重建议从0.005起步试探,过大会引入脏噪点和颜色漂移。
三、细节注入:把高频信息显式地补回去
对抗训练是一种隐式手段,逼迫模型自己学出纹理。而细节注入是显式思路:直接从输入或其他来源中提取高频分量,注入到网络中间层。最经典的做法是残差在SFT(Spatial Feature Transform)模块中的条件注入:用一个分支提取输入图像的边缘图或高频残差(例如输入减去高斯模糊结果),将其编码为调制参数,对主干特征做逐通道的缩放和平移。
这种结构的好处是显式可控。当输入本身包含可用的高频线索时(比如视频超分中的相邻帧),细节注入分支能直接把运动细节搬进当前帧特征;当输入完全没有高频信息时,注入分支会自动衰减权重,不会引入噪声。对单图超分,可以把上采样后的初步结果再做一次拉普拉斯金字塔分解,把浅层高频分量经卷积对齐后加回深层特征,同样属于细节注入范畴。
class SFTLayer(nn.Module):
def __init__(self, channels=64, cond_channels=32):
super().__init__()
# 从高频条件图预测调制参数
self.shared = nn.Sequential(
nn.Conv2d(cond_channels, channels, 3, 1, 1),
nn.LeakyReLU(0.2, inplace=True)
)
self.gamma = nn.Conv2d(channels, channels, 3, 1, 1)
self.beta = nn.Conv2d(channels, channels, 3, 1, 1)
def forward(self, x, cond):
# x: 主干特征,cond: 高频条件特征
t = self.shared(cond)
gamma = 1.0 + self.gamma(t) # 中心化在1附近,初始化更稳
beta = self.beta(t)
return gamma * x + beta
def extract_high_freq(img):
# 高频残差提取:原图减高斯模糊
blur = F.gaussian_blur2d(img, kernel_size=(5, 5), sigma=(1.5, 1.5))
return img - blur细节注入模块一般插在网络的中间层而非输出层,因为输出层注入容易造成无法对齐的伪影,中间层的特征空间容忍度更高。调参上有两个经验:一是gamma分支初始化为接近恒等映射,加1.0的偏置让模块训练初期相当于跳过;二是条件分支不要过深,两层卷积足够,过深反而会学到低频结构干扰主干。
四、训练策略与常见坑
把纹理对抗和细节注入组合起来时,总损失通常是像素损失加感知损失加纹理对抗损失三部分。一个经过验证的权重比例是1比0.01比0.005,具体任务需要微调。训练顺序上建议先只跑像素损失加感知损失若干epoch,等重建结果基本稳定后再开启对抗损失,这样判别器面对的是已经有一定质量的图像,训练更平稳,最终纹理也更干净。
评估时不要只看PSNR和SSIM。对抗训练生成的纹理会带来像素级偏差,PSNR往往反而下降零点几个dB,但肉眼观感明显更好,这是正常现象。建议同时报告LPIPS和NIQE这类感知指标,或者做用户主观评分。另外要警惕过强的对抗信号导致模型凭空编造纹理,比如给模糊的人脸加上不存在的痣或皱纹,这时需要降低纹理对抗权重,或者对判别器加更强的正则。
最后是数据层面的收尾工作:推理时对输出做轻微的锐化(比如非锐化掩模,半径0.8、强度80左右)可以在不引入伪影的前提下进一步提升观感,但这只是锦上添花,真正解决纹理模糊还是要靠前面训练阶段的两个核心手段。整体来看,纹理空间对抗训练负责让模型生成统计上真实的纹理分布,细节注入负责把可用的高频线索显式搬进特征流,两者互补,配合得当基本可以根治重建结果的糊感问题。