샘플링 스텝

Sampling steps

노이즈 이미지를 최종 그림으로 다듬어가는 반복 횟수. 스텝이 늘수록 디테일이 살아나지만 어느 지점부터는 차이가 거의 없어집니다.

다른 이름: Inference stepsDenoising steps
···
html
<div class="wrap">
  <canvas id="cv"></canvas>
  <div class="panel">Step <b id="s">0</b> / <b id="tot">24</b></div>
</div>
css
.wrap{position:relative;width:100%;height:100%}
#cv{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated}
.panel{position:absolute;top:10px;left:10px;z-index:2;padding:6px 10px;border-radius:10px;
  background:rgba(13,13,18,0.55);backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,0.15);
  font-family:ui-monospace,monospace;font-size:11.5px;color:#f1f0ec}
.panel b{color:var(--accent)}
js
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const sEl = document.getElementById('s');
const SW = 48, SH = 30;
const small = document.createElement('canvas'); small.width = SW; small.height = SH;
const sctx = small.getContext('2d');
function fit() { const dpr = Math.min(devicePixelRatio || 1, 2); cv.width = innerWidth * dpr; cv.height = innerHeight * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.imageSmoothingEnabled = false; }
addEventListener('resize', fit); fit();

function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }

function paintTarget() {
  const r = mulberry32(15);
  const g = sctx.createLinearGradient(0, 0, 0, SH);
  g.addColorStop(0, 'hsl(200 55% 65%)'); g.addColorStop(1, 'hsl(210 45% 30%)');
  sctx.fillStyle = g; sctx.fillRect(0, 0, SW, SH);
  sctx.beginPath(); sctx.arc(SW * 0.7, SH * 0.25, SH * 0.14, 0, Math.PI * 2); sctx.fillStyle = 'hsl(45 90% 75%)'; sctx.fill();
  for (let l = 0; l < 2; l++) {
    sctx.beginPath(); sctx.moveTo(0, SH);
    for (let i = 0; i <= 6; i++) { const x = SW * i / 6; const y = SH * (0.5 + l * 0.18) - r() * SH * 0.14; sctx.lineTo(x, y); }
    sctx.lineTo(SW, SH); sctx.closePath();
    sctx.fillStyle = 'hsl(150 40% ' + (18 + l * 12) + '%)'; sctx.fill();
  }
}
function noiseCanvas(seed) {
  const r = mulberry32(seed);
  const tmp = document.createElement('canvas'); tmp.width = SW; tmp.height = SH;
  const tctx = tmp.getContext('2d');
  const id = tctx.createImageData(SW, SH);
  for (let i = 0; i < id.data.length; i += 4) { const v = Math.floor(r() * 255); id.data[i] = v; id.data[i + 1] = v; id.data[i + 2] = v; id.data[i + 3] = 255; }
  tctx.putImageData(id, 0, 0);
  return tmp;
}
function drawStep(k, N) {
  paintTarget();
  const f = 1 - k / N;
  if (f > 0.01) { sctx.globalAlpha = f; sctx.drawImage(noiseCanvas(1000 + k), 0, 0); sctx.globalAlpha = 1; }
  ctx.clearRect(0, 0, innerWidth, innerHeight);
  ctx.drawImage(small, 0, 0, innerWidth, innerHeight);
}
const N = 24;
let k = 0;
function tick() {
  drawStep(Math.min(k, N), N);
  sEl.textContent = Math.min(k, N);
  const delay = k > N ? 1300 : 130;
  k++;
  if (k > N + 1) k = 0;
  setTimeout(tick, delay);
}
tick();

생성은 한 번에 끝나는 연산이 아니라 반복문입니다. 각 스텝에서 샘플러는 모델이 예측한 노이즈를 현재 이미지에서 조금 덜어내고, 그 결과를 다음 스텝의 입력으로 넘깁니다. 스텝 수는 이 반복을 몇 번 돌릴지 정하는 값입니다.

스텝이 너무 적으면 노이즈를 충분히 걷어내지 못해 거칠고 흐릿한 결과가 나옵니다. 스텝을 늘리면 디테일이 점점 살아나지만, 각 스텝이 더해주는 개선폭은 갈수록 줄어드는 수확 체감 구간에 들어갑니다 — 스텝을 배로 늘려도 이미지가 두 배 좋아지지는 않습니다.

같은 스텝 수라도 샘플러(반복 알고리즘)가 다르면 "충분한" 지점이 달라집니다. 계산량은 스텝 수에 거의 비례해서 늘어나므로, 체감 품질과 소요 시간 사이에서 스텝 수를 고르는 절충이 됩니다.

아래 데모는 실제 샘플러 대신, 작은 해상도로 그린 목표 그림 위에 시드 기반 노이즈를 스텝이 진행될수록 점점 옅게 겹쳐서 "노이즈가 걷히며 그림이 드러나는" 과정을 흉내 낸 시뮬레이션입니다.

언제 쓰나

빠른 초안을 보고 싶을 때는 스텝을 줄이고, 최종 결과물을 뽑을 때는 늘립니다. 한계 이상으로 계속 늘리는 건 시간만 쓰고 눈에 띄는 개선은 없을 수 있습니다.