"""ExMR: Extremity Motion Refinement for Monocular Human Motion Capture. A ZeroGPU Gradio demo that reconstructs 3D body, hand and foot motion (SMPL-X) of the main person in a monocular video, using the ExMR model (https://huggingface.co/papers/2610.26213). Adapted from the official tools/demo.py of https://github.com/twehrbein/ExMR. Note on the pipeline: the official exmr.pipeline.run_video runs SimpleVO visual odometry in a separate PROCESS (ProcessPoolExecutor). ZeroGPU forbids spawning child processes inside @spaces.GPU, so this app runs SimpleVO synchronously in a background THREAD instead (it is CPU-only SIFT + COLMAP two-view geometry, so GIL contention is minimal), and keeps the static camera path (no odometry at all) as the default, matching the authors' example video. """ import os os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") # SimpleVO / pycolmap must not fork; OpenCV's threads inside the ZeroGPU worker are fine os.environ.setdefault("OMP_NUM_THREADS", "8") import spaces # noqa: E402 (must precede any torch / CUDA import) import json import shutil import subprocess import sys import tempfile import threading import time import traceback from concurrent.futures import ThreadPoolExecutor from pathlib import Path _HERE = Path(__file__).resolve().parent sys.path.insert(0, str(_HERE / "lib")) def _patch_torch_dynamo_config(): """ZeroGPU's torch 2.11.0+cu130 ships a torch._dynamo.config that passes an unknown `deprecated=` kwarg into torch.utils._config_module.Config, which breaks `import pytorch_lightning` (needed by HaMeR and SAM 3D Body). Drop unknown kwargs.""" try: import torch.utils._config_module as cm _orig = cm.Config.__init__ def _safe_init(self, **kwargs): allowed = getattr(cm.Config, "_defaults", None) if allowed: kwargs = {k: v for k, v in kwargs.items() if k in allowed} return _orig(self, **kwargs) cm.Config.__init__ = _safe_init except Exception: pass _patch_torch_dynamo_config() import gradio as gr # noqa: E402 import numpy as np # noqa: E402 import torch # noqa: E402 from huggingface_hub import hf_hub_download # noqa: E402 import logging # noqa: E402 log = logging.getLogger("exmr-demo") logging.basicConfig(level=logging.INFO) # --------------------------------------------------------------------------- # Checkpoint download: ~12 GB of models fetched at startup into a local cache. # Mirrors are SHA256-verified against ExMR's own tools/download_models.py pin table; the SMPL-X # body model and the MANO hand model use the public mirrors of the registered MPS files. # --------------------------------------------------------------------------- CKPT_DIR = Path(os.environ.get("EXMR_CHECKPOINT_DIR", _HERE / "checkpoints")) _DINOV3_COMMIT = "6876159a11b4df116f30f667f8c9888617df0751" _DINOV3_TREE_SHA256 = "0389aafef4614add2090f2907f210698df48f16d3f2b845b6002c4ebb6aea0f8" def _download_dinov3_code(): """Pre-fill torch.hub's cache with the pinned DINOv3 commit SAM 3D Body's backbone needs. torch.hub.load(..., source="github") downloads the repo zip on first use; doing it here (from the bundled copy at startup) keeps the first GPU call from doing a 40 MB network fetch with weird failure modes inside the ZeroGPU worker. """ import zipfile hub_dir = Path(torch.hub.get_dir()) repo_dir = hub_dir / "facebookresearch_dinov3_main" if repo_dir.exists(): return url = f"https://github.com/facebookresearch/dinov3/archive/{_DINOV3_COMMIT}.zip" hub_dir.mkdir(parents=True, exist_ok=True) tmp = hub_dir / f"_dinov3_{_DINOV3_COMMIT[:12]}.zip" if not tmp.exists(): subprocess.run( ["curl", "-sSL", "--retry", "3", "-o", str(tmp), url], check=True ) repo_dir.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(tmp) as z: z.extractall(str(hub_dir / "_dinov3_tmp")) extracted = hub_dir / "_dinov3_tmp" / f"dinov3-{_DINOV3_COMMIT}" shutil.move(str(extracted), str(repo_dir)) shutil.rmtree(str(hub_dir / "_dinov3_tmp"), ignore_errors=True) tmp.unlink(missing_ok=True) marker = repo_dir / "hubconf.py" if not marker.exists(): raise RuntimeError("DINOv3 hub prefetch failed: hubconf.py missing") def _fetch(name, repo, filename): """Download one file of an HF repo into CKPT_DIR/ (the exmr.assets layout).""" import urllib.request target = CKPT_DIR / name if target.exists(): return target log.info(f"downloading {repo}/{filename} -> {target}") target.parent.mkdir(parents=True, exist_ok=True) # hf_hub_download(local_dir=CKPT_DIR) places the file at CKPT_DIR/; # rename it into the target layout (same filesystem, so a cheap os.rename). fetched = hf_hub_download(repo, filename, local_dir=str(CKPT_DIR)) src = Path(fetched) if src != target: if target.exists(): target.unlink() src.rename(target) return target def _convert_mano_pickle(path) -> None: """Re-serialize hamer/data/mano/MANO_RIGHT.pkl without chumpy (in place). The upstream MANO v1.2 pickle stores `shapedirs` as a `chumpy.Select`, and chumpy does not work on Python 3.12 / numpy 2. `smplx.MANOLayer` (inside HaMeR's MANO wrapper) does a plain `pickle.load(mano_file, encoding='latin1')`, which therefore dies with `ModuleNotFoundError: No module named 'chumpy'`. The authors' own downloader (tools/download_models.py) converts the pickle to plain numpy/scipy and verifies the resulting arrays against a hash of the tested MANO v1.2 model -- we do exactly the same. Idempotent: a file that already loads as a plain pickle is left alone. """ import hashlib import pickle import numpy as np import scipy.sparse path = Path(path) try: # already converted? with open(path, "rb") as fh: pickle.load(fh, encoding="latin1") log.info("MANO_RIGHT.pkl already chumpy-free") return except Exception: pass class _ChumpyStub: """Stands in for chumpy objects (chumpy does not work on Python 3.12 / numpy 2).""" def __init__(self, *args, **kwargs): pass def __setstate__(self, state): self.state = state class _ManoUnpickler(pickle.Unpickler): ALLOWED = { ("numpy", "ndarray"), ("numpy", "dtype"), ("numpy.core.multiarray", "_reconstruct"), ("numpy._core.multiarray", "_reconstruct"), ("scipy.sparse.csc", "csc_matrix"), ("scipy.sparse._csc", "csc_matrix"), ("__builtin__", "set"), ("builtins", "set"), ("__builtin__", "object"), ("builtins", "object"), ("copy_reg", "_reconstructor"), ("copyreg", "_reconstructor"), } def find_class(self, module, name): if module in ("chumpy.ch", "chumpy.reordering"): return type(name, (_ChumpyStub,), {}) if (module, name) not in self.ALLOWED: raise pickle.UnpicklingError(f"Unexpected object in the MANO model: {module}.{name}") if name == "csc_matrix": # pickled under the deprecated module scipy.sparse.csc return scipy.sparse.csc_matrix return super().find_class(module, name) def _resolve_chumpy(obj): kind, state = type(obj).__name__, obj.state if kind == "Ch": return np.asarray(state["x"]) if kind == "Select": a = _resolve_chumpy(state["a"]) return a.ravel()[np.asarray(state["idxs"]).ravel()].reshape(state["preferred_shape"]) raise RuntimeError(f"Unsupported chumpy object in the MANO model: {kind}") with open(path, "rb") as fh: model = _ManoUnpickler(fh, encoding="latin1").load() for key, value in model.items(): if isinstance(value, _ChumpyStub): model[key] = _resolve_chumpy(value) digest = hashlib.sha256() for key in sorted(model): value = model[key] if scipy.sparse.issparse(value): value = value.toarray() value = np.ascontiguousarray(value) if isinstance(value, np.ndarray) else str(value) digest.update(key.encode() + b"\0") if isinstance(value, np.ndarray): digest.update(f"{value.dtype.str}{value.shape}".encode() + value.tobytes()) else: digest.update(value.encode()) actual = digest.hexdigest() expected = "a7a63a5c0b352fbf7e1b8a565e353e126c63066de0cc0c9f8af031dff59d98c2" if actual != expected: raise RuntimeError( f"MANO_RIGHT.pkl converted arrays differ from the tested MANO v1.2 model:\n" f" expected {expected}\n got {actual}" ) with open(path, "wb") as fh: pickle.dump(model, fh) log.info(f"MANO_RIGHT.pkl converted to a chumpy-free pickle (arrays sha256 {actual[:16]}...)") def _prepare_assets() -> None: """Materialize the checkpoint layout expected by exmr.assets.Assets.""" import urllib.request # 1) ExMR transformer weights _fetch("exmr/exmr.safetensors", "twehrbein/ExMR", "exmr.safetensors") # 2) SMPL-X neutral body model (registered model; public mirror, SHA-verified upstream) _fetch( "body_models/smplx/SMPLX_NEUTRAL.npz", "artabatest/smplx_modelv2", "smplx/SMPLX_NEUTRAL.npz" ) # 3) YOLO person detectors straight from Ultralytics' release assets for yolo in ("yolo11m", "yolo11x"): target = CKPT_DIR / "yolo" / f"{yolo}.pt" if target.exists(): continue target.parent.mkdir(parents=True, exist_ok=True) def _dl(name): url = f"https://github.com/ultralytics/assets/releases/download/v8.3.0/{name}.pt" with urllib.request.urlopen( urllib.request.Request(url, headers={"User-Agent": "exmr-demo"}) ) as r, open(target, "wb") as f: shutil.copyfileobj(r, f, 1 << 20) _dl(yolo) # 4) Sapiens2 1B pose _fetch( "sapiens2/sapiens2_1b_pose.safetensors", "facebook/sapiens2-pose-1b", "sapiens2_1b_pose.safetensors", ) # 5) SAM 3D Body (DINOv3-H+ backbone; weights all in model.ckpt, mhr body model separate) _fetch("sam-3d-body-dinov3/model.ckpt", "mingjiexie/sam-3d-body-dinov3", "model.ckpt") _fetch( "sam-3d-body-dinov3/model_config.yaml", "mingjiexie/sam-3d-body-dinov3", "model_config.yaml", ) _fetch( "sam-3d-body-dinov3/assets/mhr_model.pt", "mingjiexie/sam-3d-body-dinov3", "assets/mhr_model.pt", ) # 6) HaMeR (checkpoint + config + MANO + mean params) _fetch("hamer/hamer_ckpts/checkpoints/hamer.ckpt", "CometsFeiyu/hamer", "hamer_ckpts/checkpoints/hamer.ckpt") _fetch("hamer/hamer_ckpts/model_config.yaml", "CometsFeiyu/hamer", "hamer_ckpts/model_config.yaml") _fetch("hamer/hamer_ckpts/dataset_config.yaml", "CometsFeiyu/hamer", "hamer_ckpts/dataset_config.yaml") _fetch("hamer/data/mano_mean_params.npz", "CometsFeiyu/hamer", "data/mano_mean_params.npz") # The upstream MANO_RIGHT.pkl is a chumpy pickle; convert it in place before HaMeR loads it. _convert_mano_pickle( _fetch("hamer/data/mano/MANO_RIGHT.pkl", "CometsFeiyu/hamer", "data/mano/MANO_RIGHT.pkl") ) # 7) the DINOv3 hub code cache _download_dinov3_code() log.info("all checkpoints ready") _prepare_assets() # The pipeline modules import heavy third-party code (pytorch_lightning, ultralytics, sapiens); # import them AFTER the dynamo patch above. from exmr.assets import Assets # noqa: E402 from exmr.model.exmr import ExMR # noqa: E402 ASSETS = Assets(root=CKPT_DIR) ASSETS.check("sapiens2", "yolo11m", False) # --------------------------------------------------------------------------- # Model loading (module scope, eagerly on the GPU): every stage of the pipeline is loaded once # --------------------------------------------------------------------------- log.info("loading models...") DEVICE = "cuda" from exmr.body_models.smplx_model import SmplxModel # noqa: E402 from exmr.model.exmr import ExMR as _ExMR # noqa: F401, E402 from exmr.preprocessing.body_features import SamBodyFeatures # noqa: E402 from exmr.preprocessing.hand_features import HamerHandFeatures # noqa: E402 from exmr.preprocessing.keypoints.mappings import ( # noqa: E402 COCO23, COCO63_LEFT_HAND, COCO63_RIGHT_HAND, ) from exmr.preprocessing.keypoints import VISIBILITY_THRESHOLD # noqa: E402 from exmr.preprocessing.keypoints.sapiens2 import Sapiens2Keypoints # noqa: E402 from exmr.preprocessing.tracker import Tracker # noqa: E402 from exmr.vis.render import render_videos # noqa: E402 from exmr.geometry.camera import ( # noqa: E402 bbx_xys_from_xyxy, cam_angvel_from_rotations, estimate_K, ) from exmr.preprocessing import video as V # noqa: E402 torch.set_grad_enabled(False) # Load every GPU stage eagerly at module scope, as ZeroGPU requires: the weights get packed to # disk once (at boot) and streamed into VRAM on each @spaces.GPU entry, instead of paying the # multi-minute load on the first request. log.info("loading ExMR + SMPL-X") EXMR = ExMR.from_pretrained(ASSETS.exmr, ASSETS.smplx).to(DEVICE).eval() SMPLX = SmplxModel(ASSETS.smplx).to(DEVICE) log.info("loading YOLO trackers") TRACKER = { "yolo11m": Tracker(ASSETS.yolo("yolo11m")), "yolo11x": Tracker(ASSETS.yolo("yolo11x")), } log.info("loading Sapiens2 (448x336)") SAPIENS2 = {res: None for res in ("448x336", "576x432", "1024x768")} # num_workers=0: the ZeroGPU worker process is daemonic, and DataLoader workers are # child processes -> "daemonic processes are not allowed to have children". SAPIENS2["448x336"] = Sapiens2Keypoints(ASSETS.sapiens2, resolution="448x336", num_workers=0) log.info("loading SAM 3D Body") SAM_BODY = SamBodyFeatures(ASSETS.sam_3d_body, num_workers=0) log.info("loading HaMeR") HAMER = HamerHandFeatures(ASSETS.hamer, num_workers=0) MODELS_LOCK = threading.Lock() def _get_sapiens2(resolution: str) -> Sapiens2Keypoints: """The 448x336 model loads at startup; other resolutions lazily on first use.""" with MODELS_LOCK: if SAPIENS2.get(resolution) is None: SAPIENS2[resolution] = Sapiens2Keypoints( ASSETS.sapiens2, resolution=resolution, num_workers=0 ) return SAPIENS2[resolution] # --------------------------------------------------------------------------- # The pipeline (synchronous port of exmr.pipeline.run_video, without process pools) # --------------------------------------------------------------------------- def _simplevo_in_thread(video_path): """Run SimpleVO in a background THREAD (its numpy/pycolmap calls release the GIL).""" from exmr.preprocessing.visual_odometry import SimpleVO def work(): try: result["rot"] = SimpleVO(progress=False)(video_path) except BaseException as e: result["err"] = e result = {} thread = threading.Thread(target=work, daemon=True) thread.start() return result, thread def _run_exmr(video_path, output_dir: Path, detector, resolution, static_cam): """Run the full ExMR pipeline on one video; returns (results, out_paths).""" from exmr.preprocessing.hand_features import hand_boxes_from_keypoints name = Path(video_path).stem output_dir.mkdir(parents=True, exist_ok=True) frame_dir = output_dir / "frames" num_frames = V.extract_frames(video_path, frame_dir) if V.video_lwh(video_path)[0] != num_frames: log.warning("frame count mismatch; rebuilding constant-frame-rate video") cfr = output_dir / f"{name}_cfr.mp4" if not cfr.exists(): V.encode_frames(frame_dir, cfr, fps=V.video_fps(video_path)) video_path = str(cfr) width, height = V.frame_size(frame_dir) fps = V.video_fps(video_path) log.info(f"{video_path}: {num_frames} frames, {width}x{height}, {fps:.2f} fps") L = num_frames # SimpleVO (CPU) overlaps the GPU stages below vo_thread = None vo_result = None if not static_cam: vo_result, vo_thread = _simplevo_in_thread(video_path) # --- tracking (YOLO11 + ByteTrack) tracker = TRACKER[detector] bbx_xyxy = tracker.get_main_track(video_path).float() # (L, 4) bbx_xys = bbx_xys_from_xyxy(bbx_xyxy, base_enlarge=1.2).float() K = estimate_K(width, height)[None].repeat(L, 1, 1) # --- 2D whole-body keypoints kp2d = _get_sapiens2(resolution)(frame_dir, bbx_xyxy) # --- SAM 3D Body image features body_feat = SAM_BODY(frame_dir, bbx_xyxy, K) # --- HaMeR hand features threshold = VISIBILITY_THRESHOLD["sapiens2"] boxes = torch.stack( [ hand_boxes_from_keypoints(kp2d[:, COCO63_LEFT_HAND], threshold), hand_boxes_from_keypoints(kp2d[:, COCO63_RIGHT_HAND], threshold), ], dim=1, ) hand_feat = HAMER(frame_dir, boxes) # --- camera rotations if static_cam: R_w2c = torch.eye(3).repeat(L, 1, 1) else: if vo_thread is not None: vo_thread.join() if "err" in vo_result: raise RuntimeError(f"visual odometry failed: {vo_result['err']}") R_w2c = vo_result["rot"] kp23 = kp2d[:, COCO23].clone() inputs = { "kp2d": kp23[None], "bbx_xys": bbx_xys[None], "K_fullimg": K[None], "cam_angvel": cam_angvel_from_rotations(R_w2c)[None], "body_feat": body_feat[None], "hand_feat": hand_feat[None], } with torch.no_grad(): out = EXMR( **{k: v.to(DEVICE) for k, v in inputs.items()}, static_cam=static_cam, ) results = { "smplx_incam": {k: v[0].cpu() for k, v in out["smplx_incam"].items()}, "smplx_global": {k: v[0].cpu() for k, v in out["smplx_global"].items()}, "static_conf_logits": out["static_conf_logits"][0].cpu(), "K_fullimg": K, "bbx_xyxy": bbx_xyxy, } # --- render the three views with torch.no_grad(): verts_incam = SMPLX(**{k: v.to(DEVICE) for k, v in results["smplx_incam"].items()}) verts_global = SMPLX(**{k: v.to(DEVICE) for k, v in results["smplx_global"].items()}) out_paths = [ output_dir / f"{name}_{view}.mp4" for view in ("incam", "global", "incam_global") ] render_videos( frame_dir, verts_incam, verts_global, SMPLX.faces, SMPLX.J_regressor.to(DEVICE), K[0], fps, out_paths, ) # results.npz for download flat = {} for key, value in results.items(): if isinstance(value, dict): flat |= {f"{key}_{k}": v.numpy() for k, v in value.items()} else: flat[key] = np.asarray(value) np.savez(output_dir / "results.npz", **flat) return results, out_paths # --------------------------------------------------------------------------- # GPU entry points # --------------------------------------------------------------------------- MAX_FRAMES = 900 # 30 s at 30 fps: the demo's practical cap (quota-friendly) DURATION_FIXED = 12.0 # per-call overhead: frame extraction + the weight stream into VRAM DURATION_PER_FRAME = {"448x336": 0.085, "576x432": 0.107, "1024x768": 0.190} # yolo11m DURATION_DETECTOR_EXTRA = {"yolo11m": 0.0, "yolo11x": 0.027} # per-frame, yolo11x is heavier DURATION_MOVING_EXTRA = 6.0 # SimpleVO visual odometry DURATION_MARGIN = 1.2 # ~15-25% over the worst measured run, after rounding to 5 s def _estimate_duration(video, detector, resolution, static_cam, *args, **kwargs): """ZeroGPU duration estimate for one call. Interpolated from end-to-end timings of the live Space (900 frames = the cap): 300 fr yolo11m 448x336 static -> 37 s 900 fr yolo11m 448x336 static -> 88 s 900 fr yolo11m 448x336 moving -> 91 s 900 fr yolo11m 576x432 static -> 108 s 900 fr yolo11x 1024x768 static -> 207 s 900 fr yolo11x 1024x768 moving -> 212 s We request that fit x1.2, rounded up to 5 s (each preset therefore asks ~15-25% more than the slowest run measured on the live Space). Keep this honest rather than generous: ZeroGPU compares the *requested* duration against the visitor's *remaining* quota, not the actual runtime, so an inflated request locks a visitor out of a call that would have cost them a fraction of it. (On this Blackwell runtime the `spaces` package also applies a `duration_factor` of 1.5, so the wall-clock reservation is 1.5x this value; the 300-frame default example reserves ~75 s of a free account's 300 s daily quota.) """ cap = int(os.environ.get("ZERO_GPU_DURATION", "0")) or None frames = 300 video = video if isinstance(video, str) else getattr(video, "name", None) if video: try: frames = int(V.video_lwh(video)[0]) except Exception: frames = 300 frames = max(60, min(frames, MAX_FRAMES)) per_frame = DURATION_PER_FRAME.get(resolution, DURATION_PER_FRAME["1024x768"]) per_frame += DURATION_DETECTOR_EXTRA.get(detector, DURATION_DETECTOR_EXTRA["yolo11x"]) raw = DURATION_FIXED + frames * per_frame if not static_cam: raw += DURATION_MOVING_EXTRA seconds = int(raw * DURATION_MARGIN + 0.999) # ceil seconds = ((seconds + 4) // 5) * 5 # round up to a multiple of 5 s if cap: seconds = min(seconds, cap) log.info( "reserving %ss of GPU time (%s frames, %s, %s, static_cam=%s)", seconds, frames, detector, resolution, static_cam, ) return seconds @spaces.GPU(duration=_estimate_duration) def reconstruct( video, detector="yolo11m", resolution="448x336", static_cam=True, progress=gr.Progress(), ): """Reconstruct 3D body, hand and foot motion (SMPL-X) of the main person in a video. Args: video: input monocular video (mp4/webm/mov), up to ~30 s, one dominant person. detector: YOLO person detector preset: "yolo11m" (fast) or "yolo11x" (accurate). resolution: Sapiens2 crop resolution, "448x336" (balanced) to "1024x768" (accurate). static_cam: whether the camera is static (stationary tripod). Default True: skips visual odometry. Returns: A side-by-side video (input view + reconstructed mesh), and results.npz with the SMPL-X parameters (in-camera and global). """ if video is None: raise gr.Error("Upload a video first") path = video if isinstance(video, str) else video.name L = V.video_lwh(path)[0] work = Path(tempfile.mkdtemp(prefix="exmr_")) if L > MAX_FRAMES: # trim with ffmpeg to MAX_FRAMES frames (keeps the head of the video) subprocess.run( [ "ffmpeg", "-hide_banner", "-nostdin", "-y", "-loglevel", "error", "-i", path, "-frames:v", str(MAX_FRAMES), "-an", "-c:v", "libx264", "-preset", "veryfast", "-crf", "18", "-pix_fmt", "yuv420p", str(work / "input.mp4"), ], check=True, ) else: shutil.copy(path, work / "input.mp4") path = str(work / "input.mp4") out_dir = work / "out" try: results, out_paths = _run_exmr( path, out_dir, detector, resolution, bool(static_cam) ) side_by_side = out_paths[2] # _incam_global.mp4 npz = out_dir / "results.npz" # copy the two deliverables into a stable tempdir that outlives the workdir cleanup keep = Path(tempfile.mkdtemp(prefix="exmr_out_")) shutil.copy(side_by_side, keep / "exmr_incam_global.mp4") shutil.copy(npz, keep / "results.npz") return str(keep / "exmr_incam_global.mp4"), str(keep / "results.npz") finally: shutil.rmtree(work, ignore_errors=True) # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- DEFAULT_EXAMPLE = str(_HERE / "assets" / "example_dance_10s.mp4") description = """**Reconstruct 3D body, hand and foot motion from a monocular video** with [ExMR](https://huggingface.co/papers/2610.26213) — a transformer that refines extremities (hands, feet) on top of whole-body tokens from SAM 3D Body, HaMeR and Sapiens2, and outputs SMPL-X parameters. Upload a short clip of **one person** (up to ~30 s, longer inputs are trimmed). The output shows the input view with the reconstructed mesh overlaid, side by side with the world-frame motion. `results.npz` holds the raw SMPL-X parameters.""" article = """### How it works 1. **Track** the main person (YOLO11 + ByteTrack) → **2D whole-body keypoints** (Sapiens2-1B). 2. **Body token** per frame from **SAM 3D Body** (DINOv3-H+); **hand tokens** from **HaMeR**. 3. Optionally estimate **camera motion** (SimpleVO) when the camera moves. 4. **ExMR** fuses everything into SMPL-X body + hand pose (with Foot/Hand Refiner modules). 5. **Render** the input-view and world-view meshes with nvdiffrast. ### Credits & licenses Model by Tobias Weinehrbein et al. ([paper](https://huggingface.co/papers/2610.26213), [code](https://github.com/twehrbein/ExMR)). Research-use-only components: ExMR, SMPL-X, MANO, SAM 3D Body, Sapiens2; HaMeR is MIT; YOLO is AGPL-3.0. Example video from [Pexels](https://www.pexels.com/video/woman-doing-dancing-exercise-in-the-garden-3690649/) (free to use).""" theme = gr.themes.Citrus() with gr.Blocks(theme=theme, title="ExMR — Monocular Human Motion Capture") as demo: gr.Markdown("# ExMR: Extremity Motion Refinement for Monocular Human Motion Capture") with gr.Row(): with gr.Column(): video_in = gr.Video( label="Input video (one person, ≤ 30 s)", sources=["upload"], format="mp4", ) static_cam = gr.Checkbox( value=True, label="Static camera (tripod; uncheck if the camera moves)", ) with gr.Accordion("Advanced options", open=False): detector = gr.Radio( ["yolo11m", "yolo11x"], value="yolo11m", label="Person detector", ) resolution = gr.Radio( ["448x336", "576x432", "1024x768"], value="448x336", label="Sapiens2 resolution (higher = slower, slightly better)", ) go = gr.Button("Reconstruct motion", variant="primary") with gr.Column(): video_out = gr.Video(label="Input view + world view (side by side)") npz_out = gr.File(label="SMPL-X parameters (results.npz)") gr.Examples( examples=[[DEFAULT_EXAMPLE]], inputs=[video_in], fn=reconstruct, outputs=[video_out, npz_out], cache_examples=True, cache_mode="lazy", label="Example: the authors' Pexels dance clip (10 s, 300 frames)", ) gr.Markdown(description) gr.Markdown(article) go.click( reconstruct, inputs=[video_in, detector, resolution, static_cam], outputs=[video_out, npz_out], api_name="reconstruct", ) if __name__ == "__main__": demo.launch(ssr_mode=False, mcp_server=True)