這篇整理 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 上踩到的坑):下一篇筆記