twanghcmut's picture
download
raw
7.33 kB
#!/usr/bin/env python
"""Render the Franka+Robotiq arm waving through random joint poses, no input video.
PYTHONPATH=src python scripts/render_robot_random.py --out outputs/robot_random.mp4
Unlike ``render_robot.py`` (which replays a real DROID trajectory over that
episode's camera footage), this samples a chaotic joint trajectory from
scratch -- a cubic spline through random waypoints per joint, each within that
joint's URDF ``<limit>`` range -- and renders it against a plain background
with a slowly orbiting camera. Written out as slow motion: the joint
trajectory is sampled at ``fps * slow_factor`` points across ``--duration``
seconds of "real" arm motion, then encoded at ``fps``, so the on-screen motion
plays back ``slow_factor``x slower than it actually happened.
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
import numpy as np
from scipy.interpolate import CubicSpline
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "src"))
from fpgm.geometry.camera import Camera # noqa: E402
from fpgm.geometry.transforms import invert_se3 # noqa: E402
from fpgm.robot.render import RobotRenderer # noqa: E402
from fpgm.robot.urdf import RobotModel # noqa: E402
from fpgm.types import CameraIntrinsics # noqa: E402
from fpgm.viz.overlays import VideoWriter # noqa: E402
_DEFAULT_URDF = (
REPO_ROOT
/ "third_party"
/ "robot_description"
/ "pointworld_franka_robotiq_2f85"
/ "franka_panda_robotiq_2f85.urdf"
)
def _random_joint_spline(
rng: np.random.Generator, duration: float, waypoint_rate: float, arm_joint_limits: np.ndarray
):
"""Build a per-joint cubic spline through random waypoints within each joint's limits.
A different random waypoint count/timing per joint (rather than one shared
set of keyframe times) is what makes the motion look chaotic rather than a
single rigid-body wave -- every joint independently changes direction at
its own pace.
"""
splines = []
for lower, upper in arm_joint_limits:
n_waypoints = max(2, int(rng.poisson(waypoint_rate * duration)) + 2)
times = np.sort(rng.uniform(0.0, duration, size=n_waypoints))
times[0], times[-1] = 0.0, duration
values = rng.uniform(lower, upper, size=n_waypoints)
splines.append(CubicSpline(times, values))
return splines
def _random_gripper_spline(rng: np.random.Generator, duration: float, waypoint_rate: float):
n_waypoints = max(2, int(rng.poisson(waypoint_rate * duration)) + 2)
times = np.sort(rng.uniform(0.0, duration, size=n_waypoints))
times[0], times[-1] = 0.0, duration
values = rng.uniform(0.0, 1.0, size=n_waypoints)
return CubicSpline(times, values)
def _look_at_world_to_cam(eye: np.ndarray, target: np.ndarray) -> np.ndarray:
"""Build an OpenCV-convention (+Z forward, +Y down) world-to-camera SE3 matrix."""
world_up = np.array([0.0, 0.0, 1.0])
forward = target - eye
forward = forward / np.linalg.norm(forward)
right = np.cross(forward, world_up)
right = right / np.linalg.norm(right)
down = np.cross(forward, right)
cam_to_world = np.eye(4)
cam_to_world[:3, 0] = right
cam_to_world[:3, 1] = down
cam_to_world[:3, 2] = forward
cam_to_world[:3, 3] = eye
return invert_se3(cam_to_world)
def _orbit_camera(t_frac: float, intrinsics: CameraIntrinsics, revolutions: float) -> Camera:
"""Camera slowly circling the robot's workspace, `t_frac` in [0, 1] over the clip."""
angle = 2.0 * np.pi * revolutions * t_frac
radius, elevation, target = 1.6, 0.55, np.array([0.0, 0.0, 0.5])
eye = target + np.array([radius * np.cos(angle), radius * np.sin(angle), elevation])
return Camera(intrinsics, _look_at_world_to_cam(eye, target))
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--urdf", type=Path, default=_DEFAULT_URDF)
p.add_argument("--out", type=Path, default=REPO_ROOT / "outputs" / "robot_random.mp4")
p.add_argument("--duration", type=float, default=5.0, help="seconds of simulated arm motion")
p.add_argument("--fps", type=float, default=30.0, help="output video frame rate")
p.add_argument("--slow-factor", type=float, default=5.0, help="slow-motion factor")
p.add_argument("--width", type=int, default=480)
p.add_argument("--height", type=int, default=480)
p.add_argument("--waypoint-rate", type=float, default=1.0, help="random waypoints/second/joint")
p.add_argument("--orbit-revolutions", type=float, default=0.5)
p.add_argument("--seed", type=int, default=0)
return p.parse_args()
def main() -> int:
args = parse_args()
if not args.urdf.exists():
raise SystemExit(f"URDF not found: {args.urdf}")
robot = RobotModel(str(args.urdf), load_meshes=True)
rng = np.random.default_rng(args.seed)
joint_splines = _random_joint_spline(
rng, args.duration, args.waypoint_rate, robot.arm_joint_limits()
)
gripper_spline = _random_gripper_spline(rng, args.duration, args.waypoint_rate)
link_meshes = robot.visual_meshes()
intrinsics = CameraIntrinsics(
fx=0.9 * args.width, fy=0.9 * args.width,
cx=args.width / 2.0, cy=args.height / 2.0,
width=args.width, height=args.height,
)
# pyrender's OSMesa (software-GL) backend on this host leaks GL/Mesa state
# every render() call regardless of scene reuse (~180MB/frame at 480x480,
# confirmed with a standalone probe) -- recreating the renderer every
# `_RENDERER_RECYCLE` frames bounds that growth instead of letting a long
# clip OOM the (shared) host.
_RENDERER_RECYCLE = 10
def _new_renderer():
return RobotRenderer(link_meshes, args.width, args.height, bg_color=(0.05, 0.05, 0.08, 1.0))
renderer = _new_renderer()
n_frames = int(round(args.duration * args.fps * args.slow_factor))
sim_times = np.linspace(0.0, args.duration, n_frames, endpoint=False)
args.out.parent.mkdir(parents=True, exist_ok=True)
writer = VideoWriter(args.out, fps=args.fps)
try:
for i, t in enumerate(sim_times):
if i > 0 and i % _RENDERER_RECYCLE == 0:
renderer.close()
renderer = _new_renderer()
joints = np.array([s(t) for s in joint_splines])
gripper = float(np.clip(gripper_spline(t), 0.0, 1.0))
link_poses = robot.link_poses(joints, gripper)
camera = _orbit_camera(i / n_frames, intrinsics, args.orbit_revolutions)
result = renderer.render(link_poses, camera)
writer.write(result.color[:, :, ::-1]) # RGB -> BGR
if i % 50 == 0:
print(f"frame {i}/{n_frames} t={t:.2f}s mask coverage={result.mask.mean():.3f}")
finally:
writer.close()
renderer.close()
playback_seconds = n_frames / args.fps
print("\n" + "=" * 78)
print(f"frames written: {n_frames}")
print(f"simulated motion: {args.duration:.1f}s -> playback: {playback_seconds:.1f}s "
f"({args.slow_factor:.0f}x slow motion)")
print(f"output: {args.out}")
print("=" * 78)
return 0
if __name__ == "__main__":
raise SystemExit(main())

Xet Storage Details

Size:
7.33 kB
·
Xet hash:
5d0920645244fc36ba4187ede3d400badbc2ca2d87bc82051b9761310d26599a

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.