marionette / tests /pose_utils.py
RemiFabre
Track previously untracked test support files
f851c34
Raw
History Blame
3.86 kB
"""Pose comparison utilities for hardware integration tests.
Provides trajectory comparison using distance_between_poses() from
the reachy_mini SDK. Returns per-frame and aggregate metrics (RMSE,
max, mean) for translation, rotation, and "magic distance".
"""
import numpy as np
from dataclasses import dataclass
from reachy_mini.utils.interpolation import distance_between_poses
@dataclass
class TrajectoryMetrics:
"""Aggregate metrics from comparing two trajectories."""
n_frames: int
# Magic distance (mm + degrees combined)
magic_mean: float
magic_rmse: float
magic_max: float
magic_p95: float
# Translation (meters)
trans_mean: float
trans_rmse: float
trans_max: float
# Rotation (radians)
rot_mean: float
rot_rmse: float
rot_max: float
# Per-frame arrays
magic_distances: np.ndarray
trans_distances: np.ndarray
rot_distances: np.ndarray
def summary(self) -> str:
return (
f"Trajectory comparison ({self.n_frames} frames):\n"
f" Magic: mean={self.magic_mean:.1f} rmse={self.magic_rmse:.1f} "
f"max={self.magic_max:.1f} p95={self.magic_p95:.1f}\n"
f" Trans: mean={self.trans_mean*1000:.1f}mm "
f"rmse={self.trans_rmse*1000:.1f}mm max={self.trans_max*1000:.1f}mm\n"
f" Rot: mean={np.degrees(self.rot_mean):.1f}deg "
f"rmse={np.degrees(self.rot_rmse):.1f}deg max={np.degrees(self.rot_max):.1f}deg"
)
def frames_to_poses(frames: list[dict]) -> list[np.ndarray]:
"""Extract head poses (4x4 matrices) from recorded frames."""
poses = []
for f in frames:
head = f["head"]
poses.append(np.array(head, dtype=np.float64))
return poses
def compare_trajectories(
ref_times: list[float],
ref_frames: list[dict],
rec_times: list[float],
rec_frames: list[dict],
) -> TrajectoryMetrics:
"""Compare two recorded trajectories frame-by-frame.
Interpolates the recorded trajectory timestamps to match the reference
timestamps using nearest-neighbor lookup, then computes per-frame
distances using distance_between_poses().
Args:
ref_times: timestamps from the reference recording
ref_frames: frame dicts from the reference recording
rec_times: timestamps from the new recording
rec_frames: frame dicts from the new recording
Returns:
TrajectoryMetrics with per-frame and aggregate distances.
"""
ref_poses = frames_to_poses(ref_frames)
rec_poses = frames_to_poses(rec_frames)
rec_times_arr = np.array(rec_times)
trans_dists = []
rot_dists = []
magic_dists = []
for i, (t, ref_pose) in enumerate(zip(ref_times, ref_poses)):
# Find nearest recorded frame by timestamp
idx = int(np.argmin(np.abs(rec_times_arr - t)))
rec_pose = rec_poses[idx]
d_trans, d_rot, d_magic = distance_between_poses(ref_pose, rec_pose)
trans_dists.append(d_trans)
rot_dists.append(d_rot)
magic_dists.append(d_magic)
trans_arr = np.array(trans_dists)
rot_arr = np.array(rot_dists)
magic_arr = np.array(magic_dists)
return TrajectoryMetrics(
n_frames=len(ref_times),
magic_mean=float(np.mean(magic_arr)),
magic_rmse=float(np.sqrt(np.mean(magic_arr**2))),
magic_max=float(np.max(magic_arr)),
magic_p95=float(np.percentile(magic_arr, 95)),
trans_mean=float(np.mean(trans_arr)),
trans_rmse=float(np.sqrt(np.mean(trans_arr**2))),
trans_max=float(np.max(trans_arr)),
rot_mean=float(np.mean(rot_arr)),
rot_rmse=float(np.sqrt(np.mean(rot_arr**2))),
rot_max=float(np.max(rot_arr)),
magic_distances=magic_arr,
trans_distances=trans_arr,
rot_distances=rot_arr,
)