Files
hand-motion-pipeline/scripts/render_l20.py
T

71 lines
4.1 KiB
Python

"""Render fixed-world and wrist-following views of a moving L20 trajectory."""
import os
os.environ.setdefault('MUJOCO_GL','osmesa')
import argparse,json
from pathlib import Path
import numpy as np
import mujoco,imageio.v2 as imageio
from PIL import Image,ImageDraw
from scipy.spatial.transform import Rotation
def main():
parser=argparse.ArgumentParser()
parser.add_argument('--directory',type=Path,default=Path(__file__).resolve().parents[1]/'output/l20_2047635068')
parser.add_argument('--frames',type=int,default=0)
args=parser.parse_args();out=args.directory
with np.load(out/'motion.npz') as archive:
motion={name:archive[name] for name in archive.files}
q=motion['qpos'][:args.frames or None];positions=motion['wrist_pos'][:len(q)]
model=mujoco.MjModel.from_xml_path(str(out/'l20_moving.xml'));data=mujoco.MjData(model)
addr=[model.jnt_qposadr[mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_JOINT,str(n))] for n in motion['joint_names']]
wrist=model.jnt_qposadr[mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_JOINT,'wrist_free')]
rotations=Rotation.from_quat(motion['wrist_quat_wxyz'][:,[1,2,3,0]]).as_matrix()
world=np.einsum('tij,tkj->tki',rotations,motion['actual'])+motion['wrist_pos'][:,None]
center=(world.min((0,1))+world.max((0,1)))/2
span=np.ptp(world,axis=(0,1)).max()
floor=mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_GEOM,'floor')
model.geom_pos[floor,2]=world[:,:,2].min()-.035
model.vis.quality.shadowsize=1024;model.vis.quality.offsamples=1
model.vis.headlight.ambient[:]=.65;model.vis.headlight.diffuse[:]=.7
renderer=mujoco.Renderer(model,height=480,width=640)
cameras=[]
for azimuth in [135,25]:
cam=mujoco.MjvCamera();cam.lookat[:]=center;cam.distance=max(.65,span*2.3);cam.azimuth=azimuth;cam.elevation=10
cameras.append(cam)
option=mujoco.MjvOption();option.sitegroup[:]=0
fps=float(motion['fps'])
coupling=str(motion['coupling']) if 'coupling' in motion else 'JSON-calibrated'
writer=imageio.get_writer(out/'l20_moving_replay.mp4',fps=fps,codec='libx264',quality=8)
max_wrist_error=0.
try:
for t,pose in enumerate(q):
data.qpos[addr]=pose;data.qpos[wrist:wrist+3]=positions[t];data.qpos[wrist+3:wrist+7]=motion['wrist_quat_wxyz'][t]
mujoco.mj_forward(model,data)
base=mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_BODY,'hand_base_link')
max_wrist_error=max(max_wrist_error,float(np.abs(data.xpos[base]-positions[t]).max()))
panels=[]
cameras[1].lookat[:]=world[t].mean(0);cameras[1].distance=.58
for vi,cam in enumerate(cameras):
renderer.update_scene(data,camera=cam,scene_option=option)
if vi==0:
points=positions[np.unique(np.linspace(0,t,min(t+1,60),dtype=int))]
for a,b in zip(points[:-1],points[1:]):
scene=renderer.scene
mujoco.mjv_initGeom(scene.geoms[scene.ngeom],mujoco.mjtGeom.mjGEOM_CAPSULE,np.ones(3)*.001,a,np.eye(3).ravel(),np.array([.2,.8,1,1]))
mujoco.mjv_connector(scene.geoms[scene.ngeom],mujoco.mjtGeom.mjGEOM_CAPSULE,.0015,a,b)
scene.ngeom+=1
panels.append(renderer.render().copy())
img=Image.fromarray(np.concatenate(panels,axis=1));draw=ImageDraw.Draw(img)
draw.rectangle([0,0,1280,45],fill=(15,22,30))
draw.text((10,8),f'L20 right | frame {t+1}/{len(q)} | moving 6D wrist + {coupling} fingers',fill='white')
draw.text((10,27),'Fixed world camera / blue wrist trail',fill='white')
draw.text((650,27),'Following camera / finger detail',fill='white')
writer.append_data(np.asarray(img))
if t in [0,len(q)//2,len(q)-1]:img.save(out/f'preview_{t:04d}.png')
if t%200==0:print(f'render L20 {t}/{len(q)}',flush=True)
finally:writer.close();renderer.close()
(out/'render_validation.json').write_text(json.dumps(dict(frames=len(q),fps=fps,max_wrist_position_error_m=max_wrist_error),indent=2))
print('COMPLETE',flush=True)
if __name__=='__main__':main()