상세 포트폴리오

AR 사진관 — 실시간 AI 합성 포토부스

2024–2026 · 국내 퍼리 컨벤션 7개 행사 운영 · 기획/개발/운영 단독 수행

저장소: https://github.com/auejin/mirror-selfie


목차

  1. Executive Summary
  2. 문제 정의
  3. 시스템 아키텍처
  4. 기술 하이라이트
  5. 프로덕트 기획과 반복
  6. 현장 운영과 브랜드 지속
  7. 정량 임팩트
  8. 사고와 구조적 해결
  9. 한계와 로드맵
  10. 부록 — 기술 스택

증거 등급 규약

이 문서의 모든 수치에는 근거 등급이 붙는다.

등급 의미
[검증] 코드·파일·git에서 직접 산출. 재현 명령이 존재
[운영기록] 운영자가 행사 가계부에 기록한 실측치. 제3자 재현 불가
[히스토리] git 커밋 이력에서 추론

등급이 낮다고 수치를 빼지 않았고, 높은 것처럼 쓰지도 않았다.


1. Executive Summary

AR 사진관은 웹캠 한 대로 전신 코스튬 착용자를 배경에서 분리하고, 캐릭터 일러스트 레이어 사이에 합성해 그 자리에서 인화하는 무인 키오스크다. 전 과정이 온디바이스에서 실행되며 네트워크를 필요로 하지 않는다.

2024년 8월부터 2026년 4월까지 7개 컨벤션에서 703장을 촬영했고 [운영기록], 2026년 두 행사는 앱이 원본·마스크·합성본을 자동 저장해 141세트가 남아 있다 [검증]. 1일 최장 가동 시간은 7시간 53분, 촬영 간격 중앙값은 2분 13초[검증].

이 문서가 다루는 것은 모델 정확도가 아니라 제약 조건 아래의 설계 판단이다. 참가자가 화면을 조작할 수 없다는 제약이 프롬프트 자동 생성 구조를 만들었고, 행사장에 인터넷이 없다는 제약이 온디바이스 아키텍처를 만들었으며, 그 아키텍처가 결과적으로 프라이버시 정책과 일치했다.


2. 문제 정의

2.1 도메인 컨텍스트

퍼리 팬덤 / 퍼슈트 / 컨벤션에 대해

퍼리 팬덤은 의인화된 동물 캐릭터를 중심으로 하는 창작·코스튬 문화다. 퍼슈트(fursuit)는 이 캐릭터를 구현한 전신 코스튬으로, 머리 전체를 덮는 조형 헤드와 인조모피 재질의 몸체로 구성된다. 국내에서는 연 여러 차례 컨벤션이 열리며, 참가자 상당수가 퍼슈트를 착용하고 참여한다.

이 문서에서 이 도메인이 중요한 이유는 문화적 맥락이 아니라 기술적 성격 때문이다. 퍼슈트는 인물 분리(human matting) 모델에게 자연 발생한 적대 샘플이다.

2.2 왜 만들었나

컨벤션 참가자들은 자신의 퍼슈트 사진을 남기고 싶어 하지만, 전문 촬영은 예약·대기· 후보정 리드타임을 수반한다. 그 자리에서 결과물을 받아가는 경험은 별개의 가치다.

상용 포토부스는 이 요구의 절반만 채운다. 촬영과 즉석 인화는 되지만 참가자의 캐릭터와 어울리는 배경·소품 합성이 불가능하다. 크로마키 부스는 배경 합성은 되지만 부스 면적, 설치 시간, 조명 통제를 요구하는데 컨벤션 부스는 그 어느 것도 통제할 수 없는 환경이다.

2.3 무엇이 어려웠나

(a) 퍼슈트는 범용 인물 분리 모델을 무너뜨린다

범용 모델이 의존하는 단서가 거의 전부 무효화된다:

모델이 기대하는 것 퍼슈트에서 실제
인체 통계에 맞는 실루엣 머즐·귀·꼬리로 윤곽이 이탈
선명한 피부/의복 경계 인조모피가 픽셀 단위로 반투명
무광 표면 광택 소재가 배경을 반사
얼굴·피부색 단서 전신 착용으로 전무
일관된 조명 컨벤션장 혼합 조명

(b) 조작할 사람이 없다

참가자는 조형 헤드를 쓰고 있어 시야가 제한되고 손 조작이 어렵다. 운영자는 다음 참가자를 응대 중이다. 즉 파이프라인 어디에도 사람이 개입할 수 없다.

(c) 현장은 개발 환경의 가정을 전부 깬다

인터넷 없음, 카메라가 여러 개로 잡힘, 창이 실수로 닫힘, 프린터가 용지를 뭄. 그리고 이 모든 일이 대기열이 있는 상태에서 발생한다.

(d) 정답이 없는 품질 문제

합성이 자연스러운지는 자동 지표로 측정되지 않는다. IoU가 높아도 귀 끝이 잘리면 참가자는 실망한다. 평가 함수가 사람뿐이라는 뜻이다.


3. 시스템 아키텍처

3.1 전체 구조

graph TB
    subgraph Entry["진입점"]
        Main["main.py · Application<br/>tkinter + cv2 듀얼 루프"]
    end
    subgraph Cam["카메라 계층"]
        ICamera["ICamera (ABC)"] --> WC["Webcam<br/>4K · OS별 백엔드 선택"]
    end
    subgraph AI["AI 파이프라인"]
        DEPTH["DepthEstimator<br/>Depth Anything V2 Small"]
        SEG["SAM2ImageProcessor<br/>SAM 2.1 Tiny"]
        DEPTH -->|"rough mask + smart points"| SEG
    end
    subgraph Comp["합성 계층"]
        MIRROR["MirrorCompositor<br/>레이어 합성 + 색 조화"]
        CV["OpenCVUtils<br/>UMat/numpy 추상화"]
        MIRROR --> CV
    end
    PRINTER["BasePrinter<br/>CUPS / Win32"]

    Main --> ICamera
    Main --> DEPTH
    Main --> SEG
    Main --> MIRROR
    Main --> PRINTER

3.2 세그멘테이션 파이프라인 — depth가 프롬프트를 만든다

flowchart LR
    A["RGB 프레임<br/>4K"] --> B["1024px 다운스케일"]
    B --> C["Depth Anything V2<br/>깊이 추정"]
    C --> D["suppress_depth_gradient<br/>바닥·벽 저주파 제거"]
    D --> E["get_rough_mask<br/>depth 0.7 + 중심가중 0.3<br/>→ Otsu → 연결성분"]
    E --> F["FPS 샘플링<br/>FG/BG 포인트"]
    E --> G["rough mask"]
    F --> H["SAM 2.1<br/>hybrid prompt"]
    G --> H
    H --> I["합집합"]
    G --> I
    I --> J["depth gradient<br/>엣지 정제"]
    J --> K["4K 업스케일 마스크"]

핵심은 SAM 2가 요구하는 프롬프트를 depth가 대신 만든다는 점이다. SAM 2는 promptable 모델이지만, 무인 부스에는 프롬프트를 줄 사람이 없다.

3.3 스레딩 모델 — 프리뷰와 저장의 분리

sequenceDiagram
    participant T0 as 메인 스레드
    participant T3 as Detection 스레드
    participant D as 화면/저장

    loop 매 프레임
        T0->>T0: get_frame()
        T0->>T0: _pending_result 소비 (lock)
        alt 이전 detection 완료
            T0->>T3: _run_detection_async 시작
            T3->>T3: depth → points → SAM2 → 정제
            T3->>T0: _pending_result 적재 (lock)
        end
        T0->>D: 캐시 마스크로 합성 → imshow
    end

    Note over T0,D: 촬영 버튼
    T0->>T3: join(timeout=5s)
    T0->>T0: 현재 프레임 동기 재추론
    T0->>D: 픽셀 정확 합성본 저장 + 인쇄

프리뷰 경로와 저장 경로가 분리되어 있다. 프리뷰 마스크는 최대 1프레임 낡을 수 있고, 이것은 의도적으로 허용한 비용이다. 포즈를 잡는 중에는 오차가 인지되지 않는다.

3.4 레이어 합성 구조

배경 (background.png)
  └ 뒤 캐릭터 레이어      ← 피사체 뒤에 놓이는 일러스트
      └ 인물 컷아웃        ← 마스크로 분리된 참가자 (색 조화 적용)
          └ 공통 스티커
              └ 앞 캐릭터 레이어  ← 피사체를 가리는 일러스트

캐릭터 에셋을 앞/뒤 두 장으로 나눠 그려야 이 구조가 성립한다. 소프트웨어 아키텍처가 그림 작업 방식을 규정하고, 그림의 구도가 다시 세그멘테이션 파라미터(center_ratio)를 규정한다. 5장에서 다시 다룬다.


4. 기술 하이라이트

4.1 프롬프트 자동 생성 — 사람을 파이프라인에서 제거하기

문제. SAM 2는 점·박스·마스크 프롬프트를 받아 임의 객체를 분할한다. 클래스 학습이 필요 없다는 게 장점이지만, 뒤집으면 누군가 프롬프트를 줘야 한다는 뜻이다. 무인 부스에는 그 사람이 없다.

해결. 단안 깊이 추정을 프롬프트 생성기로 사용했다.

  1. Depth Anything V2 Small로 깊이 맵 추정
  2. suppress_depth_gradient() — 바닥·벽처럼 서서히 변하는 저주파 성분을 제거. 가우시안 블러로 저주파를 뽑아 빼고 백분위 [2,98]로 재정규화
  3. get_rough_mask() — 깊이 0.7 + 중심 가우시안 가중 0.3의 혼합 스코어에 Otsu 임계화, 모폴로지 정리 후 중심 연결성분만 채택(바닥·벽 제거), LAB 색거리 기반 확장
  4. rough mask 내부에서 FPS(Farthest Point Sampling) 로 전경 포인트를 뽑고, 이미지 경계 8점을 배경 포인트로 고정
  5. rough mask(logits) + 포인트를 SAM 2.1에 hybrid prompt로 투입

트레이드오프. 모델을 하나 더 얹어 지연이 늘었다. 대신 신장·포즈·코스튬 종류가 바뀌어도 사람 개입 없이 적응한다. 늘어난 지연은 4.2의 비동기 구조로 상쇄했다.

고정 좌표 프롬프트라는 더 싼 선택지가 있었다. 기각한 이유는 참가자 신장 편차와 포즈 자유도 때문이다. 부스의 가치는 참가자가 원하는 포즈를 취하는 데 있는데, 고정 프롬프트는 그 자유도를 그대로 실패율로 바꾼다.

근거: utils/depth.py, utils/segment.py, main.py:_run_auto_detect, 커밋 b7d9114, 06c7856, 75546e4

4.2 비동기 더블 버퍼 — 정확도를 필요한 순간에만

문제. Depth + SAM2 + 정제를 매 프레임 동기 실행하면 프리뷰가 끊긴다. 추론 주기를 늦추면 촬영 순간의 마스크가 이전 포즈의 것이 되어 결과물에서 팔이나 귀가 잘린다. 두 요구가 시간 척도가 다른데 같은 경로에 있었다.

해결. 경로를 분리했다.

  • 메인 루프: 캐시 마스크로 즉시 합성 → imshow. detection 스레드가 놀고 있으면 새 작업 투입, 완료된 결과는 lock 하에 소비
  • 촬영 시: _flush_detection() — 진행 중 스레드를 join(timeout 5초)하고, 대기 중 결과를 소비한 뒤, 현재 프레임에 대해 동기 추론을 1회 더 실행하고 그 마스크로 재합성해 저장

트레이드오프. 프리뷰 마스크가 1프레임 낡는다. 포즈를 잡는 중에는 인지되지 않고, 결정적 순간인 촬영에서만 정확도 비용을 지불한다.

부수 효과. 스레드가 6종(메인, 세그멘터 로딩, 깊이 로딩, detection, 에셋 로딩, 파일 쓰기, 인쇄)으로 늘면서 공유 상태 보호가 문제가 됐다. 특히 _smart_points/_rough_mask 캐시는 lock이 아니라 구조적 직렬화로 보호된다 — 같은 스레드 안에서만 읽고 쓰며, _flush_detection은 join 이후에만 실행된다. 이 판단은 코드만 봐서는 재구성되지 않으므로 .agent/knowledge/threading-model.md에 명문화했다.

근거: main.py:253-357, .agent/knowledge/threading-model.md, 커밋 6837615, 3bf6ddf

4.3 해상도 분리 — 인쇄 품질과 실시간성 양립

인쇄에는 4K가 필요하지만 4K로 추론하면 실시간이 불가능하다. 저해상도 마스크를 그냥 확대하면 경계에 계단이 생겨 인쇄물에서 즉시 보인다.

마스크만 긴 축 1024px로 계산하고 픽셀은 4K를 유지했다. 업스케일 계단은 다음 항목에서 해결한다.

근거: main.py:33, 195-211, 커밋 026c54d

4.4 엣지 정제 — 깊이를 판별자로 재활용

문제. 업스케일 계단을 없애려 균일 블러를 걸면 귀 끝·손가락·꼬리 같은 실제 디테일도 함께 뭉개진다. 안 걸면 계단이 남는다. 어느 픽셀이 진짜 경계이고 어느 픽셀이 해상도 아티팩트인지 마스크만 봐서는 구분할 수 없다.

해결. 이미 계산해둔 깊이 맵을 판별자로 재사용했다.

  1. 깊이 맵에 Sobel을 걸어 gradient 크기를 구하고 0~1로 정규화 (강할수록 진짜 경계)
  2. 마스크를 모폴로지 Close/Open으로 스무딩한 버전을 준비
  3. erode/dilate 차집합으로 경계 밴드를 정의하고, 그 안에서만 원본 × edge_weight + 스무딩 × (1 - edge_weight) 로 블렌딩
  4. 최종 이진화 → 벡터풍 샤프 엣지 유지

깊이 불연속이 있는 곳은 원본 엣지가 보존되고, 평탄한 곳의 계단만 뭉개진다. 추가 모델 없이 기존 연산 결과를 재활용해 푼 것이 이 설계의 요점이다.

근거: main.py:374-425, 커밋 7b5a2b7

4.5 색 조화 — 3세대에 걸친 개선과 회귀 테스트

정답 이미지가 없는 문제다. 자동 지표로 검증할 수 없고, 행사장 조명은 매번 다르며, 인조모피의 반사 특성은 사람 피부와 다르다.

세대 방식 시점
1세대 LAB 색공간에서 배경 통계에 맞춰 정합 2025-10
2세대 Teal & Orange 시네마틱 LUT — 곡선 앵커 5점을 채널별로 보간해 톤커브 생성 2026-04-02
3세대 Dynamic luma matching 2026-04-04

3세대의 요점은 밝기를 선형으로 옮기지 않았다는 것이다.

  • 목표 명도 = 피사체 L 평균 × 0.4 + 배경 L 평균 × 0.6, [80, 180]로 클립
  • 감마 지수 = log(목표_정규화) / log(현재_정규화), [0.4, 2.5]로 클립
  • L 채널에 감마 적용

선형 오프셋은 어두운 부분을 뭉개거나 밝은 부분을 날린다. 감마 보정은 계조를 보존하면서 전체 명도를 옮긴다. 배경에 100%가 아니라 6:4로 맞춘 것은 "배경에 완전히 녹아들면 피사체가 죽는다"는 연출 판단이고, 두 클립은 극단 입력에서의 발산을 막는 가드레일이다.

검증 방법. scripts/test_color_grading.py가 동일 입력에 대해 원본 / 1세대 / 2세대 / 3세대 4단 비교 이미지를 생성한다. 자동 지표가 없는 문제를 사람 눈 회귀 테스트로 대체한 것이다. 새 알고리즘이 특정 조명에서 이전보다 나빠지는지를 눈으로 확인할 수 있다.

근거: utils/mirror.py:106-184, scripts/test_color_grading.py, 커밋 ba9489b3e9355f845d41e

4.6 크로스플랫폼 가속 추상화

cv2.UMat은 macOS에서 동작하지 않고, 스칼라-UMat 연산(1.0 - mask)은 Windows에서 실패한다. 이런 비대칭이 코드 전역에 흩어지면 플랫폼 하나가 조용히 깨진다.

OpenCVUtils 정적 메서드 계층으로 전 호출을 통과시키고, 직접 cv2.UMat() 호출을 금지 규칙으로 명문화했다. PyTorch 디바이스는 MPS → CUDA → CPU 순 자동 선택.

Windows macOS
OpenCV 가속 cv2.UMat (OpenCL) np.ndarray
PyTorch CUDA → CPU MPS → CPU
카메라 백엔드 CAP_MSMF CAP_AVFOUNDATION
인쇄 Win32 API lpr (CUPS)

근거: utils/opencv.py, .agent/knowledge/cross-platform-patterns.md, 커밋 b8be00e, 62a6f4e

4.7 암묵지를 에이전트가 읽는 규칙으로

스레드 6종이 모델·마스크 캐시·컴포지터 레이어를 공유하는 구조에서 "이 변수는 왜 lock이 필요 없는가"(같은 스레드 내 직렬화) 같은 판단은 코드만 봐서 재구성되지 않는다. 3개월 뒤의 본인도, AI 코딩 에이전트도 마찬가지다.

저장소에 .agent/ 트리를 만들어 총 1,371줄을 작성했다 [검증]:

디렉터리 내용 줄 수
knowledge/ 아키텍처, AI 파이프라인, 스레딩 모델, 크로스플랫폼 패턴 512
skills/ SAM2 통합, 스레드 안전성, 크로스플랫폼 OpenCV, 컴포지터 레이어 459
workflows/ 환경 설정, 실행, 캐릭터 추가, 카메라 추가, 모듈 리팩토링 400

threading-safety skill은 서술이 아니라 검사 가능한 형태다 — "절대 금지 사항" 4항과 신규 기능 추가 시 체크리스트 5항으로 구성된다.

근거: .agent/ 전체, 커밋 05ce587, 3bf6ddf, 358ab59


5. 프로덕트 기획과 반복

5.1 행사가 곧 릴리스 사이클이었다

git 커밋 61개를 행사 D-day 기준으로 분류하면 이렇게 나온다 [히스토리]:

행사 D-day D-30 ~ D-1 D-day ~ D+7
2025 FUR:RAID 2025-10-12 26 0
2026 HowlSoup 2026-01-24 16 1
2026 FurryJoA 2026-02-21 22 3
2026 FurstClass 2026-04-04 2 1

커밋 61개 중 61개(100%)가 행사 D-30 ~ D+7 구간 안에 있다.

베이스레이트를 함께 밝힌다. 저장소 기간은 2025-09-21 ~ 2026-04-04로 196일이고, 4개 행사의 D-30~D+7 창을 합치면 126일(64%) 을 덮는다. 즉 커밋이 무작위로 흩어져 있어도 64%는 이 구간에 들어간다. 실제는 100%이므로 유의미한 쏠림이지만, "100%"라는 숫자만 떼어 보면 실제보다 극적으로 읽힌다.

또한 행사 D-day 중 확정 근거가 있는 것은 3건이다 (2024 FUR:RAID 파일명, 2026 FurryJoA·FurstClass 촬영 타임스탬프). 나머지는 아카이브 파일 수정시각과 행사 폴더명으로 추정했다. ±3일 오차로는 창 판정이 뒤집히지 않지만, 추정임을 밝혀둔다.

이 프로젝트에 "한가할 때 하는 개발"은 존재하지 않는다. 모든 코드가 다음 행사라는 외부 마감에 묶여 있었고, 그것이 우선순위 결정 방식이었다.

정직하게 쓰자면 이건 리스크 높은 방식이다. 2026 HowlSoup D-1에는 9커밋이 들어갔고 그중에는 MODNet→SAM2 교체라는 아키텍처 변경이 있었다. 실패했으면 행사 당일 부스가 열리지 못했을 것이다. 완충 장치는 두 가지였다 — 롤백 가능한 단위로 쪼개 커밋했고, 이전 버전이 항상 동작하는 상태로 남아 있었다. 결과적으로 7회 모두 정상 운영됐지만, 이것을 좋은 프로세스라고 주장하지는 않는다.

5.2 무엇을 만들지보다 무엇을 안 만들지

기획 역량은 추가한 기능보다 쳐낸 것에서 드러난다. 이 저장소에는 그 증거가 있다.

(a) 수동 편집 기능을 삭제했다 — 커밋 d463b89 "remove manual editing"

초기에는 운영자가 마스크를 클릭으로 보정할 수 있었다. 정확도는 올라가지만 촬영 1건당 운영자가 붙잡히는 시간이 생긴다. 대기열이 있는 부스에서 이것은 처리량을 직접 깎는다. 자동 인식(06c7856)이 충분히 동작하는 것을 확인한 뒤 기능을 통째로 제거했다. 정확도 상한을 낮추고 처리량을 택한 결정이다.

(b) 뎁스카메라를 도입했다가 제거했다e42dc1a(2025-10) → b0e64e7(2026-02)

8장 CASE 2에서 자세히 다룬다.

(c) 완전 자동화를 고집하지 않았다 — 커밋 9b7fe70

캐릭터마다 구도가 다르니 피사체 중심 위치도 다르다. 이것을 자동 추정하려면 또 하나의 문제를 풀어야 한다. 대신 CHARACTER_CONFIG에 캐릭터별 center_ratio_x/y를 상수로 두었다. 9종 × 2개 값 = 18개 숫자로 끝나는 문제를 알고리즘으로 풀지 않았다.

5.3 사용자 피드백이 코드가 되는 경로

행사 아카이브에는 행사별 후기 폴더가 194개 파일로 남아 있다 [검증].

행사 후기 파일
2024 FUR:RAID 36
2024 HowlSoup 28
2025 FurryJoA 97
2026 HowlSoup 10
2026 FurryJoA 23

피드백이 코드로 이어진 경로가 커밋에 남아 있다:

현장에서 드러난 것 이어진 커밋 시점
행사장에 인터넷이 없다 de9d569 오프라인 모드 2026 HowlSoup D+0
macOS가 아이폰을 카메라로 잡는다 b3d2b61 Continuity Camera 배제 2026 FurryJoA D+2
현장 조명에서 합성이 뜬다 845d41e dynamic luma matching 2026 FurstClass D+0
모델 로딩 중 크래시 25f3b6a 로딩 버그 수정 2026 FurryJoA D+0

행사 당일에 커밋이 찍혔다는 것은 부스를 운영하면서 문제를 발견하고 그 자리에서 고쳤다는 뜻이다. 이 4건이 현장 피드백 루프의 가장 직접적인 증거다.

5.4 수요를 보고 공급을 늘렸다

2024 HowlSoup 촬영본을 캐릭터별로 분류한 결과 [검증]:

순위 캐릭터 촬영 점유율
1 A 42 31.1%
2 B 27 20.0%
3 C 25 18.5%
4 D 25 18.5%
5 E 10 7.4%
6 F 6 4.4%

상위 2종이 51%, 하위 2종은 합쳐도 12%다. 이 분포는 단순 촬영 기록이 아니라 어떤 에셋에 제작 시간을 더 쓸지 결정하는 제품 지표다.

이후 캐릭터는 6종 → 9종으로 늘었고, 추가 시점은 전부 특정 행사 직전이다 (6443616 "new characters for howlsoup 2026", a812ad2 캐릭터 1종 추가 — D-2).

5.5 소프트웨어가 그림을 규정하고, 그림이 파라미터를 규정한다

레이어 합성 구조(3.4절)는 캐릭터 에셋을 앞/뒤 두 장으로 나눠 그려야 성립한다. 동시에 그림의 구도가 결정되면 그에 맞춰 세그멘테이션의 center_ratio를 조정해야 한다.

디자이너와 개발자가 나뉘어 있다면 이 왕복은 며칠이 걸린다. 단독 수행이라 왕복이 0이었고, 그래서 행사 D-2에 새 캐릭터를 투입할 수 있었다.

품질 상한이 존재한다는 대가는 인정한다. 전문 일러스트레이터가 그렸다면 더 나았을 에셋이 있고, 전담 개발자가 있었다면 더 나은 구조가 있었을 것이다. 단독 수행은 품질 상한을 감수하고 반복 속도를 얻는 교환이었다.


6. 현장 운영과 브랜드 지속

6.1 3년 7회, 그리고 부스는 매번 열렸다

행사 시기 촬영 [운영기록]
FUR:RAID 2024-08 80
HowlSoup 2024-12 120
FurryJoA 2025-02 64
FUR:RAID 2025-10 200
HowlSoup 2026-01 100
FurryJoA 2026-02 64
FurstClass 2026-04 75
합계 3년 7회 703

성장 추이로는 FUR:RAID 기준 80(2024-08) → 200(2025-10)으로 2.5배다. 다만 인과 주장은 하지 않는다. 행사 규모, 부스 위치, 운영 시간, 동시기 경쟁 부스가 통제되지 않았다. 확실한 것은 3년간 7회 연속 재참가했고 매회 부스를 유지했다는 사실이다.

6.2 부스 운영 실측

2026년 두 행사는 앱이 촬영 시 3종 파일을 자동 저장하므로 운영 지표가 직접 산출된다 [검증]:

지표 2026 FurryJoA 2026 FurstClass
촬영 건수 63 75
가동 시간 10:08 → 16:02 (5h 53m) 08:29 → 16:22 (7h 53m)
시간당 처리량 10.7건/h 9.5건/h
촬영 간격 p50 2분 13초 2분 31초
촬영 간격 p90 16분 46초 17분 25초
피크 15시 (18건) 15시 (22건)

이 수치가 증명하는 것과 증명하지 않는 것. 위 가동 시간은 첫 촬영과 마지막 촬영의 타임스탬프 간격이다. 그 사이에 부스가 계속 촬영을 받았다는 것은 증명하지만, 프로그램이 한 번도 재시작되지 않았다는 것은 증명하지 않는다. 중간에 크래시 후 재실행이 있었어도 타임스탬프 범위는 동일하게 나온다. "무중단"이 아니라 "가동 시간"으로 읽어야 정확하다.

p50 약 2.5분이 1팀당 실제 처리 시간이다 — 안내, 캐릭터 선택, 포즈, 촬영, 인쇄, 전달. 대기열이 있는 구간에서는 이것이 시스템의 처리 한계다.

p50과 p90의 격차(2.5분 vs 17분)는 시스템 불안정이 아니라 수요의 시간대 편차다. 개장 직후와 점심 전후가 한산하고 15시에 피크가 온다. 두 행사 모두 같은 패턴이다.

FurstClass는 가계부 75장과 자동저장 75세트가 완전히 일치한다. 가계부 기록 방식이 신뢰할 만하다는 교차검증 결과다.

6.3 부스 브랜딩을 직접 만들었다

행사 아카이브에는 소프트웨어 외의 산출물이 함께 남아 있다 [검증]:

부스 간판 · 메뉴판 · 문패 · A3 포스터 4종 · 명함 · 참가 카드 · 패널 · 키오스크 버튼 UI 아이콘 · 캐릭터별 얼굴 아이콘 · 굿즈 · 부스컷 템플릿 · 한국어 운영 설명서

연도별로 무엇이 추가됐는지 추적하면 부스가 어떻게 정교해졌는지 보인다: 2024 메뉴판 → 2025 참가 카드·패널 → 2026 A3 포스터 4종.

키오스크 화면의 캐릭터 선택 버튼 아이콘과 main.pychange_character() 팝업이 대응한다. 디자인과 구현을 같은 사람이 왕복했다는 증거다.

6.4 커뮤니티와의 관계 — 데이터를 얻을 자격

퍼슈트 사진은 커뮤니티에서 소유권·초상권 의식이 매우 높은 자산이다. 무단 수집은 기술적으로 쉽지만 커뮤니티 신뢰를 영구히 잃는다.

수집 절차 [운영기록]

  1. 퍼리 팬덤 커뮤니티 내에서 트위터 공개 모집
  2. 사용 목적 사전 고지 — AR 사진관 + 향후 개발할 퍼리·퍼슈트 팬덤용 AI 학습
  3. 구글 폼 opt-in 접수, 기증자가 본인 드라이브 링크 제출
  4. 부스 참가자 촬영물은 로컬 저장만, 외부 반출 없음

설계상 짚을 점 4가지

  • 크롤링이 아닌 opt-in 공개 모집 — 수집 출처의 정당성
  • 목적 고지에 미래 학습 용도까지 포함 — purpose specification
  • 기증 데이터(학습용)와 참가자 촬영물(서비스 산출물)의 취급 등급 분리
  • 참가자 촬영물 로컬 온리 원칙이 온디바이스 추론 아키텍처와 일치한다. 클라우드 API를 쓰지 않은 것은 지연·비용 최적화이자 동시에 프라이버시 설계였다 — 하나의 아키텍처 결정이 두 요구를 함께 만족시킨 사례다.

결과적으로 규모는 작지만 사용 자격이 명확한 데이터셋을 얻었다. 크롤링이었다면 며칠이면 될 일을 수개월에 걸쳐 했고, 그 대신 커뮤니티 신뢰를 얻었다.

이 문서도 그 정책을 지킨다. 기증 이미지와 참가자 촬영물은 여기에 싣지 않았다. 결과물 예시가 적은 것은 문서의 결함이 아니라 위 정책을 문서 자신에게 적용한 결과다.


7. 정량 임팩트

7.1 요약

수치 등급
운영 3년 7회, 703장 [운영기록]
가동 1일 최장 가동 7h 53m (첫 촬영~마지막 촬영) [검증]
처리량 9.5 ~ 10.7건/시간, 간격 p50 2분 13초 [검증]
데이터 동의 기반 기증 9명 1,289장 + 운영 자동수집 141세트 [검증]
개발 커밋 61개 전부 행사 D-30~D+7 창 내 [히스토리]
코드 Python 3,436줄 + 에이전트 지식문서 1,371줄 [검증]
확장 캐릭터 6종 → 9종, 신규 투입 리드타임 D-2 [검증]

7.2 유닛 이코노믹스 — 가계부 실측 [운영기록]

부스는 인쇄 1장당 5,000원을 받았다. 아래는 운영자 행사 가계부에 기록된 실지출·수입이며, 업계 추정치가 아니라 실제 거래 금액이다.

장당 경제성

항목 금액
판매가 5,000원 / 장
인화지 원가 (RP-54, 27,500원 / 54장) 509원 / 장
장당 공헌이익 4,491원 (마진 90%)

집계 범위 — 사진관 세그먼트만

같은 부스에서 굿즈·아크릴·인형 등 별도 상품군도 함께 운영했다. 그쪽 제작비와 매출은 이 손익에서 전부 제외했다. 사진관의 수익성을 보려면 사진관의 직접비와 직접 매출만 대응시켜야 한다. 아래는 AR 사진관 단일 세그먼트의 손익이다.

고정비

항목 금액 성격
배경 그림 외주 1,000,000원 1회성. AR 합성 배경 원화
뎁스카메라 317,517원 1회성. CASE 2에서 제거 → 매몰비용
웹캠 (4K) 206,520원 1회성
포토프린터 (염료승화형) 186,990원 1회성
부스 인테리어 소품 73,000원 1회성
1회성 지출 소계 1,784,027원
부스·행사 참가비 80,000원 × 7 = 560,000원 행사별. 공통비 (굿즈와 공유)
입간판 재인쇄 16,000원 × 7 = 112,000원 행사별. 사진관 직접비

단일 최대 지출은 기술이 아니라 그림이었다. 배경 원화 외주 100만원은 1회성 지출의 56%, 유료 인쇄 223장분으로, AI 장비 일체(711,027원)의 1.4배다. 파운데이션 모델은 무료로 쓸 수 있었지만 참가자가 그 앞에 서고 싶어지는 배경은 돈을 주고 사야 했다. 이 프로젝트에서 기술은 가장 싼 부품이었다.

손익분기 — 공통비 배부 기준에 따라 두 가지

배부 기준 고정비 손익분기 누적 703장 대비
A. 직접비만 (참가비는 공통비로 제외) 1,896,027원 422장 60%
B. 참가비 전액 배부 (기준값, 보수적) 2,456,027원 547장 78%

부스 면적·운영 시간의 대부분을 사진관이 썼으므로 B가 현실에 가깝고, 이 문서는 B를 기준값으로 삼는다 — 유리한 쪽이 아니라 불리한 쪽을 기준으로 잡는다.

손익분기까지 2년 5개월, 다섯 번째 행사가 걸렸다.

회차 행사 촬영 누적
1 2024-08 FUR:RAID 80 80
2 2024-12 HowlSoup 120 200
3 2025-02 FurryJoA 64 264
4 2025-10 FUR:RAID 200 464
5 2026-01 HowlSoup 100 564 ← 547장 돌파
6 2026-02 FurryJoA 64 628
7 2026-04 FurstClass 75 703

행사별 반복 비용(참가비 + 입간판 96,000원)만 보면 21장이면 회수된다. 행사별 촬영이 64~200장이므로 7개 행사 전부가 그 행사의 반복 비용은 넉넉히 넘겼다.매 행사는 처음부터 흑자였고, 초기 투자 회수에 5회가 걸린 구조다. 이것이 "매번 적자를 감수하고 버텼다"가 아니라 "매번 남는데 초기 투자가 컸다"는 뜻이며, 3년 7회 연속 운영이 가능했던 실제 이유다.

폐기한 뎁스카메라는 317,517원, 유료 인쇄 71장분이었다. AI 장비 예산(711,027원)의 45%, 1회성 지출 전체의 18%다. CASE 2에서 "정확도만 보고 도입하고 현장 운영 리스크를 의사결정에 넣지 않았다"고 쓴 판단 착오의 금전적 크기가 이것이다. 이 장비를 사지 않았다면 손익분기는 547장이 아니라 476장, 즉 한 행사 빨랐다.

누적 손익

촬영본은 인쇄 오류(5% 미만)를 제외하고 전량 유료 인쇄로 이어졌다 [운영기록]. 오류분은 인화지를 소모하지만 매출이 없으므로 보수 시나리오에 반영했다.

매출은 유료 인쇄 668~703장 × 5,000원 = 3,339,000 ~ 3,515,000원, 인화지비는 소모 703~738장 × 509원 = 358,000 ~ 376,000원이다.

배부 기준 순손익 (보수) 순손익 (상한)
A. 직접비만 +1,067,000원 +1,261,000원
B. 참가비 전액 배부 (기준값) +507,000원 +701,000원

기준값 순손익 약 51만 ~ 70만원. 3년 7회를 운영하고 남은 금액이다. 인쇄 성공률이 95%를 넘어 두 시나리오의 폭이 20만원 이내로 좁다 — 마진이 얇을수록 인쇄 신뢰성이 곧 손익이었다는 뜻이다. 실패율이 20%였다면 순손익은 절반 이하로 떨어진다.

집계 범위에서 제외한 것과 그 이유

제외 항목 이유
굿즈·아크릴·인형 제작비 별도 상품군. 자체 매출이 있으므로 사진관 손익에 섞으면 왜곡
퍼슈트 운반비·이동 잡비 증분원가가 아님. 부스를 열지 않았어도 컨벤션에 참가했을 것이므로 사진관 때문에 추가로 발생한 비용이 아니다
인건비 아래 참조

사진관 세그먼트의 직접비는 위 표에 열거된 항목으로 집계가 닫혀 있다. 추가로 드러날 항목을 남겨두지 않았다.

이 숫자를 읽을 때의 단서 2가지 — 반드시 함께 볼 것

  1. 인건비가 0으로 잡혀 있다. 기획·개발·에셋 제작·현장 운영이 전부 무보수 자기노동이다. 개발 시간만 커밋 61개 분량이고, 여기에 시장 인건비를 넣으면 순손익은 즉시 음수가 된다. 3년간 남은 51만~70만원은 노동의 대가가 아니라 현금흐름이 마이너스가 아니었다는 사실뿐이다.
  2. 이 부스의 목적은 수익이 아니었다. 위 수치는 "3년간 자기 돈을 태우지 않고 지속 가능했다" 는 것을 보여주는 것이지 사업성을 주장하는 것이 아니다. 지속 가능성이야말로 7회 연속 운영을 가능하게 한 조건이다.

7.3 대체비용 환산 (참고)

아래는 실지출이 아니라 대안과의 비교다. 모든 가정을 명시하고 범위로 제시한다.

① 상용 포토부스 렌탈 대체

7회 × 50만~80만원 = 350만 ~ 560만원 상당.

가정: 각 행사 1일 운영, 렌탈에 무제한 인화·스태프 포함, 부가세 별도. 단가는 국내 공개 시세 기준(심플큐브 50만원~, 포토키오스크 렌탈 최소 80만원).

한계 — 중요: 상용 렌탈은 캐릭터별 AR 합성이라는 핵심 기능을 제공하지 않는다. 따라서 이것은 동등 비교가 아니라 참고치다. 기능 동등성을 요구하면 커스텀 개발 견적이 되어 훨씬 커지지만, 그 값은 검증할 수 없으므로 싣지 않는다.

② 후보정 리드타임 제거

수동 합성 기준 1장당 5~15분(누끼 + 레이어 합성 + 색보정)을 가정하면 703장 × 5~15분 = 59 ~ 176시간.

가정: 포토샵 숙련자가 캐릭터 레이어 템플릿을 갖춘 상태. 장당 5분(낙관) ~ 15분(보수). 이 범위는 저자의 가정이며 출처 기반 수치가 아니다.

핵심: 시간 절약보다 리드타임 자체의 제거가 본질이다. 수동 후보정은 아무리 빨라도 참가자가 그 자리에서 받아갈 수 없다. 이 시스템의 가치는 인건비 절감이 아니라 "즉석에서 받아간다"는 경험이 성립하게 만든 것이다.

③ 캐릭터 확장 한계비용

물리 세트·조형물 방식에서 캐릭터 1종 추가는 제작·운반·보관 비용을 수반한다. 이 시스템에서는 PNG 2장 + 설정 3줄이면 된다. 증거: 커밋 a812ad2가 행사 D-2에 캐릭터를 추가했고 정상 운영됐다. 물리 방식에서 D-2 신규 캐릭터 투입은 불가능하다.

7.2의 세그먼트 직접비는 집계가 닫혀 있으므로, 위 대체비용 환산만이 추정 영역이다.


8. 사고와 구조적 해결

CASE 1 · 범용 매팅 모델이 퍼슈트에서 무너지다

증상 — 사람 대상으로 잘 동작하던 매팅 모델이 퍼슈트 착용자에게서 실루엣을 크게 잘라먹거나 배경을 인물로 오인했다.

현장 영향 — 결과물 품질이 참가자마다 들쭉날쭉해졌다. 어떤 참가자는 만족하고 어떤 참가자는 실망하는 상태는 부스 신뢰도에 직접 타격이다.

원인 — 2.3(a)의 5가지. 범용 모델이 의존하는 단서가 거의 전부 무효화된다.

선택지와 트레이드오프

장점 단점 판단
데이터로 해결 (파인튜닝) 도메인 정확도 우수 재학습·라벨링·버전관리 부채를 1인이 부담 1차 채택
구조로 해결 (사람 개념 제거) 유지보수 거의 없음, 일반화 도메인 오차 잔존 2차 채택
물리로 해결 (크로마키) 확실 부스 면적·설치 시간·조명 통제 필요 기각

해결 — 1차로 커뮤니티 기증 이미지 기반 MODNet 파인튜닝 + HITL 반복 검증을 운영했다. 이후 2차로 depth 기반 전경 분리 + SAM2 promptable 조합으로 전환했다(b7d9114).

전환한 이유가 핵심이다. 파인튜닝이 부정확해서가 아니다. 파인튜닝은 모델 하나를 만드는 일이 아니라 데이터 수집·라벨링·재학습·버전관리·기증자 관계 전체를 1인이 3년간 떠안는 결정이다. 새 퍼슈트 유형이 나올 때마다 이 사이클을 다시 돌려야 한다. "정확도가 더 높은 쪽"이 아니라 "1인이 지속 가능한 쪽" 을 택했다.

재발 방지 — "사람"이라는 사전지식을 파이프라인에서 제거해 어떤 코스튬에도 일반화되도록 했다. 잔여 오차는 캐릭터별 상수 18개로 흡수(9b7fe70).

CASE 2 · 뎁스카메라 도입, 그리고 4개월 뒤 제거

증상 — 뎁스카메라 도입(e42dc1a, 2025-10)은 분리 정확도를 실제로 높였다. 문제는 정확도가 아니라 현장이었다.

현장 영향 — 장비 추가 운반, USB 대역폭, OS별 드라이버, 카메라 정렬 유지. 그리고 결정적으로 그 장비가 고장나면 부스 전체가 멈추는 단일 장애점이었다.

원인 — 정확도 최적화만 보고 도입했고, 현장 운영 리스크를 의사결정에 넣지 않았다. 이것이 진짜 실수였다.

금전적 크기 — 이 장비는 317,517원으로 AI 장비 예산(711,027원)의 45%, 유료 인쇄 71장분이었다. 4개월 만에 파이프라인에서 제거했으므로 전액 매몰비용이다. 사지 않았다면 프로젝트 손익분기는 547장이 아니라 476장 — 회수가 한 행사 빨랐다. 판단 착오를 정성적으로 서술하지 않고 숫자로 남겨두는 이유는, 이 크기를 기억해야 다음에 같은 실수를 안 하기 때문이다.

해결 — Depth Anything V2 Small로 전면 대체(b0e64e7, 2026-02). 잃은 정확도는 SAM2 하이브리드 프롬프트로 보전했다.

재발 방지 — 이후 하드웨어 추가 결정 시 "이 장비가 고장나면 부스가 멈추는가" 를 먼저 묻게 됐다. 정확도를 내주고 운영 리스크를 제거하는 교환을 명시적 판단 기준으로 채택했다.

CASE 3 · 프리뷰 부드러움 vs 저장본 정확도

증상 — AI 파이프라인을 매 프레임 동기 실행하니 프리뷰가 끊겼다. 추론 주기를 늦추면 촬영 순간의 마스크가 이전 포즈의 것이 되어 저장본이 어긋났다.

현장 영향 — 참가자가 포즈를 잡는 동안 화면이 끊겨 조정이 어렵거나, 결과물에서 팔·귀가 잘렸다. 인쇄되어 나온 뒤에는 되돌릴 수 없다.

원인 — 두 요구가 서로 다른 시간 척도를 갖는데 같은 경로에서 처리했다. 프리뷰는 연속적 부드러움이, 저장은 단일 시점 정확도가 필요하다.

해결 — 비동기 detection + 더블 버퍼. 촬영 시에만 동기 flush(6837615). 4.2절 참조.

재발 방지 — 스레드 6종의 공유 상태 보호 규약을 .agent/knowledge/threading-model.md에 명문화하고, 신규 기능 체크리스트를 .agent/skills/threading-safety/에 규칙화했다(3bf6ddf).

CASE 4 · 현장 환경이 만드는 실패 모드

증상 — ① 행사장에 인터넷이 없어 모델 자동 다운로드 실패 ② macOS가 아이폰을 Continuity Camera로 잡아 엉뚱한 카메라 선택 ③ 운영 중 버튼창을 실수로 닫으면 조작 불가 ④ 프린터가 준비되지 않았는데 인쇄 버튼이 눌림

현장 영향 — 전부 대기열이 있는 상태에서 발생한다. 참가자가 지켜보는 앞에서 부스가 멈춘다.

원인 — 개발 환경의 암묵적 가정(인터넷 있음, 웹캠 하나, 창 안 닫음, 프린터 항상 준비)이 현장에서 전부 깨진다.

해결

실패 모드 대응 커밋
네트워크 부재 오프라인 모드 + 모델 다운로드 사전 스크립트 분리 de9d569, cb375ba
카메라 오인식 USB 카메라 존재 시 Continuity Camera 자동 배제 b3d2b61 (D+2)
창 닫힘 닫힘 감지 후 자동 재생성 main.py:138-146
프린터 미준비 3초 폴링 + 버튼 게이팅 main.py:561-579

재발 방지 — 각 실패 모드를 크래시-재시작이 아니라 개별 복구 경로로 흡수했다. 코드 복잡도가 올라가는 대가를 치렀고, 이를 .agent/skills/의 규칙 문서로 상쇄했다. 원클릭 실행 스크립트 3종(run.command/run.bat/run.ps1)으로 운영자 인계 경로도 확보했다.

CASE 5 · 커뮤니티가 맡긴 데이터를 잃다 — 미해결

증상 — 공개 모집으로 21명의 기증을 받았으나 현재 9명분(1,289장) 만 남아 있다.

원인수집 역량은 있었으나 보존 정책이 없었다. 무료 저장소 용량 한계, 백업·생애주기 정책 부재, 수집 즉시 로컬 미러링 미실시. 기증자 재요청이 가능한 연락 채널 관리도 체계화되어 있지 않았다.

잃은 것 — 기증자 기준 57%. 단순 파일이 아니라 커뮤니티가 신뢰해서 맡긴 자산이며, 재수집하려면 같은 사람들에게 다시 요청해야 한다. 기술적 손실보다 이쪽이 무겁다.

지금이라면

  • 수집 즉시 로컬 미러링 + 콜드 스토리지 이중화
  • 체크섬 기반 인벤토리
  • 기증 대장(기증자 · 수량 · 수집일 · 동의 범위) 별도 관리
  • 용량 임계 알림

재발 방지 — 미해결. 현존 9명분에 대한 이중화도 아직 적용되지 않았다. 로드맵 최우선 항목으로 이관한다.


9. 한계와 로드맵

9.1 알려진 한계

한계 설명
정량 품질 지표 부재 세그멘테이션 정확도를 IoU 등으로 측정한 적이 없다. 품질 판정이 전부 육안이었다. photos/의 141세트 raw+mask 쌍이 있으므로 지금은 측정 가능하다
기증 데이터 미이중화 CASE 5. 남은 9명분도 백업 정책이 없다
파인튜닝 파이프라인 미보존 학습 코드·설정이 저장소 밖에 있어 재현 불가
main.py 단일 클래스 597줄 Application이 카메라·AI·합성·UI·인쇄를 모두 보유. 리팩토링 가이드는 .agent/workflows/refactor-module.md에 작성해뒀으나 미실행
자동화 테스트 없음 scripts/test_*.py는 비교 이미지 생성기이지 단위 테스트가 아니다
에이전트 문서 stale 브랜치 변경 후 일부 문서에 옛 브랜치명 언급이 남아 있다
2024–2025 운영 지표 부재 자동 저장 기능이 2026-02에 추가되어, 이전 5개 행사는 스루풋 실측이 없다
성장 추이의 교란 변수 행사 규모·부스 위치·운영 시간이 통제되지 않아 촬영 수 증가를 시스템 개선 효과로 귀속할 수 없다

9.2 로드맵

우선순위 1 — 데이터 보존 (CASE 5 해소) 로컬 미러링 + 콜드 스토리지 이중화, 체크섬 인벤토리, 기증 대장 정비.

우선순위 2 — 품질 측정 체계 photos/ 141세트에 대해 수동 GT 마스크를 일부 만들고 IoU·경계 F1을 측정. 색 조화 3세대의 개선을 육안이 아닌 수치로 확인.

우선순위 3 — 구조 개선 main.py 책임 분리, 세그멘테이션 파이프라인 단위 테스트, 에이전트 문서 갱신.

우선순위 4 — 기능 확장 다인 촬영(현재는 단일 피사체 중심 가정), 동영상/GIF 출력, 참가자 셀프 조작 UI.


10. 부록 — 기술 스택

모델

모델 역할 크기
SAM 2.1 Hiera Tiny promptable 세그멘테이션 (mask + points) ~149 MB
Depth Anything V2 Small 단안 깊이 추정 → 프롬프트 자동 생성 HuggingFace snapshot
MediaPipe Selfie Segmenter / Pose Landmarker 초기 실험 잔재. 다운로드 스크립트에만 남아 있고 현 파이프라인에서 import되지 않음

런타임

Python 3.11 · PyTorch (MPS / CUDA / CPU 자동 선택) · OpenCV(UMat/OpenCL) · transformers · NumPy · tkinter · uv

플랫폼 대응

Windows macOS
OpenCV 가속 cv2.UMat (OpenCL) np.ndarray
PyTorch CUDA → CPU MPS → CPU
카메라 백엔드 CAP_MSMF CAP_AVFOUNDATION
인쇄 Win32 API lpr (CUPS)

하드웨어

4K USB 웹캠 · 염료승화형 포토프린터 · 노트북 (GPU 가속 선택)

저장소 구성

main.py                 Application (597줄)
camera/                 ICamera ABC + Webcam
utils/
  depth.py              DepthEstimator, rough mask, smart points  (602줄)
  segment.py            SAM2ImageProcessor, hybrid prompt          (498줄)
  mirror.py             MirrorCompositor, 색 조화                  (259줄)
  opencv.py             UMat/numpy 추상화                          (149줄)
  printer.py            OS별 프린터 팩토리                          (250줄)
scripts/                모델 다운로드, 세그멘테이션/색보정 비교 테스트
asset/                  캐릭터 앞·뒤 레이어 + 배경 + 스티커 (PNG 23종)
.agent/                 knowledge 4 / skills 4 / workflows 5 (1,371줄)

표기 원칙

  • 캐릭터 명칭은 타인 소유분을 포함하므로 전부 익명 라벨로 표기했다.
  • 기증자·사진작가 등 제3자 식별 정보는 기재하지 않았다.
  • 기증 이미지와 부스 참가자 촬영물은 동의 범위가 학습 용도이므로 이 문서에 싣지 않았다.
  • 날짜는 연-월 수준까지만 표기했다.