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,Wx.shape# 对每个通道独立做FFTx_ffttorch.fft.rfft2(x,normortho)# 实数FFT节省一半计算# 构造高频掩码保留中心区域外的频率# 别这样写直接硬编码阈值不同图像的最佳阈值差异很大h_center,w_centerH//2,W//2# 我习惯用高斯高通滤波器过渡更平滑y,x_coordtorch.meshgrid(torch.linspace(-1,1,H,devicex.device),torch.linspace(-1,1,W,devicex.device),indexingij)radiustorch.sqrt(x_coord**2y_coord**2)# sigma控制保留多少高频这个参数需要根据数据集调sigma0.3# 经验值遥感图像可以设小一点high_mask1-torch.exp(-radius**2/(2*sigma**2))# 注意rfft2的输出形状是[H, W//21]需要截取high_maskhigh_mask[:,:x_fft.shape[-1]]# 应用掩码并逆变换x_fft_highx_fft*high_mask.unsqueeze(0).unsqueeze(0)x_hightorch.fft.irfft2(x_fft_high,s(H,W),normortho)returnx_high这段代码看起来简单但实际调参时我吃了不少苦头。sigma设太大高频成分保留过多逆变换后全是噪声设太小又回到原点。后来我改成可学习的sigma让网络自己决定保留多少高频。DCT与傅里叶的取舍除了FFT离散余弦变换DCT在图像压缩领域用得更多。JPEG就是基于DCT的。DCT和FFT的区别在于DCT只使用余弦函数边界条件不同能量集中性更好。对于超分任务DCT有个天然优势——低频分量集中在左上角高频在右下角分块处理非常直观。我尝试过把DCT嵌入到残差块里defdct_2d(x,normortho):# 实现二维DCTPyTorch没有原生实现需要自己写# 这里踩过坑直接用矩阵乘法实现DCT显存爆炸# 推荐用可分离的1D DCT实现B,C,H,Wx.shape# 构造DCT基矩阵NH ntorch.arange(N,devicex.device)kn.unsqueeze(1)# DCT-II公式dct_mattorch.cos(np.pi/N*(n0.5)*k)dct_mat[:,0]dct_mat[:,0]/np.sqrt(2)# 归一化第一行dct_matdct_mat*np.sqrt(2.0/N)# 可分离实现先对行做DCT再对列做DCTx_dcttorch.matmul(dct_mat,x.permute(0,1,3,2))x_dcttorch.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_branchnn.Sequential(nn.Conv2d(in_ch,out_ch,3,padding2,dilation2),nn.ReLU(inplaceTrue),nn.Conv2d(out_ch,out_ch,3,padding1))# 频域分支先FFT在频域做卷积再逆变换# 别这样写直接在频域做卷积相位信息会被破坏self.freq_convnn.Conv2d(in_ch*2,out_ch*2,1)# 处理实部和虚部self.gatenn.Sequential(nn.Conv2d(out_ch*2,2,3,padding1),nn.Sigmoid())defforward(self,x):# 空间域spatial_featself.spatial_branch(x)# 频域B,C,H,Wx.shape x_ffttorch.fft.rfft2(x,normortho)# 把实部和虚部分开作为两个通道fft_realx_fft.real fft_imagx_fft.imag fft_cattorch.cat([fft_real,fft_imag],dim1)# 频域卷积fft_fusedself.freq_conv(fft_cat)# 这里踩过坑直接逆变换会导致梯度不稳定加个门控机制gateself.gate(fft_fused)fft_fusedfft_fused*gate# 恢复形状fft_real,fft_imagfft_fused.chunk(2,dim1)x_fft_outtorch.complex(fft_real,fft_imag)freq_feattorch.fft.irfft2(x_fft_out,s(H,W),normortho)# 融合用可学习的权重alphatorch.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。