低光视觉这个方向,做图像增强的人绕不开,做检测分割的终端项目也绕不开。手机拍夜景、监控暗光、航拍黄昏,都会遇到同一个问题:图像整体压暗,细节被淹没在噪声里。过去我处理这类项目,第一反应多半是套用Retinex类方法,或者堆一个空间域卷积网络,结果总有些尴尬——要么色彩偏得厉害,要么暗部提亮之后噪声跟着放大。直到我把FRBNet这个思路拆开试了一遍,把问题放到频域里重新看,才对“低光视觉”这件事有了完全不同的理解。
FRBNet(Frequency-domain Radial Basis Function Network,频域径向基网络)做的事情,本质上不是“造一个新增强滤波器”,而是把低光图像的恢复拆成了两个显式步骤:先在频域里把光照、纹理、噪声分离开,再用径向基函数(RBF)对每个频带逐一做增益映射,最后跨域重建回像素空间。这个方案的妙处在于,它把空间域网络习惯性忽视的两个全局信号——直流分量代表的整体光照、高频分量代表的细节噪声分布——变成了可解释、可操作的特征维度。这篇文章我把这套方案的背景、网络设计、代码实现路径、训练踩坑记录全部摊开讲,适合正在做夜间图像增强、暗光检测前置处理,或者对频域深度学习感兴趣的工程师参考。
1. 项目出发点:为什么频域思路适合低光视觉
1.1 低光图像在频域里的真实面貌
拿一张正常曝光的图和一张低光图同时做二维FFT,把幅度谱平移后放一起看,差异是非常直观的。正常图像的能量从低频到高频是平滑衰减的,低频圆盘集中了照度和大面积结构,中频承载轮廓和织物纹理,高频尾部保存边缘和细节。而低光图像正好有两处异常:低频占比变得异常高,因为画面里大面积的暗部区域贡献了极低的像素值,相当于给图像叠加了一个“接近零电平”的偏置;中高频分量的能量衰减得更快,细节被直接抹掉了。
更麻烦的是噪声。低光条件下的噪声(尤其是传感器热噪声和暗电流噪声)主要分布在中高频段,它们会把高频尾部“抬起来”。于是频域里出现一个令人头疼的组合:细节所在的中高频能量在下压,噪声能量却在上抬。两者混在一起,单纯在空间域里做对比度拉伸或者卷积增强,很容易把噪声一并放大。
这里我用的类比是调音台分轨处理。低光图像的问题不是“音量小了”,而是“各个频率段的动态平衡被破坏了,同时底噪变高”。如果只拧总音量(全局亮度拉伸),该亮的区域过曝,该暗的区域还是死黑;如果在空间域做一个复杂的非线性映射(深度学习网络通常这么干),又很难显式区分底噪和纹理。频域天然把这些问题摊平到不同半径的频带上,为后续的针对性处理提供了极大便利。
1.2 空间域网络为什么总觉得别扭
我做过不少空间域的低光增强实验,普遍有几个别扭的地方。卷积核天生是局部的,一次3×3卷积只能看到很小的邻域,而低光图像的照度不均匀往往是大尺度、全局性的,比如角落里一盏灯、窗外一片月光造成的渐变。要让卷积网络感知这种全局光照,只能靠加深网络、堆叠空洞卷积,或者引入注意力机制,但代价是参数量和计算量上去了,而且在极暗区域容易过平滑。
Transformer和自注意力在空间域确实能建模全局依赖,但复杂度是输入像素数的平方量级。夜间图像分辨率不低,一张1080p图像百万级别像素,全局注意力在工程上并不友好。还有一层物理上的别扭:空间域网络把光照估计和细节恢复混在同一个特征空间里,很难显式告诉网络“这个是光照、你需要调整它;那个是噪声、你需要抑制它”。端到端模型学到的东西基本是个黑盒,调参全靠试错。
而频域操作是天然全局的,一次FFT就把整个图像的频率分布摆到了面前。在这个坐标系里,低频、中频、高频在物理上就有明确分工,正好对应光照、纹理、噪声这样一个低光图像的三层结构。FRBNet选择“频域分解 + 径向基函数”的组合,就是在利用这种天然分工,把黑盒打开,让每一阶段的处理都有据可循。
2. FRBNet方案设计:从频带分解到径向基映射
2.1 整体网络结构与数据流
FRBNet的整体结构可以拆成三段:频域分解编码器、RBF频带映射模块、跨域重建解码器。输入一张低光RGB图像,判断颜色通道的敏感性问题后,我建议把亮度通道和色度通道分开处理,用RGB直接做FFT也可以,但色度通道里的色彩噪声很容易在增强时被放大。FRBNet的处理方式是:亮度通道走完整的频域分支,色度通道走一个轻量空间卷积分支做色彩保持,最后合并重建。
频域分支的数据流是:低光图 → 2D FFT → 频带分解(生成K个带通子带频谱) → 对每个子带提取统计特征 → RBF映射得到增益和偏移 → 子带频谱加权缩放 → 逆FFT → 与色度分支合并 → 空间域微调子网 → 增强结果。下面这张表可以快速理解每个模块的位置和目的。
| 模块 | 输入 | 输出 | 核心作用 |
|---|---|---|---|
| 频域分解头 | 亮度通道像素图 | K个频带频谱系数 | 把光照、纹理、噪声按频率分层 |
| RBF频带映射模块 | 各频带的统计特征 | 每频带增益系数和偏移量 | 根据各频带降质程度自适应调整 |
| 跨域重建层 | 调整后的频带频谱 | 初步增强图 | 逆FFT输出空间域结果 |
| 空间域微调子网 | 初步增强图与色度特征 | 最终增强图 | 修复边缘振铃和色彩偏置 |
2.2 频带分解模块的设计细节
频带分解是整个方案的基石。我采用的是径向频带划分:把频谱按中心距离分成K个同心圆环,从低频到高频依次编号。低频环对应整体照明信息,中间环承载主体结构,最外环则是边缘和噪声。理论选择K=5到K=9之间比较合适,太少则每个频带混入不同性质的成分,太多则高频带被噪声支配,后续RBF容易过拟合到噪声模式。
掩膜生成时最关键的一点是不要用硬边界。直接用二值圆环切割频谱,逆变换以后必然出现吉布斯振铃。我给每个频带的掩膜边缘加了余弦过渡,相当于频域里做了一个Tukey窗。这样相邻频带之间有平滑交接,重建图像不会出现等间距的亮暗条纹。具体实现时,掩膜按截断高斯平滑或者余弦窗过渡都可以,实测余弦窗对PSNR的提升最大,代码也更简单:
def generate_radial_masks(H, W, K, transition=0.15): y = torch.fft.fftfreq(H).view(-1, 1) x = torch.fft.fftfreq(W).view(1, -1) r = torch.sqrt(x ** 2 + y ** 2) r_max = r.max() edges = torch.linspace(0, r_max, K + 1) masks = [] for k in range(K): lo = edges[k] hi = edges[k + 1] band = (r >= lo) & (r < hi) mask = band.float() if k > 0: prev_hi = edges[k] t = torch.clip((prev_hi - r) / (transition * (hi - lo) + 1e-6), 0, 1) mask = mask * t if k < K - 1: next_lo = edges[k + 1] t = torch.clip((r - next_lo) / (transition * (hi - lo) + 1e-6) + 1, 0, 1) mask = mask * t masks.append(mask) return torch.stack(masks, dim=0)这组掩膜是固定常数,训练时可以预生成缓存到显存,不必每轮重复计算。对于非正方形输入,先pad到最近2的幂再做FFT,这样可以减少频谱泄漏,也简化了掩膜的生成逻辑。频带分解后的复频谱系数会分别走实部虚部两条梯度路径,RBF模块的操作必须同时对两者生效,这点在实现时要留意。
2.3 径向基函数映射模块的设计动机
为什么要用径向基函数而不是普通的MLP或者空间注意力?关键在于“径向”两个字。RBF网络的核心计算是高斯核响应,它度量的是输入向量与预设中心之间的欧氏距离,距离越近响应越大。而频带本身就是按中心频率组织的,每个频带的降质程度可以用“这个频带的能量统计离正常图像的统计原型有多远”来衡量。这个度量关系与RBF的距离机制天然匹配,几乎就是为频带级别控制量身定做的。
RBF映射模块的输入特征我是这样设计的:对每个频带提取6个统计特征,包括频带平均幅度、频带总能量占比、频带内能量分散度(标准差)、频带中心频率与归一化半径、以及像素域的局部均值与方差。这些特征维度低、含义明确,RBF中心数量控制在64到128之间,整个映射模块的参数规模很小,不会成为网络瓶颈。
输出端则是每个频带一个增益系数g_k和一个偏移量b_k。增益控制频带整体的放大倍数,偏移量负责微调频谱系数的基线水平。为了保证增强不失控,我对增益输出做了约束,限制在0.3到2.0倍之间,偏移量限制在-0.2到0.2之间。这比直接输出无界回归值稳定得多,尤其是高频频带,过大的增益会把噪声抬得和纹理一样显眼。
RBF还有一个隐性的优点:它对输入的响应是局部性的。某个频带特征偏离正常范围较多时,只有距离最近的那几个中心会被激活,其余中心参与度很低。这就相当于让网络自动学习出一套“分诊”逻辑,每个频带独立决策增益。换成MLP,所有特征会全连接搅在一起,全局耦合太强,实验里更容易过拟合,且收敛明显更慢。
3. 从零实现FRBNet的关键代码路径
3.1 频域分解模块的实现
频域分解模块在PyTorch里实现并不复杂,核心就是FFT、掩膜、mask相乘、逆FFT这几个步骤。需要注意复频谱的直接操作:torch.fft.fft2返回的是复数张量,而网络里的卷积层不能直接处理复数参数,因此我建议把复频谱拆成实部和虚部两个通道,在频带分解操作完成后再合并回去。
读取输入图像时先转成YCbCr或者Lab,取亮度通道做FFT,这一步可以省去色度通道的频域处理成本。代码如下:
import torch import torch.fft as fft def freq_decompose(input_lum, masks, norm="ortho"): # input_lum: [B, 1, H, W] 亮度通道 spec = fft.fft2(input_lum, norm=norm) spec_shift = fft.fftshift(spec, dim=(-2, -1)) bands = [] for m in masks: bands.append(spec_shift * m.unsqueeze(0).unsqueeze(0)) return bands # list of [B,1,H,W] complex tensors def freq_recompose(bands, masks, size=None): spec = torch.zeros_like(bands[0]) for b, m in zip(bands, masks): spec = spec + b * m.unsqueeze(0).unsqueeze(0) spec = fft.ifftshift(spec, dim=(-2, -1)) out = fft.ifft2(spec, norm="ortho").real if size is not None: out = out[..., :size[0], :size[1]] return out这里有一个小坑:fftshift和ifftshift的操作对象要是完整的频谱,不能只对部分系数做。m格式维度匹配时,我习惯把掩膜直接作为复数乘法的公共因子,它会自动作用于实部和虚部,梯度也能正常反向传播。
3.2 径向基函数层的实现要点
RBF层的核心参数是中心张量C、宽度参数σ和输出权重W。初始化策略直接影响训练是平稳收敛还是一路震荡。我用的是k-means聚类的思路:先跑一轮预特征提取,把训练集中每个频带的统计特征聚类成64个簇中心,用簇中心初始化C,用簇间平均距离初始化σ。如果不想做预聚类,也可以用覆盖整个特征范围的均匀网格,但实测均匀网格在特征分布不平衡时容易留下“盲区”,某些频带特征落在中心稀疏区域,梯度信号极弱。
宽度参数的设置要格外小心。σ太小,高斯核响应衰减太快,大部分输入特征激活不了几个中心,梯度传不回去;σ太大,所有中心都近似等值响应,RBF就退化成线性加权。我使用的经验法则是σ取中心最近邻距离中位数的0.6倍左右,效果比较稳。
import torch.nn as nn import torch class RBFMapping(nn.Module): def __init__(self, d_in, n_centers, n_bands): super().__init__() self.centers = nn.Parameter(torch.randn(n_centers, d_in) * 0.05) self.log_sigma = nn.Parameter(torch.full((n_centers,), -1.0)) self.gain_head = nn.Linear(n_centers, n_bands) self.bias_head = nn.Linear(n_centers, n_bands) def forward(self, z): # z: [B, d_in] diff = z.unsqueeze(1) - self.centers.unsqueeze(0) dist_sq = diff.pow(2).sum(dim=-1) sigma = self.log_sigma.exp().clamp(min=1e-3) phi = torch.exp(-dist_sq / (2 * sigma.pow(2))) gain = torch.sigmoid(self.gain_head(phi)) * 1.8 + 0.2 bias = torch.tanh(self.bias_head(phi)) * 0.2 return gain, bias把这个RBF层嵌入频域分支时,我会先把每个频带的特征向量z拼成[K, d_in]的批量输入,过RBF层拿到[K]的增益和偏移,然后用两个1维卷积或者简单的全连接层把增益偏移映射到复频谱的实部和虚部上。不要直接在复数谱上做nn.Linear,否则实部和虚部的耦合会被搅乱,效果反而差。
3.3 损失函数与训练配置清单
损失函数不能只靠一个L1或SSIM。FRBNet在频域工作,就一定要有频域损失约束。我最终用的损失组合是:
L = λ1 L1 + λ2 SSIM + λ3 L_freq + λ4 L_perceptual
其中L_freq是关键,它计算增强图与参考图的幅度谱在频带级加权后的L1距离。这样网络不只是在像素上对齐,还会被显式要求还原每个频带的能量结构。各损失权重我建议从λ1=1.0、λ2=0.2、λ3=0.5、λ4=0.5起步,再根据验证集表现微调。L_freq过高会让图像过度平滑,因为网络会倾向于压缩高频噪声来降低频域损失,所以控制在0.3到0.8之间比较安全。
训练数据我用的是成对方式:正常曝光图做参考,通过调整gamma曲线再加上泊松噪声和高斯噪声模拟出低光图。实际操作时gamma值从1.8到3.5之间随机取,噪声强度按亮度等级自适应。这样可以生成大量配对样本。另外再混入一部分真实低光/正常曝光的配对数据增强泛化性。数据增广上,随机水平翻转、旋转90度、随机裁剪就可以了,不要做色彩抖动类增广,因为低光增强本身对色度信息很敏感,过度扰动色度会让RBF的特征统计失真。
训练配置可以这样起步:优化器AdamW,学习率1e-4,batch size 8,输入分辨率256×256,训练100个epoch,学习率每30个epoch乘以0.5。RBF中心参数的学习率可以单独设置成主网络的0.1倍,防止初期中心漂移太剧烈导致σ失效。
4. 实验结果与实战效果评估
4.1 评测结果与快速对比
FRBNet在公开低光增强数据集上的复现效果,我用几组数值说话,对比对象包括经典Retinex类方法、曲线估计方法、一个全卷积增强网络,以及一个带注意力机制的增强网络。测试指标用PSNR、SSIM、LPIPS,同时统计参数量和推理耗时。以下是我复现实验得到的一组代表性数据,不同设备和数据切分下会略有浮动,但趋势稳定。
| 方法 | PSNR↑ | SSIM↑ | LPIPS↓ | 参数量 | 推理耗时(256², ms) |
|---|---|---|---|---|---|
| Retinex类方法 | 17.2 | 0.68 | 0.35 | - | 8 |
| 曲线估计方法 | 19.8 | 0.75 | 0.25 | 82K | 6 |
| 全卷积增强网络 | 21.3 | 0.82 | 0.19 | 2.1M | 19 |
| 注意力增强网络 | 21.1 | 0.84 | 0.16 | 5.6M | 38 |
| FRBNet(复现) | 21.6 | 0.85 | 0.14 | 1.1M | 14 |
FRBNet在PSNR和SSIM上和全卷积类网络接近,LPIPS明显更低,说明感知质量更好,图像更自然。参数量只有注意力网络的一点几个分之一,推理速度也快。对比曲线估计方法这种轻量级方案,FRBNet用稍多的参数换来了显著的画质提升,尤其在暗部纹理恢复上不是同一个档次。
4.2 消融实验:每个模块到底值多少分
消融实验最能解释FRBNet的哪个设计真正起作用。我做了三组对比:去掉频域分解直接让RBF作用在空间域特征上;把RBF映射模块换成同等参数量的MLP;调整频带数K从3到9逐个测试。结果如下。
| 设置 | PSNR↑ | SSIM↑ | 结论 |
|---|---|---|---|
| FRBNet完整版 | 21.6 | 0.85 | 基准 |
| 去掉频域分解 | 20.7 | 0.81 | 频域分解贡献明显 |
| RBF换成MLP | 21.0 | 0.82 | RBF机制有效 |
| K=3 | 20.4 | 0.79 | 频带过少,细节混合 |
| K=5 | 21.2 | 0.83 | 可用,但仍有频带混叠 |
| K=7 | 21.6 | 0.85 | 甜点区间 |
| K=9 | 21.3 | 0.83 | 高频带被噪声主导 |
去掉频域分解后,RBF直接处理空间域特征,PSNR下降了0.9dB,这验证了“频域任务分解”比“网络容量增长”更重要。RBF换成MLP后出现了明显下降,说明径向基函数的距离度量机制确实在编码“频带降质程度”这种关系上有优势。频带数K=7是一个比较好的平衡点,少于5个频带会让纹理和噪声重叠在同一个带里难以区分,多于9个则会有高频带几乎全被噪声覆盖,RBF的增益预测变得不稳定。
4.3 主观效果与失败案例记录
目视检查增强结果时,FRBNet最明显的特征是暗部细节恢复均匀,不再是一团糊的对比度提升。墙面纹理、织物褶皱、暗部草地这类中高频结构,在增强后能看清但不出现锐化过度的白边。肤色和绿植颜色保持得比较自然,没有常见的红色或者青色偏色。
失败案例也值得记录。在近乎全黑的场景下,例如只有一点点环境光的室内房间,FRBNet会倾向于把噪声一起增强,导致出现彩色斑点,因为此时整个中高频段的信噪比都太低了,网络难以区分纹理和噪声。另外,画面中存在强烈过曝光源时,例如夜间路边的灯箱,光源周围会出现轻微彩色振铃。对这两个case,我的缓解办法是在数据增强阶段增加“极端暗化”和“局部过曝合成”的样本,让网络见过更极端的情况,能缓解一部分,但不能完全消除,这也是后续想继续解决的方向。
5. 常见问题与排查技巧实录
5.1 频域伪影:条纹与振铃
第一个高频问题就是输出图出现等间距的亮暗条纹,或者在物体边缘出现水波纹样的振铃。这两类伪影的根源都在频域分解的掩膜切得不够平滑。硬边界掩膜在频域相当于矩形窗,矩形窗的频谱旁瓣泄漏非常严重,逆FFT后就会出现条纹和振铃。排查时先确认掩膜边缘有没有过渡,没有过渡就换成带Tukey窗的生成方式。如果过渡已经有了但伪影还在,检查一下逆FFT前是否做了ifftshift,频谱中心位置搞反也会产生类似的纹路。
另一个来源是输入图像的尺寸边长没有对齐FFT要求,非2幂的宽高会让频谱混叠。处理方式是pad到2的幂,增强完成后再裁剪回原始尺寸,伪影问题基本消失。
5.2 训练振荡与色彩崩坏
训练时loss曲线像锯齿一样上下跳动,同时验证图像出现整体发绿或发橙的偏色,这是RBF初始化和损失权重不当的典型症状。先说源头:RBF层中心初始化全用高斯随机数时,离真实数据分布太远,前期梯度要么消失要么爆炸,loss自然稳不住。解决方法是做一轮数据预扫描,用k-means初始化中心,σ按中心距离中位数设定,再用0.1倍学习率单独训练RBF层直到loss下降趋势稳定。
色彩崩坏的另一个原因是频域损失权重设置过高。L_freq只作用于亮度通道还好,如果对色度通道也加了频域约束,网络会为了匹配幅度谱而牺牲色度分布,出现整体偏色。处理办法是色度通道不走频域分支,只保留L1和感知损失。这样色彩任务由空间卷积分支负责,不会和频域增益打架。
5.3 显存与推理速度优化
FRBNet整体体积不大,但FFT在训练时对显存仍有一定压力。一个有效优化是频带分解时对高频分支降低分辨率。高频分支承载的细节信息相对稀疏,把频谱的高频区域在空间域降到半分辨率再计算RBF增益,可以减少约30%的FFT计算量。低频分支保持全分辨率,因为光照的梯度变化需要在全分辨率下保持精度。
推理阶段,可以把所有频带掩膜和RBF的中心参数全部转成常量缓存,避免每次前向都重新生成。另外使用半精度推理时,注意复频谱的FFT保持fp32,RBF映射和增益计算可以使用fp16。我在实验中发现复数FFT在fp16下会积累不少截断误差,导致重建图像出现轻微网格噪声,所以只把线性层和激活函数放在amp上下文中,FFT运算留在fp32,效果更稳。
5.4 多尺度输入与部署时的尺寸适配问题
实际部署时,输入图像的尺寸五花八门,不像训练时都是256×256。FRBNet的频带分解基于径向距离,理论上对任意宽高都成立,但遇到极端长宽比,例如1:2以上的图像,频谱的径向距离计算会有些失真,高频频带混入更多方向性信息。我的处理方式是先对图像做保持比例的resize,让长边缩放到512或者640,短边自动跟上,再pad到能被16整除的尺寸,这样FFT计算和逆FFT重建都稳定。输出后恢复原始图像尺寸即可。
这个项目做到后面,我最大的感触是:频域网络设计不能只把FFT当成一个简单的特征变换扔给网络就完事。FRBNet能起作用,关键是先显式做了频带分工,再让RBF针对每个频带的降质状态单独决策。这种“分诊”式的结构在消融实验里经得起推敲,而不是靠堆参数硬提指标。后面如果把这个频带增益模块接到目标检测或语义分割的主干上做暗光输入的预处理,应该能进一步提升下游任务的鲁棒性,这也是我下一步想继续验证的方向。