aptol commited on
Commit
ccf0740
·
verified ·
1 Parent(s): 68429b3

Upload 8 files

Browse files
.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: gray
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- short_description: genshin
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