Poisson Denoising + 4x Super-Resolution
A 1.52M-parameter residual CNN that jointly denoises and 4x upscales low-light images (160x256 -> 640x1024), trained for the DLP 26T2 NPPE3 competition.
Kaggle public leaderboard: 39.68299 PSNR.
The key finding
The task is described as low-light enhancement, but it is not. Ground-truth images are exactly as dark as the inputs (brightness ratio GT/LR = 1.0118, both with mean intensity ~41/255), so any tone mapping or gamma correction destroys PSNR. The model must preserve darkness, not remove it.
Degradation model
The degradation was reverse-engineered from paired data rather than assumed:
| property | measured | conclusion |
|---|---|---|
| residual std / sqrt(I) | 1.57, flat across a 10x intensity range | Poisson, Var = g*I |
| skew | +0.2478 (predicted 1/sqrt(I/g) = 0.245) | confirms Poisson |
| kurtosis | 3.408 | near-Gaussian tails |
| inter-channel correlation | < 0.002 | independent per channel |
| spatial autocorrelation (lags 1-3) | < 0.002 | white noise |
| residual mean | -0.48 | uint8 truncation, not bias |
Best fit: Poisson with gain g = 2.55, INTER_AREA downsampling, truncation to uint8. Matching all four moments (mean -0.4811 vs -0.4796, std 10.2583 vs 10.2701, skew +0.2500 vs +0.2478, kurtosis 3.4163 vs 3.4082).
Knowing the exact forward model let 75% of training samples be freshly synthesised from ground truth each epoch, so the network effectively never sees the same noise realisation twice.
Ceiling analysis
Denoising the low-res input perfectly and then applying plain bicubic upsampling scores 41.29 dB on validation, above every leaderboard entry. Denoising, not detail synthesis, is the dominant lever; capacity was allocated accordingly.
| method | val PSNR |
|---|---|
| bicubic (cv2) | 33.03 |
| bicubic (PIL) | 33.54 |
| NLM + bicubic | 35.17 |
| median3 + bicubic | 35.44 |
| this model | 39.47 |
| this model + x8 self-ensemble | 39.50 |
| perfect denoise + bicubic (ceiling) | 41.29 |
Architecture
Denoising happens at low resolution (16x cheaper than at high resolution, and the noise lives in LR space), followed by a learned PixelShuffle x4 tail.
- Head: 3 -> 64 conv
- Body: 16 residual blocks, residual scaling 0.1, global skip
- Tail: two PixelShuffle(2) stages -> 3 channels
- Additive bicubic skip from input to output
Trained with L1(luma) + 0.2 * L1(RGB), since the competition metric scores
the ITU-R 601-2 luma channel only. RGB output is retained rather than
predicting luma directly; measured cost of that choice is 0.02 dB.
Usage
import torch, torch.nn as nn, torch.nn.functional as F, numpy as np
from PIL import Image
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file
class ResBlock(nn.Module):
def __init__(self, c, rs=0.1):
super().__init__()
self.c1 = nn.Conv2d(c, c, 3, 1, 1); self.c2 = nn.Conv2d(c, c, 3, 1, 1); self.rs = rs
def forward(self, x):
return x + self.c2(F.relu(self.c1(x), inplace=True)) * self.rs
class EDSR(nn.Module):
def __init__(self, ch=64, nb=16, rs=0.1):
super().__init__()
self.head = nn.Conv2d(3, ch, 3, 1, 1)
self.body = nn.Sequential(*[ResBlock(ch, rs) for _ in range(nb)],
nn.Conv2d(ch, ch, 3, 1, 1))
self.up = nn.Sequential(
nn.Conv2d(ch, ch * 4, 3, 1, 1), nn.PixelShuffle(2), nn.ReLU(inplace=True),
nn.Conv2d(ch, ch * 4, 3, 1, 1), nn.PixelShuffle(2), nn.ReLU(inplace=True),
nn.Conv2d(ch, 3, 3, 1, 1))
def forward(self, x):
base = F.interpolate(x, scale_factor=4, mode='bicubic', align_corners=False)
f = self.head(x)
return self.up(f + self.body(f)) + base
net = EDSR().eval()
net.load_state_dict(load_file(hf_hub_download('sru-pat/poisson-denoise-sr-x4', 'model.safetensors')))
lr = np.array(Image.open('low_res_noisy.png').convert('RGB'), dtype=np.uint8)
x = torch.from_numpy(lr.transpose(2, 0, 1)).float().div(255)[None]
with torch.no_grad():
y = net(x).clamp(0, 1)
out = (y[0].numpy().transpose(1, 2, 0) * 255).round().astype(np.uint8)
Image.fromarray(out).save('output.png')
x8 self-ensembling (averaging over the 8 dihedral transforms) adds a further 0.035 dB. The gain is small because the model was trained with full D8 augmentation and is already close to equivariant.
Training data
1105 paired low-res/high-res images. 75% of samples per epoch were regenerated from ground truth using the fitted Poisson model; the remainder used the original noisy inputs.
Limitations
- Fixed geometry: expects 160x256 input, produces 640x1024
- Fitted to this specific degradation (Poisson g=2.55). Gaussian, compression, or blur artefacts are out of distribution
- Trained on dark scenes (mean intensity ~41/255); brighter inputs are untested
- Optimised for luma PSNR, not perceptual quality. No adversarial or perceptual loss, so output is smooth rather than sharp at edges
Error analysis
On validation, 47% of pixels (flat regions) carry 14% of squared error, while 10% of pixels near edges carry 40%. Remaining headroom is in high-frequency reconstruction, not denoising.