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.
// 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
//!PARAM: DETAIL:integer=620,min=0,max=4096
//!PARAM: SEED:integer=1,min=0,max=1000000
// SEED: gleiche Zahl → gleiches Rauschen (reproduzierbar); 0 → jedes Mal neu.
// 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.
// Ausschnitt statt Verkleinerung — und das ist beim Entrauschen kein
// Geschmack, sondern die Sache selbst. Rauschen ist ein Pixel-
// Phaenomen: Wer das fertige 2x2-Raster herunterskaliert, mittelt
// genau das Korn weg, dessen Beseitigung hier vorgefuehrt wird, und
// alle vier Tafeln sehen gleich sauber aus. Ein mittiger Ausschnitt
// zeigt die Pixel dagegen 1:1.
//
// DETAIL = 0 nimmt das ganze Bild (fuer einen Blick auf die
// Gesamtwirkung); die Vorgabe 620 haelt das Raster knapp unter
// 1280 Breite.
const full = Engine.loadImage(INPUT);
let src = full;
if (DETAIL > 0 && full.width > DETAIL) {
const cw = Math.min(DETAIL, full.width);
const ch = Math.min(Math.round(DETAIL * full.height / full.width), full.height);
src = full.clone().crop(
Math.round((full.width - cw) / 2),
Math.round((full.height - ch) / 2),
cw, ch);
full.free();
}
const W = src.width;
const H = src.height;
// Add synthetic noise to the source so all four panels start
// from the same noisy reference.
// Gleichverteiltes Helligkeitsrauschen in [-NOISE_AMOUNT, NOISE_AMOUNT] —
// dasselbe wie filmGrain(NOISE_AMOUNT), aber mit Seed.
const noisy = src.clone().addNoise({ type: "uniform", sigma: NOISE_AMOUNT, color: false, seed: SEED || undefined });
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