7e4ef6f98b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
5.4 KiB
Python
74 lines
5.4 KiB
Python
"""Full-length contact registration; object poses are inferred targets, not physics."""
|
|
import json
|
|
from pathlib import Path
|
|
import numpy as np
|
|
from scipy.spatial.transform import Rotation,Slerp
|
|
from l20_contact import ContactRefiner
|
|
from l20_calibrated import ROOT
|
|
|
|
BASE=ROOT/'output/l20_2047635068';OUT=BASE/'bottle_grasp'
|
|
def main():
|
|
motion=np.load(BASE/'motion.npz');spec=json.loads((OUT/'object_spec.json').read_text())
|
|
solver=ContactRefiner(spec,OUT)
|
|
count=len(motion['qpos']);times=np.arange(count)
|
|
rotation=Rotation.from_quat(motion['wrist_quat_wxyz'][:,[1,2,3,0]])
|
|
reference=666
|
|
# Infer a constant bottle-to-palm registration over the holding phase.
|
|
rel_pos=rotation[reference].inv().apply(np.array(spec['position'])-motion['wrist_pos'][reference])
|
|
rel_rot=rotation[reference].inv()*Rotation.from_quat(np.array(spec['quat_wxyz'])[[1,2,3,0]])
|
|
bottle_pos=rotation.apply(np.tile(rel_pos,(count,1)))+motion['wrist_pos']
|
|
bottle_rot=rotation*rel_rot
|
|
# Contact onset bracket comes from visual inspection: open at 100, grasped by 200.
|
|
onset=160
|
|
bottle_pos[:onset]=bottle_pos[onset]
|
|
bottle_quat=bottle_rot.as_quat()[:,[3,0,1,2]]
|
|
bottle_quat[:onset]=bottle_quat[onset]
|
|
samples=np.unique(np.r_[np.arange(onset,count,8),count-1])
|
|
fits=[];reports=[]
|
|
for t in samples:
|
|
active=np.array([motion['qpos'][t,list(motion['joint_names']).index(n)] for n in solver.cal.active])
|
|
x,q,report=solver.solve(active,motion['wrist_pos'][t],motion['wrist_quat_wxyz'][t],bottle_pos[t],bottle_quat[t])
|
|
fits.append(x);reports.append(report)
|
|
if len(fits)%20==0:print(f'contact keyframes {len(fits)}/{len(samples)}',flush=True)
|
|
fits=np.array(fits)
|
|
interp=np.stack([np.interp(times,samples,fits[:,j]) for j in range(22)],axis=1)
|
|
source_active=np.stack([motion['qpos'][:,list(motion['joint_names']).index(n)] for n in solver.cal.active],axis=1)
|
|
# Blend acquisition only; preserve pre-contact source motion.
|
|
weight=np.clip((times-(onset-30))/30,0,1);weight=weight*weight*(3-2*weight)
|
|
interp[:,:16]=source_active*(1-weight[:,None])+interp[:,:16]*weight[:,None]
|
|
interp[:,16:]*=weight[:,None]
|
|
repairs=[]
|
|
for t in range(onset,count):
|
|
solver.set_pose(interp[t],motion['wrist_pos'][t],motion['wrist_quat_wxyz'][t],bottle_pos[t],bottle_quat[t])
|
|
if -solver.distances().min()>.0015:
|
|
x,_,r=solver.solve(source_active[t],motion['wrist_pos'][t],motion['wrist_quat_wxyz'][t],bottle_pos[t],bottle_quat[t],initial=interp[t])
|
|
if r['max_penetration_after_mm']>1.5 and t>onset:
|
|
alternative,_,retry=solver.solve(source_active[t],motion['wrist_pos'][t],motion['wrist_quat_wxyz'][t],bottle_pos[t],bottle_quat[t],initial=interp[t-1])
|
|
if retry['max_penetration_after_mm']<r['max_penetration_after_mm']:x,r=alternative,retry
|
|
interp[t]=x;repairs.append(dict(frame=t,**r))
|
|
if t%100==0:print(f'frame collision repair {t}/{count}: {len(repairs)} repaired',flush=True)
|
|
qpos=[];wp=[];wq=[];penetration=[]
|
|
for t,x in enumerate(interp):
|
|
solver.set_pose(x,motion['wrist_pos'][t],motion['wrist_quat_wxyz'][t],bottle_pos[t],bottle_quat[t])
|
|
values=solver.expand(x[:16]);qpos.append([values[str(n)] for n in motion['joint_names']])
|
|
wp.append(solver.data.qpos[solver.wrist:solver.wrist+3].copy());wq.append(solver.data.qpos[solver.wrist+3:solver.wrist+7].copy())
|
|
penetration.append(max(0,-solver.distances().min()))
|
|
qpos=np.array(qpos);wp=np.array(wp);wq=np.array(wq)
|
|
commands=np.array([solver.cal.command(dict(zip(motion['joint_names'],q))) for q in qpos])
|
|
np.savez_compressed(OUT/'registered_motion.npz',qpos=qpos,joint_names=motion['joint_names'],wrist_pos=wp,wrist_quat_wxyz=wq,
|
|
bottle_pos=bottle_pos,bottle_quat_wxyz=bottle_quat,command_u8=commands,fps=30,
|
|
keyframes=samples,optimization_variables=interp,penetration_m=penetration,contact_onset_frame=onset)
|
|
np.savetxt(OUT/'registered_trajectory.csv',np.c_[times/30,wp,wq,qpos,bottle_pos,bottle_quat],delimiter=',',
|
|
header=','.join(['time_s','wrist_x','wrist_y','wrist_z','wrist_qw','wrist_qx','wrist_qy','wrist_qz']+list(motion['joint_names'])+['bottle_x','bottle_y','bottle_z','bottle_qw','bottle_qx','bottle_qy','bottle_qz']),comments='')
|
|
report=dict(frames=count,fps=30,contact_keyframes=len(samples),converged_keyframes=sum(r['converged'] for r in reports),
|
|
mean_keyframe_gap_before_mm=float(np.mean([r['mean_selected_surface_gap_before_mm'] for r in reports])),
|
|
mean_keyframe_gap_after_mm=float(np.mean([r['mean_selected_surface_gap_after_mm'] for r in reports])),
|
|
max_holding_penetration_mm=float(np.max(penetration[onset:])*1000),max_all_frame_penetration_mm=float(np.max(penetration)*1000),
|
|
contact_onset_frame=onset,source='current 2047635068 video + supplied tri-camera bottle mesh',
|
|
repaired_frames=len(repairs),repair_reports=repairs,
|
|
alignment='Inferred fixed bottle-to-palm transform during holding, with contact pose corrections; no measured object trajectory or camera extrinsic alignment',
|
|
boundary='Object poses in this file are prescribed registration targets. Dynamic grasp tests are separate and have not passed.',keyframe_reports=reports)
|
|
(OUT/'sequence_validation.json').write_text(json.dumps(report,indent=2));print({k:v for k,v in report.items() if not k.endswith('_reports')},flush=True)
|
|
|
|
if __name__=='__main__':main()
|