SRNet โ Low-Light Denoising + 4x Super-Resolution
Submission model for DLP 26T2 NPPE3. Takes a dark, noisy 256x160 RGB image and produces a clean, correctly exposed 1024x640 RGB image in a single forward pass โ denoising, brightening, and 4x upscaling are learned jointly.
Results
| PSNR | |
|---|---|
| Kaggle public LB (42% of test set) | 39.611 dB |
| Held-out val, 48 images, x8 ensemble | 38.986 dB |
| Bicubic upsample baseline | 33.424 dB |
The competition metric is PSNR on PIL.Image.convert('L') output sampled
every 8th pixel, so all figures above are computed that way.
Architecture
RCAN-style residual channel-attention network.
- Input stem: the LR image is concatenated with a gamma-corrected copy
(
x ** 0.4545), giving 6 input channels. Low-light pixels cluster near 0 where the raw signal is poorly conditioned; the gamma copy hands the first convolution a well-spread version of the same data. - Body: N residual channel-attention blocks (RCAB), each a conv-ReLU-conv-SE stack with a 0.2-scaled residual, wrapped in a long skip connection.
- Upsampler: two PixelShuffle x2 stages.
- Output: the final conv bias is initialised to the training-set mean RGB, so the network does not spend its first thousands of updates learning a DC brightness offset through the residual stack.
This checkpoint: 64 channels, 24 RCAB blocks, 2.12 M parameters.
Width and depth were selected at runtime by a probe that timed three candidate
configurations on the actual T4 and picked the largest that still fit the
iteration budget (64ch reached 7.32 it/s; 96ch and 128ch were too slow to
converge in the time available). load_model() reads both back from the
checkpoint, so it reconstructs the right size automatically.
Training
- Loss: Charbonnier on RGB plus an equal-weight Charbonnier on ITU-R
601-2 luma (0.299 / 0.587 / 0.114). The competition metric scores
PIL.Image.convert('L')output, so luma error is what is actually graded. - Optimiser: Adam (0.9, 0.99), cosine schedule with linear warmup, gradient clipping at 1.0.
- Precision: AMP fp16 with
channels_last, on a single Kaggle T4. - Weights: EMA of the training weights, decay 0.999. Checkpointed on best validation PSNR.
- Inference: x8 dihedral self-ensemble.
- Run: 68,026 iterations at batch 16 on 64x64 LR crops (~985 epochs over 1,105 training pairs) in 2h54m on a single T4. Best checkpoint at iteration 62,000; validation PSNR was flat from ~50k onward.
Usage
import numpy as np
from PIL import Image
from model import load_model, enhance
model = load_model('best.pt', device='cuda')
lr = np.array(Image.open('test_00000.png').convert('RGB')) # 256x160
hr = enhance(model, lr, device='cuda') # 1024x640
Image.fromarray(hr).save('out.png')
Files
| File | Description |
|---|---|
best.pt |
EMA weights + best val PSNR + iteration count |
model.py |
Architecture, checkpoint loader, inference helper |