Spaces:
Running on Zero
Running on Zero
| """PoseShield: Neural Collision Fields for Human Self-Collision Resolution. | |
| A Gradio demo that takes a colliding SMPL-H pose and resolves self-collisions | |
| using the PoseShield neural collision field with SLSQP optimization. | |
| """ | |
| import os | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| import spaces # MUST come before torch | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import numpy as np | |
| import pickle | |
| import tempfile | |
| import time | |
| import yaml | |
| import struct | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from mpl_toolkits.mplot3d import Axes3D | |
| from scipy.optimize import minimize | |
| from huggingface_hub import hf_hub_download | |
| import smplx | |
| import gradio as gr | |
| # --------------------------------------------------------------------------- | |
| # Rotation utilities (from poseshield.common.utils) | |
| # --------------------------------------------------------------------------- | |
| def normalize(x, axis=-1, eps=1e-8): | |
| norm = np.linalg.norm(x, axis=axis, keepdims=True) + eps | |
| return x / norm | |
| def axis_angle_to_matrix(axis_angle): | |
| """Convert (N, 3) axis-angle to (N, 3, 3) rotation matrices (Rodrigues).""" | |
| aa = np.asarray(axis_angle, dtype=np.float64) | |
| N = aa.shape[0] | |
| theta = np.linalg.norm(aa, axis=1, keepdims=True) | |
| eps = 1e-8 | |
| k = aa / (theta + eps) | |
| kx, ky, kz = k[:, 0], k[:, 1], k[:, 2] | |
| K = np.zeros((N, 3, 3), dtype=np.float64) | |
| K[:, 0, 1] = -kz | |
| K[:, 0, 2] = ky | |
| K[:, 1, 0] = kz | |
| K[:, 1, 2] = -kx | |
| K[:, 2, 0] = -ky | |
| K[:, 2, 1] = kx | |
| I = np.eye(3, dtype=np.float64)[None, :, :] | |
| sin_t = np.sin(theta)[:, None].reshape(N, 1, 1) | |
| cos_t = np.cos(theta)[:, None].reshape(N, 1, 1) | |
| K2 = K @ K | |
| small = (theta.reshape(N) < 1e-4) | |
| A = np.empty((N, 1, 1), dtype=np.float64) | |
| B = np.empty((N, 1, 1), dtype=np.float64) | |
| A[~small] = sin_t[~small] | |
| B[~small] = (1.0 - cos_t[~small]) | |
| th = theta.reshape(N, 1, 1) | |
| A[small] = th[small] - (th[small]**3) / 6.0 | |
| B[small] = (th[small]**2) / 2.0 - (th[small]**4) / 24.0 | |
| R = I + A * K + B * K2 | |
| return R | |
| def matrix_to_axis_angle(R): | |
| """Convert (N, 3, 3) rotation matrices to (N, 3) axis-angle vectors.""" | |
| trace = np.trace(R, axis1=1, axis2=2) | |
| trace = np.clip(trace, -1.0, 3.0) | |
| angles = np.arccos((trace - 1.0) / 2.0) | |
| rx = R[:, 2, 1] - R[:, 1, 2] | |
| ry = R[:, 0, 2] - R[:, 2, 0] | |
| rz = R[:, 1, 0] - R[:, 0, 1] | |
| axes = np.stack([rx, ry, rz], axis=1) | |
| sin_angles = np.linalg.norm(axes, axis=1, keepdims=True) / 2.0 | |
| axes = axes / (2.0 * (sin_angles + 1e-8)) | |
| axis_angle = axes * angles[:, None] | |
| return axis_angle | |
| def rotation_6d_to_matrix(d6): | |
| """Convert (..., 6) 6D rotation to (..., 3, 3) rotation matrix (Gram-Schmidt).""" | |
| a1 = d6[..., :3] | |
| a2 = d6[..., 3:] | |
| b1 = normalize(a1, axis=-1) | |
| dot = np.sum(b1 * a2, axis=-1, keepdims=True) | |
| b2 = a2 - dot * b1 | |
| b2 = normalize(b2, axis=-1) | |
| b3 = np.cross(b1, b2, axis=-1) | |
| rotation_mats = np.stack((b1, b2, b3), axis=-2) | |
| return rotation_mats | |
| def matrix_to_rotation_6d(R): | |
| """Convert (..., 3, 3) rotation matrix to (..., 6) 6D rotation.""" | |
| b1 = R[..., 0, :] | |
| b2 = R[..., 1, :] | |
| d6 = np.concatenate([b1, b2], axis=-1) | |
| return d6 | |
| def rotation_6d_to_matrix_torch(d6): | |
| """Torch differentiable version of rotation_6d_to_matrix.""" | |
| a1, a2 = d6[..., :3], d6[..., 3:] | |
| b1 = F.normalize(a1, dim=-1) | |
| b2 = a2 - (b1 * a2).sum(dim=-1, keepdim=True) * b1 | |
| b2 = F.normalize(b2, dim=-1) | |
| b3 = torch.cross(b1, b2, dim=-1) | |
| return torch.stack((b1, b2, b3), dim=-2) | |
| def matrix_to_axis_angle_torch(R): | |
| """Torch differentiable version of matrix_to_axis_angle.""" | |
| trace = R[:, 0, 0] + R[:, 1, 1] + R[:, 2, 2] | |
| cos_angle = ((trace - 1.0) / 2.0).clamp(-1.0 + 1e-7, 1.0 - 1e-7) | |
| angle = torch.acos(cos_angle) | |
| rx = R[:, 2, 1] - R[:, 1, 2] | |
| ry = R[:, 0, 2] - R[:, 2, 0] | |
| rz = R[:, 1, 0] - R[:, 0, 1] | |
| axis_raw = torch.stack([rx, ry, rz], dim=1) | |
| safe_sin = torch.sin(angle).abs().clamp(min=1e-7).unsqueeze(1) | |
| unit_axis = axis_raw / (2.0 * safe_sin) | |
| return unit_axis * angle.unsqueeze(1) | |
| # --------------------------------------------------------------------------- | |
| # PoseShield model (from poseshield.common.network) | |
| # --------------------------------------------------------------------------- | |
| class ResidualMLP(nn.Module): | |
| """Residual MLP: 21x6 joint rotations -> scalar collision field value.""" | |
| def __init__(self, in_dim=126, hidden_dim=512, num_layers=12, activation="relu"): | |
| super().__init__() | |
| self.input_layer = nn.Linear(in_dim, hidden_dim) | |
| self.hidden_layers = nn.ModuleList([ | |
| nn.Linear(hidden_dim, hidden_dim) for _ in range(num_layers) | |
| ]) | |
| if activation == "relu": | |
| self.act = nn.ReLU() | |
| elif activation == "leaky_relu": | |
| self.act = nn.LeakyReLU() | |
| elif activation == "elu": | |
| self.act = nn.ELU() | |
| else: | |
| raise ValueError(f"Unsupported activation: {activation}") | |
| self.output_layer = nn.Linear(hidden_dim, 1) | |
| def forward(self, x): | |
| bs = x.shape[0] | |
| x_reshaped = x.reshape(-1, 6) | |
| x_raw = x_reshaped[:, :3] | |
| x_norm = x_raw / x_raw.norm(dim=1, keepdim=True) | |
| y_raw = x_reshaped[:, 3:] | |
| dot = (x_norm * y_raw).sum(dim=1, keepdim=True) | |
| y_perp = y_raw - dot * x_norm | |
| y_norm = y_perp / y_perp.norm(dim=1, keepdim=True) | |
| x_valid = torch.cat([x_norm, y_norm], dim=1).reshape(bs, -1) | |
| x = self.act(self.input_layer(x_valid)) | |
| for layer in self.hidden_layers: | |
| x = self.act(layer(x)) + x | |
| return self.output_layer(x) | |
| # --------------------------------------------------------------------------- | |
| # Cost & constraint functions (from poseshield.pose.utils) | |
| # --------------------------------------------------------------------------- | |
| _SUBTREE_SIZES = [4, 4, 13, 3, 3, 12, 2, 2, 11, 1, 1, 2, 4, 4, 1, 3, 3, 2, 2, 1, 1] | |
| SMPLH_POSE_WEIGHTS = torch.tensor(_SUBTREE_SIZES, dtype=torch.float32) | |
| SMPLH_POSE_WEIGHTS /= SMPLH_POSE_WEIGHTS.sum() | |
| def constraint_function(model, x): | |
| """Collision field value for a pose. Positive = collision-free.""" | |
| output = model(x.unsqueeze(0)) | |
| return output.squeeze(0).squeeze(0) | |
| def cost_function_weighted(x, x_ref, weights=None): | |
| """Weighted L2 pose distance preserving kinematic-chain importance.""" | |
| if weights is None: | |
| weights = SMPLH_POSE_WEIGHTS.to(x.device) | |
| diff = (x - x_ref).reshape(21, 6) | |
| per_joint_norm = torch.linalg.norm(diff, dim=-1) | |
| return (per_joint_norm * weights).sum() | |
| def cost_function(x, x_ref): | |
| return cost_function_weighted(x, x_ref) | |
| # --------------------------------------------------------------------------- | |
| # SLSQP optimizer (from poseshield.pose.resolve_slsqp) | |
| # --------------------------------------------------------------------------- | |
| def optimize_slsqp(sample, model, device, max_itr=300, threshold=0.1, | |
| cost_type="normal", tol=0.03): | |
| """SLSQP optimization to resolve collisions while preserving pose.""" | |
| x0 = sample.reshape(-1).astype(np.float64) | |
| x_ref_np = x0.copy() | |
| def to_torch(x_np, requires_grad=False): | |
| return torch.tensor(x_np, dtype=torch.float32, device=device, | |
| requires_grad=requires_grad) | |
| x_ref_t = to_torch(x_ref_np, requires_grad=False) | |
| def cost_fn_np(x_np): | |
| x_t = to_torch(x_np) | |
| val = cost_function(x_t, x_ref_t) if cost_type != "weighted" else cost_function_weighted(x_t, x_ref_t) | |
| return float(val.detach().cpu().item()) | |
| def cost_fn_jac(x_np): | |
| x_t = to_torch(x_np, requires_grad=True) | |
| val = cost_function(x_t, x_ref_t) if cost_type != "weighted" else cost_function_weighted(x_t, x_ref_t) | |
| grad = torch.autograd.grad(val, x_t)[0] | |
| return grad.detach().cpu().numpy().astype(np.float64) | |
| def cons_ineq_fun(x_np): | |
| x_t = to_torch(x_np) | |
| val = constraint_function(model, x_t) - threshold | |
| return float(val.detach().cpu().item()) | |
| def cons_ineq_jac(x_np): | |
| x_t = to_torch(x_np, requires_grad=True) | |
| cons_val = constraint_function(model, x_t) | |
| grad = torch.autograd.grad(cons_val, x_t)[0] | |
| return grad.detach().cpu().numpy().astype(np.float64) | |
| def view6(x_t): | |
| return x_t.view(-1, 6) | |
| def ineq_r1_upper_fun(x_np): | |
| x_t = to_torch(x_np) | |
| r1 = view6(x_t)[:, :3] | |
| return float((1.0 + tol - r1.norm(dim=1).mean()).detach().cpu().item()) | |
| def ineq_r1_upper_jac(x_np): | |
| x_t = to_torch(x_np, requires_grad=True) | |
| r1 = view6(x_t)[:, :3] | |
| val = 1.0 + tol - r1.norm(dim=1).mean() | |
| grad = torch.autograd.grad(val, x_t)[0] | |
| return grad.detach().cpu().numpy().astype(np.float64) | |
| def ineq_r1_lower_fun(x_np): | |
| x_t = to_torch(x_np) | |
| r1 = view6(x_t)[:, :3] | |
| return float((r1.norm(dim=1).mean() - (1.0 - tol)).detach().cpu().item()) | |
| def ineq_r1_lower_jac(x_np): | |
| x_t = to_torch(x_np, requires_grad=True) | |
| r1 = view6(x_t)[:, :3] | |
| val = r1.norm(dim=1).mean() - (1.0 - tol) | |
| grad = torch.autograd.grad(val, x_t)[0] | |
| return grad.detach().cpu().numpy().astype(np.float64) | |
| def ineq_r2_upper_fun(x_np): | |
| x_t = to_torch(x_np) | |
| r2 = view6(x_t)[:, 3:] | |
| return float((1.0 + tol - r2.norm(dim=1).mean()).detach().cpu().item()) | |
| def ineq_r2_upper_jac(x_np): | |
| x_t = to_torch(x_np, requires_grad=True) | |
| r2 = view6(x_t)[:, 3:] | |
| val = 1.0 + tol - r2.norm(dim=1).mean() | |
| grad = torch.autograd.grad(val, x_t)[0] | |
| return grad.detach().cpu().numpy().astype(np.float64) | |
| def ineq_r2_lower_fun(x_np): | |
| x_t = to_torch(x_np) | |
| r2 = view6(x_t)[:, 3:] | |
| return float((r2.norm(dim=1).mean() - (1.0 - tol)).detach().cpu().item()) | |
| def ineq_r2_lower_jac(x_np): | |
| x_t = to_torch(x_np, requires_grad=True) | |
| r2 = view6(x_t)[:, 3:] | |
| val = r2.norm(dim=1).mean() - (1.0 - tol) | |
| grad = torch.autograd.grad(val, x_t)[0] | |
| return grad.detach().cpu().numpy().astype(np.float64) | |
| def ineq_orth_upper_fun(x_np): | |
| x_t = to_torch(x_np) | |
| r1, r2 = view6(x_t).split(3, dim=1) | |
| dot_mean = (r1 * r2).sum(dim=1).mean() | |
| return float((tol - dot_mean).detach().cpu().item()) | |
| def ineq_orth_upper_jac(x_np): | |
| x_t = to_torch(x_np, requires_grad=True) | |
| r1, r2 = view6(x_t).split(3, dim=1) | |
| val = tol - (r1 * r2).sum(dim=1).mean() | |
| grad = torch.autograd.grad(val, x_t)[0] | |
| return grad.detach().cpu().numpy().astype(np.float64) | |
| def ineq_orth_lower_fun(x_np): | |
| x_t = to_torch(x_np) | |
| r1, r2 = view6(x_t).split(3, dim=1) | |
| dot_mean = (r1 * r2).sum(dim=1).mean() | |
| return float((tol + dot_mean).detach().cpu().item()) | |
| def ineq_orth_lower_jac(x_np): | |
| x_t = to_torch(x_np, requires_grad=True) | |
| r1, r2 = view6(x_t).split(3, dim=1) | |
| val = tol + (r1 * r2).sum(dim=1).mean() | |
| grad = torch.autograd.grad(val, x_t)[0] | |
| return grad.detach().cpu().numpy().astype(np.float64) | |
| constraints = [ | |
| {"type": "ineq", "fun": cons_ineq_fun, "jac": cons_ineq_jac}, | |
| {"type": "ineq", "fun": ineq_r1_upper_fun, "jac": ineq_r1_upper_jac}, | |
| {"type": "ineq", "fun": ineq_r1_lower_fun, "jac": ineq_r1_lower_jac}, | |
| {"type": "ineq", "fun": ineq_r2_upper_fun, "jac": ineq_r2_upper_jac}, | |
| {"type": "ineq", "fun": ineq_r2_lower_fun, "jac": ineq_r2_lower_jac}, | |
| {"type": "ineq", "fun": ineq_orth_upper_fun, "jac": ineq_orth_upper_jac}, | |
| {"type": "ineq", "fun": ineq_orth_lower_fun, "jac": ineq_orth_lower_jac}, | |
| ] | |
| loss_history, cons_history = [], [] | |
| def callback(xk): | |
| try: | |
| loss_history.append(cost_fn_np(xk)) | |
| cons_history.append(constraint_function(model, to_torch(xk)).detach().cpu().item()) | |
| except Exception: | |
| pass | |
| res = minimize( | |
| cost_fn_np, x0, method="SLSQP", jac=cost_fn_jac, | |
| constraints=constraints, bounds=None, | |
| options={"maxiter": max_itr, "ftol": 1e-6, "disp": False}, | |
| callback=callback, | |
| ) | |
| x_opt = res.x.astype(np.float32) | |
| if len(loss_history) == 0: | |
| loss_history.append(cost_fn_np(x_opt)) | |
| if len(cons_history) == 0: | |
| cons_history.append(constraint_function(model, to_torch(x_opt)).detach().cpu().item()) | |
| return x_opt, loss_history, cons_history, bool(res.success), str(res.message) | |
| # --------------------------------------------------------------------------- | |
| # Mesh visualization | |
| # --------------------------------------------------------------------------- | |
| def pose_to_mesh(smpl_model, r_6d, device): | |
| """Convert 21x6 6D rotation to SMPL mesh vertices and faces.""" | |
| rot_mats = rotation_6d_to_matrix(r_6d) | |
| axis_angles = matrix_to_axis_angle(rot_mats) | |
| body_pose = torch.from_numpy(axis_angles.reshape(1, -1)).float().to(device) | |
| output = smpl_model( | |
| global_orient=None, | |
| body_pose=body_pose, | |
| betas=None, | |
| transl=None, | |
| return_verts=True, | |
| ) | |
| vertices = output.vertices[0].detach().cpu().numpy() | |
| faces = smpl_model.faces.astype(np.int32) | |
| return vertices, faces | |
| def visualize_smpl(vertices, faces, save_path, color="#ff7675"): | |
| """Render SMPL mesh to PNG using matplotlib.""" | |
| fig = plt.figure(figsize=(8, 6), facecolor="white") | |
| ax = fig.add_subplot(111, projection="3d") | |
| ax.set_facecolor("white") | |
| x_plt = vertices[:, 0] | |
| y_plt = vertices[:, 2] | |
| z_plt = vertices[:, 1] | |
| ax.plot_trisurf(x_plt, y_plt, z_plt, triangles=faces, | |
| shade=True, color=color, edgecolor="none", alpha=0.9) | |
| all_coords = np.stack([x_plt, y_plt, z_plt], axis=-1) | |
| min_vals = np.min(all_coords, axis=0) | |
| max_vals = np.max(all_coords, axis=0) | |
| ranges = max_vals - min_vals | |
| max_range = max(ranges) | |
| mid = (max_vals + min_vals) / 2 | |
| ax.set_xlim(mid[0] - max_range / 2, mid[0] + max_range / 2) | |
| ax.set_ylim(mid[1] - max_range / 2, mid[1] + max_range / 2) | |
| ax.set_zlim(mid[2] - max_range / 2, mid[2] + max_range / 2) | |
| ax.view_init(elev=15, azim=90) | |
| ax.axis("off") | |
| ax.grid(False) | |
| plt.tight_layout() | |
| plt.savefig(save_path, dpi=150, bbox_inches="tight", facecolor="white") | |
| plt.close(fig) | |
| # --------------------------------------------------------------------------- | |
| # Model loading at module scope | |
| # --------------------------------------------------------------------------- | |
| # Download PoseShield model from HF | |
| _model_dir = hf_hub_download("ZYYY99/PoseShield", "model.pth", repo_type="model") | |
| _config_path = hf_hub_download("ZYYY99/PoseShield", "config.yaml", repo_type="model") | |
| with open(_config_path, "r") as f: | |
| _config = yaml.safe_load(f) | |
| MODEL_HIDDEN_DIM = _config["MODEL"]["HIDDEN_DIM"] | |
| MODEL_NUM_LAYERS = _config["MODEL"]["NUM_LAYERS"] | |
| MODEL_ACTIVATION = _config["MODEL"].get("ACTIVATION", "relu") | |
| # Load collision field model | |
| collision_model = ResidualMLP( | |
| in_dim=126, | |
| hidden_dim=MODEL_HIDDEN_DIM, | |
| num_layers=MODEL_NUM_LAYERS, | |
| activation=MODEL_ACTIVATION, | |
| ).to("cuda") | |
| _ckpt = torch.load(_model_dir, map_location="cuda", weights_only=True) | |
| collision_model.load_state_dict(_ckpt) | |
| collision_model.eval() | |
| print(f"PoseShield collision field loaded: hidden_dim={MODEL_HIDDEN_DIM}, layers={MODEL_NUM_LAYERS}") | |
| # Download SMPL-H neutral body model and set up directory structure for smplx | |
| # The community SMPLH model lacks hand PCA components, so we add dummy ones. | |
| _smplh_file = hf_hub_download("Tevior/smplh", "neutral/model.npz", repo_type="model") | |
| _smplh_root = tempfile.mkdtemp() | |
| _smplh_target_dir = os.path.join(_smplh_root, "smplh") | |
| os.makedirs(_smplh_target_dir, exist_ok=True) | |
| # Load the npz, add missing hand component keys, and re-save for smplx | |
| _orig_smplh = dict(np.load(_smplh_file, allow_pickle=True)) | |
| _orig_smplh["hands_componentsl"] = np.zeros((1, 45), dtype=np.float32) | |
| _orig_smplh["hands_componentsr"] = np.zeros((1, 45), dtype=np.float32) | |
| _orig_smplh["hands_meanl"] = np.zeros((45,), dtype=np.float32) | |
| _orig_smplh["hands_meanr"] = np.zeros((45,), dtype=np.float32) | |
| np.savez(os.path.join(_smplh_target_dir, "SMPLH_NEUTRAL.npz"), **_orig_smplh) | |
| _smpl_model = smplx.create( | |
| _smplh_root, | |
| model_type="smplh", | |
| gender="neutral", | |
| ext="npz", | |
| use_pca=False, | |
| ).to("cuda") | |
| print("SMPL-H neutral body model loaded") | |
| # --------------------------------------------------------------------------- | |
| # Pre-bundled example poses (from the PoseShield demo_asset directory) | |
| # --------------------------------------------------------------------------- | |
| EXAMPLE_POSES = { | |
| "Colliding Pose #210": "x_ori_210.pkl", | |
| "Colliding Pose #408": "x_ori_408.pkl", | |
| "Colliding Pose #436": "x_ori_436.pkl", | |
| } | |
| # The example .pkl files are bundled in the Space repo root (same dir as app.py) | |
| _APP_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| _example_files = {} | |
| for name, fname in EXAMPLE_POSES.items(): | |
| local_path = os.path.join(_APP_DIR, fname) | |
| if os.path.exists(local_path): | |
| _example_files[name] = local_path | |
| print(f"Loaded example pose: {name} -> {local_path}") | |
| else: | |
| # Fallback: download from the Space repo | |
| _path = hf_hub_download("hugging-apps/poseshield-collision-fix", fname, repo_type="space") | |
| _example_files[name] = _path | |
| print(f"Downloaded example pose: {name} -> {_path}") | |
| def load_pose_pickle(path): | |
| """Load a pose from a pickle file (format: dict with 'pose' key, shape (63,)).""" | |
| with open(path, "rb") as f: | |
| data = pickle.load(f, encoding="latin1") | |
| pose_aa = data["pose"].reshape(21, 3) | |
| rot_mat = axis_angle_to_matrix(pose_aa) | |
| rot_6d = matrix_to_rotation_6d(rot_mat) | |
| sample_flat = rot_6d.reshape(-1) | |
| return sample_flat, data | |
| # --------------------------------------------------------------------------- | |
| # Inference | |
| # --------------------------------------------------------------------------- | |
| def resolve_pose(pose_file, threshold=0.1, max_itr=150, progress=gr.Progress()): | |
| """Resolve self-collisions in a SMPL-H pose using the PoseShield collision field. | |
| Args: | |
| pose_file: A .pkl file containing a SMPL-H pose with a 'pose' key (21 joints x 3 axis-angle). | |
| threshold: Collision field threshold (lower = stricter constraint). | |
| max_itr: Maximum SLSQP optimization iterations. | |
| """ | |
| if pose_file is None: | |
| return None, None, "Please upload a pose file or select an example.", "" | |
| progress(0.1, desc="Loading pose...") | |
| sample_flat, raw_data = load_pose_pickle(pose_file) | |
| device = torch.device("cuda") | |
| progress(0.2, desc="Computing initial collision score...") | |
| with torch.no_grad(): | |
| init_val = constraint_function( | |
| collision_model, | |
| torch.from_numpy(sample_flat).float().to(device), | |
| ).item() | |
| progress(0.3, desc="Running SLSQP optimization...") | |
| start_time = time.time() | |
| optimized_x, loss_hist, cons_hist, success, message = optimize_slsqp( | |
| sample_flat, collision_model, device, | |
| max_itr=max_itr, threshold=threshold, | |
| ) | |
| elapsed = time.time() - start_time | |
| progress(0.7, desc="Computing final collision score...") | |
| final_val = constraint_function( | |
| collision_model, | |
| torch.from_numpy(optimized_x).float().to(device), | |
| ).item() | |
| final_error = cost_function( | |
| torch.from_numpy(optimized_x).to(device), | |
| torch.from_numpy(sample_flat).float().to(device), | |
| ).item() | |
| constraint_satisfied = final_val >= threshold | |
| progress(0.85, desc="Rendering meshes...") | |
| # Render before and after meshes | |
| with tempfile.NamedTemporaryFile(suffix="_before.png", delete=False) as f_before: | |
| before_path = f_before.name | |
| with tempfile.NamedTemporaryFile(suffix="_after.png", delete=False) as f_after: | |
| after_path = f_after.name | |
| r_6d_orig = sample_flat.reshape(21, 6) | |
| r_6d_opt = optimized_x.reshape(21, 6) | |
| verts_orig, faces = pose_to_mesh(_smpl_model, r_6d_orig, device) | |
| visualize_smpl(verts_orig, faces, before_path, color="#ff7675") | |
| verts_opt, _ = pose_to_mesh(_smpl_model, r_6d_opt, device) | |
| visualize_smpl(verts_opt, faces, after_path, color="#2ed573") | |
| progress(1.0, desc="Done!") | |
| status_text = ( | |
| f"**Optimization Results**\n" | |
| f"- Time: {elapsed:.2f}s\n" | |
| f"- Solver success: {success}\n" | |
| f"- Solver message: {message}\n" | |
| f"- Initial collision score: {init_val:.6f}\n" | |
| f"- Final collision score: {final_val:.6f} (threshold: {threshold})\n" | |
| f"- Constraint satisfied: {constraint_satisfied}\n" | |
| f"- Mean Vertex Deviation: {final_error:.6f}\n" | |
| f"- Iterations: {len(loss_hist)}" | |
| ) | |
| return before_path, after_path, status_text, f"{elapsed:.2f}s" | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------------- | |
| CSS = """ | |
| #col-container { max-width: 1100px; margin: 0 auto; } | |
| .dark .gradio-container { color: var(--body-text-color); } | |
| """ | |
| with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo: | |
| gr.Markdown(""" | |
| # PoseShield: Neural Collision Fields for Human Self-Collision Resolution | |
| Upload a colliding SMPL-H pose (`.pkl` with a `pose` key of 21×3 axis-angle rotations) or try one of the example poses below. | |
| PoseShield resolves self-collisions using a learned neural collision field as a differentiable constraint. | |
| [Paper](https://arxiv.org/abs/2606.29686) | [GitHub](https://github.com/lzhyu/PoseShield) | [Model](https://huggingface.co/ZYYY99/PoseShield) | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| pose_input = gr.File(label="Upload SMPL-H Pose (.pkl)", file_types=[".pkl"]) | |
| with gr.Accordion("Advanced Settings", open=False): | |
| threshold = gr.Slider(0.01, 1.0, value=0.1, step=0.01, | |
| label="Constraint Threshold (lower = stricter)") | |
| max_itr = gr.Slider(50, 300, value=150, step=10, | |
| label="Max Optimization Iterations") | |
| run_btn = gr.Button("Resolve Collisions", variant="primary") | |
| with gr.Column(): | |
| before_img = gr.Image(label="Before (Colliding)", type="filepath") | |
| after_img = gr.Image(label="After (Resolved)", type="filepath") | |
| status_output = gr.Markdown(label="Status") | |
| time_output = gr.Textbox(label="Inference Time", visible=True) | |
| gr.Examples( | |
| examples=[ | |
| [_example_files["Colliding Pose #210"]], | |
| [_example_files["Colliding Pose #408"]], | |
| [_example_files["Colliding Pose #436"]], | |
| ], | |
| inputs=[pose_input], | |
| outputs=[before_img, after_img, status_output, time_output], | |
| fn=resolve_pose, | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| ) | |
| run_btn.click( | |
| fn=resolve_pose, | |
| inputs=[pose_input, threshold, max_itr], | |
| outputs=[before_img, after_img, status_output, time_output], | |
| api_name="resolve", | |
| ) | |
| demo.launch(mcp_server=True) |