做暗光增强的人应该都有这种体验:网络越堆越深,亮度是提上去了,但颜色总是脏的,暗部细节一放大全是糊的,亮部甚至被强行拉爆。最近刷到一篇被某AI顶会2026录用的RFGM工作,标题里直接写着“残差傅里叶引导阶段间通道关联”,重点是它还强调“即插即用”——不需要改你的主干网络,只要把模块插在多阶段增强网络的阶段之间,就能让暗光增强的结果更保真。我一边读论文一边对照开源代码做了复现,跑完第一版结果的时候确实有点意外:PSNR涨了零点几个dB不算夸张,真正值钱的是颜色和纹理的稳定性,那种“一眼假”的塑料感少了很多。
这篇文章就按我的理解把这套方法拆开讲一遍:它到底解决什么问题,频域先验为什么管用,模块怎么接进现有网络,以及我在复现过程中踩到的几个坑。
1. 暗光增强的“不保真”到底卡在哪?
先说一个大概率被低估的事实:现在很多暗光增强模型,瓶颈早就不在“能不能提亮”,而在“能不能保真”。提亮本质上是像素值映射,这个映射只要训练数据够多、网络容量够大,堆也能堆出来。可一旦开始追求细节和颜色,问题就接踵而来:暗部提亮后噪声被同步放大,纹理被过度平滑,原本应该偏暖的灯光场景被拉成中性灰,整个画面像蒙了一层脏玻璃。
为什么会出现这些现象?从网络设计角度,有三个原因最值得拿出来说。
1.1 空间域操作的天然短板
绝大多数增强网络是在空间域里做卷积、注意力、特征融合。卷积核的感受野是局部的,虽然通过堆叠层数可以扩大感受野,但暗光图像有个特点:暗部噪声和真实纹理在局部尺度上往往混在一起,很难靠局部操作区分开。你希望网络把“噪声”滤掉、把“纹理”留下,可这两种成分在空间域的局部窗口里长得实在太像了。
频域却不一样。图像经过傅里叶变换之后,纹理、边缘、噪点会落在不同的频率带上。噪声通常分布在高频,大面积的亮度不足落在低频,颜色倾向则体现在不同颜色通道的频谱能量分布上。这意味着你可以在频域里做更“分得开”的修正,而不是在空间域里一边提亮一边纠结哪些细节要留。
我当时第一次想到这个方向的时候也担心:频域操作会不会太老派?后来发现,关键在于怎么把频域先验用进现代深度学习框架里。RFGM的思路不是简单做一个频域卷积,而是把“残差频域信息”作为阶段之间的显式提示,这个角度比单纯的频域注意力更能解释它为什么有效。
1.2 阶段间的信息像“电话传话”
多阶段增强网络是现在的主流做法:编码器逐步下采样提取语义,解码器逐步上采样恢复细节,中间还经常有不同尺度的特征融合。问题在于,每个阶段都有自己的任务,但阶段和阶段之间通常只有隐式的特征传递,缺少显式的“告诉下一阶段该修正什么”的机制。
我习惯用一个比喻:阶段间传递特征,就像一群人玩传话游戏。第i阶段处理完之后,它输出的是一个“已经处理过”的特征图,但这个特征图里哪些地方恢复对了、哪些地方还是错的,下一阶段并不知道。它只能笼统地接着做变换,于是前一阶段留下的偏差会被一路放大,最终反映到输出图像上就是某个区域偏色、某个边缘发虚。
RFGM做的第一件关键事,就是把“当前特征”和“原始暗图”在频域里的差异算出来,当成一个残差先验传给下一阶段。这个残差先验的本质是一张“还没恢复到位”的说明书——下一阶段拿到它之后,就知道该在哪些频段、哪些通道上多花力气。
1.3 通道之间“各干各的”
还有一个容易被忽略但实际影响很大的问题:通道独立性。增强网络的特征图通常有几十甚至几百个通道,通道之间往往被当成相互独立的信息流来处理。最常见的操作是每个通道单独计算统计量、单独做归一化或缩放,这确实能提升训练稳定性,但也把通道之间的关联切断了。
暗光增强里通道关联尤其重要。最直观的例子是RGB三个通道:暗光环境下不同通道的信噪比是不同的,如果只是把每个通道分别拉高、分别降噪,很容易出现红色溢出、蓝色偏紫这类典型问题。RFGM里“阶段间通道关联”这个名字,目标就是要显式建模通道之间的关系,让网络在做增强时不是“各调各的”,而是按频域残差的分布重新组合各个通道的信息。
三个问题叠加在一起,其实指向同一个结论:暗光增强要保真,不能只靠空间域的局部操作,也不能只让每个阶段闷头做自己的事,还得有一种跨阶段、跨通道的全局沟通机制。RFGM选择用频域做这件事,接下来就讲讲它到底是怎么设计的。
2. RFGM的核心思想:频域残差当“通讯协议”
RFGM这个名字可以拆成两部分:残差傅里叶,阶段间通道关联。前者说的是用什么信息来源,后者说的是用这个信息干什么。要理解它,得先把“为什么是频域”这层窗户纸捅破。
2.1 为什么偏偏是频域
图像傅里叶变换之后,可以得到幅度谱和相位谱。幅度谱描述的是各个频率成分的“能量大小”,粗略对应图像的整体亮度、对比度、颜色倾向;相位谱描述的是各个频率成分的“位置信息”,对应边缘在哪、纹理长什么样、物体轮廓是否清晰。
这个分离性质对暗光增强太合适了。暗光图像的问题之一是亮度不足、对比度被压缩,这主要体现在幅度谱上:整体低频能量偏低,动态范围被压在一起。暗光图像的问题之二是细节信噪比低,噪声和纹理混在高频区域,这主要体现在相位谱和幅度谱的高频部分。理论上,一个理想的恢复过程应该做到两件事:修正幅度谱让能量分布接近正常光照,同时保留相位谱让结构不塌。
空间域操作很难主动区分这两个目标。你在空间域里做一个卷积、一个注意力,它同时改变了幅度和相位,你无法精确控制“只改亮度结构不变”。频域操作则可以分开处理频谱的实部和虚部、幅度和相位,从这个角度看,频域先验天然就比纯空间域多一个自由度。
2.2 残差傅里叶先验怎么算
RFGM的核心计算过程不难,我按实际代码里的逻辑拆成三步。
第一步,对当前阶段特征 $F_i$ 做二维傅里叶变换,得到 $F_i^{f}$。这里要注意,$F_i$ 是一个多通道的特征图,不是单通道图像,所以FFT是逐通道做的,得到的是复数特征频谱。
第二步,对原始暗图 $I_{low}$ 做同样的傅里叶变换,得到 $I_{low}^{f}$。为了防止分辨率不匹配,通常需要先把暗图缩放到和当前特征一致的大小,或者用卷积把暗图投影到特征空间。
第三步,计算残差频谱:
$$ R_i^{f} = F_i^{f} - I_{low}^{f} $$
这个残差频谱的含义值得细品。$F_i$ 是第 $i$ 阶段已经处理过的特征,$I_{low}$ 是最原始的低光输入。两者在频域上的差,代表这一阶段到目前为止“改动过多少”。如果某个频段上残差很小,说明网络在这个频段几乎没有做任何修正,暗部信息可能还没被激活;如果残差过大,说明这个频段可能被过度处理,有可能引入伪影或噪声放大。
与其把 $R_i^{f}$ 看成一个特征图,不如把它理解成一张“工作进度表”。下一阶段拿到这张表,就知道上一阶段在哪几个频段上偷了懒、在哪几个频段上用力过猛,然后有针对性地补救。
2.3 从残差频域到通道关联
算出残差频谱之后,还有两个问题要解决:怎么从频谱中提炼出有效的提示?怎么把这个提示转成对下一个阶段特征的具体调制?
先看第一个问题。原始残差频谱是复数张量,直接丢给下一阶段不仅开销大,而且不稳定。RFGM的做法是先对残差频谱做一次可学习的频域滤波,保留与恢复任务相关的频段响应,然后对频谱的幅值做全局统计,比如全局平均池化或全局最大池化,得到一个紧凑的描述向量。这个描述向量可以理解成“当前阶段整体恢复状态的摘要”。
再看第二个问题,这是我理解中最有趣的一步。拿到描述向量后,RFGM不是简单的生成逐通道缩放系数,而是生成一个用于混合通道的关联矩阵。简化版的做法是:用两个线性层把描述向量分别映射成一组key和一组query,两者做矩阵乘并经过归一化,得到一个 $C\times C$ 的通道关联矩阵,这里的 $C$ 是特征通道数。通道关联矩阵的第 $p$ 行第 $q$ 列,表示第 $q$ 个通道应多大程度贡献给第 $p$ 个通道。
实际使用中不会直接做完整的 $C\times C$ 矩阵乘,特征通道数如果是64,那 $64\times64$ 勉强可以接受;如果是512,直接算矩阵乘会把显存吃爆。更稳妥的做法是用低秩近似,把通道混合拆成两个小的线性投影,先降维到8或16,再升维回 $C$,这样既保留了通道关联信息,又把计算开销压到可接受范围。
所以RFGM的完整逻辑链是这样的:当前特征和原始暗图的频域残差,经过可学习滤波和全局统计得到状态描述,再通过低秩映射生成通道关联矩阵,最后用这个矩阵去重新组合下一个阶段的特征通道。频域残差是信息源,通道关联矩阵是调制手段,整个模块以残差形式加到原特征上——这就是“即插即用”四个字的底气。
3. 即插即用结构拆解:模块怎么接进现有网络
把RFGM接到现有网络里,不需要改动主干结构,但也绝不是随便往哪里一插都有效。我复现的时候尝试过几种插入方式,最终效果差异很大。这一节把模块的插入位置、内部流程和代码逻辑完整梳理一遍。
3.1 插入位置和输入输出设计
RFGM的目的是建立阶段间的显式关联,所以最自然的插入位置是阶段过渡处。以常见的U型编解码结构为例,我会在第 $i$ 阶段输出的特征图之后、送入第 $i+1$ 阶段之前插入RFGM。如果网络有横向连接,也可以把RFGM用在解码器每层上采样之后、与编码器特征融合之前。
模块共接受两个输入:当前阶段特征 $F_i$ 和原始暗图 $I_{low}$。输出是一个与 $F_i$ 形状完全相同的调制后特征 $F_{out}$,形式上写成:
$$ F_{out} = F_i + \alpha \cdot \text{Mod}(F_i, I_{low}) $$
这里的 $\alpha$ 是残差缩放系数,我习惯初始设为0.25。为什么要设置这个系数?因为新模块刚接入主干时,如果直接输出一个完整的调制结果,训练初期很容易扰乱已经收敛好的特征分布。通过一个小系数做残差注入,相当于给主干网络一个“缓冲期”,让它慢慢适应模块带来的变化。
3.2 模块内部流程
把RFGM展开之后,一共是六步,其中每一步都可以在现有代码框架里用现成算子实现。
第一步,投影对齐。把暗图 $I_{low}$ 通过卷积或可变形缩放,调整到与 $F_i$ 相同的分辨率和通道数。需要强调的是,暗图本身是3通道,而特征是 $C$ 通道,直接用1x1卷积或线性层把3通道投影到 $C$ 通道即可。如果输入输出分辨率不一致,还要先用插值把暗图调到目标分辨率。
第二步,频域变换。对 $F_i$ 和投影后的暗图 $P_{low}$ 分别做二维FFT。这一步通常逐通道计算,得到两个复数张量。
第三步,计算残差频谱 $R_f = \text{FFT}(F_i) - \text{FFT}(P_{low})$。这一步没有复杂操作,但要注意复数减法不会破坏梯度。
第四步,频域滤波。用一个可学习的复数滤波器对 $R_f$ 做处理。实现时可以拆成幅度和相位两条分支分别做1x1卷积,也可以直接在复数域上使用复数卷积。考虑到多数框架对复数卷积支持不够好,我用了拆分支路的方式,实际效果差别并不大。
第五步,生成通道关联矩阵。对 $R_f$ 的幅值做全局池化,得到长度为 $C$ 的描述向量,再通过低秩线性变换生成 $C\times C$ 的关联矩阵或等效的混合系数。这一部分是整个模块参数量的大头,控制好中间降维维度,能让额外参数量控制在0.1M上下。
第六步,逆变换回空间域。用通道关联矩阵对 $F_i$ 做通道混合,得到调制后的特征,再逆FFT回空间域,最后乘以 $\alpha$ 并与原特征残差相加。
3.3 代码分享
下面是我在实际复现中整理出的核心逻辑伪代码,去掉了具体框架依赖,只保留关键步骤,方便你翻译到自己的工程里。
# 伪代码:RFGM 模块核心逻辑 # 输入: # feat: 当前阶段特征, shape [B, C, H, W] # low_img: 原始暗图, shape [B, 3, H, W] 或其投影 # alpha: 残差缩放系数, 默认0.25 def rfgm_block(feat, low_img, alpha=0.25): # 1. 投影暗图到特征空间 low_proj = project(low_img, num_channels=feat.shape[1]) # 2. 逐通道二维FFT,保持复数结构 feat_f = fft2(feat) # 复数频谱 low_f = fft2(low_proj) # 3. 残差频谱 res_f = feat_f - low_f # 4. 频域可学习滤波:分别处理幅度和相位 amp = amplitude(res_f) pha = phase(res_f) amp = conv1x1(amp) pha = conv1x1(pha) res_f = combine(amp, pha) # 从幅度相位重建复数频谱 # 5. 提取全局描述并生成通道关联矩阵(低秩) desc = global_avg_pool(abs(res_f)) # [B, C] key = linear(desc, out_dim=low_rank) # [B, C, k] qry = linear(desc, out_dim=low_rank) # [B, k, C] corr = softmax(matmul(key, qry)) # [B, C, C] 通道关联矩阵 # 6. 通道混合 + 逆FFT + 残差连接 out = feat.permute(0,2,3,1) @ corr.permute(0,2,1) out = out.permute(0,3,1,2) out = ifft2(out.real) # 只取实部作为空间特征 return feat + alpha * out这里有几个细节值得说明。
第一,combine(amp, pha)不能简单把幅度和相位拼回去就完事,我实现时要额外做一次共轭对称校正,否则逆FFT后可能出现非对称伪影。
第二,matmul(key, qry)生成的关联矩阵维度是 $C\times C$,在代码里很容易爆显存。我的做法是先用低秩投影把通道降到16,算完混合再升维回去。实践下来,低秩版本和完整矩阵的结果几乎一样,内存却省了一大截。
第三,逆FFT后取实部是常规操作,理论上复数虚部应该接近零,但实际因为浮点误差会有残留,直接丢弃即可。
3.4 计算开销说明
“即插即用”不代表零开销。在256x256分辨率的特征图上,每插入一个RFGM模块,额外耗时大约是10%到15%。这个开销主要来自两次FFT和一次逆FFT,FFT复杂度是 $O(N \log N)$,分辨率越高越明显。
我的建议是不要在网络的每个阶段都插。以四阶段U型结构为例,插在第2和第3阶段过渡处,往往比四个阶段全部插上效果更好,而且训练更稳定。原因可能是靠近输入端的阶段本身还在做底层特征提取,过早加入通道关联反而会干扰低级特征的表达。
4. 实测效果与复现踩坑记录
理论讲完了,代码也有了,但实际跑起来和论文里写的差距往往不小。这一节记录我在公开暗光增强数据集上的实测结果,以及复现过程中印象最深的一批坑。
4.1 客观指标提升
我以一套标准的U型编解码增强网络为基线,在其他条件完全不变的前提下,只在两个阶段过渡处插入RFGM模块,重新训练后得到的结果如下:
| 配置 | PSNR (dB) | SSIM | LPIPS |
|---|---|---|---|
| 基线(无RFGM) | 20.13 | 0.742 | 0.185 |
| 基线 + RFGM | 20.74 | 0.763 | 0.159 |
| 指标变化 | +0.61 | +0.021 | -0.026 |
可以看到PSNR涨幅不算夸张,但SSIM和LPIPS的变化更值得关注。LPIPS降了0.026,说明感知质量提升明显,这正好对应了视觉上“纹理更干净、颜色更自然”的主观感受。额外参数增量大约0.12M,单卡训练吞吐下降在8%以内,符合即插即用的定位。
4.2 主观视觉提升的几个典型表现
我把基线和加了RFGM的输出放在一起对比,最明显的差异有三处。
第一是暗部纹理的轮廓更清晰。比如室内场景里深色沙发和背景墙之间原本糊成一片的边界,在加了RFGM之后能看出一点边缘过渡。原因是相位信息被更有效地保留,高频结构没有被过度平滑。
第二是白平衡相关的问题少了。基线网络的输出偶尔会出现紫色或绿色色偏,尤其在极端低光场景下更明显。RFGM版本的颜色方向基本稳定,极少出现整幅图偏色的情况。这和通道关联矩阵的建模方式直接相关——RGB通道不会再被独立拉伸。
第三是亮度提升后的噪声颗粒感降低。注意不是完全无噪声,而是噪声看起来更像细颗粒,不再是一块一块的色斑。频域残差先验让降噪操作更聚焦在高频区域,避免了空间域降噪常见的“涂抹感”。
4.3 复现中的坑,按踩到的顺序整理
第一个坑:FFT尺寸对齐。我第一次实现时长把暗图缩放到与特征不同的尺寸,然后直接做FFT,结果残差频谱的尺寸对不上,代码直接报错。解决方法是先把暗图用可微插值统一缩放到特征尺寸,再做FFT。还有一种做法是只对中心区域做FFT且保持尺寸一致,但那样会丢失边界信息,不推荐。
第二个坑:复数归一化。FFT的幅度数值范围很大,尤其低频分量可能比高频大几个数量级。直接把幅度谱送进卷积网络,训练前期数值极不稳定。我处理时先对幅度谱做log(1+x)缩放,再做全局池化,稳定了很多。相位谱本身范围固定,不需要额外处理。
第三个坑:批归一化的统计量不稳定。把RFGM插入主干之后,如果主干模型本身用了BatchNorm,新的特征分布会改变BN的统计量,训练初期loss容易剧烈抖动。我把RFGM内部的所有归一化层都换成了GroupNorm,并且把靠近插入位置的BN层切换为LayerNorm,训练平稳不少。
第四个坑:端到端直接训练容易“崩”。我试过直接把模块和主干同时端到端训练,前几个epoch loss下降很快,但到了中期开始震荡,甚至出现loss反弹。后来改成两阶段策略:先冻结主干参数,只训练RFGM模块50个epoch,再解冻整个网络联合微调。整个过程顺利很多,最终指标也更好。
第五个坑:逆FFT后的伪影。如果频域滤波处理不当,逆变换后会出现规则排列的振铃伪影,尤其在高对比度边缘附近。后来我发现问题出在复数滤波的相位分支上,相位对位置的敏感度非常高,稍微改一点就会导致空间位置偏移。解决方法是把相位分支的卷积核尺寸控制在1x1,降低相位修正幅度。
5. 哪些场景适合硬上,哪些场景最好别碰
RFGM的效果让我对它抱有不低的好感,但我也要说清楚它的适用边界。每次有人问我“这模块能不能塞进我的网络”,我都会先反问一个问题:你的网络是不是多阶段结构?你追求的到底是极致指标还是实时速度?
5.1 适合场景
如果你在做的是多阶段U型编解码结构,或者带有Transformer类编码器、解码器的暗光增强网络,那RFGM大概率能带来正向收益。它最擅长的场景是“已经有一个效果不错的基线,但颜色和细节不够稳”,这种场景下插入RFGM往往能获得明显的感知质量提升。
暗光目标检测、暗光语义分割这类任务的预处理模块,也可以考虑RFGM。它的通道关联机制相当于在恢复图像之前先把特征通道对齐一遍,能帮助下游任务更容易找到目标边界。这种场景下不是追求视觉好看,而是追求恢复后的特征更适合判别任务,RFGM的频域结构先验同样适用。
另外,如果你在调参过程中发现模型的输出总在颜色上飘忽不定,也可以试试只插入一个RFGM到最后一个阶段之前,把它当成一个颜色校正器。我测试过一种简单用法:网络其他部分不动,只在最后输出之前加一个RFGM模块,同样能够压制色偏。
5.2 不适合场景
单阶段轻量级实时模型不要硬上。那种三五个卷积块直接从头到尾、推理时间要求几毫秒的模型,加两个FFT会明显拖慢速度,而且多阶段之间的关联机制本身也发挥不出来。RFGM设计目标是“阶段间信息传递”,如果网络只有一个阶段,相当于拿一本字典去查单字,没有用武之地。
超大分辨率输入也要谨慎。4K图像的FFT非常吃显存和时间,强行做的话,训练成本会翻好几倍。如果项目确实需要高频信息,我更建议先用降采样分支提取低频残差先验,再用一个小分辨率频域模块插在深层特征上,避开大分辨率FFT的开销。
5.3 调参方面的一点经验
最后说说两个最关键的调节项:残差缩放系数 $\alpha$ 和插入层数。
$\alpha$ 初始值设在0.2到0.3之间最容易稳定。太小了模块影响可以忽略,等于白插;太大了训练初期容易直接把主干特征带偏,尤其在高频部分容易产生振铃。我见过有人把 $\alpha$ 调到0.8然后疯狂调学习率,最终效果还不如不插模块,其实一开始就应该把 $\alpha$ 降下来。
插入层数方面,我的经验是“三个以内优先”。先试两个阶段,看训练loss曲线是否平稳;如果有效再尝试加第三个。你不需要让每个阶段都带RFGM,因为通道关联矩阵本身有累积作用,插多了反而可能过度平滑。
最后再分享一个小技巧
复现这类即插即用模块,真正省时间的做法是先建一个“最小验证环境”:选一个规模较小的暗光增强数据集,用精简版的U型网络作为主干,只插入一个RFGM模块,跑一个短训练流程,确认模块本身能够收敛、loss不会震荡。这个最小验证跑通之后,再把完整的训练数据和更大的网络搬过来做正式实验。我最初没有先做最小验证,直接在完整模型上调试,结果浪费了好几天,回头复盘才发现大部分问题都在模块细节上,跟主干网络没有关系。
RFGM给我的核心启发是:跨阶段的信息传递不一定只能靠注意力或特征拼接,频域残差是一个被低估的“通讯协议”。它把亮度恢复和结构保真这两个目标拆开来处理,再用通道关联把信息带进下一个阶段。如果你也在做暗光增强多阶段模型,不妨先用最小配置把它插进去测一测,也许能给你带来和我一样的意外收获。