task 07 / cv
classic and learned bilateral filters, live on the gpu.
grainy photos are noisy photos. this page adds grain to a picture and then removes it again, with three versions of the same classic trick. everything runs in your browser, nothing is uploaded.
1pick a photo, or use your own
2pick how to clean it
a tiny neural network picks the filter settings separately for every pixel, so flat sky and sharp edges get different treatment.
drag across the photo to compare before and after.
see what the network chose
average quality score (PSNR, higher is better) on standard test photos.
the neural version beats every classic setting by about 1.4 dB and comes close to BM3D, while staying a filter that only averages nearby pixels. a bigger plain network (DnCNN) is still better.
PSNR in dB on grayscale test sets with gaussian noise (fixed seed), and on 320 real-noise crops from the SIDD validation set. (a)-(d) are bilateral variants, (e) are reference denoisers. the last three (d) rows are ablations of the network.
| method | σ=15 | σ=25 | σ=50 | SIDD | |
|---|---|---|---|---|---|
| - | noisy input | 24.79 | 20.48 | 14.91 | 27.02 |
| a | bilateral, tuned per sigma | 29.71 | 26.96 | 23.98 | 30.47 |
| b | bilateral, blind auto params | 29.60 | 26.90 | 23.97 | 30.47 |
| c | iterated bilateral | 29.81 | 27.20 | 24.36 | 30.83 |
| c | joint bilateral (smoothed guide) | 30.00 | 27.45 | 24.05 | 30.75 |
| d | neural-guided bilateral (ours) | 30.82 | 28.38 | 25.48 | 27.27 |
| d | nbf, no guide | 30.76 | 28.28 | 25.27 | 28.27 |
| d | nbf, one pass | 30.86 | 28.35 | 25.19 | 27.21 |
| d | same unet, plain residual | 30.83 | 28.36 | 25.45 | 27.98 |
| e | non-local means (cv2) | 29.37 | 26.68 | 23.69 | 32.59 |
| e | BM3D | 31.15 | 28.63 | 25.71 | 28.21 |
| e | DnCNN-B (pretrained) | 31.62 | 29.16 | 26.23 | 27.48 |
it replaces each pixel by a weighted average of its neighbours. a neighbour counts less the further away it is (σ_s, in pixels) and the more its brightness differs (σ_r). so flat areas get smoothed, while across an edge the neighbours differ a lot and are mostly ignored.
the catch: one σ_s and σ_r for the whole image has to suit both flat sky and fine texture, and with heavy noise the filter can no longer tell noise from edges.
it measures the noise with immerkaer's method, a small filter that cancels smooth image content so mostly noise is left. that number goes into a table of the best settings per noise level, found by a grid search on 32 held-out training photos. on artificial noise the estimate is within about 1 grey level.
a small u-net (93k weights, 460 kB) looks at the noisy photo and outputs σ_s, σ_r and a guide image for every pixel. two bilateral passes then use those maps. the result is still an average of nearby noisy pixels, so it cannot invent detail. it was trained on 400 photos with random noise levels and never gets told how noisy the input is.
it runs with onnx runtime in a background thread, on your gpu (webgpu) if possible and on the cpu otherwise, in 256 px tiles so it fits in a phone's memory.
the same network without the bilateral structure scores just as well, so the structure buys explainable maps rather than quality. a bigger network (DnCNN) is 0.8 dB better. on real camera noise the network barely helps, because it was trained on artificial noise only.
the full write-up with all experiments is in the report. the model is on the hugging face hub.