포스트

포즈 유지 이미지 생성, 콜랩 vs 로컬 투 트랙으로 붙어본 기록

포즈 유지 이미지 생성, 콜랩 vs 로컬 투 트랙으로 붙어본 기록

오늘 과제는 “참조 사진에서 사람 포즈만 뽑아서, 그 포즈는 유지한 채 완전히 다른 인물/장면으로 이미지를 생성하기”였다. 과제 원문은 예시 모델로 FLUX.2-klein 4B를 들고 있었다. 같은 시간에 두 세션이 각자 다른 환경에서 이 문제에 붙었다 — 하나는 Claude Desktop이 Google Colab을 자동으로 조작해서 진행했고, 하나는 이 Claude Code 세션이 로컬 Apple Silicon Mac에서 진행했다. 콜랩 트랙은 4시간 넘게 붙잡고도 이미지 한 장을 못 뽑았고, 로컬 트랙은 성공했다. 콜랩 세션의 실제 대화 로그를 직접 열어서 어디서 왜 막혔는지 확인했다.

콜랩 트랙 — 왜 4시간 동안 이미지 한 장을 못 뽑았나

콜랩 세션은 19:31에 시작해서 23:49에 끝났다(정확히 4시간 19분). 로그를 순서대로 읽어보면 막힌 지점이 명확하다.

1) Colab에는 “셀을 실행해주는” 공식 API가 없다. 이 세션은 처음에 colab-mcp라는 전용 연결 도구로 붙으려 했지만 두 번 다 연결 시간초과로 실패했다. Google Colab 자체가 노트북 조작을 위한 공식 API를 제공하지 않기 때문에, 남은 방법은 브라우저를 직접 흉내내서 마우스로 클릭하고 키보드로 타이핑하는 것뿐이었다.

2) 그 브라우저 창이 사용자 화면에 보이지 않는(hidden) 상태로 백그라운드에서 돌았다. 이게 진짜 원인이다. Colab은 웹페이지 탭이 “보이지 않음”으로 판단되면 브라우저 탭이 비활성 상태라고 보고, 리소스 절약을 위해 몇 분 안에 GPU 런타임 연결을 끊어버린다. 화면에 아무것도 안 보이니 사용자가 손대지 않는 탭 = 안 쓰는 탭이라고 판단하는 것이다. 세션 로그에는 이 문제를 스스로 발견하고 보고한 대목이 남아 있다.

“지금 자동화 브라우저 창(Browser pane)이 화면에 보이지 않는 상태(hidden)로 백그라운드에서 돌고 있는데, 이 상태에서 Colab이 탭을 ‘비활성’으로 판단해서 몇 분마다 런타임 연결이 계속 끊기고 있습니다. 끊길 때마다 GPU 세션이 통째로 초기화돼서, 지금까지 3번이나 설치·업로드 작업이 날아갔습니다.”

3) 끊길 때마다 처음부터 다시. GPU 재할당 → 라이브러리 재설치(~20초) → 모델 재다운로드(SD1.5+ControlNet, ~5GB, 2~4분) → 포즈 추출 → 이미지 생성, 이 순서를 통째로 다시 밟아야 했다. 이게 이 세션에서만 최소 3번 반복됐다. 게다가 브라우저 자동화로 셀에 여러 줄 코드를 “타이핑”하다 보니, Colab 에디터(Monaco)의 자동 들여쓰기 기능과 충돌해서 코드가 중간에 깨지는 문제도 반복적으로 발생했다 — 이건 나중에 좌표 클릭 대신 Monaco 에디터의 JS API를 직접 호출하는 방식으로 우회했다.

4) 결국 마지막 메시지는 “사용자가 직접 실행해달라”는 제안이었다. 세션이 끝나기 직전 마지막 메시지에서 스스로 이렇게 정리했다.

“제가 완성된 노트북 코드를 지금 바로 드리면, 직접 Colab에 붙여넣고 ‘런타임 → 모두 실행’만 누르시면 됩니다. 사용자가 직접 보고 있는 탭이라 끊길 위험도 없고, 다운로드+생성까지 아마 4~5분이면 끝납니다.”

즉 자동화가 실패한 이유는 코드가 틀려서가 아니라, “화면에 보이지 않는 탭은 비활성으로 간주해 끊는다”는 Colab의 정책과 “화면 없이 조작해야 하는” 브라우저 자동화 방식이 정면으로 충돌했기 때문이다. 코드 자체는 세션이 끝날 때쯤엔 문법적으로 완성돼 있었다 — 그걸 사람이 보고 있는 탭에서 “런타임 → 모두 실행” 한 번만 눌러줬으면 4~5분 만에 끝났을 일이었다.

로컬 Mac 트랙 — 쓴 도구는 두 개, 서로 다른 이유로

여기서부터는 콜랩을 아예 쓰지 않고 이 맥북에서 전부 실행했다. 다만 도구가 중간에 한 번 바뀐다 — 헷갈리기 쉬운 부분이라 명확히 구분해서 적는다.

도구 1: ComfyUI (텍스트→이미지, ControlNet 없이)

첫 삽질은 ollama run x/flux2-klein:4b였는데, Ollama가 v0.32.6부터 이미지 생성 러너 자체를 (일시적으로) 꺼놔서 바로 막혔다. ComfyUI로 갈아타서 Flux.2 Klein 4B(distilled) 모델을 로드해 순수 텍스트→이미지 생성까지는 금방 성공했다. 이 단계에는 ControlNet도, 포즈 참조도 없었다 — 그냥 프롬프트만으로 이미지 한 장을 만든 것이다.

도구 2: DiffSynth-Studio (포즈 유지 ControlNet 생성) — ComfyUI가 아니다

참조 사진과 같은 포즈로 다른 인물을 만들려면 ControlNet이 필요했는데, ComfyUI에는 FLUX.2-klein용 ControlNet을 붙이는 방법 자체가 없었다. 콜랩 세션이 헤맨 것과 정확히 같은 지점이다. 차이는 확인 방법이었다 — Hugging Face Hub API를 직접 두드려서(api/models?search=...) 5분 만에 “공식 ControlNet 없음, 대신 커뮤니티(ModelScope)가 만든 DiffSynth-Studio/Template-KleinBase4B-ControlNet이 있음”을 확인했다.

이걸 쓰려면 ComfyUI가 아니라 DiffSynth-Studio라는 완전히 별도의 Python 라이브러리로 갈아타야 했다. 그래서 이 시점부터 파이프라인은:

1
2
3
4
참조 사진 → OpenPose(controlnet_aux) → 스켈레톤 이미지
        → DiffSynth-Studio Flux2ImagePipeline
          + TemplatePipeline(Template-KleinBase4B-ControlNet)
        → 프롬프트별 이미지 3장

여기서도 순탄하지만은 않았다.

  • FLUX.2-klein-base-4B 트랜스포머(7.75GB) + 텍스트 인코더(8.04GB) + VAE + ControlNet 가중치(7.75GB), 총 24GB 다운로드.
  • VAE 인코딩이 16의 배수 해상도를 요구하는 걸 모르고 첫 실행에서 바로 einops 에러로 죽었다 — 리사이즈 한 줄로 해결.
  • 참조 사진(팔다리가 심하게 겹치는 극단적인 무용 동작)에서 기본 OpenPose가 양쪽 팔꿈치·손목을 아예 못 찾았다. 부분 스켈레톤인 채로 일단 생성해봤더니, 프롬프트에 portrait류 정적 구도 단어가 섞여 있으니 포즈 조건이 통째로 무시되고 평범한 정면 인물사진이 나왔다.
  • 자세를 프롬프트에 직접 구체적으로 서술하고(“한 팔은 하늘로, 다른 팔은 구부린 다리를 감싸고, 상체는 앞으로 숙이고…”) 스텝을 12→25로 늘리니, 발레리나·노년 남성·로봇 세 장 모두 원하는 역동적 포즈로 나왔다.
  • 다만 “원하는 포즈로 나왔다”고 완전히 똑같은 자세라는 뜻은 아니다. 두 가지 이유로 원본과는 미묘하게 다르다. 첫째, 애초에 스켈레톤에 팔꿈치·손목 좌표가 없었으니 ControlNet 입장에서는 “팔을 정확히 어느 각도로 굽힐지”를 강제할 조건 자체가 없었다 — 그 부분은 프롬프트 텍스트 서술에만 의존했다. 둘째, 이 ControlNet(Template-KleinBase4B-ControlNet)은 참조 이미지를 픽셀 단위로 강제하는 고전적 방식이 아니라 KV-cache로 인코딩해 attention에 얹는 방식이라 구속력이 원래 약하다 — 그래서 발레리나 결과물은 다리를 얼굴 쪽으로 들어 올린 자세로 나왔는데, 원본은 다리를 낮게 감싸 안은 자세였다. “같은 역동성을 재현”하는 수준이지 포즈를 픽셀 단위로 복제하는 수준은 아니었다.

왜 투 트랙이 결과적으로 맞았나

  1. 같은 벽에 두 세션이 독립적으로 부딪혔다는 사실 자체가 신호다. 둘 다 “FLUX.2-klein 4B + ControlNet”이라는 조합에서 막혔다는 건, 내가 뭘 잘못 설정해서가 아니라 이 생태계에 실제로 구멍이 있다는 뜻이다.
  2. 조사 방법이 결과를 갈랐다. 콜랩 세션은 이 사실을 세션 초반(작업 시작 후 약 2분 만에)에 이미 알아냈지만, 그 뒤로는 “브라우저 자동화로 실제 실행까지 끝내기”라는 훨씬 더 어려운 문제에 발이 묶였다. 나는 같은 사실을 API로 확인한 뒤, 로컬 파이썬 환경에서 코드를 직접 실행할 수 있었다 — 실행 환경 자체의 난이도가 달랐다.
  3. 콜랩 자동화의 근본적인 한계. Google Colab은 노트북을 코드로 조작할 공식 API가 없어서, 결국 “화면을 보고 클릭하는” 방식으로 우회해야 한다. 그런데 그 자동화 창이 사용자 화면에서 안 보이면 Colab이 비활성 탭으로 판단해 런타임을 끊어버린다 — 사람이 안 보고 있어도 되는(headless) 자동화와, “누군가 보고 있어야 안 끊기는” Colab의 설계가 정면으로 충돌한 것이다. 로컬 실행은 이런 제약 자체가 없어서 한 번 모델을 받아두면 몇 시간이고 끊기지 않고 반복 실험할 수 있었다.

다음에 해볼 것

  • 콜랩 쪽은 (코드 자체는 완성됐으니) 사람이 직접 탭을 보면서 “런타임 → 모두 실행”으로 한 번 돌려서 로컬 결과와 나란히 비교해보기.
  • DWPose로 극단적인 포즈에서 팔·손 관절까지 잡아보기 (mmcv/mmdet/mmpose 설치가 Mac에서 까다로워서 이번엔 보류했다).
  • 결과물과 코드는 pose_image_tool/에 정리해서 PR로 올려뒀다.

업데이트 (2026-09-21)

  • 과제는 aiffel_work 레포 안 pose_image_tool/ 폴더로 이미 제출했는데, 원래 이 도구는 따로 관리하고 싶었던 거라 별도 저장소로 옮겼다: pose-image-tool
  • 옮기면서 빠져 있던 걸 하나 채웠다 — 그동안 samples/에는 포즈에서 뽑아낸 스켈레톤(pose_01.png)과 생성 결과만 있고, 정작 비교 기준이 되는 원본 사진이 없었다. 원본 사진(original_01.jpg)을 추가해서 “원본 → 스켈레톤 → 생성 결과 3장”을 한 화면에서 바로 비교할 수 있게 README에 표로 정리했다.
  • 제출한 aiffel_work 쪽(제출 브랜치·main)은 그대로 두고 링크만 살렸다. 두 곳 모두 README 맨 위에 “최신 버전은 새 저장소에서 확인” 안내를 달아서, 제출 주소는 그대로 유효하면서도 이후 작업은 새 레포에서 이어가는 구조로 정리했다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.