Files
liyang ae28d55f81 Update to 2026-09-17 pipeline snapshot; add weights, L20 assets and recording via Git LFS
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>
2026-09-17 11:43:37 +08:00

70 lines
4.8 KiB
Python

"""Independently check both URDF constraints and export common-world trajectories."""
from pathlib import Path
import json,xml.etree.ElementTree as E,argparse
import cv2,mujoco,numpy as np
from scipy.spatial.transform import Rotation
ROOT=Path(__file__).resolve().parents[1]
parser=argparse.ArgumentParser();parser.add_argument('--base',type=Path,default=ROOT/'output/20260915_171525');args=parser.parse_args()
BASE=args.base.resolve();OUT=BASE/'replay'
motion=np.load(OUT/'motion.npz');q=motion['qpos'];model=mujoco.MjModel.from_xml_path(str(OUT/'scene.xml'));data=mujoco.MjData(model)
bundle=dict(time=motion['time'],fps=motion['fps'],camera_world_from_cv=motion['camera_world_from_cv'])
reports={};samples={side:[] for side in ['right','left']}
for side in ['right','left']:
prefix='' if side=='right' else 'left_';hand=np.load(BASE/f'l20_{side}_stable/motion.npz')
names=hand['joint_names'].tolist();addrs=[model.jnt_qposadr[model.joint(prefix+n).id] for n in names]
angles=q[:,addrs];assert np.allclose(angles,hand['qpos'],atol=1e-12)
root=E.parse(ROOT/f'third_party/l20_assets/L20/{side.upper()}/linkerhand_g20_{side}.urdf').getroot()
violation=0.;mimic=0.;fk=0.;positions=[];quats=[]
Rshared=np.load(BASE/'l20_right_stable/motion.npz')['scene_rotation'];tshared=np.load(BASE/'l20_right_stable/motion.npz')['scene_translation']
for j in root.findall('joint'):
if j.get('type')=='fixed':continue
col=angles[:,names.index(j.get('name'))];lim=j.find('limit')
violation=max(violation,float(np.maximum(float(lim.get('lower'))-col,0).max()),float(np.maximum(col-float(lim.get('upper')),0).max()))
mi=j.find('mimic')
if mi is not None:
mimic=max(mimic,float(np.abs(col-float(mi.get('multiplier','1'))*angles[:,names.index(mi.get('joint'))]-float(mi.get('offset','0'))).max()))
for i,row in enumerate(q):
data.qpos[:]=row;mujoco.mj_forward(model,data)
pos=data.xpos[model.body(prefix+'hand_base_link').id].copy();quat=data.xquat[model.body(prefix+'hand_base_link').id].copy()
positions.append(pos);quats.append(quat)
localR=Rotation.from_quat(hand['wrist_quat_wxyz'][i,[1,2,3,0]]).as_matrix()
expectedR=Rshared@hand['scene_rotation'].T@localR
expectedp=Rshared@hand['scene_rotation'].T@(hand['wrist_pos'][i]-hand['scene_translation'])+tshared
expected=hand['actual'][i]@expectedR.T+expectedp
actual=np.array([data.site_xpos[model.site(prefix+f'landmark_{k:02d}').id] for k in range(21)])
fk=max(fk,float(np.abs(actual-expected).max()))
positions=np.asarray(positions);quats=np.asarray(quats)
for i in range(1,len(quats)):
if quats[i]@quats[i-1]<0:quats[i]*=-1
bundle.update({side+'_joint_names':np.asarray(names),side+'_joint_position':angles,
side+'_wrist_position':positions,side+'_wrist_quaternion_wxyz':quats,
side+'_valid':hand['detection_valid']})
np.savetxt(OUT/f'{side}_hand_trajectory.csv',np.c_[motion['time'],positions,quats,angles],delimiter=',',
header=','.join(['time_s','wrist_x','wrist_y','wrist_z','qw','qx','qy','qz']+names),comments='')
assert violation<1e-8 and mimic<1e-10 and fk<1e-5
reports[side]=dict(frames=len(q),valid_frames=int(hand['detection_valid'].sum()),urdf_limit_violation_rad=violation,mimic_error_rad=mimic,independent_world_fk_max_error_m=fk)
for name in ['upper','lower']:
addr=model.jnt_qposadr[model.joint(name+'_free').id]
bundle[name+'_position']=q[:,addr:addr+3];bundle[name+'_quaternion_wxyz']=q[:,addr+3:addr+7]
fit=np.load(BASE/'object_poses.npz')
observed=np.zeros(len(q),dtype=bool)
observed[fit[name+'_keyframes'][fit[name+'_keyframe_valid']]]=True
bundle[name+'_accepted_fit_keyframe']=observed
bundle[name+'_interpolated_or_held']=~observed
for suffix in ['assembly_assumed','occluded_transition_assumed']:
key=name+'_'+suffix
if key in fit:bundle[key]=fit[key]
bundle['world_convention']='Common right-hand replay scene transform applied to first-camera RGBD odometry world; display axes, gravity not measured.'
bundle['valid_semantics']='Per-hand source validity; see human_joints source metadata. Object poses are partial-surface estimates; not contact labels.'
assert np.isfinite(q).all() and np.all(np.diff(motion['time'])>0)
np.savez_compressed(OUT/'demonstrations_bimanual.npz',**bundle)
cap=cv2.VideoCapture(str(OUT/'original_vs_l20_bimanual.mp4'));n=0
while True:
ok,im=cap.read()
if not ok:break
assert im.shape==(360,1280,3);n+=1
cap.release();assert n==len(q)
reports.update(video_decoded_frames=n,time_last_s=float(motion['time'][-1]),finite=True,
scope='Dual-hand kinematic retargeting with separate RGBD CAD pose estimates; no contact or dynamics success validation.')
(OUT/'validation.json').write_text(json.dumps(reports,indent=2));print(json.dumps(reports,indent=2))