레벨(레벨 보정)

Levels

검정점·흰점·감마 세 지점으로 톤 범위를 다시 매핑하는 도구. 히스토그램 양끝의 빈 공간을 채워 대비를 만듭니다.

다른 이름: Levels adjustment레벨 조정
···
html
<div class="wrap">
  <div class="stage"><canvas id="c" width="300" height="180"></canvas></div>
  <div class="hud"><span>검정점 <b id="bl">30</b> · 흰점 <b id="wh">225</b></span><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}
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);
var t = 0;
function frame() {
  t += 0.015;
  var black = 30 + Math.sin(t) * 28;
  var white = 225 - Math.sin(t) * 28;
  var lut = new Uint8ClampedArray(256);
  for (var v = 0; v < 256; v++) {
    var f = (v - black) / (white - black);
    f = f < 0 ? 0 : f > 1 ? 1 : f;
    lut[v] = Math.round(f * 255);
  }
  var out = ctx.createImageData(W, H);
  var od = out.data, id = orig.data;
  for (var i = 0; i < id.length; i += 4) { od[i] = lut[id[i]]; od[i + 1] = lut[id[i + 1]]; od[i + 2] = lut[id[i + 2]]; od[i + 3] = 255; }
  var frac = (Math.sin(t * 0.6) + 1) / 2 * 0.7 + 0.15;
  ctx.putImageData(out, 0, 0);
  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('bl').textContent = Math.round(black);
  document.getElementById('wh').textContent = Math.round(white);
  requestAnimationFrame(frame);
}
frame();

레벨은 입력값 하나를 출력값 하나로 바꾸는 룩업테이블(LUT)입니다. 검정점보다 어두운 픽셀은 전부 0으로, 흰점보다 밝은 픽셀은 전부 255로 밀어버리고 그 사이 값은 재분배합니다. 이 데모는 매 프레임 검정점·흰점을 다시 계산해 256칸짜리 LUT를 만들고, 원본 픽셀 하나하나에 대입해 다시 그립니다.

히스토그램 양 끝이 비어 있는(카메라가 그 밝기를 실제로 기록하지 못한) 사진에서 흔히 씁니다. 안개 낀 날 찍어 붕 뜬 회색빛 사진의 검정점을 살짝 올리거나, 노출이 부족해 칙칙한 사진의 흰점을 낮춰 대비를 되살리는 식입니다.

가장 흔한 실수는 히스토그램이 이미 양 끝에 딱 걸쳐 있는데도 점을 더 안쪽으로 밀어 넣는 것입니다. 그러면 존재하던 디테일이 순수한 검정·흰색으로 뭉개지는 클리핑이 생깁니다. 밝기만 조절하고 싶다면 검정점·흰점은 히스토그램 끝에 그대로 두고 감마만 움직이는 편이 안전합니다.

언제 쓰나

히스토그램 양 끝이 비어 있을 때 대비를 되살리는 용도. 이미 끝까지 꽉 찬 히스토그램에서는 점을 더 당기지 마세요 — 클리핑됩니다.