Download scripts/render_smooth_olympic_all5.py from Raghav-16/Microduck_Manipulation: direct link, hf CLI and curl.
- Browser
- Download file 11.8 kB
-
https://huggingface.co/Raghav-16/Microduck_Manipulation/resolve/main/scripts/render_smooth_olympic_all5.py
- Command line
-
hf download hf://Raghav-16/Microduck_Manipulation/scripts/render_smooth_olympic_all5.py
-
curl -L -o render_smooth_olympic_all5.py https://huggingface.co/Raghav-16/Microduck_Manipulation/resolve/main/scripts/render_smooth_olympic_all5.py
11.8 kB
| import sys, os, math | |
| from pathlib import Path | |
| import numpy as np | |
| import mujoco | |
| import imageio | |
| import onnxruntime as ort | |
| ws = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ws / "scripts")) | |
| from test_duck_racer_lemon_balance import create_racer_scene_xml, DUCK_CONFIGS, JOINT_NAMES | |
| from infer_policy import DEFAULT_POSE | |
| sim_duration_sec = 80.0 # 80.0s simulation -> 40.0s video at 2x | |
| fps = 25 # 25 fps -> 2000 frames | |
| raw_path = "/tmp/smooth_olympic_raw_all5_80s.mp4" | |
| mp4_path = str(ws / "docs" / "media" / "duck_racer_competition_2x.mp4") | |
| webp_path = str(ws / "docs" / "media" / "duck_racer_competition_2x.webp") | |
| policy_path = str(ws / "policies" / "onnx" / "payload_clamping_locomotion.onnx") | |
| print(f"[OLYMPIC ALL-5 ACTIVE] Generating track scene XML with smooth policy {policy_path}...") | |
| xml = create_racer_scene_xml() | |
| xml = xml.replace('pos="2.0 0.0 0.0005" size="6.0 0.88 0.0005"', 'pos="10.0 0.0 0.0005" size="14.0 0.88 0.0005"') | |
| for div in ["lane_div_0", "lane_div_1", "lane_div_2", "lane_div_3", "lane_div_4", "lane_div_5"]: | |
| xml = xml.replace(f'name="{div}" type="box" pos="2.0', f'name="{div}" type="box" pos="10.0') | |
| xml = xml.replace('size="6.0 0.012 0.001"', 'size="14.0 0.012 0.001"') | |
| xml = xml.replace('name="barrier_left" type="box" pos="2.0', 'name="barrier_left" type="box" pos="10.0') | |
| xml = xml.replace('name="barrier_right" type="box" pos="2.0', 'name="barrier_right" type="box" pos="10.0') | |
| xml = xml.replace('size="6.0 0.025 0.04"', 'size="14.0 0.025 0.04"') | |
| model = mujoco.MjModel.from_xml_string(xml) | |
| model.opt.timestep = 0.002 | |
| data = mujoco.MjData(model) | |
| session = ort.InferenceSession(policy_path, providers=["CPUExecutionProvider"]) | |
| class ActiveOlympicDuck: | |
| def __init__(self, duck_id, cfg, target_vx): | |
| self.duck_id = duck_id | |
| self.cfg = cfg | |
| self.target_vx = target_vx | |
| self.prefix = f"d{duck_id}_" | |
| self.trunk_bid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, f"{self.prefix}trunk_base") | |
| self.jaw_bid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, f"{self.prefix}jaw_soft") | |
| self.spoon_bid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, f"{self.prefix}spoon") | |
| self.lemon_bid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, f"{self.prefix}lemon") | |
| self.trunk_jid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, f"{self.prefix}trunk_base_freejoint") | |
| self.trunk_qadr = model.jnt_qposadr[self.trunk_jid] | |
| self.trunk_dof = model.jnt_dofadr[self.trunk_jid] | |
| self.beak_jid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, f"{self.prefix}beak") | |
| self.beak_qpos = model.jnt_qposadr[self.beak_jid] | |
| self.beak_dof = model.jnt_dofadr[self.beak_jid] | |
| self.spoon_jid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, f"{self.prefix}spoon_free") | |
| self.spoon_qadr = model.jnt_qposadr[self.spoon_jid] | |
| self.spoon_dof = model.jnt_dofadr[self.spoon_jid] | |
| self.lemon_jid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, f"{self.prefix}lemon_free") | |
| self.lemon_qadr = model.jnt_qposadr[self.lemon_jid] | |
| self.lemon_dof = model.jnt_dofadr[self.lemon_jid] | |
| self.sensor_gyro_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, f"{self.prefix}imu_ang_vel") | |
| self.sensor_gyro_adr = model.sensor_adr[self.sensor_gyro_id] | |
| self.act_ids = [mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, f"{self.prefix}{name}") for name in JOINT_NAMES] | |
| self.beak_act_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, f"{self.prefix}beak") | |
| self.joint_qpos_indices = [int(model.jnt_qposadr[model.actuator_trnid[a, 0]]) for a in self.act_ids] | |
| self.joint_dof_indices = [int(model.jnt_dofadr[model.actuator_trnid[a, 0]]) for a in self.act_ids] | |
| self.default_pose = DEFAULT_POSE[:14].copy() | |
| self.default_pose[5] = 0.3491 | |
| self.default_pose[6] = 0.3491 | |
| self.last_action = np.zeros(14, dtype=np.float32) | |
| self.current_vx = 0.0 | |
| self.head_pos_steadicam = 0.0 | |
| self.target_clamp_angle = 1.550 | |
| self.clamp_torque = 0.45 | |
| self.q_s = np.array([0.0, 0.7071068, 0.7071068, 0.0]) | |
| def reset_posture(self): | |
| y0 = self.cfg["y"] | |
| data.qpos[self.trunk_qadr:self.trunk_qadr+3] = [0.0, y0, 0.125] | |
| data.qpos[self.trunk_qadr+3:self.trunk_qadr+7] = [1.0, 0.0, 0.0, 0.0] | |
| data.qvel[self.trunk_dof:self.trunk_dof+6] = 0.0 | |
| for i, idx in enumerate(self.joint_qpos_indices): | |
| data.qpos[idx] = self.default_pose[i] | |
| data.qvel[self.joint_dof_indices[i]] = 0.0 | |
| for i, act_id in enumerate(self.act_ids): | |
| data.ctrl[act_id] = self.default_pose[i] | |
| data.qpos[self.beak_qpos] = self.target_clamp_angle | |
| data.qvel[self.beak_dof] = 0.0 | |
| data.qfrc_applied[self.beak_dof] = -self.clamp_torque | |
| data.ctrl[self.beak_act_id] = 1.0 | |
| def place_spoon(self): | |
| p_jaw = data.xpos[self.jaw_bid] | |
| R_jaw = data.xmat[self.jaw_bid].reshape(3, 3) | |
| data.qpos[self.spoon_qadr:self.spoon_qadr+3] = p_jaw + R_jaw @ np.array([-0.0119, 0.0, -0.0465]) | |
| q_jaw = np.empty(4); mujoco.mju_mat2Quat(q_jaw, data.xmat[self.jaw_bid]) | |
| q_s = np.empty(4); mujoco.mju_mulQuat(q_s, q_jaw, np.array([0.0, 0.7071068, 0.7071068, 0.0])) | |
| data.qpos[self.spoon_qadr+3:self.spoon_qadr+7] = q_s | |
| data.qvel[self.spoon_dof:self.spoon_dof+6] = 0.0 | |
| self.q_s = q_s | |
| def place_lemon(self): | |
| p_spoon = data.xpos[self.spoon_bid] | |
| R_spoon = data.xmat[self.spoon_bid].reshape(3, 3) | |
| data.qpos[self.lemon_qadr:self.lemon_qadr+3] = p_spoon + R_spoon @ np.array([0.0, 0.0052, 0.070]) | |
| data.qpos[self.lemon_qadr+3:self.lemon_qadr+7] = self.q_s | |
| data.qvel[self.lemon_dof:self.lemon_dof+6] = 0.0 | |
| def step_clamp(self): | |
| beak_err = self.target_clamp_angle - data.qpos[self.beak_qpos] | |
| data.qfrc_applied[self.beak_dof] = float(np.clip(-self.clamp_torque + 10.0 * beak_err - 0.3 * data.qvel[self.beak_dof], -1.0, 0.2)) | |
| def step_control(self): | |
| if self.current_vx < self.target_vx: | |
| self.current_vx = min(self.target_vx, self.current_vx + 0.008) | |
| base_ang_vel = data.sensordata[self.sensor_gyro_adr:self.sensor_gyro_adr+3].astype(np.float32) | |
| R_trunk = data.xmat[self.trunk_bid].reshape(3, 3) | |
| proj_grav = (R_trunk.T @ np.array([0.0, 0.0, -1.0], dtype=np.float32)).astype(np.float32) | |
| qpos_rel = np.array([data.qpos[idx] - self.default_pose[i] for i, idx in enumerate(self.joint_qpos_indices)], dtype=np.float32) | |
| qvel = np.array([data.qvel[idx] for idx in self.joint_dof_indices], dtype=np.float32) | |
| last_act = self.last_action.copy() | |
| yaw = math.atan2(R_trunk[1, 0], R_trunk[0, 0]) | |
| y_err = float(data.xpos[self.trunk_bid][1] - self.cfg["y"]) | |
| target_yaw = math.atan2(-y_err, 0.60) | |
| yaw_err = (yaw - target_yaw + math.pi) % (2 * math.pi) - math.pi | |
| cmd_wz = float(np.clip(-2.5 * yaw_err, -0.45, 0.45)) | |
| cmd = np.zeros(13, dtype=np.float32) | |
| cmd[0] = self.current_vx | |
| cmd[2] = cmd_wz | |
| obs = np.concatenate([base_ang_vel, proj_grav, qpos_rel, qvel, last_act, cmd]).astype(np.float32)[np.newaxis, :] | |
| action = session.run(None, {session.get_inputs()[0].name: obs})[0][0].astype(np.float32) | |
| self.last_action = action.copy() | |
| h_fwd = np.array([R_trunk[0, 0], R_trunk[1, 0], 0.0]) | |
| h_fwd /= (np.linalg.norm(h_fwd) + 1e-6) | |
| R_s = data.xmat[self.spoon_bid].reshape(3, 3) | |
| b_norm = R_s @ np.array([0.0, 1.0, 0.0]) | |
| spoon_pitch = math.asin(np.clip(float(np.dot(b_norm, h_fwd)), -0.999, 0.999)) | |
| rate_cmd = np.clip(1.20 * spoon_pitch, -0.30, 0.30) | |
| self.head_pos_steadicam = np.clip(self.head_pos_steadicam + rate_cmd * 0.02, -0.40, 0.40) | |
| action[5] = 0.0 + self.head_pos_steadicam | |
| action[6] = 0.0 - self.head_pos_steadicam | |
| action[7] = 0.0 | |
| action[8] = 0.0 | |
| targets = self.default_pose + action | |
| for i, act_id in enumerate(self.act_ids): | |
| data.ctrl[act_id] = targets[i] | |
| # All paces >= 0.185 m/s so NO duck stalls: | |
| # Ruby Coral: 0.240 m/s (2nd place) | |
| # Riviera Sky: 0.185 m/s (5th place) | |
| # Duck Gold: 0.220 m/s (3rd place) | |
| # Emerald Green: 0.260 m/s (1st place Winner) | |
| # Royal Amethyst: 0.200 m/s (4th place) | |
| pacing = [0.240, 0.185, 0.220, 0.260, 0.200] | |
| ducks = [ActiveOlympicDuck(c["id"], c, pacing[i]) for i, c in enumerate(DUCK_CONFIGS)] | |
| for d in ducks: d.reset_posture() | |
| mujoco.mj_forward(model, data) | |
| for d in ducks: d.place_spoon() | |
| mujoco.mj_forward(model, data) | |
| for d in ducks: d.place_lemon() | |
| mujoco.mj_forward(model, data) | |
| for _ in range(150): | |
| for d in ducks: | |
| data.qpos[d.trunk_qadr:d.trunk_qadr+3] = [0.0, d.cfg["y"], 0.125] | |
| data.qvel[d.trunk_dof:d.trunk_dof+6] = 0.0 | |
| d.step_clamp() | |
| mujoco.mj_step(model, data) | |
| # Warmstart sprint steps so ducks are actively moving from frame 0 (NO static pause) | |
| for step in range(200): | |
| for d in ducks: d.step_clamp() | |
| if step % 10 == 0: | |
| for d in ducks: d.step_control() | |
| mujoco.mj_step(model, data) | |
| renderer = mujoco.Renderer(model, width=1280, height=720) | |
| vopt = mujoco.MjvOption() | |
| vopt.geomgroup[3] = 0 | |
| cam = mujoco.MjvCamera() | |
| cam.type = mujoco.mjtCamera.mjCAMERA_FREE | |
| cam.distance = 2.45 | |
| cam.elevation = -22.0 | |
| cam.azimuth = 68.0 | |
| total_sim_steps = int(sim_duration_sec / model.opt.timestep) | |
| sim_steps_per_frame = int(round(1.0 / (fps * model.opt.timestep))) | |
| decimation = 10 | |
| sim_dt = model.opt.timestep | |
| print(f"[OLYMPIC ALL-5 ACTIVE] Recording {sim_duration_sec:.1f}s race ({total_sim_steps} steps, {fps} fps)...", flush=True) | |
| writer = imageio.get_writer(raw_path, fps=fps, codec="libx264", quality=8) | |
| frame_count = 0 | |
| smooth_lookat_x = float(np.mean([data.qpos[d.trunk_qadr] for d in ducks])) + 0.45 | |
| for step in range(total_sim_steps): | |
| t_sim = step * sim_dt | |
| for d in ducks: | |
| d.step_clamp() | |
| if step % decimation == 0: | |
| for d in ducks: | |
| d.step_control() | |
| mujoco.mj_step(model, data) | |
| if step % sim_steps_per_frame == 0: | |
| xs = [float(data.qpos[d.trunk_qadr]) for d in ducks] | |
| pack_mean_x = float(np.mean(xs)) | |
| target_x = pack_mean_x + 0.45 | |
| smooth_lookat_x = 0.96 * smooth_lookat_x + 0.04 * target_x | |
| cam.lookat = [smooth_lookat_x, 0.0, 0.16] | |
| renderer.update_scene(data, camera=cam, scene_option=vopt) | |
| writer.append_data(renderer.render()) | |
| frame_count += 1 | |
| if frame_count % (fps * 5) == 0: | |
| xs_str = ", ".join([f"D{d.duck_id}:{data.qpos[d.trunk_qadr]:.2f}m" for d in ducks]) | |
| print(f" Frame {frame_count:4d}/{int(sim_duration_sec*fps)} (t={t_sim:5.1f}s) | Progress: [{xs_str}]", flush=True) | |
| writer.close() | |
| print(f"[OLYMPIC ALL-5 ACTIVE] Saved {frame_count} raw frames to {raw_path}", flush=True) | |
| cmd_2x = f'ffmpeg -y -i "{raw_path}" -filter:v "setpts=0.5*PTS" -r 30 "{mp4_path}"' | |
| os.system(cmd_2x) | |
| print(f"[OLYMPIC ALL-5 ACTIVE] Saved 2x video to {mp4_path}", flush=True) | |
| root_mp4 = str(ws / "docs" / "media" / "duck_racer_competition_2x.mp4") | |
| root_webp = str(ws / "docs" / "media" / "duck_racer_competition_2x.webp") | |
| os.system(f'cp "{mp4_path}" "{root_mp4}"') | |
| # Generate optimized WebP animation (< 3MB for instant GitHub Camo rendering) | |
| cmd_webp = f'ffmpeg -y -i "{mp4_path}" -vf "fps=8,scale=420:-1:flags=lanczos" -vcodec libwebp -lossless 0 -compression_level 4 -q:v 50 -loop 0 "{webp_path}"' | |
| os.system(cmd_webp) | |
| os.system(f'cp "{webp_path}" "{root_webp}"') | |
| sz = os.path.getsize(webp_path) / 1e6 | |
| print(f"[OLYMPIC ALL-5 ACTIVE SUCCESS] Saved WebP to {webp_path} - Size: {sz:.2f} MB", flush=True) | |
| if os.path.exists(raw_path): | |
| os.remove(raw_path) | |