대비 vs 클래리티

Contrast vs. clarity

둘 다 '대비를 준다'고 부르지만, 대비는 전체 톤 분포를 넓히고 클래리티는 중간톤 경계의 국소 대비만 끌어올립니다.

다른 이름: Contrast vs Clarity전역 대비와 국소 대비
···
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><b id="amt">0%</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;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: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 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 orig = 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;
}
var blurred = boxBlur(orig.data, W, H, 3);
var t = 0;
function frame() {
  t += 0.02;
  var amt = (Math.sin(t) + 1) / 2;
  var co = lx.createImageData(W, H);
  var cd = co.data, id = orig.data;
  var cf = 1 + amt * 1.1;
  for (var i = 0; i < id.length; i += 4) {
    cd[i] = Math.min(255, Math.max(0, (id[i] - 128) * cf + 128));
    cd[i + 1] = Math.min(255, Math.max(0, (id[i + 1] - 128) * cf + 128));
    cd[i + 2] = Math.min(255, Math.max(0, (id[i + 2] - 128) * cf + 128));
    cd[i + 3] = 255;
  }
  lx.putImageData(co, 0, 0);
  var ro = rx.createImageData(W, H);
  var rd = ro.data;
  var amount = amt * 2.2;
  for (var j = 0; j < id.length; j += 4) {
    for (var ch = 0; ch < 3; ch++) {
      var v = id[j + ch];
      var hiFreq = v - blurred[j + ch];
      var midW = 1 - Math.abs((v - 128) / 128);
      rd[j + ch] = Math.min(255, Math.max(0, v + hiFreq * amount * midW));
    }
    rd[j + 3] = 255;
  }
  rx.putImageData(ro, 0, 0);
  document.getElementById('amt').textContent = Math.round(amt * 100) + '%';
  requestAnimationFrame(frame);
}
frame();

대비(Contrast)는 전역 연산입니다 — 128을 기준으로 그보다 어두운 값은 더 어둡게, 밝은 값은 더 밝게 밀어 톤 분포 자체를 넓힙니다. 클래리티(Clarity)는 반대로 국소 연산입니다 — 원본을 흐릿하게 만든 저주파 버전을 만들고, 원본에서 그 저주파를 뺀 차이(경계 디테일)를 중간톤 가중치를 걸어 다시 더합니다. 이 데모는 두 연산을 같은 사진에 동시에 걸어 나란히 보여줍니다.

대비는 사진 전체 무드를 잡을 때(어둡고 묵직한 느낌 등) 씁니다. 클래리티는 하늘이나 피부처럼 평평한 면은 그대로 두면서 나뭇잎·자갈·직물 같은 질감이 있는 부분만 또렷하게 만들고 싶을 때 씁니다 — 풍경 사진의 "입체감"은 대개 클래리티에서 옵니다.

대비를 과하게 올리면 그림자와 하이라이트가 순식간에 클리핑됩니다. 클래리티를 과하게 올리면 경계 주변에 밝고 어두운 테두리(헤일로)가 생기고, 인물 사진에서는 모공·잔주름이 과장되어 피부가 거칠어 보입니다 — 그래서 인물 피부에는 클래리티를 낮추거나 마스크로 제외하는 게 일반적입니다.

언제 쓰나

사진 전체 무드를 잡을 땐 대비, 질감만 또렷하게 하고 싶을 땐 클래리티를 씁니다. 인물 피부에는 클래리티를 낮추거나 마스크로 빼세요.