색수차 보정

Chromatic aberration fix

렌즈가 빛의 파장마다 다른 위치로 굴절시켜 생기는 색 번짐(특히 사진 가장자리)을 채널별로 다시 정렬해 없애는 보정.

다른 이름: CA correction자보정
···
html
<div class="wrap">
  <div class="stage"><canvas id="c" width="300" height="180"></canvas></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}
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 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 radialSample(src, w, h, x, y, scale, channel, cx0, cy0) {
  var sx = cx0 + (x - cx0) * scale, sy = cy0 + (y - cy0) * scale;
  var xi = Math.round(sx), yi = Math.round(sy);
  if (xi < 0) xi = 0; if (xi >= w) xi = w - 1;
  if (yi < 0) yi = 0; if (yi >= h) yi = h - 1;
  return src[(yi * w + xi) * 4 + channel];
}
function applyCA(src, w, h, amt) {
  var out = new Uint8ClampedArray(src.length);
  var cx0 = w / 2, cy0 = h / 2;
  for (var y = 0; y < h; y++) {
    for (var x = 0; x < w; x++) {
      var idx = (y * w + x) * 4;
      out[idx] = radialSample(src, w, h, x, y, 1 + amt, 0, cx0, cy0);
      out[idx + 1] = src[idx + 1];
      out[idx + 2] = radialSample(src, w, h, x, y, 1 - amt, 2, cx0, cy0);
      out[idx + 3] = 255;
    }
  }
  return out;
}
var t = 0;
function frame() {
  t += 0.018;
  var amt = 0.015 + (Math.sin(t) + 1) / 2 * 0.04;
  var aberrated = applyCA(orig.data, W, H, amt);
  ctx.putImageData(new ImageData(aberrated, W, H), 0, 0);
  var frac = (Math.sin(t * 0.5) + 1) / 2 * 0.7 + 0.15;
  var sw = Math.round(W * frac);
  if (sw > 0) {
    var fixed = applyCA(aberrated, W, H, -amt);
    ctx.putImageData(new ImageData(fixed, W, H), 0, 0, 0, 0, sw, H);
  }
  ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(sw, 0); ctx.lineTo(sw, H); ctx.stroke();
  requestAnimationFrame(frame);
}
frame();

렌즈는 파장마다 굴절률이 살짝 달라서 빨강·초록·파랑이 센서 위 정확히 같은 자리에 맺히지 못하고 아주 조금씩 어긋납니다 — 특히 가장자리, 밝고 어두운 경계에서 보라색·청록색 테두리로 보입니다. 보정은 반대로 각 채널을 이미지 중심에서부터 살짝 다른 배율로 늘리거나 줄여 세 채널을 다시 겹치게 만듭니다. 이 데모는 먼저 R·B 채널을 중심에서 바깥으로 반대 방향으로 밀어 색수차를 인위적으로 만들고, 그다음 정확히 반대 방향으로 채널을 되돌려 보정합니다.

밝은 하늘을 배경으로 어두운 나뭇가지·전깃줄이 지나가는 사진, 광각 렌즈로 찍어 가장자리 왜곡이 큰 사진에서 흔히 필요합니다. 요즘은 카메라가 렌즈 프로파일을 인식해 자동으로 이 보정을 적용하는 경우도 많습니다.

보정량을 잘못 추정하면(과하게 밀면) 있지도 않던 반대색 테두리가 새로 생깁니다 — 보라색이던 자리가 초록색으로 바뀌는 식입니다. 색수차는 렌즈마다, 같은 렌즈라도 조리개·초점거리마다 패턴이 달라서, 사진 전체에 하나의 배율만 적용하는 단순한 보정으로는 가장자리 끝부분까지 완전히 잡히지 않는 경우도 많습니다.

언제 쓰나

밝은 배경에 어두운 가는 선(나뭇가지·전깃줄)이 지나가거나 광각으로 찍은 가장자리에서 색 테두리가 보일 때 씁니다. 과하게 밀면 반대색 테두리가 새로 생깁니다.