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.

Downloads last month
2
Safetensors
Model size
1.52M params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support