twanghcmut's picture
download
raw
10.8 kB
#!/usr/bin/env python
"""Render the articulated robot mesh over a DROID episode's exterior camera video.
PYTHONPATH=src python scripts/render_robot.py --episode <uuid> --camera ext1
Reads joint/gripper state and per-frame camera extrinsics straight from a local
``data/droid_raw/<uuid>/trajectory.h5`` (no download attempted -- these assets are
expected to already be on disk), forward-kinematics them through a
:class:`~fpgm.robot.urdf.RobotModel`, and renders the posed mesh with
:class:`~fpgm.robot.render.RobotRenderer` (OSMesa offscreen GL; see that module's
docstring for why hardware GL is not used on this box).
Two modes:
* Full render (default): writes ``robot_overlay.mp4`` (video composited with the
rendered robot), ``robot_mask.mp4`` (mask visualised as white-on-black), and
``robot_mask.npz`` (the same masks, bit-packed).
* ``--fk-only``: skips mesh loading and rendering entirely and instead projects
each link's origin into the image as a coloured dot (``robot_fk_dots.mp4``) --
a cheap way to sanity-check the kinematic chain and camera convention before
paying for full rendering.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import h5py
import numpy as np
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "src"))
from fpgm.data.droid_raw import cam2world_vector_to_world2cam, read_mp4_properties # noqa: E402
from fpgm.data.pointworld import FlowsReader # noqa: E402
from fpgm.geometry.camera import Camera # noqa: E402
from fpgm.robot.overlay import composite, draw_link_dots, mask_to_bgr # noqa: E402
from fpgm.types import CameraIntrinsics, DataError # noqa: E402
from fpgm.utils.logging import get_logger, setup_logging # noqa: E402
from fpgm.viz.overlays import VideoWriter, read_frames_bgr # noqa: E402
logger = get_logger("render_robot")
#: Falls back to this annotation resolution when a clip has no decoded
#: `initial_rgb` to measure it from -- see `_read_base_intrinsics`.
_FALLBACK_ANNOTATION_SIZE = (320, 180)
_DEFAULT_URDF = (
REPO_ROOT
/ "third_party"
/ "robot_description"
/ "pointworld_franka_robotiq_2f85"
/ "franka_panda_robotiq_2f85.urdf"
)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
p.add_argument("--episode", required=True, help="DROID episode uuid")
p.add_argument(
"--camera", default="ext1", choices=["ext1", "ext2"], help="exterior camera role"
)
p.add_argument(
"--max-frames", type=int, default=None, help="cap the number of frames processed"
)
p.add_argument(
"--urdf",
type=Path,
default=_DEFAULT_URDF,
help="robot URDF path (default: PointWorld's DROID Franka+Robotiq asset)",
)
p.add_argument("--stride", type=int, default=1, help="process every Nth trajectory step")
p.add_argument(
"--outputs",
type=Path,
default=None,
help="output directory (default: outputs/<episode-uuid>/robot)",
)
p.add_argument(
"--fk-only",
action="store_true",
help="skip mesh rendering; project link origins as dots (cheap sanity check)",
)
return p.parse_args()
def _episode_dir(uuid: str) -> Path:
return REPO_ROOT / "data" / "droid_raw" / uuid
def _flows_path(uuid: str) -> Path:
return REPO_ROOT / "data" / "pointworld" / "droid" / "flows-fs-optimized" / f"{uuid}_flows.h5"
def _load_metadata(uuid: str) -> dict:
path = _episode_dir(uuid) / "metadata.json"
if not path.exists():
raise DataError(
f"no local DROID metadata for episode {uuid!r} at {path}; this script "
"does not download -- fetch the episode first."
)
return json.loads(path.read_text())
def _read_base_intrinsics(uuid: str, camera_serial: str) -> CameraIntrinsics:
"""Read the PointWorld scene-flow intrinsic for `camera_serial`, at its native resolution.
Raises:
DataError: if the episode's flows.h5 is absent, or has no clip for
`camera_serial` -- this intrinsic is the only source of calibration
this script has, so there is no fallback.
"""
flows_path = _flows_path(uuid)
if not flows_path.exists():
raise DataError(
f"PointWorld flows.h5 not found for episode {uuid!r} at {flows_path}; "
"cannot recover camera intrinsics without it."
)
with FlowsReader(flows_path, episode_uuid=uuid) as reader:
clip_key = next(
(k for k in reader.clip_keys() if camera_serial in reader.camera_serials(k)), None
)
if clip_key is None:
raise DataError(
f"no scene-flow clip for camera serial {camera_serial!r} in {flows_path}"
)
clip = reader.read_clip(clip_key, camera_serial)
if clip.initial_rgb is not None:
height, width = clip.initial_rgb.shape[:2]
else:
width, height = _FALLBACK_ANNOTATION_SIZE
logger.warning(
"clip %s has no initial_rgb; assuming annotation resolution %dx%d",
clip_key, width, height,
)
return CameraIntrinsics.from_matrix(clip.intrinsic, width=width, height=height)
def main() -> int:
args = parse_args()
setup_logging()
logger.info("episode=%s camera=%s fk_only=%s", args.episode, args.camera, args.fk_only)
if not args.urdf.exists():
raise DataError(
f"URDF not found: {args.urdf}. Fetch it with "
"`python scripts/fetch_robot_description.py --source pointworld`, or pass "
"--urdf to point at a different robot description."
)
metadata = _load_metadata(args.episode)
serial = metadata.get(f"{args.camera}_cam_serial")
if serial is None:
raise DataError(f"metadata for {args.episode!r} has no {args.camera}_cam_serial entry")
mp4_path = _episode_dir(args.episode) / "recordings" / "MP4" / f"{serial}.mp4"
if not mp4_path.exists():
raise DataError(f"mp4 not found for camera {serial!r}: {mp4_path}")
trajectory_path = _episode_dir(args.episode) / "trajectory.h5"
if not trajectory_path.exists():
raise DataError(f"trajectory.h5 not found: {trajectory_path}")
with h5py.File(trajectory_path, "r") as traj:
joint_positions = np.asarray(traj["observation/robot_state/joint_positions"]) # (T, 7)
gripper_position = np.asarray(traj["observation/robot_state/gripper_position"]) # (T,)
extrinsics_key = f"observation/camera_extrinsics/{serial}_left"
if extrinsics_key not in traj:
raise DataError(f"trajectory.h5 has no dataset {extrinsics_key!r}")
extrinsics_6d = np.asarray(traj[extrinsics_key]) # (T, 6)
n_steps = joint_positions.shape[0]
logger.info("trajectory has %d steps", n_steps)
base_intrinsics = _read_base_intrinsics(args.episode, serial)
mp4_fps, _frame_count, (video_w, video_h) = read_mp4_properties(mp4_path)
mp4_fps = mp4_fps or 15.0
scaled_intrinsics = base_intrinsics.scaled(video_w, video_h)
logger.info(
"intrinsics %dx%d -> %dx%d (fx=%.1f fy=%.1f)",
base_intrinsics.width, base_intrinsics.height, video_w, video_h,
scaled_intrinsics.fx, scaled_intrinsics.fy,
)
outputs_dir = args.outputs or (REPO_ROOT / "outputs" / args.episode / "robot")
outputs_dir.mkdir(parents=True, exist_ok=True)
# Deferred: fpgm.robot.urdf is owned by another workstream and may not exist
# yet at import time even though this script is written against its API.
from fpgm.robot.urdf import RobotModel
robot = RobotModel(str(args.urdf), load_meshes=not args.fk_only)
n_target = n_steps if args.max_frames is None else min(n_steps, args.max_frames)
renderer = None
overlay_writer = None
mask_writer = None
packed_masks: list[np.ndarray] = []
coverage_sum = 0.0
n_written = 0
if not args.fk_only:
from fpgm.robot.render import RobotRenderer
renderer = RobotRenderer(robot.visual_meshes(), video_w, video_h)
overlay_writer = VideoWriter(outputs_dir / "robot_overlay.mp4", fps=mp4_fps)
mask_writer = VideoWriter(outputs_dir / "robot_mask.mp4", fps=mp4_fps)
else:
overlay_writer = VideoWriter(outputs_dir / "robot_fk_dots.mp4", fps=mp4_fps)
try:
for t, frame_bgr in enumerate(read_frames_bgr(mp4_path)):
if n_written >= n_target:
break
if t >= n_steps:
break
if t % args.stride != 0:
continue
world_to_cam = cam2world_vector_to_world2cam(extrinsics_6d[t])
camera = Camera(scaled_intrinsics, world_to_cam)
joints = joint_positions[t]
gripper = float(gripper_position[t])
if args.fk_only:
names, origins = robot.link_origins(joints, gripper)
uv, depth = camera.project(origins)
visible = depth > 0
out = draw_link_dots(
frame_bgr,
uv[visible],
[n for n, v in zip(names, visible, strict=True) if v],
)
overlay_writer.write(out)
else:
link_poses = robot.link_poses(joints, gripper)
result = renderer.render(link_poses, camera)
out = composite(frame_bgr, result)
overlay_writer.write(out)
mask_writer.write(mask_to_bgr(result.mask))
packed_masks.append(np.packbits(result.mask))
coverage = float(result.mask.mean())
coverage_sum += coverage
if n_written % 20 == 0:
logger.info("frame %d: mask coverage %.2f%%", t, coverage * 100.0)
n_written += 1
finally:
overlay_writer.close()
if mask_writer is not None:
mask_writer.close()
if renderer is not None:
renderer.close()
if not args.fk_only and packed_masks:
npz_path = outputs_dir / "robot_mask.npz"
np.savez_compressed(
npz_path,
packed=np.stack(packed_masks, axis=0),
shape=np.array([len(packed_masks), video_h, video_w]),
)
logger.info("wrote %s", npz_path)
print("\n" + "=" * 78)
print(f"episode: {args.episode}")
print(f"camera: {args.camera} ({serial})")
print(f"frames written: {n_written}")
if not args.fk_only and n_written:
print(f"mean mask coverage: {100.0 * coverage_sum / n_written:.2f}%")
print(f"outputs: {outputs_dir}")
print("=" * 78)
return 0
if __name__ == "__main__":
raise SystemExit(main())

Xet Storage Details

Size:
10.8 kB
·
Xet hash:
28dc48dbec4fc3e8d1f2388053dcf99e9409a0c3464dca4ab1c96f64b0d18c9b

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