ae28d55f81
Source: RGB-D -> Dyn-HaMR -> L20 retargeting -> FoundationPose -> reference repair -> SPIDER, documented in docs/PIPELINE_LATEST.md and docs/SETUP_AND_WEIGHTS.md. Adds FoundationPose and nvdiffrast upstream snapshots, requirements/pipeline_venv.txt and the FoundationPose weight manifest/downloader. Assets (Git LFS): weights/ (WiLoR detector, HandFlow denoiser, UniDepth-L), FoundationPose checkpoints, HaMeR checkpoint, Dyn-HaMR HMP model and BMC constraints, L20 URDF/meshes, the 20260915_171525 D405 recording and the two box CADs. MANO models are not redistributed (third_party/hamer/_DATA/data/mano/README.txt). Environments, caches and run outputs excluded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
5.9 KiB
Python
85 lines
5.9 KiB
Python
"""Side-by-side interactive playback: the same scene duplicated in one MuJoCo world.
|
|
Left copy = trajectory A (e.g. kinematic reference), right copy = trajectory B (e.g. physics rollout), time-synchronised."""
|
|
import argparse, os, time, json, copy
|
|
from pathlib import Path
|
|
import xml.etree.ElementTree as E
|
|
p = argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument('--scene', type=Path, required=True); p.add_argument('--a', type=Path, required=True); p.add_argument('--b', type=Path, required=True)
|
|
p.add_argument('--label-a', default='A'); p.add_argument('--label-b', default='B'); p.add_argument('--offset', type=float, default=.7); p.add_argument('--check', action='store_true'); p.add_argument('--out', type=Path)
|
|
a = p.parse_args(); os.environ['MUJOCO_GL'] = 'osmesa' if a.check else 'glfw'
|
|
import numpy as np, mujoco
|
|
root = E.parse(a.scene).getroot(); world = root.find('worldbody')
|
|
NAMED = ['name', 'joint', 'joint1', 'joint2', 'site', 'body', 'target', 'geom1', 'geom2', 'body1', 'body2']
|
|
def prefix_tree(el, pre):
|
|
for e in el.iter():
|
|
for k in NAMED:
|
|
v = e.get(k)
|
|
if v and k != 'mesh' and k != 'material' and k != 'class': e.set(k, pre + v)
|
|
originals = list(world)
|
|
copyB = E.Element('body', name='B_root', pos=f'0 {a.offset} 0')
|
|
for e in originals:
|
|
if e.tag == 'camera' or e.tag == 'light': continue
|
|
c = copy.deepcopy(e); prefix_tree(c, 'B_'); copyB.append(c)
|
|
for e in originals:
|
|
if e.tag not in ('camera', 'light'): prefix_tree(e, 'A_')
|
|
world.append(copyB)
|
|
for sec in ['actuator', 'equality', 'contact', 'sensor', 'tendon']:
|
|
s = root.find(sec)
|
|
if s is None: continue
|
|
items = list(s)
|
|
for e in items:
|
|
c = copy.deepcopy(e); prefix_tree(c, 'B_'); s.append(c); prefix_tree(e, 'A_')
|
|
out = a.out or (a.a.parent / 'compare_scene.xml'); E.ElementTree(root).write(out)
|
|
m = mujoco.MjModel.from_xml_path(str(out)); d = mujoco.MjData(m)
|
|
def load(path):
|
|
z = np.load(path); return z['qpos'], z['time']
|
|
qa, ta = load(a.a); qb, tb = load(a.b); n = qa.shape[1]; assert qb.shape[1] == n and m.nq == 2 * n, (m.nq, n)
|
|
# qpos layout: joints of A come first (original order), then B (same order) -> verify via joint names
|
|
ja = [i for i in range(m.njnt) if m.joint(i).name.startswith('A_')]; jb = [i for i in range(m.njnt) if m.joint(i).name.startswith('B_')]
|
|
adrA = np.concatenate([np.arange(m.jnt_qposadr[i], m.jnt_qposadr[i] + (7 if m.jnt_type[i] == 0 else 4 if m.jnt_type[i] == 1 else 1)) for i in ja]); adrB = np.concatenate([np.arange(m.jnt_qposadr[i], m.jnt_qposadr[i] + (7 if m.jnt_type[i] == 0 else 4 if m.jnt_type[i] == 1 else 1)) for i in jb])
|
|
assert len(adrA) == n and len(adrB) == n
|
|
tmax = float(min(ta[-1], tb[-1]))
|
|
def frame(tq, q, t): i = int(np.clip(np.searchsorted(tq, t, side='right') - 1, 0, len(tq) - 1)); return q[i]
|
|
def set_time(t):
|
|
d.qpos[adrA] = frame(ta, qa, t); d.qpos[adrB] = frame(tb, qb, t); d.time = t; mujoco.mj_forward(m, d)
|
|
if a.check:
|
|
for t in [0, tmax / 2, tmax]: set_time(t)
|
|
print(json.dumps({'nq': m.nq, 'duration_s': tmax, 'finite': bool(np.isfinite(d.qpos).all()), 'scene': str(out)})); raise SystemExit
|
|
import mujoco.viewer
|
|
state = {'paused': False, 'time': 0., 'speed': 1., 'reset_camera': False}
|
|
def key(k):
|
|
if k == 32: state['paused'] = not state['paused']
|
|
elif k in [82, 114]: state['time'] = 0.
|
|
elif k in [262, 263]: state['time'] = float(np.clip(state['time'] + (1 if k == 262 else -1) / 30, 0, tmax)); state['paused'] = True
|
|
elif k == 265: state['speed'] = min(4., state['speed'] * 2)
|
|
elif k == 264: state['speed'] = max(.125, state['speed'] / 2)
|
|
elif k in [67, 99]: state['reset_camera'] = True
|
|
m.vis.headlight.ambient[:] = .7; set_time(0)
|
|
centers = [np.mean([d.site_xpos[m.site(pre + s + '_object_track').id] for s in ['right', 'left']], axis=0) for pre in ['A_', 'B_']]; center = np.mean(centers, axis=0)
|
|
cid = m.camera('A_source_camera').id if any(m.camera(i).name == 'A_source_camera' for i in range(m.ncam)) else 0; offset = d.cam_xpos[cid] - centers[0]
|
|
camera = {'distance': float(np.linalg.norm(offset)) * 1.6, 'azimuth': float(np.degrees(np.arctan2(offset[1], offset[0]))), 'elevation': float(-np.degrees(np.arctan2(offset[2], np.linalg.norm(offset[:2]))))}
|
|
status = out.parent / 'compare_playback_status.json'
|
|
print(f'LEFT (y=0): {a.label_a} | RIGHT (y=+{a.offset}): {a.label_b} | SPACE pause | R restart | LEFT/RIGHT frame step | UP/DOWN speed | C camera', flush=True)
|
|
with mujoco.viewer.launch_passive(m, d, key_callback=key) as viewer:
|
|
def reset_camera():
|
|
viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FREE; viewer.cam.lookat[:] = center
|
|
for k, x in camera.items(): setattr(viewer.cam, k, x)
|
|
with viewer.lock():
|
|
reset_camera(); viewer.opt.sitegroup[:] = 0; viewer.opt.geomgroup[3:] = 0
|
|
# colour markers: green sphere above A, red above B
|
|
for i, (c, rgba) in enumerate([(centers[0], (0, .8, 0, 1)), (centers[1], (.9, .1, .1, 1))]):
|
|
g = viewer.user_scn.geoms[i]; mujoco.mjv_initGeom(g, mujoco.mjtGeom.mjGEOM_SPHERE, np.array([.02, 0, 0]), np.array(c) + np.array([0, 0, .35]), np.eye(3).ravel(), np.array(rgba, dtype=np.float32))
|
|
viewer.user_scn.ngeom = 2
|
|
last = time.perf_counter(); last_status = 0
|
|
while viewer.is_running():
|
|
now = time.perf_counter(); el = now - last; last = now
|
|
if not state['paused']: state['time'] = (state['time'] + el * state['speed']) % tmax
|
|
with viewer.lock():
|
|
if state['reset_camera']: reset_camera(); state['reset_camera'] = False
|
|
set_time(state['time'])
|
|
viewer.sync()
|
|
if now - last_status > 1:
|
|
status.write_text(json.dumps({'running': True, 'pid': os.getpid(), 'left': a.label_a, 'right': a.label_b, 'time_s': state['time'], 'speed': state['speed'], 'paused': state['paused'], 'updated_unix': time.time()}, indent=2)); last_status = now
|
|
time.sleep(.008)
|
|
status.write_text(json.dumps({'running': False, 'pid': os.getpid(), 'updated_unix': time.time()}))
|