Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import List, Tuple
|
|
| 12 |
from PIL import Image
|
| 13 |
from easydict import EasyDict as edict
|
| 14 |
|
| 15 |
-
# Add missing imports for
|
| 16 |
from gradio_client import Client, handle_file
|
| 17 |
from gradio_client.exceptions import AppError
|
| 18 |
|
|
@@ -21,8 +21,7 @@ from trellis.pipelines import TrellisImageTo3DPipeline
|
|
| 21 |
from trellis.representations import Gaussian, MeshExtractResult
|
| 22 |
from trellis.utils import render_utils, postprocessing_utils
|
| 23 |
|
| 24 |
-
|
| 25 |
-
RIGNET_AVAILABLE = True
|
| 26 |
|
| 27 |
# Configuration
|
| 28 |
MAX_SEED = np.iinfo(np.int32).max
|
|
@@ -97,7 +96,7 @@ def unpack_state(state: dict) -> Tuple[Gaussian, edict]:
|
|
| 97 |
gs._scaling = torch.tensor(state['gaussian']['_scaling'], device='cuda')
|
| 98 |
gs._rotation = torch.tensor(state['gaussian']['_rotation'], device='cuda')
|
| 99 |
gs._opacity = torch.tensor(state['gaussian']['_opacity'], device='cuda')
|
| 100 |
-
|
| 101 |
mesh = edict(
|
| 102 |
vertices=torch.tensor(state['mesh']['vertices'], device='cuda'),
|
| 103 |
faces=torch.tensor(state['mesh']['faces'], device='cuda'),
|
|
@@ -108,43 +107,51 @@ def get_seed(randomize_seed: bool, seed: int) -> int:
|
|
| 108 |
"""Get random seed for generation"""
|
| 109 |
return np.random.randint(0, MAX_SEED) if randomize_seed else seed
|
| 110 |
|
| 111 |
-
def
|
| 112 |
"""
|
| 113 |
-
Call
|
| 114 |
-
|
| 115 |
Args:
|
| 116 |
-
obj_path: Path to OBJ file
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
Returns:
|
| 121 |
-
Tuple of (
|
| 122 |
-
-
|
| 123 |
-
-
|
|
|
|
| 124 |
"""
|
| 125 |
try:
|
| 126 |
-
print(f"𦴠Connecting to
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
print("π€ Uploading OBJ to
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
bandwidth=float(bandwidth),
|
| 133 |
api_name="/predict"
|
| 134 |
)
|
| 135 |
-
|
| 136 |
-
#
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
except AppError as e:
|
| 143 |
error_msg = str(e)
|
| 144 |
-
print(f"β οΈ
|
| 145 |
raise
|
| 146 |
except Exception as e:
|
| 147 |
-
print(f"β οΈ
|
| 148 |
raise
|
| 149 |
|
| 150 |
@spaces.GPU(duration=180)
|
|
@@ -158,18 +165,17 @@ def generate_3d_with_rigging(
|
|
| 158 |
mesh_simplify: float,
|
| 159 |
texture_size: int,
|
| 160 |
req: gr.Request,
|
| 161 |
-
) -> Tuple[dict, str, str, str, str, str]:
|
| 162 |
"""
|
| 163 |
-
Complete pipeline: Image -> 3D Model (TRELLIS) -> OBJ -> Rigging (
|
| 164 |
-
All processing happens in this single GPU-decorated function to avoid quota issues.
|
| 165 |
"""
|
| 166 |
try:
|
| 167 |
user_dir = os.path.join(TMP_DIR, str(req.session_hash))
|
| 168 |
-
|
| 169 |
# ============ STEP 1: TRELLIS 3D GENERATION ============
|
| 170 |
print("π¨ Generating 3D model with TRELLIS...")
|
| 171 |
init_pipeline()
|
| 172 |
-
|
| 173 |
outputs = pipeline.run(
|
| 174 |
image,
|
| 175 |
seed=seed,
|
|
@@ -184,25 +190,26 @@ def generate_3d_with_rigging(
|
|
| 184 |
"cfg_strength": slat_guidance_strength,
|
| 185 |
},
|
| 186 |
)
|
| 187 |
-
|
| 188 |
# Extract Gaussian and Mesh
|
| 189 |
gs = outputs['gaussian'][0]
|
| 190 |
mesh = outputs['mesh'][0]
|
| 191 |
-
|
| 192 |
# ============ STEP 2: RENDER VIDEO ============
|
| 193 |
print("πΉ Rendering 360Β° preview video...")
|
| 194 |
video = render_utils.render_video(gs, num_frames=120)['color']
|
| 195 |
video_geo = render_utils.render_video(mesh, num_frames=120)['normal']
|
| 196 |
video = [np.concatenate([video[i], video_geo[i]], axis=1) for i in range(len(video))]
|
|
|
|
| 197 |
video_path = os.path.join(user_dir, 'sample.mp4')
|
| 198 |
imageio.mimsave(video_path, video, fps=15)
|
| 199 |
-
|
| 200 |
# ============ STEP 3: EXTRACT GLB ============
|
| 201 |
print("π Extracting GLB with textures...")
|
| 202 |
glb = postprocessing_utils.to_glb(gs, mesh, simplify=mesh_simplify, texture_size=texture_size, verbose=False)
|
| 203 |
glb_path = os.path.join(user_dir, 'sample.glb')
|
| 204 |
glb.export(glb_path)
|
| 205 |
-
|
| 206 |
# ============ STEP 4: CONVERT GLB TO OBJ ============
|
| 207 |
print("π Converting GLB to OBJ format...")
|
| 208 |
obj_path = os.path.join(user_dir, "model.obj")
|
|
@@ -210,7 +217,7 @@ def generate_3d_with_rigging(
|
|
| 210 |
original_vertices = len(mesh_trimesh.vertices)
|
| 211 |
original_faces = len(mesh_trimesh.faces)
|
| 212 |
mesh_trimesh.export(obj_path)
|
| 213 |
-
|
| 214 |
mesh_info = f"""
|
| 215 |
π Mesh Statistics:
|
| 216 |
β’ Vertices: {original_vertices:,}
|
|
@@ -218,50 +225,56 @@ def generate_3d_with_rigging(
|
|
| 218 |
β’ Texture Size: {texture_size}px
|
| 219 |
β’ Status: β Ready for rigging
|
| 220 |
"""
|
| 221 |
-
|
| 222 |
-
# ============ STEP 5:
|
| 223 |
-
print("𦴠Calling
|
| 224 |
rig_info = ""
|
| 225 |
rig_file = None
|
| 226 |
-
|
|
|
|
| 227 |
try:
|
| 228 |
-
# Call
|
| 229 |
-
rig_result_path, rig_info_text =
|
| 230 |
obj_path=obj_path,
|
| 231 |
-
|
| 232 |
-
rignet_space="ckc99u/spaceB" # β Update with your RigNet space
|
| 233 |
)
|
| 234 |
-
|
| 235 |
if rig_result_path and os.path.exists(rig_result_path):
|
| 236 |
-
# Copy
|
| 237 |
-
rig_file = os.path.join(user_dir, '
|
| 238 |
shutil.copy(rig_result_path, rig_file)
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
{rig_info_text}
|
| 241 |
|
| 242 |
-
π₯
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
"""
|
| 244 |
-
|
| 245 |
-
# Partial failure: Create fallback
|
| 246 |
-
rig_file = os.path.join(user_dir, 'rig_result.txt')
|
| 247 |
-
with open(rig_file, 'w') as f:
|
| 248 |
-
f.write(rig_info_text)
|
| 249 |
-
rig_info = rig_info_text
|
| 250 |
-
|
| 251 |
except Exception as e:
|
| 252 |
-
print(f"β οΈ
|
| 253 |
# Create error file with instructions
|
| 254 |
-
rig_file = os.path.join(user_dir, '
|
| 255 |
with open(rig_file, 'w') as f:
|
| 256 |
-
f.write(f"
|
| 257 |
f.write("Workaround: Download OBJ and rig manually in Blender.")
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
| 260 |
# ============ STEP 6: PACK RESULTS ============
|
| 261 |
print("π¦ Packaging results...")
|
| 262 |
state = pack_state(gs, mesh)
|
| 263 |
torch.cuda.empty_cache()
|
| 264 |
-
|
| 265 |
combined_info = f"""
|
| 266 |
π¨ TRELLIS Generation:
|
| 267 |
β’ Seed: {seed}
|
|
@@ -278,23 +291,24 @@ def generate_3d_with_rigging(
|
|
| 278 |
β Video preview (360Β° rotation)
|
| 279 |
β GLB file (textured 3D model)
|
| 280 |
β OBJ file (standard 3D format)
|
| 281 |
-
β
|
|
|
|
| 282 |
|
| 283 |
π§ Next Steps:
|
| 284 |
-
1. Download OBJ
|
| 285 |
2. Import into Blender/Maya/C4D
|
| 286 |
-
3. Apply
|
| 287 |
-
4.
|
| 288 |
|
| 289 |
π‘ Pro Tips:
|
| 290 |
-
β’
|
| 291 |
-
β’
|
| 292 |
-
β’
|
| 293 |
"""
|
| 294 |
-
|
| 295 |
print("β
All processing complete!")
|
| 296 |
-
return state, video_path, glb_path, obj_path, rig_file, combined_info
|
| 297 |
-
|
| 298 |
except Exception as e:
|
| 299 |
import traceback
|
| 300 |
error_detail = traceback.format_exc()
|
|
@@ -313,32 +327,32 @@ def extract_gaussian(state: dict, req: gr.Request) -> Tuple[str, str]:
|
|
| 313 |
return gaussian_path, gaussian_path
|
| 314 |
|
| 315 |
# ============ GRADIO UI ============
|
| 316 |
-
with gr.Blocks(title="Image to Rigged 3D Model (
|
| 317 |
gr.Markdown("""
|
| 318 |
-
# π Image β 3D β Rigging (
|
| 319 |
|
| 320 |
-
**
|
| 321 |
|
| 322 |
This unified pipeline combines:
|
| 323 |
- **TRELLIS** (Image-to-3D, Microsoft Research)
|
| 324 |
-
- **
|
| 325 |
|
| 326 |
### π Workflow:
|
| 327 |
1. π€ Upload image of object/character
|
| 328 |
2. π¨ TRELLIS generates high-quality 3D mesh (GPU)
|
| 329 |
3. π Convert to OBJ format
|
| 330 |
-
4. π¦΄
|
| 331 |
-
5. πΎ Download mesh + rigging for animation
|
| 332 |
|
| 333 |
### β¨ Benefits:
|
| 334 |
-
- β
|
| 335 |
-
- β
|
| 336 |
-
- β
Complete control over parameters
|
| 337 |
- β
Production-ready output for Blender/Maya
|
|
|
|
| 338 |
|
| 339 |
β±οΈ **Estimated time:** 2-5 minutes
|
| 340 |
""")
|
| 341 |
-
|
| 342 |
with gr.Row():
|
| 343 |
with gr.Column(scale=1):
|
| 344 |
gr.Markdown("### π₯ Input")
|
|
@@ -349,11 +363,11 @@ This unified pipeline combines:
|
|
| 349 |
type="pil",
|
| 350 |
height=300
|
| 351 |
)
|
| 352 |
-
|
| 353 |
with gr.Accordion("βοΈ TRELLIS Parameters", open=False):
|
| 354 |
seed = gr.Slider(0, MAX_SEED, label="Seed", value=0, step=1)
|
| 355 |
randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
|
| 356 |
-
|
| 357 |
gr.Markdown("**Stage 1: Sparse Structure**")
|
| 358 |
ss_guidance = gr.Slider(
|
| 359 |
0.0, 10.0,
|
|
@@ -367,7 +381,7 @@ This unified pipeline combines:
|
|
| 367 |
value=12,
|
| 368 |
step=1
|
| 369 |
)
|
| 370 |
-
|
| 371 |
gr.Markdown("**Stage 2: Structured Latent**")
|
| 372 |
slat_guidance = gr.Slider(
|
| 373 |
0.0, 10.0,
|
|
@@ -381,7 +395,7 @@ This unified pipeline combines:
|
|
| 381 |
value=12,
|
| 382 |
step=1
|
| 383 |
)
|
| 384 |
-
|
| 385 |
with gr.Accordion("βοΈ Output Settings", open=False):
|
| 386 |
mesh_simplify = gr.Slider(
|
| 387 |
0.9, 0.98,
|
|
@@ -395,21 +409,21 @@ This unified pipeline combines:
|
|
| 395 |
value=1024,
|
| 396 |
step=512
|
| 397 |
)
|
| 398 |
-
|
| 399 |
generate_btn = gr.Button(
|
| 400 |
"π Generate Rigged Model",
|
| 401 |
variant="primary",
|
| 402 |
size="lg"
|
| 403 |
)
|
| 404 |
-
|
| 405 |
extract_gs_btn = gr.Button(
|
| 406 |
"π₯ Extract Gaussian (PLY)",
|
| 407 |
interactive=False
|
| 408 |
)
|
| 409 |
-
|
| 410 |
with gr.Column(scale=1):
|
| 411 |
gr.Markdown("### π€ Outputs")
|
| 412 |
-
|
| 413 |
with gr.Tabs():
|
| 414 |
with gr.Tab("πΉ Preview"):
|
| 415 |
video_output = gr.Video(
|
|
@@ -418,13 +432,13 @@ This unified pipeline combines:
|
|
| 418 |
loop=True,
|
| 419 |
height=300
|
| 420 |
)
|
| 421 |
-
|
| 422 |
with gr.Tab("π¨ 3D Viewer"):
|
| 423 |
model_output = gr.Model3D(
|
| 424 |
label="GLB Viewer",
|
| 425 |
height=400
|
| 426 |
)
|
| 427 |
-
|
| 428 |
with gr.Tab("π¦ Files"):
|
| 429 |
glb_download = gr.DownloadButton(
|
| 430 |
label="π₯ Download GLB",
|
|
@@ -435,34 +449,38 @@ This unified pipeline combines:
|
|
| 435 |
interactive=False
|
| 436 |
)
|
| 437 |
rig_download = gr.DownloadButton(
|
| 438 |
-
label="𦴠Download
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
interactive=False
|
| 440 |
)
|
| 441 |
gs_download = gr.DownloadButton(
|
| 442 |
label="β¨ Download Gaussian (PLY)",
|
| 443 |
interactive=False
|
| 444 |
)
|
| 445 |
-
|
| 446 |
with gr.Tab("βΉοΈ Info"):
|
| 447 |
info_output = gr.Textbox(
|
| 448 |
label="Pipeline Information",
|
| 449 |
lines=20,
|
| 450 |
max_lines=30
|
| 451 |
)
|
| 452 |
-
|
| 453 |
# State management
|
| 454 |
output_buf = gr.State()
|
| 455 |
-
|
| 456 |
# Event handlers
|
| 457 |
demo.load(start_session)
|
| 458 |
demo.unload(end_session)
|
| 459 |
-
|
| 460 |
input_image.upload(
|
| 461 |
preprocess_image,
|
| 462 |
inputs=[input_image],
|
| 463 |
outputs=[input_image],
|
| 464 |
)
|
| 465 |
-
|
| 466 |
generate_btn.click(
|
| 467 |
get_seed,
|
| 468 |
inputs=[randomize_seed, seed],
|
|
@@ -475,27 +493,29 @@ This unified pipeline combines:
|
|
| 475 |
slat_guidance, slat_steps,
|
| 476 |
mesh_simplify, texture_size
|
| 477 |
],
|
| 478 |
-
outputs=[output_buf, video_output, model_output, obj_download, rig_download, info_output],
|
| 479 |
).then(
|
| 480 |
lambda: (
|
| 481 |
gr.Button(interactive=True),
|
| 482 |
gr.DownloadButton(interactive=True),
|
| 483 |
gr.DownloadButton(interactive=True),
|
| 484 |
gr.DownloadButton(interactive=True),
|
|
|
|
| 485 |
),
|
| 486 |
-
outputs=[extract_gs_btn, glb_download, obj_download, rig_download],
|
| 487 |
)
|
| 488 |
-
|
| 489 |
video_output.clear(
|
| 490 |
lambda: (
|
| 491 |
gr.Button(interactive=False),
|
| 492 |
gr.DownloadButton(interactive=False),
|
| 493 |
gr.DownloadButton(interactive=False),
|
| 494 |
gr.DownloadButton(interactive=False),
|
|
|
|
| 495 |
),
|
| 496 |
-
outputs=[extract_gs_btn, glb_download, obj_download, rig_download],
|
| 497 |
)
|
| 498 |
-
|
| 499 |
extract_gs_btn.click(
|
| 500 |
extract_gaussian,
|
| 501 |
inputs=[output_buf],
|
|
@@ -507,4 +527,4 @@ This unified pipeline combines:
|
|
| 507 |
|
| 508 |
if __name__ == "__main__":
|
| 509 |
init_pipeline()
|
| 510 |
-
demo.launch(
|
|
|
|
| 12 |
from PIL import Image
|
| 13 |
from easydict import EasyDict as edict
|
| 14 |
|
| 15 |
+
# Add missing imports for MagicArticulate API
|
| 16 |
from gradio_client import Client, handle_file
|
| 17 |
from gradio_client.exceptions import AppError
|
| 18 |
|
|
|
|
| 21 |
from trellis.representations import Gaussian, MeshExtractResult
|
| 22 |
from trellis.utils import render_utils, postprocessing_utils
|
| 23 |
|
| 24 |
+
MAGIC_ARTICULATE_URL = "https://62157d4fdd84d3addb.gradio.live/"
|
|
|
|
| 25 |
|
| 26 |
# Configuration
|
| 27 |
MAX_SEED = np.iinfo(np.int32).max
|
|
|
|
| 96 |
gs._scaling = torch.tensor(state['gaussian']['_scaling'], device='cuda')
|
| 97 |
gs._rotation = torch.tensor(state['gaussian']['_rotation'], device='cuda')
|
| 98 |
gs._opacity = torch.tensor(state['gaussian']['_opacity'], device='cuda')
|
| 99 |
+
|
| 100 |
mesh = edict(
|
| 101 |
vertices=torch.tensor(state['mesh']['vertices'], device='cuda'),
|
| 102 |
faces=torch.tensor(state['mesh']['faces'], device='cuda'),
|
|
|
|
| 107 |
"""Get random seed for generation"""
|
| 108 |
return np.random.randint(0, MAX_SEED) if randomize_seed else seed
|
| 109 |
|
| 110 |
+
def call_magic_articulate_api(obj_path: str, api_url: str = MAGIC_ARTICULATE_URL) -> Tuple[str, str, str]:
|
| 111 |
"""
|
| 112 |
+
Call MagicArticulate Colab API to generate rigging and skeleton from OBJ mesh
|
| 113 |
+
|
| 114 |
Args:
|
| 115 |
+
obj_path: Path to OBJ file
|
| 116 |
+
api_url: MagicArticulate Colab gradio URL
|
| 117 |
+
|
|
|
|
| 118 |
Returns:
|
| 119 |
+
Tuple of (rig_pred_path, skeleton_obj_path, info_text)
|
| 120 |
+
- rig_pred_path: Path to generated rig prediction TXT file
|
| 121 |
+
- skeleton_obj_path: Path to generated skeleton OBJ file
|
| 122 |
+
- info_text: Info about rigging results
|
| 123 |
"""
|
| 124 |
try:
|
| 125 |
+
print(f"𦴠Connecting to MagicArticulate API ({api_url})...")
|
| 126 |
+
magic_client = Client(api_url)
|
| 127 |
+
|
| 128 |
+
print("π€ Uploading OBJ to MagicArticulate...")
|
| 129 |
+
result = magic_client.predict(
|
| 130 |
+
input_mesh=handle_file(obj_path),
|
|
|
|
| 131 |
api_name="/predict"
|
| 132 |
)
|
| 133 |
+
|
| 134 |
+
# MagicArticulate returns (rig_pred.txt, skeleton.obj, normalized_mesh.obj)
|
| 135 |
+
rig_pred_file = result[0]
|
| 136 |
+
skeleton_file = result[1]
|
| 137 |
+
|
| 138 |
+
print("β
MagicArticulate generation successful!")
|
| 139 |
+
|
| 140 |
+
# Read skeleton info
|
| 141 |
+
info_text = "Skeleton generated with hierarchical bone ordering"
|
| 142 |
+
if skeleton_file and os.path.exists(skeleton_file):
|
| 143 |
+
skeleton_mesh = trimesh.load(skeleton_file, force='mesh')
|
| 144 |
+
num_vertices = len(skeleton_mesh.vertices)
|
| 145 |
+
info_text = f"Joints: {num_vertices // 2}, Hierarchical structure"
|
| 146 |
+
|
| 147 |
+
return rig_pred_file, skeleton_file, info_text
|
| 148 |
+
|
| 149 |
except AppError as e:
|
| 150 |
error_msg = str(e)
|
| 151 |
+
print(f"β οΈ MagicArticulate error: {error_msg}")
|
| 152 |
raise
|
| 153 |
except Exception as e:
|
| 154 |
+
print(f"β οΈ MagicArticulate API error: {str(e)}")
|
| 155 |
raise
|
| 156 |
|
| 157 |
@spaces.GPU(duration=180)
|
|
|
|
| 165 |
mesh_simplify: float,
|
| 166 |
texture_size: int,
|
| 167 |
req: gr.Request,
|
| 168 |
+
) -> Tuple[dict, str, str, str, str, str, str]:
|
| 169 |
"""
|
| 170 |
+
Complete pipeline: Image -> 3D Model (TRELLIS) -> OBJ -> Rigging (MagicArticulate)
|
|
|
|
| 171 |
"""
|
| 172 |
try:
|
| 173 |
user_dir = os.path.join(TMP_DIR, str(req.session_hash))
|
| 174 |
+
|
| 175 |
# ============ STEP 1: TRELLIS 3D GENERATION ============
|
| 176 |
print("π¨ Generating 3D model with TRELLIS...")
|
| 177 |
init_pipeline()
|
| 178 |
+
|
| 179 |
outputs = pipeline.run(
|
| 180 |
image,
|
| 181 |
seed=seed,
|
|
|
|
| 190 |
"cfg_strength": slat_guidance_strength,
|
| 191 |
},
|
| 192 |
)
|
| 193 |
+
|
| 194 |
# Extract Gaussian and Mesh
|
| 195 |
gs = outputs['gaussian'][0]
|
| 196 |
mesh = outputs['mesh'][0]
|
| 197 |
+
|
| 198 |
# ============ STEP 2: RENDER VIDEO ============
|
| 199 |
print("πΉ Rendering 360Β° preview video...")
|
| 200 |
video = render_utils.render_video(gs, num_frames=120)['color']
|
| 201 |
video_geo = render_utils.render_video(mesh, num_frames=120)['normal']
|
| 202 |
video = [np.concatenate([video[i], video_geo[i]], axis=1) for i in range(len(video))]
|
| 203 |
+
|
| 204 |
video_path = os.path.join(user_dir, 'sample.mp4')
|
| 205 |
imageio.mimsave(video_path, video, fps=15)
|
| 206 |
+
|
| 207 |
# ============ STEP 3: EXTRACT GLB ============
|
| 208 |
print("π Extracting GLB with textures...")
|
| 209 |
glb = postprocessing_utils.to_glb(gs, mesh, simplify=mesh_simplify, texture_size=texture_size, verbose=False)
|
| 210 |
glb_path = os.path.join(user_dir, 'sample.glb')
|
| 211 |
glb.export(glb_path)
|
| 212 |
+
|
| 213 |
# ============ STEP 4: CONVERT GLB TO OBJ ============
|
| 214 |
print("π Converting GLB to OBJ format...")
|
| 215 |
obj_path = os.path.join(user_dir, "model.obj")
|
|
|
|
| 217 |
original_vertices = len(mesh_trimesh.vertices)
|
| 218 |
original_faces = len(mesh_trimesh.faces)
|
| 219 |
mesh_trimesh.export(obj_path)
|
| 220 |
+
|
| 221 |
mesh_info = f"""
|
| 222 |
π Mesh Statistics:
|
| 223 |
β’ Vertices: {original_vertices:,}
|
|
|
|
| 225 |
β’ Texture Size: {texture_size}px
|
| 226 |
β’ Status: β Ready for rigging
|
| 227 |
"""
|
| 228 |
+
|
| 229 |
+
# ============ STEP 5: MAGIC ARTICULATE RIGGING ============
|
| 230 |
+
print("𦴠Calling MagicArticulate API for automatic skeleton generation...")
|
| 231 |
rig_info = ""
|
| 232 |
rig_file = None
|
| 233 |
+
skeleton_file = None
|
| 234 |
+
|
| 235 |
try:
|
| 236 |
+
# Call MagicArticulate Colab API
|
| 237 |
+
rig_result_path, skeleton_result_path, rig_info_text = call_magic_articulate_api(
|
| 238 |
obj_path=obj_path,
|
| 239 |
+
api_url=MAGIC_ARTICULATE_URL
|
|
|
|
| 240 |
)
|
| 241 |
+
|
| 242 |
if rig_result_path and os.path.exists(rig_result_path):
|
| 243 |
+
# Copy rig prediction file to user directory
|
| 244 |
+
rig_file = os.path.join(user_dir, 'rig_pred.txt')
|
| 245 |
shutil.copy(rig_result_path, rig_file)
|
| 246 |
+
|
| 247 |
+
if skeleton_result_path and os.path.exists(skeleton_result_path):
|
| 248 |
+
# Copy skeleton file to user directory
|
| 249 |
+
skeleton_file = os.path.join(user_dir, 'skeleton.obj')
|
| 250 |
+
shutil.copy(skeleton_result_path, skeleton_file)
|
| 251 |
+
|
| 252 |
+
rig_info = f"""β
MagicArticulate Skeleton Generated:
|
| 253 |
{rig_info_text}
|
| 254 |
|
| 255 |
+
π₯ Downloads:
|
| 256 |
+
β’ rig_pred.txt - Joint positions & bone hierarchy
|
| 257 |
+
β’ skeleton.obj - 3D skeleton visualization
|
| 258 |
+
|
| 259 |
+
π§ Import into Blender/Maya for animation
|
| 260 |
"""
|
| 261 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
except Exception as e:
|
| 263 |
+
print(f"β οΈ MagicArticulate API error: {str(e)}")
|
| 264 |
# Create error file with instructions
|
| 265 |
+
rig_file = os.path.join(user_dir, 'rig_pred.txt')
|
| 266 |
with open(rig_file, 'w') as f:
|
| 267 |
+
f.write(f"MagicArticulate Error: {str(e)}\n\n")
|
| 268 |
f.write("Workaround: Download OBJ and rig manually in Blender.")
|
| 269 |
+
|
| 270 |
+
rig_info = f"β οΈ MagicArticulate API unavailable: {str(e)}\n\n**Solution:** Download OBJ and use Blender Rigify add-on"
|
| 271 |
+
skeleton_file = None
|
| 272 |
+
|
| 273 |
# ============ STEP 6: PACK RESULTS ============
|
| 274 |
print("π¦ Packaging results...")
|
| 275 |
state = pack_state(gs, mesh)
|
| 276 |
torch.cuda.empty_cache()
|
| 277 |
+
|
| 278 |
combined_info = f"""
|
| 279 |
π¨ TRELLIS Generation:
|
| 280 |
β’ Seed: {seed}
|
|
|
|
| 291 |
β Video preview (360Β° rotation)
|
| 292 |
β GLB file (textured 3D model)
|
| 293 |
β OBJ file (standard 3D format)
|
| 294 |
+
β Rig prediction (TXT)
|
| 295 |
+
β Skeleton (OBJ)
|
| 296 |
|
| 297 |
π§ Next Steps:
|
| 298 |
+
1. Download OBJ + Skeleton files
|
| 299 |
2. Import into Blender/Maya/C4D
|
| 300 |
+
3. Apply rigging from rig_pred.txt
|
| 301 |
+
4. Animate your model
|
| 302 |
|
| 303 |
π‘ Pro Tips:
|
| 304 |
+
β’ Skeleton shows joint hierarchy visually
|
| 305 |
+
β’ Rig prediction contains exact joint coordinates
|
| 306 |
+
β’ Model is optimized for animation workflow
|
| 307 |
"""
|
| 308 |
+
|
| 309 |
print("β
All processing complete!")
|
| 310 |
+
return state, video_path, glb_path, obj_path, rig_file, skeleton_file, combined_info
|
| 311 |
+
|
| 312 |
except Exception as e:
|
| 313 |
import traceback
|
| 314 |
error_detail = traceback.format_exc()
|
|
|
|
| 327 |
return gaussian_path, gaussian_path
|
| 328 |
|
| 329 |
# ============ GRADIO UI ============
|
| 330 |
+
with gr.Blocks(title="Image to Rigged 3D Model (MagicArticulate)", delete_cache=(600, 600)) as demo:
|
| 331 |
gr.Markdown("""
|
| 332 |
+
# π Image β 3D β Rigging (MagicArticulate Pipeline)
|
| 333 |
|
| 334 |
+
**Automated 3D generation with hierarchical skeleton rigging!**
|
| 335 |
|
| 336 |
This unified pipeline combines:
|
| 337 |
- **TRELLIS** (Image-to-3D, Microsoft Research)
|
| 338 |
+
- **MagicArticulate** (Auto-skeleton generation, CVPR 2025)
|
| 339 |
|
| 340 |
### π Workflow:
|
| 341 |
1. π€ Upload image of object/character
|
| 342 |
2. π¨ TRELLIS generates high-quality 3D mesh (GPU)
|
| 343 |
3. π Convert to OBJ format
|
| 344 |
+
4. 𦴠MagicArticulate generates hierarchical skeleton
|
| 345 |
+
5. πΎ Download mesh + rigging + skeleton for animation
|
| 346 |
|
| 347 |
### β¨ Benefits:
|
| 348 |
+
- β
Hierarchical bone ordering for better animation
|
| 349 |
+
- β
Automatic joint placement and bone connections
|
|
|
|
| 350 |
- β
Production-ready output for Blender/Maya
|
| 351 |
+
- β
Visual skeleton + rig data included
|
| 352 |
|
| 353 |
β±οΈ **Estimated time:** 2-5 minutes
|
| 354 |
""")
|
| 355 |
+
|
| 356 |
with gr.Row():
|
| 357 |
with gr.Column(scale=1):
|
| 358 |
gr.Markdown("### π₯ Input")
|
|
|
|
| 363 |
type="pil",
|
| 364 |
height=300
|
| 365 |
)
|
| 366 |
+
|
| 367 |
with gr.Accordion("βοΈ TRELLIS Parameters", open=False):
|
| 368 |
seed = gr.Slider(0, MAX_SEED, label="Seed", value=0, step=1)
|
| 369 |
randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
|
| 370 |
+
|
| 371 |
gr.Markdown("**Stage 1: Sparse Structure**")
|
| 372 |
ss_guidance = gr.Slider(
|
| 373 |
0.0, 10.0,
|
|
|
|
| 381 |
value=12,
|
| 382 |
step=1
|
| 383 |
)
|
| 384 |
+
|
| 385 |
gr.Markdown("**Stage 2: Structured Latent**")
|
| 386 |
slat_guidance = gr.Slider(
|
| 387 |
0.0, 10.0,
|
|
|
|
| 395 |
value=12,
|
| 396 |
step=1
|
| 397 |
)
|
| 398 |
+
|
| 399 |
with gr.Accordion("βοΈ Output Settings", open=False):
|
| 400 |
mesh_simplify = gr.Slider(
|
| 401 |
0.9, 0.98,
|
|
|
|
| 409 |
value=1024,
|
| 410 |
step=512
|
| 411 |
)
|
| 412 |
+
|
| 413 |
generate_btn = gr.Button(
|
| 414 |
"π Generate Rigged Model",
|
| 415 |
variant="primary",
|
| 416 |
size="lg"
|
| 417 |
)
|
| 418 |
+
|
| 419 |
extract_gs_btn = gr.Button(
|
| 420 |
"π₯ Extract Gaussian (PLY)",
|
| 421 |
interactive=False
|
| 422 |
)
|
| 423 |
+
|
| 424 |
with gr.Column(scale=1):
|
| 425 |
gr.Markdown("### π€ Outputs")
|
| 426 |
+
|
| 427 |
with gr.Tabs():
|
| 428 |
with gr.Tab("πΉ Preview"):
|
| 429 |
video_output = gr.Video(
|
|
|
|
| 432 |
loop=True,
|
| 433 |
height=300
|
| 434 |
)
|
| 435 |
+
|
| 436 |
with gr.Tab("π¨ 3D Viewer"):
|
| 437 |
model_output = gr.Model3D(
|
| 438 |
label="GLB Viewer",
|
| 439 |
height=400
|
| 440 |
)
|
| 441 |
+
|
| 442 |
with gr.Tab("π¦ Files"):
|
| 443 |
glb_download = gr.DownloadButton(
|
| 444 |
label="π₯ Download GLB",
|
|
|
|
| 449 |
interactive=False
|
| 450 |
)
|
| 451 |
rig_download = gr.DownloadButton(
|
| 452 |
+
label="𦴠Download Rig Prediction (TXT)",
|
| 453 |
+
interactive=False
|
| 454 |
+
)
|
| 455 |
+
skeleton_download = gr.DownloadButton(
|
| 456 |
+
label="𦴠Download Skeleton (OBJ)",
|
| 457 |
interactive=False
|
| 458 |
)
|
| 459 |
gs_download = gr.DownloadButton(
|
| 460 |
label="β¨ Download Gaussian (PLY)",
|
| 461 |
interactive=False
|
| 462 |
)
|
| 463 |
+
|
| 464 |
with gr.Tab("βΉοΈ Info"):
|
| 465 |
info_output = gr.Textbox(
|
| 466 |
label="Pipeline Information",
|
| 467 |
lines=20,
|
| 468 |
max_lines=30
|
| 469 |
)
|
| 470 |
+
|
| 471 |
# State management
|
| 472 |
output_buf = gr.State()
|
| 473 |
+
|
| 474 |
# Event handlers
|
| 475 |
demo.load(start_session)
|
| 476 |
demo.unload(end_session)
|
| 477 |
+
|
| 478 |
input_image.upload(
|
| 479 |
preprocess_image,
|
| 480 |
inputs=[input_image],
|
| 481 |
outputs=[input_image],
|
| 482 |
)
|
| 483 |
+
|
| 484 |
generate_btn.click(
|
| 485 |
get_seed,
|
| 486 |
inputs=[randomize_seed, seed],
|
|
|
|
| 493 |
slat_guidance, slat_steps,
|
| 494 |
mesh_simplify, texture_size
|
| 495 |
],
|
| 496 |
+
outputs=[output_buf, video_output, model_output, obj_download, rig_download, skeleton_download, info_output],
|
| 497 |
).then(
|
| 498 |
lambda: (
|
| 499 |
gr.Button(interactive=True),
|
| 500 |
gr.DownloadButton(interactive=True),
|
| 501 |
gr.DownloadButton(interactive=True),
|
| 502 |
gr.DownloadButton(interactive=True),
|
| 503 |
+
gr.DownloadButton(interactive=True),
|
| 504 |
),
|
| 505 |
+
outputs=[extract_gs_btn, glb_download, obj_download, rig_download, skeleton_download],
|
| 506 |
)
|
| 507 |
+
|
| 508 |
video_output.clear(
|
| 509 |
lambda: (
|
| 510 |
gr.Button(interactive=False),
|
| 511 |
gr.DownloadButton(interactive=False),
|
| 512 |
gr.DownloadButton(interactive=False),
|
| 513 |
gr.DownloadButton(interactive=False),
|
| 514 |
+
gr.DownloadButton(interactive=False),
|
| 515 |
),
|
| 516 |
+
outputs=[extract_gs_btn, glb_download, obj_download, rig_download, skeleton_download],
|
| 517 |
)
|
| 518 |
+
|
| 519 |
extract_gs_btn.click(
|
| 520 |
extract_gaussian,
|
| 521 |
inputs=[output_buf],
|
|
|
|
| 527 |
|
| 528 |
if __name__ == "__main__":
|
| 529 |
init_pipeline()
|
| 530 |
+
demo.launch()
|