这篇整理 LumiGrid 的来龙去脉。除了结果,也会写一个对我自己的方法不太有利的发现:模型其实几乎没有用到它名字里的「亮度」那一轴。
起点:一份不够好的课堂作业
低光图像增强(LLIE)要把很暗的照片还原成正常曝光。我先在 NTIRE 2025 挑战赛的数据上,把原本的课堂作法重做了一遍:Zero-DCE 加上双边滤波、gamma 和对比调整。在我切出来的 20 张保留测试图上,它只有 16.48 dB / SSIM 0.742,甚至比直接用 Zero-DCE 预训练权重的 19.00 dB 还低。
问题很清楚:Zero-DCE 对每个像素套用同一种曲线,看不到整张图的脉络;后处理再把杂讯一起放大。
设计:先决定整张图怎么亮,再修细节
LumiGrid 分成两段:
- 全域曲线网格。 一个小 CNN 只看整张图缩成 256 × 256 的缩略图,输出一个 16 × 16(空间)× 8(亮度)的网格。每一格存 8 轮 Zero-DCE 曲线(
LE(x) = x + a·x·(1 − x),每个 RGB 信道各一组)和一个 3 × 4 色彩矩阵。 - 切片。 每个像素用自己的位置,加上一个学出来的「亮度座标」,在网格里做三线性内插,拿到自己的曲线再套用。因为网格只从缩略图算一次,全域分支的成本几乎不随图像大小增加。
- NAFNet 细修。 一个宽度 24 的三层 NAFNet U-Net 同时看原图和上一步的结果,预测一个残差,负责去杂讯、补细节。它在 1024 × 1024 的图块上跑、以 64 像素线性融合,所以 2400 万像素的照片也能放进 8 GB 显卡。
损失函数是 Charbonnier + 0.25·(1 − SSIM) + 0.05·FFT 振幅,再加上全域分支的辅助损失;在一张笔记本电脑 RTX 4060 上训练 20k 次迭代(batch 8、裁切 320)。
结果
全部在 20 组没有用来训练或挑模型的 NTIRE 2025 图像上、以全分辨率、用官方评分程序计算:
| 方法 | PSNR ↑ | SSIM ↑ |
|---|---|---|
| 输入(不处理) | 10.65 | 0.381 |
| Zero-DCE 预训练权重 | 19.00 | 0.682 |
| 课堂作法(Zero-DCE + 滤波 + gamma + 对比) | 16.48 | 0.742 |
| Zero-DCE,用同样数据监督训练 | 20.91 | 0.717 |
| LumiGrid | 24.57 | 0.840 |
| LumiGrid + TTA(四种翻转平均) | 24.63 | 0.841 |
这不是排行榜成绩:挑战赛测试集的正解没有公开,这只是我自己的保留切分。
消融:两个分支各补对方的洞
| 变体 | PSNR | SSIM |
|---|---|---|
| Zero-DCE 网络(全分辨率卷积、没有全域脉络) | 20.91 | 0.717 |
| 只有曲线网格 | 22.85 | 0.768 |
| 只有 NAFNet 细修 | 22.85 | 0.833 |
| 完整 LumiGrid | 24.57 | 0.840 |
最有意思的是中间两列:两者 PSNR 一模一样,SSIM 却差很多。 细修网络单独使用时,结构和纹理(SSIM)好得多;但 PSNR 没有赢,代表它剩下的误差多半是大范围的亮度和色调偏差。这是从数字推论的,我没有另外直接量测。曲线网格正好擅长这件事,所以两者合起来再多 1.7 dB。
一个对自己不太有利的发现
LumiGrid 的卖点是「依亮度分层」:同一个区域里,亮的灯和旁边的暗影可以走不同的曲线。但我把学到的亮度座标拿出来看,发现它在 20 张测试图的所有像素上只落在 0.46 到 0.55 之间:86% 的像素落在 8 个亮度层的第 4 层,其余 14% 在第 5 层,另外 6 层完全没被用到。
换句话说,这个模型的提升几乎都来自空间上的变化(不同区域不同曲线),而不是亮度分层。README 里我也照实写了这一点。如果要让这个设计名副其实,下一步应该是:
- 加一个让亮度座标分布变宽的正则项,看看效果会不会更好;
- 或者做一个对照组:把亮度座标固定成常数。如果成绩几乎不变,就更确定分层没有被用到。
两个失败案例
- #305,夜空(13.5 dB)。 参考图像把天空保留成暗的,LumiGrid 却把它提亮了。「该不该亮」本身是有歧义的。
- #143,参考图像对不齐(11.9 dB)。 输出看起来是对的,但参考图像和输入的取景不同,逐像素的指针就会很难看。
这两张把平均往下拉了不少。20 张的切分很小,所以 0.2 dB 以内的差异我都不会过度解读。
延伸
- 代码、权重与完整表格:github.com/niansia/LumiGrid
- 用你自己的暗照片试试看:在浏览器运行的 LumiGrid
- 浏览器版是怎么做出来的(以及我在 WebGPU 上踩到的坑):下一篇笔记