Chromatic aberration fix

색수차 보정

Realigns the R/G/B channels to remove the color fringing a lens creates by bending different wavelengths of light to slightly different points.

Also known as: 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();

A lens refracts different wavelengths at slightly different angles, so red, green and blue never quite land on the exact same spot on the sensor — most visible as a purple or cyan fringe at high-contrast edges near the frame's border. Correction works in reverse: scale each channel by a slightly different amount from the image center so all three line back up. This demo first pushes the R and B channels outward from center in opposite directions to fabricate the fringing, then pulls them back by the exact opposite amount to fix it.

It's commonly needed on dark branches or power lines crossing a bright sky, or on wide-angle shots where edge distortion is more pronounced. Many cameras now recognize the attached lens and apply this correction automatically from a built-in profile.

Guess the correction amount wrong (push it too far) and a fringe that wasn't there appears in the opposite color — a spot that was purple turns green. Chromatic aberration also varies by lens, and even by aperture and focal length on the same lens, so a single global scale factor for the whole frame often can't fully clean up the extreme corners.

When to use

Use it when a dark thin subject crosses a bright background, or a wide-angle frame’s edges show color fringing. Overcorrect and a fringe appears in the opposite color.