067、频域超分:基于傅里叶变换与 DCT 的高频细节恢复方法
去年做某个遥感图像超分项目时,客户反馈说我们的模型在建筑边缘总是糊成一团,纹理细节像被磨皮了一样。我盯着PSNR指标看了半天——明明数值不低,怎么视觉上这么拉胯?后来在调试过程中偶然把特征图做了个FFT可视化,才发现问题出在哪儿:模型压根没学到高频成分,全在低频区域打转。这让我意识到,空间域的卷积操作天然偏向低频,想恢复高频细节,得换个战场——频域。
频域视角下的超分问题
超分辨率重建本质上是个逆问题:已知低分辨率图像y,要恢复高分辨率图像x。在空间域,这通常被建模为:
y = (x ⊗ k) ↓ + n
其中k是模糊核,↓是下采样,n是噪声。但换个角度,在频域里看这个问题会更清晰。对图像做二维离散傅里叶变换(DFT),低频分量对应平滑区域,高频分量对应边缘和纹理。下采样操作在频域里会引发混叠——高频信息折叠到低频区域,这就是为什么直接插值出来的图像总是模糊。
我常用的做法是把频域变换当作一个可微的层嵌入到网络里。PyTorch里实现FFT很简单,但有几个坑要注意:
importtorchimporttorch.nn.functionalasFdeffft_high_freq_extract(x):# x: [B, C, H, W],建议先做padding避免边界伪影# 这里踩过坑:不padding的话,FFT后的频谱会受图像边界不连续影响B,C,H,W=x.shape# 对每个通道独立做FFTx_fft=torch.fft.rfft2(x,norm='ortho')# 实数FFT,节省一半计算# 构造高频掩码:保留中心区域外的频率# 别这样写:直接硬编码阈值,不同图像的最佳阈值差异很大h_center,w_center=H//2,W//2# 我习惯用高斯高通滤波器,过渡更平滑y,x_coord=torch.meshgrid(torch.linspace(-1,1,H,device=x.device),torch.linspace(-1,1,W,device=x.device),indexing='ij')radius=torch.sqrt(x_coord**2+y_coord**2)# sigma控制保留多少高频,这个参数需要根据数据集调sigma=0.3# 经验值,遥感图像可以设小一点high_mask=1-torch.exp(-radius**2/(2*sigma**2))# 注意rfft2的输出形状是[H, W//2+1],需要截取high_mask=high_mask[:,:x_fft.shape[-1]]# 应用掩码并逆变换x_fft_high=x_fft*high_mask.unsqueeze(0).unsqueeze(0)x_high=torch.fft.irfft2(x_fft_high,s=(H,W),norm='ortho')returnx_high这段代码看起来简单,但实际调参时我吃了不少苦头。sigma设太大,高频成分保留过多,逆变换后全是噪声;设太小,又回到原点。后来我改成可学习的sigma,让网络自己决定保留多少高频。
DCT与傅里叶的取舍
除了FFT,离散余弦变换(DCT)在图像压缩领域用得更多。JPEG就是基于DCT的。DCT和FFT的区别在于:DCT只使用余弦函数,边界条件不同,能量集中性更好。对于超分任务,DCT有个天然优势——低频分量集中在左上角,高频在右下角,分块处理非常直观。
我尝试过把DCT嵌入到残差块里:
defdct_2d(x,norm='ortho'):# 实现二维DCT,PyTorch没有原生实现,需要自己写# 这里踩过坑:直接用矩阵乘法实现DCT,显存爆炸# 推荐用可分离的1D DCT实现B,C,H,W=x.shape# 构造DCT基矩阵N=H n=torch.arange(N,device=x.device)k=n.unsqueeze(1)# DCT-II公式dct_mat=torch.cos(np.pi/N*(n+0.5)*k)dct_mat[:,0]=dct_mat[:,0]/np.sqrt(2)# 归一化第一行dct_mat=dct_mat*np.sqrt(2.0/N)# 可分离实现:先对行做DCT,再对列做DCTx_dct=torch.matmul(dct_mat,x.permute(0,1,3,2))x_dct=torch.matmul(dct_mat,x_dct.permute(0,1,3,2))returnx_dct.permute(0,1,3,2)但说实话,DCT在超分里的效果并没有比FFT好太多。我个人的经验是:如果图像纹理有方向性(比如遥感图像里的道路、建筑),DCT的块效应反而会引入伪影。FFT的全局特性更适合处理这类问题。
频域特征融合的实战技巧
真正让频域方法起作用的,不是简单地把图像变换到频域再逆变换回来,而是把频域特征和空间域特征融合。我设计过一个双分支结构:
classFreqSpatialFusion(nn.Module):def__init__(self,in_ch,out_ch):super().__init__()# 空间域分支:用空洞卷积扩大感受野self.spatial_branch=nn.Sequential(nn.Conv2d(in_ch,out_ch,3,padding=2,dilation=2),nn.ReLU(inplace=True),nn.Conv2d(out_ch,out_ch,3,padding=1))# 频域分支:先FFT,在频域做卷积,再逆变换# 别这样写:直接在频域做卷积,相位信息会被破坏self.freq_conv=nn.Conv2d(in_ch*2,out_ch*2,1)# 处理实部和虚部self.gate=nn.Sequential(nn.Conv2d(out_ch*2,2,3,padding=1),nn.Sigmoid())defforward(self,x):# 空间域spatial_feat=self.spatial_branch(x)# 频域B,C,H,W=x.shape x_fft=torch.fft.rfft2(x,norm='ortho')# 把实部和虚部分开作为两个通道fft_real=x_fft.real fft_imag=x_fft.imag fft_cat=torch.cat([fft_real,fft_imag],dim=1)# 频域卷积fft_fused=self.freq_conv(fft_cat)# 这里踩过坑:直接逆变换会导致梯度不稳定,加个门控机制gate=self.gate(fft_fused)fft_fused=fft_fused*gate# 恢复形状fft_real,fft_imag=fft_fused.chunk(2,dim=1)x_fft_out=torch.complex(fft_real,fft_imag)freq_feat=torch.fft.irfft2(x_fft_out,s=(H,W),norm='ortho')# 融合:用可学习的权重alpha=torch.sigmoid(self.alpha)ifhasattr(self,'alpha')else0.5returnspatial_feat*(1-alpha)+freq_feat*alpha这个结构的关键在于门控机制。我试过不加门控,直接做频域卷积,训练时loss震荡得厉害,收敛后PSNR反而比纯空间域还低。门控相当于给频域特征加了个自适应滤波器,让网络学会哪些频率成分值得保留。
实际部署时的性能优化
频域方法最大的痛点是计算量。FFT的复杂度是O(N log N),对于大尺寸图像还好,但超分任务通常要处理patch,每个patch做FFT的开销不小。我踩过的坑包括:
- 不要对每个通道独立做FFT:显存占用会线性增长。我改成把通道维度合并到batch维度,一次做完所有通道的FFT。
- 利用rfft2代替fft2:实数图像的频谱是共轭对称的,rfft2只计算一半,速度翻倍。
- 在推理时缓存频谱:如果输入图像尺寸固定,可以预计算DCT基矩阵或FFT的plan,避免重复分配内存。
有一次在嵌入式设备上部署,发现FFT的耗时占整个推理的40%。后来我把频域分支替换成可分离的DCT近似,用几个1x1卷积模拟频域变换,虽然精度掉了0.1dB,但速度提升了3倍。这让我意识到,频域方法更适合作为辅助分支,而不是主干网络。
个人经验总结
做了两年频域超分,最大的感悟是:频域不是银弹,但它是空间域最好的互补。空间域擅长捕捉局部纹理,频域擅长建模全局结构。两者结合的关键在于如何设计融合策略——简单的相加或拼接效果有限,需要让网络学会自适应地选择频率成分。
对于刚入门的同学,我建议从FFT开始,先可视化你的模型在频域的表现。很多看似复杂的超分模型,频谱分析后会发现它们只学到了低频。这时候加一个简单的频域损失(比如频谱的L1 loss)往往比加一个复杂的频域模块更有效。
最后说个玄学经验:频域方法对图像尺寸很敏感。如果训练时用128x128的patch,测试时用256x256的图像,性能会下降明显。我现在的做法是训练时随机裁剪不同尺寸的patch,让网络适应不同尺度的频率分布。这个trick让我的模型在多个数据集上提升了0.3-0.5dB。