루미노시티 마스크

Luminosity mask

픽셀의 밝기 자체를 마스크로 써서, 밝은 부분이나 어두운 부분에만 보정이 걸리는 선택 영역을 자동으로 만드는 기법.

다른 이름: Luminosity masking광도 마스크
···
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="th">130</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:center;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);
var t = 0;
function frame() {
  t += 0.015;
  var thresh = 130 + Math.sin(t) * 55;
  var maskImg = lx.createImageData(W, H);
  var resImg = rx.createImageData(W, H);
  var id = orig.data;
  for (var i = 0; i < id.length; i += 4) {
    var lum = 0.2126 * id[i] + 0.7152 * id[i + 1] + 0.0722 * id[i + 2];
    var wgt = Math.max(0, Math.min(1, (lum - thresh) / 60 + 0.5));
    var mv = Math.round(wgt * 255);
    maskImg.data[i] = mv; maskImg.data[i + 1] = mv; maskImg.data[i + 2] = mv; maskImg.data[i + 3] = 255;
    var factor = 1 - wgt * 0.45;
    resImg.data[i] = id[i] * factor; resImg.data[i + 1] = id[i + 1] * factor; resImg.data[i + 2] = id[i + 2] * factor; resImg.data[i + 3] = 255;
  }
  lx.putImageData(maskImg, 0, 0);
  rx.putImageData(resImg, 0, 0);
  document.getElementById('th').textContent = Math.round(thresh);
  requestAnimationFrame(frame);
}
frame();

루미노시티 마스크는 사진 자체의 휘도 값을 그대로 흑백 마스크로 씁니다 — 밝은 픽셀은 마스크에서도 밝고(보정을 많이 받고), 어두운 픽셀은 마스크에서도 어둡습니다(보정을 적게 받음). 손으로 올가미를 그릴 필요 없이 "하이라이트만", "그림자만" 같은 정교한 선택이 사진 내용에서 저절로 나옵니다. 이 데모는 임계값을 움직이며 하이라이트 마스크가 어떻게 변하는지, 그 마스크로 보정(하이라이트를 살짝 어둡게)을 걸면 결과가 어떻게 달라지는지 보여줍니다.

하늘만 톤을 낮추고 지면은 그대로 두고 싶을 때, 인물 사진에서 이마·코 같은 밝은 부분만 살짝 눌러주고 그림자는 그대로 두고 싶을 때 씁니다. 경계가 딱딱한 손 선택과 달리 마스크 자체가 밝기 그러데이션이라 보정도 자연스럽게 그러데이션으로 들어갑니다.

마스크를 만드는 데 쓴 채널(밝기 전체 vs 특정 색 채널)에 따라 결과가 크게 달라지는데, 이를 신경 쓰지 않고 아무 채널이나 쓰면 원치 않는 영역까지 마스크에 걸립니다. 마스크 경계를 너무 날카롭게 만들면 보정 경계가 눈에 띄는 띠로 보이므로, 대개는 마스크에 약간의 흐림을 줘서 전환을 부드럽게 만듭니다.

언제 쓰나

하늘·하이라이트처럼 밝기로 구분되는 영역만 골라 보정하고 싶을 때 씁니다. 마스크 경계가 날카로우면 보정 경계가 띠처럼 보이니 살짝 흐리세요.