Upload 8 files
Browse files- .gitattributes +35 -35
- Dockerfile +36 -0
- README.md +11 -14
- app.py +488 -0
- assets/t_pose_openpose.png +0 -0
- assets/weapons.txt +20 -0
- blender/autorig_pipeline.py +360 -0
- requirements.txt +42 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ---- Base (CUDA) ----
|
| 2 |
+
FROM nvidia/cuda:12.1.1-devel-ubuntu22.04
|
| 3 |
+
|
| 4 |
+
ENV DEBIAN_FRONTEND=noninteractive \
|
| 5 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
| 6 |
+
PYTHONUNBUFFERED=1 \
|
| 7 |
+
HF_HOME=/root/.cache/huggingface
|
| 8 |
+
|
| 9 |
+
# ---- System deps ----
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
python3 python3-pip python3-venv git wget curl ca-certificates \
|
| 12 |
+
libgl1 libglib2.0-0 libx11-6 libxext6 libxi6 libxrender1 libxrandr2 \
|
| 13 |
+
libxxf86vm1 libsm6 libxfixes3 xvfb xauth x11-apps \
|
| 14 |
+
ffmpeg unzip && \
|
| 15 |
+
rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
# ---- Blender (headless) ----
|
| 18 |
+
# 안정적인 최신 릴리스로 교체 가능: https://ftp.nluug.nl/pub/graphics/blender/release/
|
| 19 |
+
ARG BLENDER_VER=4.1.1
|
| 20 |
+
RUN wget -q https://download.blender.org/release/Blender${BLENDER_VER%.*}/blender-${BLENDER_VER}-linux-x64.tar.xz && \
|
| 21 |
+
tar -xJf blender-${BLENDER_VER}-linux-x64.tar.xz && \
|
| 22 |
+
mv blender-${BLENDER_VER}-linux-x64 /opt/blender && \
|
| 23 |
+
ln -s /opt/blender/blender /usr/local/bin/blender && \
|
| 24 |
+
rm blender-${BLENDER_VER}-linux-x64.tar.xz
|
| 25 |
+
|
| 26 |
+
# ---- Workdir & Python deps ----
|
| 27 |
+
WORKDIR /workspace
|
| 28 |
+
COPY requirements.txt .
|
| 29 |
+
RUN python3 -m pip install --upgrade pip && pip install -r requirements.txt
|
| 30 |
+
|
| 31 |
+
# ---- App ----
|
| 32 |
+
COPY . /workspace
|
| 33 |
+
|
| 34 |
+
# HF Spaces: expose Gradio on $PORT
|
| 35 |
+
ENV PORT=7860
|
| 36 |
+
CMD ["bash", "-lc", "python3 app.py --server.port $PORT --server.address 0.0.0.0"]
|
README.md
CHANGED
|
@@ -1,14 +1,11 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Genshin
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 5.42.0
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
license: mit
|
| 11 |
-
|
| 12 |
-
---
|
| 13 |
-
|
| 14 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Pixel→Genshin 3D (ZeroGPU)
|
| 3 |
+
emoji: 🧩
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.42.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
|
|
|
|
|
|
|
|
app.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py (ZeroGPU/Gradio Space용: Blender 제외, 오토리깅 ZIP 생성 포함)
|
| 2 |
+
# - 입력 PNG → 배경제거 → (체크) 무기 자동 제거 → (체크) 원신풍 리드로우
|
| 3 |
+
# → (체크) T-포즈 정렬(손가락 21점 MediaPipe로 합성 가능)
|
| 4 |
+
# → Hunyuan3D 2.1 원격 호출(GLB) → 6뷰 렌더(가능 시) → 오토리깅 번들 ZIP 생성
|
| 5 |
+
# - SDK: Gradio, Space 유형: Gradio(ZeroGPU 호환)
|
| 6 |
+
# - t_pose_openpose.png 기본 경로: assets/t_pose_openpose.png
|
| 7 |
+
|
| 8 |
+
import os, io, json, gc, math, tempfile, zipfile, shutil, subprocess
|
| 9 |
+
import numpy as np
|
| 10 |
+
from PIL import Image, ImageDraw
|
| 11 |
+
import gradio as gr
|
| 12 |
+
|
| 13 |
+
# -------------------------------------------------------
|
| 14 |
+
# 설정
|
| 15 |
+
# -------------------------------------------------------
|
| 16 |
+
DEFAULT_POSE_PATH = os.path.join("assets", "t_pose_openpose.png")
|
| 17 |
+
HUNYUAN3D_SPACE_ID = "tencent/Hunyuan3D-2.1" # 원격 호출
|
| 18 |
+
USE_GPU = False # ZeroGPU에서는 런타임이 알아서 GPU를 붙여줌. torch.cuda.is_available() 체크로 자동 전환.
|
| 19 |
+
|
| 20 |
+
# -------------------------------------------------------
|
| 21 |
+
# 유틸
|
| 22 |
+
# -------------------------------------------------------
|
| 23 |
+
def to_rgba(img: Image.Image) -> Image.Image:
|
| 24 |
+
return img.convert("RGBA")
|
| 25 |
+
|
| 26 |
+
def save_tmp(img: Image.Image, suffix=".png"):
|
| 27 |
+
f = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
|
| 28 |
+
img.save(f.name)
|
| 29 |
+
return f.name
|
| 30 |
+
|
| 31 |
+
# -------------------------------------------------------
|
| 32 |
+
# 1) 배경 제거 (알파 유지)
|
| 33 |
+
# -------------------------------------------------------
|
| 34 |
+
from rembg import remove as rembg_remove
|
| 35 |
+
def remove_bg_keep_alpha(pil_img: Image.Image) -> Image.Image:
|
| 36 |
+
arr = np.array(pil_img.convert("RGBA"))
|
| 37 |
+
out = rembg_remove(arr)
|
| 38 |
+
return Image.fromarray(out).convert("RGBA")
|
| 39 |
+
|
| 40 |
+
# -------------------------------------------------------
|
| 41 |
+
# 2) 무기 자동 제거: GroundingDINO + SAM + LaMa
|
| 42 |
+
# -------------------------------------------------------
|
| 43 |
+
import torch
|
| 44 |
+
from transformers import AutoProcessor, AutoModelForObjectDetection, SamProcessor, SamModel
|
| 45 |
+
from simple_lama_inpainting import SimpleLama
|
| 46 |
+
|
| 47 |
+
GDINO_ID = "IDEA-Research/grounding-dino-base"
|
| 48 |
+
SAM_ID = "facebook/sam-vit-huge"
|
| 49 |
+
|
| 50 |
+
_gdino_proc=_gdino=_sam_proc=_sam=_lama=None
|
| 51 |
+
def _lazy_det_models():
|
| 52 |
+
global _gdino_proc,_gdino,_sam_proc,_sam,_lama
|
| 53 |
+
if _gdino_proc is None:
|
| 54 |
+
_gdino_proc = AutoProcessor.from_pretrained(GDINO_ID)
|
| 55 |
+
_gdino = AutoModelForObjectDetection.from_pretrained(GDINO_ID)
|
| 56 |
+
if _sam_proc is None:
|
| 57 |
+
_sam_proc = SamProcessor.from_pretrained(SAM_ID)
|
| 58 |
+
_sam = SamModel.from_pretrained(SAM_ID)
|
| 59 |
+
if _lama is None:
|
| 60 |
+
_lama = SimpleLama()
|
| 61 |
+
|
| 62 |
+
DEFAULT_WEAPON_TERMS = [
|
| 63 |
+
"sword","greatsword","katana","dagger","knife","axe","halberd",
|
| 64 |
+
"spear","lance","polearm","staff","wand","bow","crossbow",
|
| 65 |
+
"gun","pistol","rifle","shield","hammer","mace","scythe","whip"
|
| 66 |
+
]
|
| 67 |
+
def load_weapon_terms():
|
| 68 |
+
p = os.path.join("assets","weapons.txt")
|
| 69 |
+
if os.path.exists(p):
|
| 70 |
+
with open(p,"r",encoding="utf-8") as f:
|
| 71 |
+
t=[x.strip() for x in f if x.strip()]
|
| 72 |
+
if t: return t
|
| 73 |
+
return DEFAULT_WEAPON_TERMS
|
| 74 |
+
|
| 75 |
+
def detect_weapon_boxes(pil_img: Image.Image, query: str):
|
| 76 |
+
_lazy_det_models()
|
| 77 |
+
inputs = _gdino_proc(images=pil_img.convert("RGB"), text=query, return_tensors="pt")
|
| 78 |
+
outputs = _gdino(**inputs)
|
| 79 |
+
target_sizes = torch.tensor([pil_img.size[::-1]])
|
| 80 |
+
if hasattr(_gdino_proc,"post_process_grounded_object_detection"):
|
| 81 |
+
res = _gdino_proc.post_process_grounded_object_detection(
|
| 82 |
+
outputs, target_sizes=target_sizes, box_threshold=0.25, text_threshold=0.25
|
| 83 |
+
)[0]
|
| 84 |
+
return res["boxes"]
|
| 85 |
+
if hasattr(_gdino_proc,"post_process_object_detection"):
|
| 86 |
+
res = _gdino_proc.post_process_object_detection(
|
| 87 |
+
outputs, threshold=0.25, target_sizes=target_sizes
|
| 88 |
+
)[0]
|
| 89 |
+
return res["boxes"]
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
def mask_from_boxes_with_sam(pil_img: Image.Image, boxes_xyxy: torch.Tensor):
|
| 93 |
+
_lazy_det_models()
|
| 94 |
+
image = np.array(pil_img.convert("RGB"))
|
| 95 |
+
inputs = _sam_proc(images=image, input_boxes=[boxes_xyxy], return_tensors="pt")
|
| 96 |
+
with torch.no_grad():
|
| 97 |
+
outputs = _sam(**inputs)
|
| 98 |
+
masks = _sam_proc.post_process_masks(
|
| 99 |
+
outputs.pred_masks, inputs["original_sizes"], inputs["reshaped_input_sizes"]
|
| 100 |
+
)[0]
|
| 101 |
+
m = masks.squeeze(1).sum(dim=0).clamp(0,1).cpu().numpy().astype(np.uint8)*255
|
| 102 |
+
return Image.fromarray(m, mode="L")
|
| 103 |
+
|
| 104 |
+
def inpaint_lama(pil_img: Image.Image, mask_img: Image.Image) -> Image.Image:
|
| 105 |
+
with tempfile.TemporaryDirectory() as td:
|
| 106 |
+
ip = os.path.join(td,"in.png"); mp = os.path.join(td,"m.png"); op = os.path.join(td,"out.png")
|
| 107 |
+
pil_img.convert("RGB").save(ip); mask_img.convert("L").save(mp)
|
| 108 |
+
_lama(ip, mp, op); out = Image.open(op).convert("RGBA")
|
| 109 |
+
base = pil_img.copy()
|
| 110 |
+
final_rgb = Image.composite(out.convert("RGB"), base.convert("RGB"), mask_img.convert("L"))
|
| 111 |
+
final_rgba = final_rgb.convert("RGBA"); final_rgba.putalpha(pil_img.split()[-1])
|
| 112 |
+
return final_rgba
|
| 113 |
+
|
| 114 |
+
def auto_remove_weapons(pil_img: Image.Image) -> Image.Image:
|
| 115 |
+
boxes = detect_weapon_boxes(pil_img, ", ".join(load_weapon_terms()))
|
| 116 |
+
if boxes is None or (hasattr(boxes,"shape") and boxes.shape[0]==0): return pil_img
|
| 117 |
+
mask = mask_from_boxes_with_sam(pil_img, boxes)
|
| 118 |
+
if np.array(mask).max()==0: return pil_img
|
| 119 |
+
return inpaint_lama(pil_img, mask)
|
| 120 |
+
|
| 121 |
+
# -------------------------------------------------------
|
| 122 |
+
# 3) 원신풍 리드로우 (픽셀감 제거) : SD1.5 + ControlNet Canny
|
| 123 |
+
# -------------------------------------------------------
|
| 124 |
+
from diffusers import StableDiffusionControlNetImg2ImgPipeline, ControlNetModel
|
| 125 |
+
import cv2
|
| 126 |
+
|
| 127 |
+
CN_CANNY_ID = "lllyasviel/control_v11p_sd15_canny"
|
| 128 |
+
SD15_ID = "runwayml/stable-diffusion-v1-5"
|
| 129 |
+
|
| 130 |
+
_cn_canny=_img2img=None
|
| 131 |
+
def _lazy_redraw():
|
| 132 |
+
global _cn_canny,_img2img
|
| 133 |
+
if _cn_canny is None:
|
| 134 |
+
_cn_canny = ControlNetModel.from_pretrained(CN_CANNY_ID, torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32)
|
| 135 |
+
if _img2img is None:
|
| 136 |
+
_img2img = StableDiffusionControlNetImg2ImgPipeline.from_pretrained(
|
| 137 |
+
SD15_ID, controlnet=_cn_canny, torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
|
| 138 |
+
)
|
| 139 |
+
if torch.cuda.is_available():
|
| 140 |
+
_img2img.enable_xformers_memory_efficient_attention()
|
| 141 |
+
_img2img.to("cuda")
|
| 142 |
+
|
| 143 |
+
PROMPT = ("masterpiece, best quality, clean lineart, anime style, genshin impact style, "
|
| 144 |
+
"flat color with soft shading, detailed costume, symmetrical, full body")
|
| 145 |
+
NEG = ("low quality, worst quality, jpeg artifacts, extra limbs, deformed, "
|
| 146 |
+
"pixelated, mosaic, noisy, text, watermark, logo")
|
| 147 |
+
|
| 148 |
+
def canny_map(pil):
|
| 149 |
+
rgb = np.array(pil.convert("RGB"))
|
| 150 |
+
edges = cv2.Canny(rgb, 100, 200)
|
| 151 |
+
return Image.fromarray(edges).convert("RGB")
|
| 152 |
+
|
| 153 |
+
def redraw_to_clean_anime(pil_img: Image.Image, strength=0.55, scale=6.5, steps=24) -> Image.Image:
|
| 154 |
+
_lazy_redraw()
|
| 155 |
+
cn = canny_map(pil_img)
|
| 156 |
+
res = _img2img(
|
| 157 |
+
prompt=PROMPT, negative_prompt=NEG,
|
| 158 |
+
image=pil_img.convert("RGB"), control_image=cn,
|
| 159 |
+
num_inference_steps=steps, strength=strength, guidance_scale=scale,
|
| 160 |
+
)
|
| 161 |
+
out = res.images[0].convert("RGBA")
|
| 162 |
+
out.putalpha(pil_img.split()[-1])
|
| 163 |
+
return out
|
| 164 |
+
|
| 165 |
+
# -------------------------------------------------------
|
| 166 |
+
# 4) T-포즈 정렬(옵션) : OpenPose ControlNet + MediaPipe Hands(정확한 21점 합성)
|
| 167 |
+
# -------------------------------------------------------
|
| 168 |
+
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel as CModel2
|
| 169 |
+
import mediapipe as mp
|
| 170 |
+
|
| 171 |
+
CONTROLNET_POSE = "lllyasviel/control_v11p_sd15_openpose"
|
| 172 |
+
_sd_pose=_cn_pose=None
|
| 173 |
+
|
| 174 |
+
_mp_hands = mp.solutions.hands.Hands(
|
| 175 |
+
static_image_mode=True,
|
| 176 |
+
max_num_hands=2,
|
| 177 |
+
model_complexity=1,
|
| 178 |
+
min_detection_confidence=0.3,
|
| 179 |
+
min_tracking_confidence=0.3
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
HAND_COL = {
|
| 183 |
+
"thumb": (255,105,180),
|
| 184 |
+
"index": ( 0,191,255),
|
| 185 |
+
"middle": ( 50,205, 50),
|
| 186 |
+
"ring": (255,140, 0),
|
| 187 |
+
"little": (186, 85,211),
|
| 188 |
+
}
|
| 189 |
+
FINGERS = {
|
| 190 |
+
"thumb": [0,1,2,3,4],
|
| 191 |
+
"index": [0,5,6,7,8],
|
| 192 |
+
"middle": [0,9,10,11,12],
|
| 193 |
+
"ring": [0,13,14,15,16],
|
| 194 |
+
"little": [0,17,18,19,20],
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
def _lazy_pose():
|
| 198 |
+
global _sd_pose,_cn_pose
|
| 199 |
+
if _cn_pose is None:
|
| 200 |
+
_cn_pose = CModel2.from_pretrained(CONTROLNET_POSE, torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32)
|
| 201 |
+
if _sd_pose is None:
|
| 202 |
+
_sd_pose = StableDiffusionControlNetPipeline.from_pretrained(
|
| 203 |
+
SD15_ID, controlnet=_cn_pose, torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
|
| 204 |
+
)
|
| 205 |
+
if torch.cuda.is_available():
|
| 206 |
+
_sd_pose.enable_xformers_memory_efficient_attention()
|
| 207 |
+
_sd_pose.to("cuda")
|
| 208 |
+
|
| 209 |
+
def detect_hands_xy(pil_img: Image.Image):
|
| 210 |
+
img_rgb = np.array(pil_img.convert("RGB"))
|
| 211 |
+
h, w, _ = img_rgb.shape
|
| 212 |
+
res = _mp_hands.process(img_rgb)
|
| 213 |
+
hands = []
|
| 214 |
+
if res.multi_hand_landmarks:
|
| 215 |
+
for lm, handed in zip(res.multi_hand_landmarks, res.multi_handedness):
|
| 216 |
+
pts = []
|
| 217 |
+
for p in lm.landmark:
|
| 218 |
+
pts.append([p.x*w, p.y*h])
|
| 219 |
+
pts = np.array(pts, dtype=np.float32) # (21,2)
|
| 220 |
+
handedness = handed.classification[0].label.lower() # 'left' or 'right'
|
| 221 |
+
hands.append((pts, handedness))
|
| 222 |
+
return hands
|
| 223 |
+
|
| 224 |
+
def _angle(p0, p1):
|
| 225 |
+
return math.atan2(p1[1]-p0[1], p1[0]-p0[0] + 1e-8)
|
| 226 |
+
|
| 227 |
+
def place_hand_on_pose(canvas: Image.Image, src_pts: np.ndarray, target_wrist_xy, facing_hint: str=None, scale_px=110):
|
| 228 |
+
draw = ImageDraw.Draw(canvas)
|
| 229 |
+
wrist = src_pts[0]
|
| 230 |
+
up_vec = src_pts[9] - wrist
|
| 231 |
+
theta = _angle([0,0], up_vec)
|
| 232 |
+
R = np.array([[math.cos(-theta-math.pi/2), -math.sin(-theta-math.pi/2)],
|
| 233 |
+
[math.sin(-theta-math.pi/2), math.cos(-theta-math.pi/2)]], dtype=np.float32)
|
| 234 |
+
norm = (src_pts - wrist) @ R.T
|
| 235 |
+
thumb_dir = norm[2][0]
|
| 236 |
+
sx = 1.0
|
| 237 |
+
if facing_hint == "left":
|
| 238 |
+
sx = -1.0
|
| 239 |
+
elif facing_hint == "right":
|
| 240 |
+
sx = 1.0
|
| 241 |
+
else:
|
| 242 |
+
sx = 1.0 if thumb_dir > 0 else -1.0
|
| 243 |
+
norm[:,0] *= sx
|
| 244 |
+
size_ref = np.linalg.norm(norm[12] - np.array([0,0],dtype=np.float32)) + 1e-6
|
| 245 |
+
s = scale_px / size_ref
|
| 246 |
+
norm *= s
|
| 247 |
+
placed = norm + np.array(target_wrist_xy, dtype=np.float32)
|
| 248 |
+
|
| 249 |
+
r = 3
|
| 250 |
+
for name, idxs in FINGERS.items():
|
| 251 |
+
col = HAND_COL[name]
|
| 252 |
+
last = None
|
| 253 |
+
for k in idxs:
|
| 254 |
+
x,y = placed[k]
|
| 255 |
+
draw.ellipse((x-r,y-r,x+r,y+r), fill=col)
|
| 256 |
+
if last is not None:
|
| 257 |
+
draw.line((last[0],last[1],x,y), fill=col, width=3)
|
| 258 |
+
last = (x,y)
|
| 259 |
+
|
| 260 |
+
def estimate_wrist_xy_from_map(posemap_rgb: Image.Image):
|
| 261 |
+
W,H = posemap_rgb.size
|
| 262 |
+
y = int(H*0.33)
|
| 263 |
+
left = (int(W*0.20), y)
|
| 264 |
+
right = (int(W*0.80), y)
|
| 265 |
+
return left, right
|
| 266 |
+
|
| 267 |
+
def build_pose_control_image(base_pose_png: Image.Image,
|
| 268 |
+
input_image_for_hands: Image.Image,
|
| 269 |
+
add_hands_with_mediapipe=True):
|
| 270 |
+
pose = base_pose_png.convert("RGB").copy()
|
| 271 |
+
if not add_hands_with_mediapipe:
|
| 272 |
+
return pose
|
| 273 |
+
lw, rw = estimate_wrist_xy_from_map(pose)
|
| 274 |
+
hands = detect_hands_xy(input_image_for_hands)
|
| 275 |
+
if not hands:
|
| 276 |
+
return pose
|
| 277 |
+
W,_ = input_image_for_hands.size
|
| 278 |
+
for pts, handed in hands:
|
| 279 |
+
cx = pts[:,0].mean()
|
| 280 |
+
if cx < W/2:
|
| 281 |
+
place_hand_on_pose(pose, pts, lw, facing_hint="left")
|
| 282 |
+
else:
|
| 283 |
+
place_hand_on_pose(pose, pts, rw, facing_hint="right")
|
| 284 |
+
return pose
|
| 285 |
+
|
| 286 |
+
def force_tpose(pil_img: Image.Image, pose_control_img: Image.Image, strength=0.25, scale=5.0) -> Image.Image:
|
| 287 |
+
_lazy_pose()
|
| 288 |
+
r = _sd_pose(
|
| 289 |
+
prompt="", image=pil_img.convert("RGB"), control_image=pose_control_img,
|
| 290 |
+
num_inference_steps=20, strength=strength, guidance_scale=scale
|
| 291 |
+
)
|
| 292 |
+
out = r.images[0].convert("RGBA"); out.putalpha(pil_img.split()[-1])
|
| 293 |
+
return out
|
| 294 |
+
|
| 295 |
+
# -------------------------------------------------------
|
| 296 |
+
# 5) Hunyuan3D 2.1 호출 (원격 Space)
|
| 297 |
+
# -------------------------------------------------------
|
| 298 |
+
from gradio_client import Client, handle_file
|
| 299 |
+
|
| 300 |
+
def run_hunyuan3d(pil_rgba: Image.Image) -> str:
|
| 301 |
+
client = Client(HUNYUAN3D_SPACE_ID, hf_token=os.environ.get("HF_TOKEN"))
|
| 302 |
+
apis = client.view_api(all_endpoints=True)
|
| 303 |
+
api = apis[0]["api_name"] if apis else "/predict"
|
| 304 |
+
path = save_tmp(pil_rgba, ".png")
|
| 305 |
+
r = client.predict(image=handle_file(path), api_name=api)
|
| 306 |
+
def pick(x):
|
| 307 |
+
if isinstance(x,str) and x.lower().endswith(".glb"): return x
|
| 308 |
+
if hasattr(x,"name") and str(x.name).lower().endswith(".glb"): return x.name
|
| 309 |
+
if isinstance(r,(list,tuple)):
|
| 310 |
+
for it in r:
|
| 311 |
+
g = pick(it)
|
| 312 |
+
if g: return g
|
| 313 |
+
g = pick(r)
|
| 314 |
+
if g: return g
|
| 315 |
+
raise gr.Error("Hunyuan3D GLB 결과를 찾지 못했습니다.")
|
| 316 |
+
|
| 317 |
+
# -------------------------------------------------------
|
| 318 |
+
# 6) 6뷰 렌더 (가능 시: pyrender/OSMesa 필요)
|
| 319 |
+
# -------------------------------------------------------
|
| 320 |
+
def sixview_render(glb_path: str, size=1024):
|
| 321 |
+
try:
|
| 322 |
+
import trimesh, pyrender
|
| 323 |
+
scene_or_mesh = trimesh.load(glb_path, force='scene')
|
| 324 |
+
if isinstance(scene_or_mesh, trimesh.Scene):
|
| 325 |
+
geos=[g for g in scene_or_mesh.geometry.values() if isinstance(g,trimesh.Trimesh)]
|
| 326 |
+
mesh = trimesh.util.concatenate(geos) if geos else scene_or_mesh.dump().sum()
|
| 327 |
+
else:
|
| 328 |
+
mesh = scene_or_mesh
|
| 329 |
+
scene = pyrender.Scene(ambient_light=[.6,.6,.6,1], bg_color=[0,0,0,0])
|
| 330 |
+
mnode = scene.add(pyrender.Mesh.from_trimesh(mesh, smooth=False))
|
| 331 |
+
cam = pyrender.OrthographicCamera(xmag=1, ymag=1, znear=0.01, zfar=1000)
|
| 332 |
+
cnode = scene.add(cam, pose=np.eye(4))
|
| 333 |
+
lnode = scene.add(pyrender.DirectionalLight(intensity=3.0), pose=np.eye(4))
|
| 334 |
+
bbox=mesh.bounds; size3=(bbox[1]-bbox[0]); scale=float(np.max(size3))
|
| 335 |
+
views=[("front",(0,0)),("left",(90,0)),("right",(-90,0)),("back",(180,0)),("top",(0,90)),("bottom",(0,-90))]
|
| 336 |
+
def pose(yaw,pitch,dist):
|
| 337 |
+
y=math.radians(yaw); p=math.radians(pitch)
|
| 338 |
+
x=dist*math.cos(p)*math.cos(y); yy=dist*math.sin(p); z=dist*math.cos(p)*math.sin(y)
|
| 339 |
+
eye=np.array([x,yy,z]); at=np.array([0,0,0]); up=np.array([0,1,0])
|
| 340 |
+
f=(at-eye); f=f/np.linalg.norm(f); s=np.cross(f,up); s=s/np.linalg.norm(s); u=np.cross(s,f)
|
| 341 |
+
M=np.eye(4); M[:3,0]=s; M[:3,1]=u; M[:3,2]=-f; M[:3,3]=eye; return M
|
| 342 |
+
r=pyrender.OffscreenRenderer(size,size); imgs=[]
|
| 343 |
+
dist=scale*2.0+1e-6
|
| 344 |
+
for _,(yaw,pitch) in views:
|
| 345 |
+
scene.set_pose(cnode, pose=pose(yaw,pitch,dist))
|
| 346 |
+
scene.set_pose(lnode, pose=pose(yaw,pitch,dist))
|
| 347 |
+
color,_=r.render(scene, flags=pyrender.RenderFlags.RGBA)
|
| 348 |
+
imgs.append(Image.fromarray(color).convert("RGBA"))
|
| 349 |
+
r.delete()
|
| 350 |
+
return imgs
|
| 351 |
+
except Exception as e:
|
| 352 |
+
print("[WARN] sixview_render skipped:", e)
|
| 353 |
+
return []
|
| 354 |
+
|
| 355 |
+
# -------------------------------------------------------
|
| 356 |
+
# 7) 오토리깅 번들 ZIP 생성 (Blender는 포함하지 않음)
|
| 357 |
+
# -------------------------------------------------------
|
| 358 |
+
AUTORIG_SCRIPT = r'''<AUTORIG_PIPELINE_PY_PLACEHOLDER>'''
|
| 359 |
+
# 위 자리에는 이전에 제공한 blender/autorig_pipeline.py 전체본을 그대로 붙여 넣으세요.
|
| 360 |
+
# (메시지 길이 제약상 플레이스홀더 처리)
|
| 361 |
+
|
| 362 |
+
RUN_SH = r'''#!/usr/bin/env bash
|
| 363 |
+
BLENDER=${BLENDER:-blender}
|
| 364 |
+
IN=${1:-model.glb}
|
| 365 |
+
OUT=${2:-rigged_toon.fbx}
|
| 366 |
+
$BLENDER -b --python blender/autorig_pipeline.py -- \
|
| 367 |
+
--in "$IN" --out "$OUT" --export fbx \
|
| 368 |
+
--outline --toon --surface_deform --limit_cloth_bones --try_faceit
|
| 369 |
+
'''
|
| 370 |
+
RUN_BAT = r'''@echo off
|
| 371 |
+
set BLENDER=blender
|
| 372 |
+
set IN=%1
|
| 373 |
+
if "%IN%"=="" set IN=model.glb
|
| 374 |
+
set OUT=%2
|
| 375 |
+
if "%OUT%"=="" set OUT=rigged_toon.fbx
|
| 376 |
+
%BLENDER% -b --python blender\autorig_pipeline.py -- ^
|
| 377 |
+
--in "%IN%" --out "%OUT%" --export fbx ^
|
| 378 |
+
--outline --toon --surface_deform --limit_cloth_bones --try_faceit
|
| 379 |
+
'''
|
| 380 |
+
README_AUTORIG = r'''자동 리깅 번들 사용법
|
| 381 |
+
1) Blender 설치(4.x 권장)
|
| 382 |
+
2) 터미널(또는 더블클릭)로 다음 실행
|
| 383 |
+
- Linux/Mac: ./run_blender_local.sh model.glb rigged_toon.fbx
|
| 384 |
+
- Windows: run_blender_local.bat model.glb rigged_toon.fbx
|
| 385 |
+
3) 완료되면 rigged_toon.fbx(또는 glb)이 생성됩니다.
|
| 386 |
+
* 의복은 Surface Deform 기반으로 바디를 추종(본 가중치 최소화), Toon/Outline 적용, 표정키(플레이스홀더) 생성.
|
| 387 |
+
'''
|
| 388 |
+
|
| 389 |
+
def make_autorig_bundle(glb_path: str) -> str:
|
| 390 |
+
out_dir = tempfile.mkdtemp()
|
| 391 |
+
# 디렉토리 구성
|
| 392 |
+
os.makedirs(os.path.join(out_dir,"blender"), exist_ok=True)
|
| 393 |
+
# 파일 쓰기
|
| 394 |
+
with open(os.path.join(out_dir,"blender","autorig_pipeline.py"),"w",encoding="utf-8") as f:
|
| 395 |
+
f.write(AUTORIG_SCRIPT)
|
| 396 |
+
with open(os.path.join(out_dir,"run_blender_local.sh"),"w",encoding="utf-8") as f:
|
| 397 |
+
f.write(RUN_SH)
|
| 398 |
+
os.chmod(os.path.join(out_dir,"run_blender_local.sh"), 0o755)
|
| 399 |
+
with open(os.path.join(out_dir,"run_blender_local.bat"),"w",encoding="utf-8") as f:
|
| 400 |
+
f.write(RUN_BAT)
|
| 401 |
+
with open(os.path.join(out_dir,"README_AUTORIG.txt"),"w",encoding="utf-8") as f:
|
| 402 |
+
f.write(README_AUTORIG)
|
| 403 |
+
# GLB 포함
|
| 404 |
+
shutil.copy2(glb_path, os.path.join(out_dir,"model.glb"))
|
| 405 |
+
# ZIP으로 묶기
|
| 406 |
+
zipf=os.path.join(out_dir,"autorig_bundle.zip")
|
| 407 |
+
with zipfile.ZipFile(zipf,"w",zipfile.ZIP_DEFLATED) as z:
|
| 408 |
+
for root,_,files in os.walk(out_dir):
|
| 409 |
+
for name in files:
|
| 410 |
+
if name.endswith(".zip"): continue
|
| 411 |
+
full=os.path.join(root,name)
|
| 412 |
+
arc=os.path.relpath(full, out_dir)
|
| 413 |
+
z.write(full, arcname=arc)
|
| 414 |
+
return zipf
|
| 415 |
+
|
| 416 |
+
# -------------------------------------------------------
|
| 417 |
+
# 8) 파이프라인
|
| 418 |
+
# -------------------------------------------------------
|
| 419 |
+
def pipeline(image, has_weapon: bool, do_redraw: bool,
|
| 420 |
+
force_pose: bool, add_hand_details: bool,
|
| 421 |
+
user_pose_map):
|
| 422 |
+
if image is None:
|
| 423 |
+
return None, None, [], None, None
|
| 424 |
+
|
| 425 |
+
img=image.convert("RGBA")
|
| 426 |
+
|
| 427 |
+
# a) 배경제거
|
| 428 |
+
clean=remove_bg_keep_alpha(img)
|
| 429 |
+
|
| 430 |
+
# b) 무기 자동 제거
|
| 431 |
+
if has_weapon:
|
| 432 |
+
try: clean=auto_remove_weapons(clean)
|
| 433 |
+
except Exception as e: print("[WARN] weapon removal:", e)
|
| 434 |
+
|
| 435 |
+
# c) 원신풍 리드로우
|
| 436 |
+
if do_redraw:
|
| 437 |
+
try: clean=redraw_to_clean_anime(clean, strength=0.55, scale=6.5, steps=24)
|
| 438 |
+
except Exception as e: print("[WARN] redraw:", e)
|
| 439 |
+
|
| 440 |
+
# d) T-포즈 컨트롤맵 생성(+손 디테일)
|
| 441 |
+
pose_src = user_pose_map or (Image.open(DEFAULT_POSE_PATH).convert("RGB") if os.path.exists(DEFAULT_POSE_PATH) else None)
|
| 442 |
+
if force_pose and pose_src is not None:
|
| 443 |
+
try:
|
| 444 |
+
pose_control = build_pose_control_image(
|
| 445 |
+
base_pose_png=pose_src,
|
| 446 |
+
input_image_for_hands=clean,
|
| 447 |
+
add_hands_with_mediapipe=add_hand_details
|
| 448 |
+
)
|
| 449 |
+
clean = force_tpose(clean, pose_control, strength=0.25, scale=5.0)
|
| 450 |
+
except Exception as e:
|
| 451 |
+
print("[WARN] tpose:", e)
|
| 452 |
+
|
| 453 |
+
# e) Hunyuan3D → GLB
|
| 454 |
+
glb_path=run_hunyuan3d(clean)
|
| 455 |
+
|
| 456 |
+
# f) 6뷰 (환경에 따라 생략될 수 있음)
|
| 457 |
+
views=sixview_render(glb_path, size=1024)
|
| 458 |
+
|
| 459 |
+
# g) 오토리깅 번들 ZIP (Blender는 포함 안 함)
|
| 460 |
+
zip_path = make_autorig_bundle(glb_path)
|
| 461 |
+
|
| 462 |
+
return clean, glb_path, views, zip_path, pose_src
|
| 463 |
+
|
| 464 |
+
# -------------------------------------------------------
|
| 465 |
+
# 9) UI
|
| 466 |
+
# -------------------------------------------------------
|
| 467 |
+
with gr.Blocks() as demo:
|
| 468 |
+
gr.Markdown("### Pixel→(BG/Weapon)→Clean Anime Redraw→(OpenPose+Hands)→Hunyuan3D→6View→AutoRig ZIP (ZeroGPU)")
|
| 469 |
+
with gr.Row():
|
| 470 |
+
inp=gr.Image(type="pil", label="입력 이미지 (PNG, 알파 가능)")
|
| 471 |
+
with gr.Column():
|
| 472 |
+
c1=gr.Checkbox(label="자동 무기 제거 (GroundingDINO + SAM + LaMa)", value=True)
|
| 473 |
+
c2=gr.Checkbox(label="원신풍 리드로우 (픽셀감 제거)", value=True)
|
| 474 |
+
c3=gr.Checkbox(label="T-포즈 강제 (OpenPose ControlNet)", value=True)
|
| 475 |
+
c4=gr.Checkbox(label="손가락 디테일 자동 추가 (MediaPipe 21점)", value=True)
|
| 476 |
+
pose_upload = gr.Image(type="pil", label="T-포즈 OpenPose 맵 (선택, 미제공이면 assets/t_pose_openpose.png 사용)")
|
| 477 |
+
btn=gr.Button("실행", variant="primary")
|
| 478 |
+
out_clean=gr.Image(label="전처리/리드로우/T-포즈 결과(알파)")
|
| 479 |
+
out_glb=gr.File(label="3D 모델 (GLB, Hunyuan3D)")
|
| 480 |
+
out_views=gr.Gallery(label="6뷰(정/좌/우/후/상/하, 투명)", columns=3)
|
| 481 |
+
out_zip=gr.File(label="AutoRig 번들 ZIP (Blender 스크립트/런처 + model.glb)")
|
| 482 |
+
out_pose=gr.Image(label="사용된 T-포즈 컨트롤맵(미리보기)", visible=True)
|
| 483 |
+
|
| 484 |
+
btn.click(pipeline, [inp,c1,c2,c3,c4,pose_upload],
|
| 485 |
+
[out_clean,out_glb,out_views,out_zip,out_pose])
|
| 486 |
+
|
| 487 |
+
if __name__=="__main__":
|
| 488 |
+
demo.launch()
|
assets/t_pose_openpose.png
ADDED
|
assets/weapons.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
sword
|
| 2 |
+
greatsword
|
| 3 |
+
katana
|
| 4 |
+
dagger
|
| 5 |
+
knife
|
| 6 |
+
dual blades
|
| 7 |
+
axe
|
| 8 |
+
halberd
|
| 9 |
+
spear
|
| 10 |
+
lance
|
| 11 |
+
polearm
|
| 12 |
+
staff
|
| 13 |
+
wand
|
| 14 |
+
bow
|
| 15 |
+
crossbow
|
| 16 |
+
gun
|
| 17 |
+
pistol
|
| 18 |
+
rifle
|
| 19 |
+
shield
|
| 20 |
+
hammer
|
blender/autorig_pipeline.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# blender/autorig_pipeline.py
|
| 2 |
+
# 헤드리스(배치) 사용 예:
|
| 3 |
+
# blender -b --python blender/autorig_pipeline.py -- \
|
| 4 |
+
# --in model.glb --out rigged_toon.fbx --export fbx \
|
| 5 |
+
# --outline --toon --surface_deform --limit_cloth_bones --try_faceit
|
| 6 |
+
#
|
| 7 |
+
# 기능 요약:
|
| 8 |
+
# 1) GLB/GLTF/FBX/OBJ 불러오기
|
| 9 |
+
# 2) Body/의복/헤어/소품 대략 분류
|
| 10 |
+
# 3) 휴머노이드 Armature(T-포즈) 자동 생성 + Body 자동 가중치
|
| 11 |
+
# 4) 의복: Surface Deform 또는 본 영향 축소
|
| 12 |
+
# 5) Toon 머티리얼 + 인버티드 헐(Outline) 적용
|
| 13 |
+
# 6) 표정: Faceit 감지 시 안내, 없으면 기본 Shape Key 플레이스홀더 생성
|
| 14 |
+
# 7) FBX 또는 GLB 내보내기
|
| 15 |
+
|
| 16 |
+
import bpy, sys, os, math, argparse
|
| 17 |
+
from mathutils import Vector, Matrix
|
| 18 |
+
|
| 19 |
+
# -----------------------
|
| 20 |
+
# CLI args
|
| 21 |
+
# -----------------------
|
| 22 |
+
def parse_args():
|
| 23 |
+
argv = sys.argv
|
| 24 |
+
if "--" in argv:
|
| 25 |
+
argv = argv[argv.index("--") + 1:]
|
| 26 |
+
else:
|
| 27 |
+
argv = []
|
| 28 |
+
p = argparse.ArgumentParser()
|
| 29 |
+
p.add_argument("--in", dest="inp", required=True, help="입력 경로 (GLB/GLTF/FBX/OBJ)")
|
| 30 |
+
p.add_argument("--out", dest="out", required=True, help="출력 파일 경로")
|
| 31 |
+
p.add_argument("--export", choices=["fbx", "glb"], default="fbx", help="내보내기 포맷")
|
| 32 |
+
p.add_argument("--outline", action="store_true", help="인버티드 헐 외곽선 생성")
|
| 33 |
+
p.add_argument("--toon", action="store_true", help="Toon 셰이딩 적용")
|
| 34 |
+
p.add_argument("--surface_deform", action="store_true", help="의복 Surface Deform 바인딩")
|
| 35 |
+
p.add_argument("--limit_cloth_bones", action="store_true", help="의복 Armature 본 영향 축소(간이)")
|
| 36 |
+
p.add_argument("--try_faceit", action="store_true", help="설치 시 Faceit 활용 안내")
|
| 37 |
+
p.add_argument("--scale", type=float, default=1.0, help="전체 스케일")
|
| 38 |
+
return p.parse_args()
|
| 39 |
+
|
| 40 |
+
args = parse_args()
|
| 41 |
+
inp_path = bpy.path.abspath(args.inp)
|
| 42 |
+
out_path = bpy.path.abspath(args.out)
|
| 43 |
+
export_fmt = args.export
|
| 44 |
+
|
| 45 |
+
# -----------------------
|
| 46 |
+
# 초기화
|
| 47 |
+
# -----------------------
|
| 48 |
+
for obj in list(bpy.data.objects):
|
| 49 |
+
obj.select_set(False)
|
| 50 |
+
|
| 51 |
+
# -----------------------
|
| 52 |
+
# Import model
|
| 53 |
+
# -----------------------
|
| 54 |
+
ext = os.path.splitext(inp_path)[1].lower()
|
| 55 |
+
if ext in [".glb", ".gltf"]:
|
| 56 |
+
bpy.ops.import_scene.gltf(filepath=inp_path)
|
| 57 |
+
elif ext in [".fbx"]:
|
| 58 |
+
bpy.ops.import_scene.fbx(filepath=inp_path)
|
| 59 |
+
elif ext in [".obj"]:
|
| 60 |
+
bpy.ops.import_scene.obj(filepath=inp_path)
|
| 61 |
+
else:
|
| 62 |
+
raise RuntimeError("지원하지 않는 포맷: " + ext)
|
| 63 |
+
|
| 64 |
+
# 모든 Mesh 수집
|
| 65 |
+
meshes = [o for o in bpy.context.scene.objects if o.type == "MESH"]
|
| 66 |
+
if not meshes:
|
| 67 |
+
raise RuntimeError("MESH 오브젝트가 없습니다.")
|
| 68 |
+
|
| 69 |
+
# 컬렉션 정리
|
| 70 |
+
coll = bpy.data.collections.new("CHARACTER")
|
| 71 |
+
bpy.context.scene.collection.children.link(coll)
|
| 72 |
+
for o in meshes:
|
| 73 |
+
# 기존 컬렉션에서 제거 후 새 컬렉션으로 이동
|
| 74 |
+
if o.users_collection:
|
| 75 |
+
for c in o.users_collection:
|
| 76 |
+
try:
|
| 77 |
+
c.objects.unlink(o)
|
| 78 |
+
except Exception:
|
| 79 |
+
pass
|
| 80 |
+
coll.objects.link(o)
|
| 81 |
+
|
| 82 |
+
# 스케일 정리
|
| 83 |
+
for o in meshes:
|
| 84 |
+
o.select_set(True)
|
| 85 |
+
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
| 86 |
+
for o in meshes:
|
| 87 |
+
o.select_set(False)
|
| 88 |
+
|
| 89 |
+
# -----------------------
|
| 90 |
+
# Body 추정: 바운딩박스 볼륨 기준
|
| 91 |
+
# -----------------------
|
| 92 |
+
def mesh_bbox_volume(obj):
|
| 93 |
+
m = obj.matrix_world
|
| 94 |
+
bb = [m @ Vector(corner) for corner in obj.bound_box]
|
| 95 |
+
minv = Vector((min(v.x for v in bb), min(v.y for v in bb), min(v.z for v in bb)))
|
| 96 |
+
maxv = Vector((max(v.x for v in bb), max(v.y for v in bb), max(v.z for v in bb)))
|
| 97 |
+
sz = maxv - minv
|
| 98 |
+
return abs(sz.x * sz.y * sz.z)
|
| 99 |
+
|
| 100 |
+
volumes = sorted([(mesh_bbox_volume(o), o) for o in meshes], key=lambda x: x[0], reverse=True)
|
| 101 |
+
body = volumes[0][1]
|
| 102 |
+
others = [o for _, o in volumes[1:]]
|
| 103 |
+
|
| 104 |
+
body.name = "Body"
|
| 105 |
+
|
| 106 |
+
# 의복/헤어/소품 키워드 분류(간이)
|
| 107 |
+
CLOTH_KEYS = ["cloth","skirt","dress","coat","cape","jacket","shirt","pants","sleeve","kimono","robe"]
|
| 108 |
+
HAIR_KEYS = ["hair","bang","ponytail","fringe","bun","pigtail"]
|
| 109 |
+
|
| 110 |
+
def classify(o):
|
| 111 |
+
name = (o.name + " " + " ".join([m.name for m in o.data.materials if m])).lower()
|
| 112 |
+
if any(k in name for k in CLOTH_KEYS): return "Cloth"
|
| 113 |
+
if any(k in name for k in HAIR_KEYS): return "Hair"
|
| 114 |
+
return "Prop"
|
| 115 |
+
|
| 116 |
+
for o in others:
|
| 117 |
+
try:
|
| 118 |
+
o["part_type"] = classify(o)
|
| 119 |
+
except Exception:
|
| 120 |
+
pass
|
| 121 |
+
|
| 122 |
+
# -----------------------
|
| 123 |
+
# Armature 생성 (간이 휴머노이드 T-포즈)
|
| 124 |
+
# -----------------------
|
| 125 |
+
bpy.ops.object.armature_add(enter_editmode=True)
|
| 126 |
+
arm = bpy.context.object
|
| 127 |
+
arm.name = "Armature"
|
| 128 |
+
eb = arm.data.edit_bones
|
| 129 |
+
|
| 130 |
+
# Body AABB 기반 중심/크기
|
| 131 |
+
bb = [body.matrix_world @ Vector(c) for c in body.bound_box]
|
| 132 |
+
minv = Vector((min(v.x for v in bb), min(v.y for v in bb), min(v.z for v in bb)))
|
| 133 |
+
maxv = Vector((max(v.x for v in bb), max(v.y for v in bb), max(v.z for v in bb)))
|
| 134 |
+
cent = (minv + maxv) / 2
|
| 135 |
+
height = (maxv.z - minv.z)
|
| 136 |
+
width = (maxv.x - minv.x)
|
| 137 |
+
|
| 138 |
+
def add_bone(name, head, tail, parent=None, roll=0.0):
|
| 139 |
+
b = eb.new(name)
|
| 140 |
+
b.head = head; b.tail = tail; b.roll = roll
|
| 141 |
+
if parent: b.parent = parent
|
| 142 |
+
return b
|
| 143 |
+
|
| 144 |
+
# 코어 스파인
|
| 145 |
+
hips = add_bone("Hips", Vector((cent.x, cent.y, minv.z + height*0.20)), Vector((cent.x, cent.y, minv.z + height*0.30)))
|
| 146 |
+
spine = add_bone("Spine", hips.tail, Vector((cent.x, cent.y, minv.z + height*0.45)), parent=hips)
|
| 147 |
+
chest = add_bone("Chest", spine.tail, Vector((cent.x, cent.y, minv.z + height*0.60)), parent=spine)
|
| 148 |
+
neck = add_bone("Neck", Vector((cent.x, cent.y, minv.z + height*0.68)), Vector((cent.x, cent.y, minv.z + height*0.74)), parent=chest)
|
| 149 |
+
headb = add_bone("Head", neck.tail, Vector((cent.x, cent.y, minv.z + height*0.86)), parent=neck)
|
| 150 |
+
|
| 151 |
+
# 팔(T-포즈)
|
| 152 |
+
arm_len = max(width*0.35, 0.05)
|
| 153 |
+
ua_L = add_bone("UpperArm.L", Vector((cent.x+0.05*width, cent.y, chest.tail.z-0.02*height)),
|
| 154 |
+
Vector((cent.x+0.05*width+arm_len*0.5, cent.y, chest.tail.z-0.02*height)), parent=chest)
|
| 155 |
+
fa_L = add_bone("LowerArm.L", ua_L.tail, ua_L.tail + Vector((arm_len*0.5, 0, 0)), parent=ua_L)
|
| 156 |
+
handL= add_bone("Hand.L", fa_L.tail, fa_L.tail + Vector((arm_len*0.2, 0, 0)), parent=fa_L)
|
| 157 |
+
ua_R = add_bone("UpperArm.R", Vector((cent.x-0.05*width, cent.y, chest.tail.z-0.02*height)),
|
| 158 |
+
Vector((cent.x-0.05*width-arm_len*0.5, cent.y, chest.tail.z-0.02*height)), parent=chest)
|
| 159 |
+
fa_R = add_bone("LowerArm.R", ua_R.tail, ua_R.tail - Vector((arm_len*0.5, 0, 0)), parent=ua_R)
|
| 160 |
+
handR= add_bone("Hand.R", fa_R.tail, fa_R.tail - Vector((arm_len*0.2, 0, 0)), parent=fa_R)
|
| 161 |
+
|
| 162 |
+
# 다리
|
| 163 |
+
leg_off = max(width*0.12, 0.02)
|
| 164 |
+
thighL = add_bone("Thigh.L", Vector((cent.x+leg_off, cent.y, hips.head.z)), Vector((cent.x+leg_off, cent.y, minv.z + height*0.05)), parent=hips)
|
| 165 |
+
shinL = add_bone("Shin.L", thighL.tail, Vector((cent.x+leg_off, cent.y, minv.z + 0.01*height)), parent=thighL)
|
| 166 |
+
footL = add_bone("Foot.L", shinL.tail, shinL.tail + Vector((0.0, 0.05*height, -0.02*height)), parent=shinL)
|
| 167 |
+
thighR = add_bone("Thigh.R", Vector((cent.x-leg_off, cent.y, hips.head.z)), Vector((cent.x-leg_off, cent.y, minv.z + height*0.05)), parent=hips)
|
| 168 |
+
shinR = add_bone("Shin.R", thighR.tail, Vector((cent.x-leg_off, cent.y, minv.z + 0.01*height)), parent=thighR)
|
| 169 |
+
footR = add_bone("Foot.R", shinR.tail, shinR.tail + Vector((0.0, 0.05*height, -0.02*height)), parent=shinR)
|
| 170 |
+
|
| 171 |
+
bpy.ops.object.mode_set(mode='OBJECT')
|
| 172 |
+
|
| 173 |
+
# -----------------------
|
| 174 |
+
# Body → Armature 자동 바인딩
|
| 175 |
+
# -----------------------
|
| 176 |
+
for o in meshes:
|
| 177 |
+
o.select_set(False)
|
| 178 |
+
body.select_set(True)
|
| 179 |
+
arm.select_set(True)
|
| 180 |
+
bpy.context.view_layer.objects.active = arm
|
| 181 |
+
bpy.ops.object.parent_set(type='ARMATURE_AUTO')
|
| 182 |
+
|
| 183 |
+
# -----------------------
|
| 184 |
+
# 의복 처리
|
| 185 |
+
# -----------------------
|
| 186 |
+
if args.surface_deform:
|
| 187 |
+
# Body 기준 Surface Deform (본가중치 최소화)
|
| 188 |
+
bpy.context.view_layer.objects.active = body
|
| 189 |
+
for o in others:
|
| 190 |
+
if o.type != "MESH":
|
| 191 |
+
continue
|
| 192 |
+
# 기존 Armature modifier 제거
|
| 193 |
+
for m in list(o.modifiers):
|
| 194 |
+
if m.type == "ARMATURE":
|
| 195 |
+
try:
|
| 196 |
+
o.modifiers.remove(m)
|
| 197 |
+
except Exception:
|
| 198 |
+
pass
|
| 199 |
+
sdef = o.modifiers.new("SurfaceDeform", "SURFACE_DEFORM")
|
| 200 |
+
sdef.target = body
|
| 201 |
+
try:
|
| 202 |
+
bpy.ops.object.surfacedeform_bind(modifier=sdef.name)
|
| 203 |
+
except Exception as e:
|
| 204 |
+
print("[WARN] SurfaceDeform bind 실패:", e)
|
| 205 |
+
|
| 206 |
+
elif args.limit_cloth_bones:
|
| 207 |
+
# Armature 유지하되 본 영향 축소(간이 훅)
|
| 208 |
+
for o in others:
|
| 209 |
+
if o.type != "MESH":
|
| 210 |
+
continue
|
| 211 |
+
has_arm = any(m.type == "ARMATURE" for m in o.modifiers)
|
| 212 |
+
if not has_arm:
|
| 213 |
+
m = o.modifiers.new("Armature", "ARMATURE")
|
| 214 |
+
m.object = arm
|
| 215 |
+
# 세부 가중치 조정은 모델마다 달라 완전 자동화 난이도 높음 → 후편집 전제
|
| 216 |
+
|
| 217 |
+
# -----------------------
|
| 218 |
+
# Toon Shader & Outline
|
| 219 |
+
# -----------------------
|
| 220 |
+
def make_toon_material(name="ToonMat", base_color=(0.8, 0.8, 0.8, 1.0)):
|
| 221 |
+
mat = bpy.data.materials.new(name)
|
| 222 |
+
mat.use_nodes = True
|
| 223 |
+
nt = mat.node_tree
|
| 224 |
+
nodes = nt.nodes; links = nt.links
|
| 225 |
+
# 초기화
|
| 226 |
+
for n in list(nodes):
|
| 227 |
+
nodes.remove(n)
|
| 228 |
+
out = nodes.new("ShaderNodeOutputMaterial"); out.location = (400, 0)
|
| 229 |
+
toon = nodes.new("ShaderNodeBsdfToon"); toon.location = (0, 0)
|
| 230 |
+
toon.inputs["Color"].default_value = base_color
|
| 231 |
+
toon.inputs["Size"].default_value = 0.5
|
| 232 |
+
toon.inputs["Smooth"].default_value = 0.0
|
| 233 |
+
links.new(toon.outputs["BSDF"], out.inputs["Surface"])
|
| 234 |
+
return mat
|
| 235 |
+
|
| 236 |
+
def apply_toon(obj):
|
| 237 |
+
if obj.type != "MESH":
|
| 238 |
+
return
|
| 239 |
+
if not obj.data.materials:
|
| 240 |
+
obj.data.materials.append(make_toon_material())
|
| 241 |
+
else:
|
| 242 |
+
for i, _ in enumerate(obj.data.materials):
|
| 243 |
+
obj.data.materials[i] = make_toon_material(f"Toon_{obj.name}_{i}")
|
| 244 |
+
|
| 245 |
+
def add_outline(obj, thickness=0.003):
|
| 246 |
+
if obj.type != "MESH":
|
| 247 |
+
return
|
| 248 |
+
outline = obj.copy()
|
| 249 |
+
outline.data = obj.data.copy()
|
| 250 |
+
outline.name = obj.name + "_Outline"
|
| 251 |
+
# 컬렉션 링크
|
| 252 |
+
for c in outline.users_collection:
|
| 253 |
+
try:
|
| 254 |
+
c.objects.unlink(outline)
|
| 255 |
+
except Exception:
|
| 256 |
+
pass
|
| 257 |
+
bpy.context.scene.collection.objects.link(outline)
|
| 258 |
+
|
| 259 |
+
# 검정 Emission 머티리얼
|
| 260 |
+
mat = bpy.data.materials.new("OutlineBlack")
|
| 261 |
+
mat.use_nodes = True
|
| 262 |
+
nodes = mat.node_tree.nodes; links = mat.node_tree.links
|
| 263 |
+
for n in list(nodes): nodes.remove(n)
|
| 264 |
+
out = nodes.new("ShaderNodeOutputMaterial"); out.location = (200, 0)
|
| 265 |
+
em = nodes.new("ShaderNodeEmission"); em.location = (0, 0)
|
| 266 |
+
em.inputs["Color"].default_value = (0, 0, 0, 1)
|
| 267 |
+
em.inputs["Strength"].default_value = 1.0
|
| 268 |
+
links.new(em.outputs["Emission"], out.inputs["Surface"])
|
| 269 |
+
|
| 270 |
+
outline.data.materials.clear()
|
| 271 |
+
outline.data.materials.append(mat)
|
| 272 |
+
|
| 273 |
+
# Solidify로 외곽선(노멀 반전)
|
| 274 |
+
bpy.context.view_layer.objects.active = outline
|
| 275 |
+
solid = outline.modifiers.new("OutlineSolidify", "SOLIDIFY")
|
| 276 |
+
solid.thickness = -abs(thickness)
|
| 277 |
+
solid.offset = 1.0
|
| 278 |
+
solid.use_flip_normals = True
|
| 279 |
+
solid.material_offset = 0
|
| 280 |
+
outline.show_in_front = True
|
| 281 |
+
|
| 282 |
+
if args.toon:
|
| 283 |
+
apply_toon(body)
|
| 284 |
+
for o in others:
|
| 285 |
+
apply_toon(o)
|
| 286 |
+
|
| 287 |
+
if args.outline:
|
| 288 |
+
add_outline(body)
|
| 289 |
+
for o in others:
|
| 290 |
+
add_outline(o, thickness=0.003)
|
| 291 |
+
|
| 292 |
+
# -----------------------
|
| 293 |
+
# 표정(Shape Keys)
|
| 294 |
+
# -----------------------
|
| 295 |
+
def ensure_basis_key(obj):
|
| 296 |
+
if obj.data.shape_keys is None:
|
| 297 |
+
obj.shape_key_add(name="Basis")
|
| 298 |
+
return obj.data.shape_keys.key_blocks["Basis"]
|
| 299 |
+
|
| 300 |
+
def add_placeholder_face_keys(obj):
|
| 301 |
+
ensure_basis_key(obj)
|
| 302 |
+
keys = [
|
| 303 |
+
"EyeBlink_L","EyeBlink_R","BrowDown_L","BrowDown_R",
|
| 304 |
+
"BrowUp","JawOpen","MouthSmile_L","MouthSmile_R",
|
| 305 |
+
"MouthFrown","MouthPucker","MouthLeft","MouthRight"
|
| 306 |
+
]
|
| 307 |
+
for k in keys:
|
| 308 |
+
try:
|
| 309 |
+
obj.shape_key_add(name=k)
|
| 310 |
+
except Exception:
|
| 311 |
+
pass
|
| 312 |
+
|
| 313 |
+
def try_faceit_generate():
|
| 314 |
+
# 애드온 설치 감지(자동 실행은 버전 의존으로 생략)
|
| 315 |
+
if "faceit" in bpy.context.preferences.addons:
|
| 316 |
+
print("[INFO] Faceit addon detected. Use GUI to auto-generate blendshapes if needed.")
|
| 317 |
+
return True
|
| 318 |
+
for k in bpy.context.preferences.addons.keys():
|
| 319 |
+
if "face" in k and "it" in k:
|
| 320 |
+
print(f"[INFO] Possibly Faceit-like addon detected: {k}")
|
| 321 |
+
return True
|
| 322 |
+
return False
|
| 323 |
+
|
| 324 |
+
if args.try_faceit and try_faceit_generate():
|
| 325 |
+
# 사용자 GUI에서 Faceit 워크플로 진행 권장
|
| 326 |
+
pass
|
| 327 |
+
else:
|
| 328 |
+
add_placeholder_face_keys(body)
|
| 329 |
+
|
| 330 |
+
# -----------------------
|
| 331 |
+
# Export
|
| 332 |
+
# -----------------------
|
| 333 |
+
# 선택 정리
|
| 334 |
+
for o in bpy.context.scene.objects:
|
| 335 |
+
o.select_set(False)
|
| 336 |
+
arm.select_set(True)
|
| 337 |
+
body.select_set(True)
|
| 338 |
+
for o in others:
|
| 339 |
+
if o.type == "MESH":
|
| 340 |
+
o.select_set(True)
|
| 341 |
+
bpy.context.view_layer.objects.active = arm
|
| 342 |
+
|
| 343 |
+
if export_fmt == "fbx":
|
| 344 |
+
bpy.ops.export_scene.fbx(
|
| 345 |
+
filepath=out_path,
|
| 346 |
+
use_selection=True,
|
| 347 |
+
apply_unit_scale=True,
|
| 348 |
+
bake_space_transform=True,
|
| 349 |
+
add_leaf_bones=False,
|
| 350 |
+
mesh_smooth_type='FACE'
|
| 351 |
+
)
|
| 352 |
+
else:
|
| 353 |
+
bpy.ops.export_scene.gltf(
|
| 354 |
+
filepath=out_path,
|
| 355 |
+
export_format='GLB',
|
| 356 |
+
use_selection=True,
|
| 357 |
+
export_apply=True
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
print("[DONE] Exported:", out_path)
|
requirements.txt
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# UI
|
| 2 |
+
gradio==4.44.0
|
| 3 |
+
gradio_client>=1.3.0
|
| 4 |
+
|
| 5 |
+
# Core
|
| 6 |
+
torch==2.3.1+cpu
|
| 7 |
+
torchvision==0.18.1+cpu
|
| 8 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 9 |
+
numpy==1.26.4
|
| 10 |
+
Pillow
|
| 11 |
+
scipy
|
| 12 |
+
tqdm
|
| 13 |
+
requests
|
| 14 |
+
huggingface_hub
|
| 15 |
+
safetensors
|
| 16 |
+
accelerate==0.31.0
|
| 17 |
+
|
| 18 |
+
# Diffusers / ControlNet
|
| 19 |
+
diffusers==0.29.2
|
| 20 |
+
transformers==4.43.4
|
| 21 |
+
opencv-python-headless==4.10.0.84
|
| 22 |
+
imageio
|
| 23 |
+
scikit-image
|
| 24 |
+
|
| 25 |
+
# Background removal
|
| 26 |
+
rembg==2.0.56
|
| 27 |
+
onnxruntime==1.18.0
|
| 28 |
+
|
| 29 |
+
# Weapon removal (inpaint)
|
| 30 |
+
simple-lama-inpainting
|
| 31 |
+
|
| 32 |
+
# Hands (21 keypoints)
|
| 33 |
+
mediapipe==0.10.14
|
| 34 |
+
protobuf<5
|
| 35 |
+
|
| 36 |
+
# 3D & optional 6-view render (headless에서 실패하면 코드가 자동 스킵)
|
| 37 |
+
trimesh
|
| 38 |
+
networkx
|
| 39 |
+
pyrender
|
| 40 |
+
PyOpenGL
|
| 41 |
+
PyOpenGL-accelerate
|
| 42 |
+
pyglet
|