컨트롤넷

ControlNet

엣지·포즈 같은 구조 정보를 담은 보조 이미지를 함께 넣어서, 생성물의 구도·형태를 원하는 대로 고정하는 방법.

다른 이름: Structural conditioningPose/edge conditioning
···
html
<div class="wrap">
  <canvas id="cv"></canvas>
  <span class="tag left">컨트롤 맵 (포즈 스켈레톤)</span>
  <span class="tag right" id="rtag">생성 결과: 스타일 A</span>
</div>
css
.wrap{position:relative;width:100%;height:100%}
#cv{position:absolute;inset:0;width:100%;height:100%}
.tag{position:absolute;top:8px;z-index:2;font-size:9.5px;font-weight:700;color:#f1f0ec;
  padding:4px 8px;border-radius:8px;background:rgba(13,13,18,0.55);border:1px solid rgba(255,255,255,0.15)}
.left{left:8px}
.right{right:8px}
js
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const rtag = document.getElementById('rtag');
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); }
addEventListener('resize', fit); fit();

const joints = { head: [0.5, 0.18], neck: [0.5, 0.32], lsh: [0.36, 0.35], rsh: [0.64, 0.35], lel: [0.28, 0.5], rel: [0.72, 0.5], lhip: [0.42, 0.58], rhip: [0.58, 0.58], lknee: [0.4, 0.76], rknee: [0.6, 0.76], lft: [0.38, 0.94], rft: [0.62, 0.94] };
const bones = [['head', 'neck'], ['neck', 'lsh'], ['neck', 'rsh'], ['lsh', 'lel'], ['rsh', 'rel'], ['neck', 'lhip'], ['neck', 'rhip'], ['lhip', 'rhip'], ['lhip', 'lknee'], ['rhip', 'rknee'], ['lknee', 'lft'], ['rknee', 'rft']];

function drawSkeleton(offsetX, halfW, h, opts) {
  ctx.lineWidth = opts.lw; ctx.lineCap = 'round'; ctx.strokeStyle = opts.stroke;
  if (opts.glow) { ctx.shadowColor = opts.stroke; ctx.shadowBlur = opts.glow; }
  for (const [a, b] of bones) {
    const ja = joints[a], jb = joints[b];
    ctx.beginPath(); ctx.moveTo(offsetX + ja[0] * halfW, ja[1] * h); ctx.lineTo(offsetX + jb[0] * halfW, jb[1] * h); ctx.stroke();
  }
  ctx.shadowBlur = 0;
  ctx.fillStyle = opts.dotColor;
  for (const k in joints) { const j = joints[k]; ctx.beginPath(); ctx.arc(offsetX + j[0] * halfW, j[1] * h, opts.dot, 0, Math.PI * 2); ctx.fill(); }
}
const themes = [
  { name: '스타일 A', stroke: 'hsl(320 80% 62%)', lw: 7, dot: 5, dotColor: 'hsl(320 90% 78%)', glow: 10 },
  { name: '스타일 B', stroke: 'hsl(160 70% 52%)', lw: 7, dot: 5, dotColor: 'hsl(160 80% 74%)', glow: 10 },
  { name: '스타일 C', stroke: 'hsl(42 90% 55%)', lw: 7, dot: 5, dotColor: 'hsl(42 95% 74%)', glow: 10 },
];
let ti = 0;
function render() {
  const w = innerWidth, h = innerHeight;
  ctx.clearRect(0, 0, w, h);
  drawSkeleton(0, w * 0.5, h, { lw: 2.5, stroke: 'rgba(255,255,255,0.85)', dot: 3.5, dotColor: '#fff' });
  drawSkeleton(w * 0.5, w * 0.5, h, themes[ti]);
  ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(w * 0.5, 0); ctx.lineTo(w * 0.5, h); ctx.stroke();
  rtag.textContent = '생성 결과: ' + themes[ti].name;
}
render();
setInterval(() => { ti = (ti + 1) % themes.length; render(); }, 2400);

프롬프트만으로는 "무엇을" 그릴지는 정할 수 있어도 "어디에, 어떤 형태로" 배치할지는 정확히 통제하기 어렵습니다. 컨트롤넷은 참조 이미지에서 뽑아낸 구조 정보 — 윤곽선, 관절 위치를 잇는 뼈대(포즈), 깊이 지도 등 — 를 별도의 조건 경로로 모델에 함께 넣어서, 그 구조를 유지하도록 학습된 추가 네트워크가 생성 과정을 계속 이 구조 쪽으로 붙잡아 둡니다.

이미지-투-이미지와 다른 점은, 넘기는 게 색과 질감을 가진 완전한 이미지가 아니라 선·점 같은 추상적인 구조 정보뿐이라는 것입니다. 그래서 레이아웃은 그대로 유지하면서 스타일과 내용은 프롬프트가 자유롭게 정하게 둘 수 있습니다.

구조 지도가 담고 있지 않은 부분(색, 질감, 배경)은 여전히 프롬프트와 모델이 채웁니다. 다만 구조를 너무 강하게 강제하면, 그 구조가 프롬프트 내용과 자연스럽게 맞지 않을 때 결과가 어색하게 뒤틀릴 수 있습니다.

아래 데모는 실제 컨트롤넷 대신, 같은 관절 좌표(스켈레톤)를 왼쪽엔 흰 선으로, 오른쪽엔 색을 바꿔가며 그려서 "구조는 고정, 스타일만 바뀐다"를 절차적으로 흉내 낸 시뮬레이션입니다.

언제 쓰나

레이아웃·구도·포즈를 정확히 맞춰야 하는데 프롬프트만으로는 반복해서 어긋날 때 씁니다. 구조 자체가 중요하지 않은 자유로운 생성에는 불필요한 제약이 될 수 있습니다.