Noise reduction

노이즈 감소

Suppresses the random speckle (noise) that high ISO or dim shooting conditions add, while trying not to smear away real detail along with it.

Also known as: Denoise디노이즈
···
html
<div class="wrap">
  <div class="stage">
    <div class="pane"><canvas id="lc" width="300" height="180"></canvas><span class="lbl">일반 블러(경계 무시)</span></div>
    <div class="pane"><canvas id="rc" width="300" height="180"></canvas><span class="lbl">경계 보존 디노이즈</span></div>
  </div>
  <div class="hud"><span>노이즈를 얹은 뒤 두 방식으로 제거</span></div>
</div>
css
.wrap{position:absolute;inset:0;padding:clamp(8px,3vmin,20px);display:flex;flex-direction:column;gap:6px}
.stage{position:relative;flex:1;min-height:0;display:flex;gap:clamp(6px,2vmin,12px)}
.pane{position:relative;flex:1;min-height:0}
.lbl{position:absolute;left:8px;bottom:8px;z-index:2;font:800 clamp(9px,2.2vmin,11px)/1 ui-monospace,monospace;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.6)}
canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;border-radius:10px;border:1px solid var(--line);background:#0a0a0a}
.hud{display:flex;justify-content:center;font:700 clamp(9px,2.2vmin,11px)/1 ui-monospace,monospace;color:var(--muted)}
js
var W = 300, H = 180;
var lc = document.getElementById('lc'), lx = lc.getContext('2d');
var rc = document.getElementById('rc'), rx = rc.getContext('2d');

function paintPhoto(cx, w, h) {
  var g = cx.createLinearGradient(0, 0, 0, h * 0.62);
  g.addColorStop(0, '#5b86c9'); g.addColorStop(0.55, '#bcd6ea'); g.addColorStop(1, '#f5e0ad');
  cx.fillStyle = g; cx.fillRect(0, 0, w, h);
  var sx = w * 0.74, sy = h * 0.22, sr = h * 0.11;
  var sg = cx.createRadialGradient(sx, sy, 0, sx, sy, sr * 3.2);
  sg.addColorStop(0, 'rgba(255,247,214,0.9)'); sg.addColorStop(1, 'rgba(255,247,214,0)');
  cx.fillStyle = sg; cx.fillRect(0, 0, w, h * 0.62);
  cx.fillStyle = '#fff8df'; cx.beginPath(); cx.arc(sx, sy, sr, 0, 7); cx.fill();
  cx.fillStyle = '#6f9152';
  cx.beginPath(); cx.moveTo(0, h * 0.64);
  for (var x = 0; x <= w; x += 14) cx.lineTo(x, h * 0.64 - Math.sin(x * 0.014) * h * 0.05 - Math.sin(x * 0.032 + 1.2) * h * 0.02);
  cx.lineTo(w, h); cx.lineTo(0, h); cx.closePath(); cx.fill();
  cx.fillStyle = '#405f38';
  cx.beginPath(); cx.moveTo(0, h * 0.79);
  for (var x2 = 0; x2 <= w; x2 += 14) cx.lineTo(x2, h * 0.79 - Math.sin(x2 * 0.022 + 0.6) * h * 0.035);
  cx.lineTo(w, h); cx.lineTo(0, h); cx.closePath(); cx.fill();
  var tx = w * 0.2, ty = h * 0.8;
  cx.strokeStyle = '#3a2a1c'; cx.lineWidth = Math.max(1.5, w * 0.008);
  cx.beginPath(); cx.moveTo(tx, ty); cx.lineTo(tx - w * 0.01, ty - h * 0.13); cx.stroke();
  cx.fillStyle = '#2e4a28';
  for (var i = 0; i < 5; i++) { cx.beginPath(); cx.arc(tx + (i - 2) * w * 0.013, ty - h * 0.15 - (i % 2) * h * 0.018, w * 0.028, 0, 7); cx.fill(); }
  cx.fillStyle = '#1e1e1c';
  cx.beginPath(); cx.arc(w * 0.56, h * 0.745, h * 0.018, 0, 7); cx.fill();
  cx.fillRect(w * 0.555, h * 0.76, w * 0.01, h * 0.06);
  var img = cx.getImageData(0, 0, w, h), d = img.data;
  for (var p = 0; p < d.length; p += 4) { var n = (Math.random() - 0.5) * 10; d[p] += n; d[p + 1] += n; d[p + 2] += n; }
  cx.putImageData(img, 0, 0);
}
paintPhoto(lx, W, H);
var base = lx.getImageData(0, 0, W, H);
function boxBlur(src, w, h, r) {
  var tmp = new Float32Array(src.length), out = new Float32Array(src.length);
  for (var y = 0; y < h; y++) {
    for (var x = 0; x < w; x++) {
      var sr = 0, sg = 0, sb = 0, n = 0;
      for (var k = -r; k <= r; k++) { var xx = x + k; if (xx < 0 || xx >= w) continue; var idx = (y * w + xx) * 4; sr += src[idx]; sg += src[idx + 1]; sb += src[idx + 2]; n++; }
      var o = (y * w + x) * 4; tmp[o] = sr / n; tmp[o + 1] = sg / n; tmp[o + 2] = sb / n;
    }
  }
  for (var x2 = 0; x2 < w; x2++) {
    for (var y2 = 0; y2 < h; y2++) {
      var sr2 = 0, sg2 = 0, sb2 = 0, n2 = 0;
      for (var k2 = -r; k2 <= r; k2++) { var yy = y2 + k2; if (yy < 0 || yy >= h) continue; var idx2 = (yy * w + x2) * 4; sr2 += tmp[idx2]; sg2 += tmp[idx2 + 1]; sb2 += tmp[idx2 + 2]; n2++; }
      var o2 = (y2 * w + x2) * 4; out[o2] = sr2 / n2; out[o2 + 1] = sg2 / n2; out[o2 + 2] = sb2 / n2; out[o2 + 3] = 255;
    }
  }
  return out;
}
function edgeAwareBlur(src, w, h, r, sigma) {
  var out = new Float32Array(src.length);
  for (var y = 0; y < h; y++) {
    for (var x = 0; x < w; x++) {
      var ci = (y * w + x) * 4;
      var cl = (src[ci] + src[ci + 1] + src[ci + 2]) / 3;
      var sr = 0, sg = 0, sb = 0, sw = 0;
      for (var dy = -r; dy <= r; dy++) {
        for (var dx = -r; dx <= r; dx++) {
          var xx = x + dx, yy = y + dy;
          if (xx < 0 || xx >= w || yy < 0 || yy >= h) continue;
          var idx = (yy * w + xx) * 4;
          var nl = (src[idx] + src[idx + 1] + src[idx + 2]) / 3;
          var diff = nl - cl;
          var wgt = Math.exp(-(diff * diff) / (2 * sigma * sigma));
          sr += src[idx] * wgt; sg += src[idx + 1] * wgt; sb += src[idx + 2] * wgt; sw += wgt;
        }
      }
      out[ci] = sr / sw; out[ci + 1] = sg / sw; out[ci + 2] = sb / sw; out[ci + 3] = 255;
    }
  }
  return out;
}
function addNoiseArr(src) {
  var out = new Uint8ClampedArray(src.length);
  for (var i = 0; i < src.length; i += 4) {
    var n = (Math.random() - 0.5) * 46;
    out[i] = src[i] + n; out[i + 1] = src[i + 1] + n; out[i + 2] = src[i + 2] + n; out[i + 3] = 255;
  }
  return out;
}
function recompute() {
  var noisy = addNoiseArr(base.data);
  var plain = boxBlur(noisy, W, H, 2);
  var edge = edgeAwareBlur(noisy, W, H, 2, 24);
  var lImg = lx.createImageData(W, H);
  for (var i = 0; i < plain.length; i++) lImg.data[i] = plain[i];
  var rImg = rx.createImageData(W, H);
  for (var j = 0; j < edge.length; j++) rImg.data[j] = edge[j];
  lx.putImageData(lImg, 0, 0);
  rx.putImageData(rImg, 0, 0);
}
recompute();
var frameCount = 0;
function frame() {
  frameCount++;
  if (frameCount % 50 === 0) recompute();
  requestAnimationFrame(frame);
}
frame();

Noise is random brightness and color jitter, so averaging with neighboring pixels cancels much of it out — the same underlying idea as a blur. But a plain blur also smears real edges, so real denoise algorithms try to average only "similar" pixels to protect edges. This demo first adds artificial noise to the photo, then applies a plain average (ignores edges) and an edge-aware average (only averages similar-brightness neighbors) side by side to show the difference.

Use it after shooting at a high ISO in low light, or after brightening an underexposed shot in post and finding the shadows full of speckle. Color noise (blotches of stray color) and luminance noise (just brightness jitter) bother the eye differently, so color noise usually gets suppressed harder while luminance noise is treated lightly to protect detail.

Push it hard and smooth surfaces like skin or sky look great, but the fine texture in grass, sand or fabric gets smeared away too, leaving the photo looking flat and plastic-y (often called a "waxy" look). Applying it uniformly is rarely ideal — flat areas can take a strong pass, textured ones need a light touch.

When to use

Use it after high-ISO shooting or after brightening shadows exposes speckle. Push it too hard and texture smears into a waxy look — vary the strength by area.