Skip to content

Denoise

currentMigrated from docs/filtering/denoise.mdx at 37c3317fd058; modified for Encode Wiki · CC BY-SA 4.0

Noise is random variation from the sensor, film grain, analog transfer or earlier compression. An encoder cannot predict it, so noisy video eats bitrate that would otherwise go to real detail. Denoising before encoding either lowers the bitrate at the same look or gives you a cleaner picture for the same bitrate.

Whether to do it depends on what the noise is. Ugly digital noise, low-light sensor noise and compression residue are worth removing. Film grain is part of the image: leave it, or remove it and have the encoder synthesise it back, but don’t denoise it away and leave nothing. Denoising always costs some detail, so use the least that gets the result you want. Clean sources need none.

hqdn3d is fast and does spatial plus temporal smoothing; nlmeans is far slower and more precise.

Terminal window
ffmpeg -i input.mkv -vf hqdn3d -c:v libx264 -crf 18 output.mkv
ffmpeg -i input.mkv -vf nlmeans -c:v libx264 -crf 18 output.mkv

The defaults of hqdn3d are 4 for luma_spatial, with the other three strengths (chroma spatial, luma temporal, chroma temporal) derived from it as 3, 6 and 4.5. If there is still too much noise, raise luma_spatial (hqdn3d=8 is the same as hqdn3d=8:6:12:9). Strong settings cause ghosting behind moving objects, banding and blocking.

nlmeans compares each pixel’s neighbourhood with others nearby and averages the similar ones. It is slow, so reserve it for very noisy sources or when you need high-quality denoising; below that, hqdn3d is more efficient. Its artefacts are less obvious than hqdn3d’s; on very noisy input the typical one is a flat, cartoonish look.

OptionMeaningDefault
sDenoising strength, from 1.0 to 30.0.1.0
pPatch size: the neighbourhood compared. Odd number.7
rResearch size: how far to look for similar patches. Odd number; larger is slower.15

nlmeans=s=3:r=31:p=15 is a stronger, much slower setting for heavy noise at high resolutions, and nlmeans=s=1:r=5:p=3 trades quality for speed.

vsdenoise in vs-jetpack offers the denoisers most people use:

  • DFTTest is a frequency-domain denoiser. It is fast, handles most noise well, and lets you set a different strength for each frequency, so you can leave coarse detail alone and hit fine noise.
  • mc_degrain is motion-compensated temporal denoising built on MVTools. It averages a pixel with its matching positions in neighbouring frames, so it removes noise from static and slowly moving areas without smearing the way plain temporal averaging does.
  • bm3d is a state-of-the-art algorithm and very slow; it has CUDA, HIP, SYCL and Metal backends as well as a CPU one.
from vsdenoise import DFTTest, mc_degrain
dn = DFTTest().denoise(clip, {0: 0, 0.25: 4, 0.5: 8, 0.75: 16, 1.0: 32}, tr=1)
# or
dn = mc_degrain(clip, tr=2)

In the DFTTest example, the dictionary maps frequency (0 is the coarsest detail, 1 the finest) to strength, and tr is the temporal radius in frames. In mc_degrain, tr is likewise the number of frames used on each side (default 1). Its strength control is thsad (default 400), how different a matching block may be before it is rejected: a noisier source needs a higher value or nothing gets averaged.

To check that you removed noise and not detail, look at what the filter took out. With the 16-bit clip from the starting script:

diff = core.std.MakeDiff(clip, dn)
diff = core.std.Expr(diff, ["x 32768 - 8 * 32768 +", ""]) # amplify 8x, grey = no change

The difference should look like random noise. If you can see outlines of objects in it, the filter is removing detail.

Look at faces, foliage, dark gradients and scene cuts. Over-denoising shows as waxy skin, flattened texture, trails behind moving objects and banding in gradients (which you can then repair with debanding). Watch for grain that pulses between frames when temporal denoising only partly matches. Full-reference metrics compare against the noisy original, so they penalise a denoise that looks better; decide by eye.