You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

199 lines
7.2 KiB

"""ScenePic skeleton visualizer. Generates interactive 3D HTML animations."""
import numpy as np
import torch
from typing import Union, List, Optional
from pathlib import Path
try:
import scenepic as sp
SCENEPIC_AVAILABLE = True
except ImportError:
SCENEPIC_AVAILABLE = False
def _normalize_vec(v, eps=1e-8):
return v / (torch.norm(v, dim=-1, keepdim=True) + eps)
def _quat_to_exp_map(q):
"""Quaternion (wxyz) to axis-angle."""
w, xyz = q[..., 0:1], q[..., 1:4]
sin_half = torch.norm(xyz, dim=-1, keepdim=True)
angle = 2 * torch.atan2(sin_half, w)
axis = xyz / (sin_half + 1e-8)
return axis * angle
def _quat_between_two_vec(v1, v2, eps=1e-6):
"""Quaternion (wxyz) rotating v1 to v2."""
v1, v2 = v1.reshape(-1, 3), v2.reshape(-1, 3)
dot = (v1 * v2).sum(-1)
cross = torch.cross(v1, v2, dim=-1)
out = _normalize_vec(torch.cat([(1 + dot).unsqueeze(-1), cross], dim=-1))
out[dot > 1 - eps] = torch.tensor([1.0, 0.0, 0.0, 0.0], device=v1.device)
return out
def _make_floor_texture():
from PIL import ImageColor
c1 = np.tile(np.asarray(ImageColor.getcolor("#81C6EB", "RGB"), dtype=np.uint8), (5, 5, 1))
c2 = np.tile(np.asarray(ImageColor.getcolor("#D4F1F7", "RGB"), dtype=np.uint8), (5, 5, 1))
return np.tile(np.block([[[c1], [c2]], [[c2], [c1]]]), (15, 15, 1))
class SkeletonActor:
"""Skeleton with joints (spheres) and bones (cones)."""
def __init__(
self, scene, name, joint_parents, num_base_bodies=None, joint_radius=0.06, bone_radius=0.04
):
self.joint_parents = joint_parents
self.num_base = num_base_bodies or len(joint_parents)
# Floor
self.floor_img = scene.create_image(image_id="floor")
self.floor_img.from_numpy(_make_floor_texture())
self.floor_mesh = scene.create_mesh(texture_id="floor", layer_id="floor")
self.floor_mesh.add_image(transform=sp.Transforms.Scale(20))
# Joints and bones
self.joint_meshes, self.bone_pairs = [], []
for j, pa in enumerate(joint_parents):
is_ext = j >= self.num_base
jcolor = sp.Colors.Red if is_ext else (sp.Colors.Yellow if j == 0 else sp.Colors.Yellow)
joint_mesh = scene.create_mesh(f"{name}_j{j}", layer_id=name)
joint_mesh.add_sphere(color=jcolor, transform=sp.Transforms.scale(joint_radius))
self.joint_meshes.append(joint_mesh)
if pa >= 0:
bcolor = sp.Colors.Orange if is_ext else sp.Colors.Green
bone_mesh = scene.create_mesh(f"{name}_b{j}", layer_id=name)
bone_mesh.add_cone(
color=bcolor,
transform=sp.Transforms.scale(np.array([1, bone_radius, bone_radius])),
)
self.bone_pairs.append((j, pa, bone_mesh))
def add_to_frame(self, frame, pos):
frame.add_mesh(self.floor_mesh)
for j, p in enumerate(pos):
frame.add_mesh(self.joint_meshes[j], transform=sp.Transforms.translate(p))
if not self.bone_pairs:
return
vecs = np.stack([pos[j] - pos[pa] for j, pa, _ in self.bone_pairs])
dists = np.linalg.norm(vecs, axis=-1)
aa = _quat_to_exp_map(
_quat_between_two_vec(
torch.tensor([-1.0, 0.0, 0.0]).expand(len(vecs), 3),
torch.tensor(vecs / (dists[:, None] + 1e-8)),
)
).numpy()
angles, axes = np.linalg.norm(aa, axis=-1, keepdims=True), aa / (
np.linalg.norm(aa, axis=-1, keepdims=True) + 1e-8
)
for (j, pa, mesh), ang, ax, d in zip(self.bone_pairs, angles, axes, dists):
t = sp.Transforms.translate((pos[pa] + pos[j]) * 0.5)
t = (
t
@ sp.Transforms.RotationMatrixFromAxisAngle(ax, ang)
@ sp.Transforms.Scale(np.array([d, 1, 1]))
)
frame.add_mesh(mesh, transform=t)
class ScenepicVisualizer:
"""Interactive 3D skeleton visualizer using ScenePic."""
def __init__(self, joint_parents, num_base_bodies=None):
if not SCENEPIC_AVAILABLE:
raise ImportError("pip install scenepic")
if isinstance(joint_parents, torch.Tensor):
joint_parents = joint_parents.cpu().numpy().tolist()
self.joint_parents = joint_parents
self.num_base = num_base_bodies or len(joint_parents)
def visualize(self, joint_positions, output_path, fps=30, title="Skeleton"):
"""Render skeleton animation to HTML. joint_positions: [T, J, 3]"""
if isinstance(joint_positions, torch.Tensor):
joint_positions = joint_positions.cpu().numpy()
if joint_positions.ndim == 4:
joint_positions = joint_positions[0]
T = joint_positions.shape[0]
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
scene = sp.Scene()
scene.framerate = fps
canvas = scene.create_canvas_3d(width=600, height=600)
canvas.set_layer_settings({title: {"filled": True}})
skeleton = SkeletonActor(scene, title, self.joint_parents, self.num_base)
camera = sp.Camera(
center=(4, 0, 1.5), look_at=(0, 0, 0.8), up_dir=(0, 0, 1), fov_y_degrees=45.0
)
for t in range(T):
frame = canvas.create_frame()
frame.camera = camera
skeleton.add_to_frame(frame, joint_positions[t])
scene.save_as_html(str(output_path))
print(f"Saved: {output_path}")
return output_path
if __name__ == "__main__":
from pathlib import Path
from omegaconf import OmegaConf
from groot.rl.utils.motion_lib.torch_humanoid_batch import Humanoid_Batch
from groot.rl.trl.utils.mujoco_fk_utils import MuJoCoFKHelper, load_qpos_from_csv
groot_root = Path(__file__).parent.parent.parent.parent
motion_yaml = (
groot_root
/ "rl"
/ "config"
/ "manager_env"
/ "commands"
/ "terms"
/ "motion_g1_extended_toe.yaml"
)
cfg = OmegaConf.load(motion_yaml).motion.motion_lib_cfg
fk_helper = MuJoCoFKHelper(Humanoid_Batch(cfg, device=torch.device("cpu")))
print(
f"Loaded: {fk_helper.num_bodies} + {fk_helper.num_bodies_augment - fk_helper.num_bodies} extended bodies"
)
csv_path = groot_root / ".." / "data" / "example_csv_g1_navigation.csv"
if csv_path.exists():
qpos = load_qpos_from_csv(str(csv_path))
else:
T, t = 60, torch.linspace(0, 4 * 3.14159, 60)
dof = torch.zeros(T, fk_helper.num_dof)
dof[:, 0], dof[:, 3] = 0.3 * torch.sin(t), 0.5 * torch.sin(t)
dof[:, 6], dof[:, 9] = 0.3 * torch.sin(t + 3.14), 0.5 * torch.sin(t + 3.14)
qpos = torch.cat(
[
torch.zeros(T, 3) + torch.tensor([0, 0, 1.0]),
torch.tensor([[1.0, 0.0, 0.0, 0.0]]).expand(T, 4),
dof,
],
dim=-1,
)
global_pos, _ = fk_helper.qpos_to_global_transforms(
qpos.unsqueeze(0), False, include_extended=True
)
viz = ScenepicVisualizer(fk_helper._parents, fk_helper.num_bodies)
viz.visualize(
global_pos[0],
Path(__file__).parent.parent.parent.parent / "scenepic_demo.html",
title="G1 Robot",
)
print("Yellow = base joints, Red = extended (head/toes)")