가우시안 블러

Gaussian blur

각 픽셀을 주변 픽셀의 가중 평균으로 바꿔 부드럽게 흐리는, 가장 기본적인 블러 알고리즘.

다른 이름: Gaussian smoothing가우시안 스무딩
···
html
<div class="wrap">
  <div class="stage"><canvas id="c" width="300" height="180"></canvas></div>
  <div class="hud"><span>왼쪽 원본 / 오른쪽 블러</span><b id="r">0px</b></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}
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:space-between;gap:8px;font:700 clamp(9px,2.2vmin,11px)/1 ui-monospace,monospace;color:var(--muted)}
.hud b{color:var(--fg)}
js
var W = 300, H = 180;
var c = document.getElementById('c'), ctx = c.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(ctx, W, H);
var orig = ctx.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;
}
var t = 0;
function frame() {
  t += 0.02;
  var radius = Math.round((Math.sin(t) + 1) / 2 * 6);
  var blurred = boxBlur(orig.data, W, H, radius);
  var out = ctx.createImageData(W, H);
  for (var i = 0; i < blurred.length; i++) out.data[i] = blurred[i];
  ctx.putImageData(out, 0, 0);
  var frac = (Math.sin(t * 0.55) + 1) / 2 * 0.7 + 0.15;
  var sw = Math.round(W * frac);
  if (sw > 0) ctx.putImageData(orig, 0, 0, 0, 0, sw, H);
  ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(sw, 0); ctx.lineTo(sw, H); ctx.stroke();
  document.getElementById('r').textContent = radius + 'px';
  requestAnimationFrame(frame);
}
frame();

진짜 가우시안 블러는 정규분포(종 모양) 가중치로 주변 픽셀을 평균 냅니다 — 가까운 픽셀은 크게, 먼 픽셀은 작게 반영됩니다. 이 데모는 계산량을 줄이려고 박스 블러(단순 평균)를 가로·세로로 여러 번 겹쳐 가우시안과 거의 같은 부드러운 결과를 냅니다(중심극한정리 덕분에 박스 블러를 반복하면 가우시안에 수렴합니다). 반경이 커질수록 더 넓은 이웃을 평균 내 더 흐려집니다.

배경을 아웃포커스처럼 부드럽게 흐릴 때, 피부 리터칭의 저주파 레이어를 만들 때, 노이즈를 억누를 때(노이즈 자체도 고주파라 블러에 묻힘) 씁니다. 포토샵 등에서 "가우시안 블러"라는 이름으로 반경(px) 하나만 조절하는 가장 단순한 블러 필터이기도 합니다.

반경을 과하게 키우면 경계가 있어야 할 곳(눈동자·머리카락 한 올)까지 뭉개져 사진처럼 안 보이고 그림처럼 보입니다. 전체에 균일하게 걸면 실제 아웃포커스처럼 안 보이는 경우가 많은데, 실제 렌즈 블러는 거리에 따라 강도가 다르기 때문입니다 — 그래서 마스크나 렌즈 블러 필터로 위치마다 강도를 다르게 주는 게 더 자연스럽습니다.

언제 쓰나

배경을 부드럽게 흐리거나 노이즈를 죽이고 싶을 때 씁니다. 전체에 균일하게 걸면 실제 아웃포커스와 다르게 보이니 마스크로 위치별 강도를 나누세요.