Low-light Joint Denoising + ×4 Super-Resolution (NAFNet-UNet)
Models for the Kaggle competition DLP 26T2 NPPE3 (dlp-26t2-nppe3).
Roll number 23f1001168.
Task
Recover a clean 1024×640 image from a noisy, low-light 256×160 observation of a
street scene (Cityscapes-like). The competition metric is PSNR on the grayscale
(PIL convert('L')) version of the 1024×640 output, sampled at every 8th pixel.
The degradation, reverse-engineered
Comparing each provided LR image against an area-downsample of its ground truth showed the residual variance is linear in intensity, with matching skewness:
var(residual) = 2.526 * I + 1.49 (per channel, mutually independent)
which is pure Poisson shot noise with gain g = 2.526 and negligible read noise. The full forward model is
LR = floor( g * Poisson( area_downsample_4x(HR) / g ) ) clipped to [0, 255]
Training a model only on noise synthesised with this model scores the same as training on the organisers' real LR images (38.960 vs 38.953 dB), which confirms the model is correct — and makes the noise an unlimited augmentation.
Architecture
DenoiseSRNet — a NAFNet U-Net over the LR grid followed by a ×4 PixelShuffle head.
The network predicts a residual on top of a blurred-and-bicubic-upsampled luma,
so it only has to model denoising and high-frequency detail. It outputs a single
luma channel, matching the metric exactly rather than wasting capacity on colour.
Results (267-image validation split)
| configuration | PSNR (dB) |
|---|---|
| bicubic upsample of LR luma | 32.60 |
| Gaussian denoise + bicubic | 36.26 |
| single model, no TTA | 39.28 |
| single model + flip TTA | 39.32 |
| 7-model ensemble, no TTA | 39.33 |
| 7-model ensemble + flip TTA | 39.34 |
| oracle: noise-free LR + lanczos | 42.98 |
The oracle row shows denoising, not upsampling, is what the task is really about.
⚠️ These are small models (32-width, 12k iterations each) trained as an ablation sweep, not a long production run. The architecture scales well beyond this.
Files
model_*.pt— checkpoints; each holdsema,model, and theargsneeded to rebuild the netmodel.py,common.py,train.py,predict.py,make_submission.py— full pipelinemanifest.json— per-checkpoint config and validation score
Usage
import torch, numpy as np
from predict import load_net, infer
net, cfg = load_net("model_3_ab_mse.pt", "cuda") # cfg comes from the checkpoint
lr = np.array(Image.open("test_00001.png"))[None] # (1,160,256,3) uint8
hr_luma = infer([net], [cfg], lr, "cuda", tta=True) # (1,640,1024) float32, 0..255