"""Export the existing right-hand replay without changing its model or motion. Uses the dataset layout of HDF5_REQUIREMENTS.md, with explicit right-hand model/coordinate exceptions. This is not a left-hand l20_tracking_v1 PASS. """ import argparse import hashlib import json from pathlib import Path import xml.etree.ElementTree as ET import h5py import numpy as np ROOT = Path(__file__).resolve().parents[1] NAMES = [f'{f}_{j}' for f in ('index', 'middle', 'pinky', 'ring') for j in ('dip', 'mcp_pitch', 'mcp_roll', 'pip')] + [ 'thumb_cmc_pitch', 'thumb_cmc_roll', 'thumb_cmc_yaw', 'thumb_ip', 'thumb_mcp'] def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--source', type=Path, default=ROOT/'output/l20_15886123_stable/motion.npz') parser.add_argument('--output', type=Path, default=ROOT/'output/hdf5_delivery/right') parser.add_argument('--scene', type=Path, help='MuJoCo scene for a full-replay qpos source') args = parser.parse_args() args.output.mkdir(parents=True, exist_ok=True) a = np.load(args.source, allow_pickle=False) scene_description = None if args.scene: import mujoco model = mujoco.MjModel.from_xml_path(str(args.scene.resolve())) data = mujoco.MjData(model) rows = a['qpos'] assert rows.ndim == 2 and rows.shape[1] == model.nq and np.isfinite(rows).all() addresses = [model.jnt_qposadr[model.joint(n).id] for n in NAMES] body_id = model.body('hand_base_link').id positions, rotations = [], [] for row in rows: data.qpos[:] = row mujoco.mj_forward(model, data) positions.append(data.xpos[body_id].copy()) rotations.append(data.xquat[body_id].copy()) positions = np.asarray(positions) assert np.allclose(positions, a['wrist_pos'], atol=1e-9, rtol=0) a = dict(qpos=rows[:,addresses], joint_names=np.asarray(NAMES), wrist_pos=positions, wrist_quat_wxyz=np.asarray(rotations), time=a['time'], detection_valid=np.ones(len(rows), dtype=bool), scene_rotation=np.eye(3), scene_translation=np.zeros(3)) scene_description = ( 'Video 2047635068, full 1333-frame right-hand replay: Dyn-HaMR -> ' 'run_dex_l20.py -> temporal smoothing -> build_l20_full_replay.py contact registration. ' 'Actual source is the final replay qpos, not the earlier raw retargeting. ' 'hand_base_link world position and active local-to-world WXYZ rotation extracted ' 'with MuJoCo mj_forward for every frame; finger columns resolved by joint name. ' 'Source world is explicitly the FINAL replay scene world; world_from_source is identity ' 'because exported FK is already in that same world, not because camera calibration is known. ' 'Axes/origin are the existing fixed right-handed replay scene with display Z up: upstream ' 'scene was aligned using estimated bottle orientation at frame 600, shifted to the display ' 'placement, then raised for floor clearance; physical gravity alignment is unverified. ' 'Original human wrist/palm mapping, temporal smoothing and per-frame contact-registration ' 'corrections are inherited unchanged. Root is robot link origin, not its center of mass. ' 'valid=true means finite kinematic reference state satisfying the actual right URDF ' 'limits and mimic; original detection validity was not supplied, so this is NOT an ' 'observed-frame mask or measurement confidence. Motion and object registration are ' 'estimated, not ground truth; no full dynamic rollout or hardware execution claim. ' 'Source scene: ' + str(args.scene.resolve())) asset = ROOT/'third_party/l20_assets/L20/RIGHT' urdf = asset/'linkerhand_g20_right.urdf' tree = ET.parse(urdf).getroot() files = {urdf} for mesh in tree.iter('mesh'): files.add((asset/mesh.attrib['filename']).resolve()) entries = [{'path': p.relative_to(asset.resolve()).as_posix(), 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for p in sorted(files, key=lambda p: p.relative_to(asset.resolve()).as_posix())] # Explicit local algorithm, not the unavailable receiver's asset hash tool. payload = ''.join(f"{e['path']}\t{e['sha256']}\n" for e in entries).encode('utf-8') digest = hashlib.sha256(payload).hexdigest() js = {j.attrib['name']: j for j in tree.findall('joint') if j.attrib['type'] != 'fixed'} manifest = dict(hand_side='right', root_link='hand_base_link', asset_sha256=digest, hash_algorithm='sha256(UTF-8 concatenation of sorted relative_path TAB file_sha256 LF); URDF plus referenced meshes only; local export convention', source_urdf=str(urdf), files=entries, joints=[]) for name in NAMES: j = js[name] item = dict(name=name, lower_rad=float(j.find('limit').get('lower')), upper_rad=float(j.find('limit').get('upper'))) m = j.find('mimic') if m is not None: item['mimic'] = dict(joint=m.get('joint'), multiplier=float(m.get('multiplier', '1')), offset=float(m.get('offset', '0'))) manifest['joints'].append(item) (args.output/'right_model_manifest.json').write_text(json.dumps(manifest, indent=2)+'\n') source_names = a['joint_names'].tolist() assert len(source_names) == len(set(source_names)) == len(NAMES) assert set(source_names) == set(NAMES) q = a['qpos'][:, [source_names.index(n) for n in NAMES]].astype(np.float32) pos = a['wrist_pos'].astype(np.float32) quat = a['wrist_quat_wxyz'].copy() quat /= np.linalg.norm(quat, axis=1, keepdims=True) for i in range(1, len(quat)): if quat[i] @ quat[i-1] < 0: quat[i] *= -1 quat = quat.astype(np.float32) valid = a['detection_valid'].astype(bool) transform = np.eye(4, dtype=np.float64) transform[:3, :3] = a['scene_rotation'] transform[:3, 3] = a['scene_translation'] description = ( 'Video 15886123; HandFlow/manopth + ViPE camera-to-world; ' 'scripts/export_handflow_dex.py -> retarget_l20_video.py -> stabilize_l20_temporal.py. ' 'Right L20 URDF linear mimic, 16 independent and 21 total joints. ' 'hand_base_link origin assigned to estimated MANO wrist; orientation uses human palm_basis ' 'and robot neutral palm_basis, not a measured anatomical mount. ' 'Positions already use the stored fixed scene transform, followed by offline temporal smoothing. ' 'Scene origin and XYZ axes follow the original replay: first unsmoothed wrist at (0,0,0.2), ' 'first unsmoothed root orientation identity. Right-handed fixed scene with display Z up; ' 'physical gravity alignment is unknown. No new pose recentering applied by exporter. ' 'world_from_source maps the upstream estimated world to this existing display world; ' 'smoothing additionally changes individual poses. ' 'valid is upstream usable detection mask, not ground-truth tracking accuracy: ' 'first 24 frames are held/interpolated finite estimates and remain false. ' 'All frames have monocular scale/depth uncertainty, possible drift/occlusion errors, ' 'future-frame temporal smoothing; no contact, dynamics or hardware validation.') attrs = dict(schema_version='l20_tracking_v1', embodiment='L20', hand_side='right', asset_sha256=digest, root_link='hand_base_link', provenance='expert_retargeted', source_description=description, metric_scale_provenance='Upstream MANO FK millimeters divided by 1000; camera translations used in upstream meter convention. Monocular/model-estimated metric scale, no measured reference length or gravity calibration; absolute scale error unknown. Input motion.npz positions already in estimated meters, so export scale_to_meters=1.0.', scale_to_meters=1.0, contract_status='RIGHT_HAND_VARIANT_NOT_STRICT_LEFT_V1; actual URDF mimic; local asset hash algorithm; display-world gravity unverified', source_sha256=hashlib.sha256(args.source.read_bytes()).hexdigest(), asset_hash_algorithm=manifest['hash_algorithm']) if scene_description: attrs['source_description'] = scene_description attrs['metric_scale_provenance'] = ( 'Final replay positions already in estimated meters, inherited from Dyn-HaMR MANO ' 'world reconstruction and robot geometry; export scale_to_meters=1.0. ' 'No measured absolute scale reference is established; error unknown. ' 'Scene alignment and contact fitting are engineering estimates, not scale measurements.') attrs['valid_semantics'] = 'kinematic_reference_valid; source_detection_validity_unavailable' attrs['source_scene_sha256'] = hashlib.sha256(args.scene.read_bytes()).hexdigest() good = np.flatnonzero(valid) runs = [r for r in np.split(good, np.flatnonzero(np.diff(good) > 1)+1) if len(r) >= 2] assert runs run = max(runs, key=len) sample = slice(int(run[0]), int(min(run[-1]+1, run[0]+90))) reports = [] for filename, selection in [('sample_right.hdf5', sample), ('demonstrations_right.hdf5', slice(0, len(q)))]: path = args.output/filename times = a['time'][selection].astype(np.float64) times -= times[0] with h5py.File(path, 'w') as f: for k, v in attrs.items(): f.attrs[k] = v meta = f.create_group('metadata') meta.create_dataset('joint_names', data=NAMES, dtype=h5py.string_dtype('utf-8')) meta.create_dataset('world_from_source', data=transform) ep = f.create_group('episodes/demo_000000') ep.attrs['source_frame_start'] = selection.start ep.attrs['source_frame_end_exclusive'] = selection.stop for name, value in dict(time=times, wrist_position=pos[selection], wrist_quaternion=quat[selection], joint_position=q[selection], valid=valid[selection]).items(): ep.create_dataset(name, data=value) # Reopen the actual serialized file and validate against the actual right URDF. with h5py.File(path, 'r') as f: ep = f['episodes/demo_000000'] t, p, r, angles, mask = [ep[n][:] for n in ('time','wrist_position','wrist_quaternion','joint_position','valid')] count = len(t) assert [(x.shape, x.dtype) for x in (t,p,r,angles,mask)] == [ ((count,),np.dtype('float64')),((count,3),np.dtype('float32')), ((count,4),np.dtype('float32')),((count,21),np.dtype('float32')),((count,),np.dtype('bool'))] assert count >= 2 and t[0] == 0 and np.all(np.diff(t)>0) and mask.any() assert all(np.isfinite(x).all() for x in (t,p,r,angles)) assert f['metadata/joint_names'].asstr()[:].tolist() == NAMES assert h5py.check_string_dtype(f['metadata/joint_names'].dtype).encoding == 'utf-8' assert np.max(np.abs(np.linalg.norm(r[mask],axis=1)-1)) < 1e-4 assert np.all(np.sum(r[1:]*r[:-1],axis=1)[mask[1:] & mask[:-1]] >= 0) assert np.allclose(transform[3], [0,0,0,1]) assert np.allclose(transform[:3,:3].T@transform[:3,:3], np.eye(3)) assert abs(np.linalg.det(transform[:3,:3])-1)<1e-8 residuals = {} for i, joint in enumerate(manifest['joints']): assert angles[mask,i].min() >= joint['lower_rad']-1e-6 assert angles[mask,i].max() <= joint['upper_rad']+1e-6 if 'mimic' in joint: m = joint['mimic'] err = np.abs(angles[mask,i]-m['multiplier']*angles[mask,NAMES.index(m['joint'])]-m['offset']) residuals[joint['name']] = float(err.max()) assert err.max() <= 1e-3 reports.append(dict(file=filename, right_model_validation='PASS', strict_left_v1='NOT_COMPATIBLE', frames=count, valid_frames=int(mask.sum()), source_frame_start=selection.start, source_frame_end_exclusive=selection.stop, time_last_s=float(t[-1]), mimic_residual_rad=residuals, sha256=hashlib.sha256(path.read_bytes()).hexdigest())) (args.output/'validation.json').write_text(json.dumps(reports,indent=2)+'\n') print(json.dumps(reports,indent=2)) if __name__ == '__main__': main()