| import os |
| os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" |
| import sys |
| import tempfile |
| from pathlib import Path |
| import gradio as gr |
| import numpy as np |
| import trimesh |
| import torch |
| from sklearn.cluster import KMeans |
| from huggingface_hub import hf_hub_download |
|
|
| |
| ROOT = Path(__file__).parent |
| sys.path.append(str(ROOT / "partfield")) |
| CKPT_DIR = ROOT / "partfield" / "model" |
| CKPT_PATH = CKPT_DIR / "model_objaverse.ckpt" |
|
|
| from partfield.config import default_argument_parser, setup |
| from partfield.model_trainer_pvcnn_only_demo import Model |
|
|
| def ensure_checkpoint(): |
| CKPT_DIR.mkdir(parents=True, exist_ok=True) |
| if CKPT_PATH.is_file(): |
| return str(CKPT_PATH) |
| print("Downloading PartField checkpoint (~1.24 GB)...") |
| downloaded = hf_hub_download( |
| repo_id="mikaelaangel/partfield-ckpt", |
| filename="model_objaverse.ckpt", |
| token=os.environ.get("HF_TOKEN"), |
| local_dir=str(CKPT_DIR), |
| ) |
| return downloaded |
|
|
| ensure_checkpoint() |
|
|
| |
| CONFIG_FILE = ROOT / "partfield" / "configs" / "final" / "demo.yaml" |
| parser = default_argument_parser() |
| args = parser.parse_args([ |
| "-c", str(CONFIG_FILE), |
| "--opts", |
| "continue_ckpt", str(CKPT_PATH), |
| "result_name", "gradio_output", |
| ]) |
| cfg = setup(args, freeze=False) |
|
|
| |
| print("Loading PartField model...") |
| model = Model(cfg) |
| state_dict = torch.load(CKPT_PATH, map_location="cpu")["state_dict"] |
| model.load_state_dict(state_dict, strict=False) |
| if torch.cuda.is_available(): |
| model = model.cuda() |
| model.eval() |
| print("Model loaded successfully.") |
|
|
| def process_mesh(mesh_file, num_clusters=5): |
| try: |
| import gc |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| |
| num_clusters = int(num_clusters) |
| import uuid |
| uid = f"gradio_upload_{uuid.uuid4().hex[:8]}" |
| |
| |
| mesh = trimesh.load(mesh_file.name, force='mesh') |
| |
| |
| if mesh.faces.shape[1] == 4: |
| new_faces = [] |
| for face in mesh.faces: |
| new_faces.append([face[0], face[1], face[2]]) |
| new_faces.append([face[0], face[2], face[3]]) |
| mesh.faces = np.array(new_faces) |
|
|
| |
| vertices = mesh.vertices |
| bbmin = vertices.min(0) |
| bbmax = vertices.max(0) |
| center = (bbmin + bbmax) * 0.5 |
| scale = 2.0 * 0.9 / (bbmax - bbmin).max() |
| vertices = (vertices - center) * scale |
| mesh.vertices = vertices |
|
|
| |
| pc, _ = trimesh.sample.sample_surface(mesh, 20000) |
|
|
| |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
| batch = { |
| 'uid': [uid], |
| 'pc': torch.tensor(pc, dtype=torch.float32).unsqueeze(0).to(device), |
| 'vertices': [torch.tensor(mesh.vertices, dtype=torch.float32).to(device)], |
| 'faces': [torch.tensor(mesh.faces, dtype=torch.int64).to(device)] |
| } |
|
|
| |
| save_dir = f"exp_results/{cfg.result_name}" |
| feat_path_1 = f'{save_dir}/part_feat_{uid}_0.npy' |
| feat_path_2 = f'{save_dir}/part_feat_{uid}_0_batch.npy' |
| pca_path = f'{save_dir}/feat_pca_{uid}_0.ply' |
| if os.path.exists(feat_path_1): os.remove(feat_path_1) |
| if os.path.exists(feat_path_2): os.remove(feat_path_2) |
|
|
| with torch.no_grad(): |
| model.predict_step(batch, 0) |
|
|
| |
| feat_path = feat_path_2 if os.path.exists(feat_path_2) else feat_path_1 |
| if not os.path.exists(feat_path): |
| return None |
| |
| point_feat = np.load(feat_path) |
| point_feat = point_feat / np.linalg.norm(point_feat, axis=-1, keepdims=True) |
|
|
| |
| clustering = KMeans(n_clusters=num_clusters, random_state=0).fit(point_feat) |
| labels = clustering.labels_ |
|
|
| |
| submeshes = [] |
| for i in range(num_clusters): |
| face_mask = (labels == i) |
| sub_faces = mesh.faces[face_mask] |
| if len(sub_faces) > 0: |
| submesh = trimesh.Trimesh(vertices=mesh.vertices, faces=sub_faces, process=True) |
| |
| |
| submeshes.append(submesh) |
|
|
| scene = trimesh.Scene(submeshes) |
| out_path = tempfile.mktemp(suffix=".glb") |
| scene.export(out_path) |
| |
| return out_path |
| except Exception as e: |
| print("Error processing mesh:", e) |
| return None |
| finally: |
| |
| for p in [feat_path_1, feat_path_2, pca_path]: |
| try: |
| if 'p' in locals() and os.path.exists(p): |
| os.remove(p) |
| except Exception: |
| pass |
|
|
| import gc |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
| with gr.Blocks() as demo: |
| gr.Markdown("# PartField Space — API Endpoint") |
| with gr.Row(): |
| inp_file = gr.File(label="Input 3D Mesh (.glb/.obj)") |
| inp_clusters = gr.Number(value=5, label="Number of Parts") |
| out_file = gr.File(label="Segmented Mesh (.glb)") |
| gr.Button("Segment Mesh").click(process_mesh, inputs=[inp_file, inp_clusters], outputs=[out_file], api_name="/segment") |
|
|
| demo.queue(default_concurrency_limit=1).launch(ssr_mode=False) |