Four-way denoise comparison — noisy / median / bilateral / NLM

Adds synthetic noise to the input, then runs three different denoisers on identical noisy copies and stacks them in a 2×2 grid for direct visual comparison. - **medianBlur** is fastest, ideal for impulse noise (dust spots, dead pixels). Smears fine texture into flat plateaus at higher radii. - **bilateralBlur** preserves edges while smoothing — good general-purpose denoiser; defaults give a gentle look. - **nonLocalMeans** is the gold-standard for natural-photo Gaussian noise: it averages pixels weighted by patch similarity across the whole image, preserving fine texture that the other two would smear. Slower than the others but the cleanest result. All three are pure pixel ops — no extensions needed. Tweak the param sliders to see how each one differs at the same noise level.

INPUT
INPUT — Four-way denoise comparison — noisy / median / bilateral / NLM
Four-way comparison
Four-way comparison — Four-way denoise comparison — noisy / median / bilateral / NLM
JavaScript
// Four-way denoise comparison: noisy / median / bilateral / NLM
// demo_denoise.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: NOISE_AMOUNT:number=0.10,min=0.0,max=0.5
//!PARAM: MEDIAN_RADIUS:number=2,min=1,max=5
//!PARAM: BILATERAL_D:number=7,min=3,max=15
//!PARAM: BILATERAL_SC:number=0.10,min=0.01,max=0.5
//!PARAM: BILATERAL_SS:number=8,min=1,max=30
//!PARAM: NLM_SEARCH:number=7,min=3,max=15
//!PARAM: NLM_PATCH:number=3,min=1,max=5
//!PARAM: NLM_H:number=0.08,min=0.01,max=0.3
//!PARAM: LABEL_SIZE:number=22,min=10,max=64

// Take the input, add `NOISE_AMOUNT` of synthetic film grain so
// there is something to denoise, then run three different
// denoisers on identical noisy copies and stack them in a 2×2
// grid for direct visual comparison:
//
//   ┌────────────────┬────────────────┐
//   │  noisy (input) │   median       │
//   │  (control)     │   (per-channel │
//   │                │    median)     │
//   ├────────────────┼────────────────┤
//   │  bilateral     │  Non-Local     │
//   │  (edge-pres.   │  Means         │
//   │   smoothing)   │  (gold std.)   │
//   └────────────────┴────────────────┘
//
// Median is fastest, ideal for impulse noise (dust spots, dead
// pixels). Bilateral preserves edges while smoothing — good for
// general use, defaults give a soft denoise. NLM is the
// gold-standard for natural-photo Gaussian noise: it averages
// pixels across the whole image weighted by patch similarity,
// preserving fine texture that the other two would smear.

const src = Engine.loadImage(INPUT);
const W = src.width;
const H = src.height;

// Add synthetic noise to the source so all four panels start
// from the same noisy reference.
const noisy = src.clone().filmGrain(NOISE_AMOUNT);
src.free();

const bandH = LABEL_SIZE + 8;
const cellW = W;
const cellH = bandH + H;
const out = Engine.createImage(1, 1);
out.setPixel(px(0, 0), new Pixel(0.05, 0.05, 0.07, 1.0));
out.resize(cellW * 2, cellH * 2);

function panel(label, image, col, row) {
  const x0 = col * cellW;
  const y0 = row * cellH;
  out.drawText(label, x0 + 16, y0 + LABEL_SIZE - 2, {
    size: LABEL_SIZE, color: NamedColor.white,
  });
  out.blendAt(image, px(x0, y0 + bandH), 1.0, BlendMode.Over);
}

// Panel 0,0 — noisy reference
panel('1) noisy input', noisy.clone(), 0, 0);

// Panel 1,0 — median
{
  const t = noisy.clone();
  t.medianBlur(MEDIAN_RADIUS);
  panel(`2) medianBlur(${MEDIAN_RADIUS})`, t, 1, 0);
  t.free();
}

// Panel 0,1 — bilateral
{
  const t = noisy.clone();
  t.bilateralBlur(BILATERAL_D, BILATERAL_SC, BILATERAL_SS);
  panel(`3) bilateralBlur(${BILATERAL_D}, ${BILATERAL_SC}, ${BILATERAL_SS})`, t, 0, 1);
  t.free();
}

// Panel 1,1 — NLM
{
  const t = noisy.clone();
  t.nonLocalMeans(NLM_SEARCH, NLM_PATCH, NLM_H);
  panel(`4) nonLocalMeans(${NLM_SEARCH}, ${NLM_PATCH}, h=${NLM_H})`, t, 1, 1);
  t.free();
}

noisy.free();
out.save(OUTPUT);
out.free();

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0