Spaces:
Running
Running
File size: 5,426 Bytes
7ea69d7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | """Render comparison plots for dance_compare.py CSV output.
For one dance, generate a figure showing the same channels across
both methods (python_native vs marionette_style), with N runs of
each overlaid. Channels: head Y translation, head pitch (derived
from rotation matrix), antenna_left, antenna_right.
"""
import csv
import glob
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
DANCE = sys.argv[1] if len(sys.argv) > 1 else "head_tilt_roll"
CSV_DIR = Path("/Users/remi/Downloads/dance-compare")
OUT_PNG = Path(f"/Users/remi/Downloads/dance-compare-{DANCE}.png")
def load(path):
rows = []
with open(path) as f:
for r in csv.DictReader(f):
rows.append({k: float(v) if v else None for k, v in r.items()})
return rows
def head_rpy(rows, prefix):
"""Extract (time, roll_deg, pitch_deg, yaw_deg) sequences from the
rotation submatrix of the stored 4x4 head transform.
Flat row-major indices:
[0 1 2 3 ] [R00 R01 R02 X ]
[4 5 6 7 ] [R10 R11 R12 Y ]
[8 9 10 11] [R20 R21 R22 Z ]
[12 13 14 15] [0 0 0 1 ]
Standard ZYX Euler decomposition:
roll = atan2( R21, R22 ) around X
pitch = atan2(-R20, sqrt(R21² + R22²)) around Y
yaw = atan2( R10, R00 ) around Z
"""
t = np.array([r["t_s"] for r in rows])
R00 = np.array([r[f"{prefix}h0"] for r in rows])
R10 = np.array([r[f"{prefix}h4"] for r in rows])
R20 = np.array([r[f"{prefix}h8"] for r in rows])
R21 = np.array([r[f"{prefix}h9"] for r in rows])
R22 = np.array([r[f"{prefix}h10"] for r in rows])
roll = np.degrees(np.arctan2(R21, R22))
pitch = np.degrees(np.arctan2(-R20, np.sqrt(R21 * R21 + R22 * R22)))
yaw = np.degrees(np.arctan2(R10, R00))
return t, roll, pitch, yaw
def antennas(rows, prefix):
t = np.array([r["t_s"] for r in rows])
l = np.array([r[f"{prefix}ant_l_deg"] for r in rows])
r = np.array([r[f"{prefix}ant_r_deg"] for r in rows])
return t, l, r
def plot_method_column(axes, csvs, method_label, color):
"""Plot all runs of one method into axes[0..4]. Channels:
roll, pitch, yaw, left antenna, right antenna."""
cmd_drawn = False
for path in csvs:
rows = load(path)
if not rows: continue
if not cmd_drawn:
t_c, roll_c, pitch_c, yaw_c = head_rpy(rows, "cmd_")
t_ca, l_c, r_c = antennas(rows, "cmd_")
axes[0].plot(t_c, roll_c, color="#444", linestyle="--", linewidth=1.0, label="commanded")
axes[1].plot(t_c, pitch_c, color="#444", linestyle="--", linewidth=1.0, label="commanded")
axes[2].plot(t_c, yaw_c, color="#444", linestyle="--", linewidth=1.0, label="commanded")
axes[3].plot(t_ca, l_c, color="#444", linestyle="--", linewidth=1.0, label="commanded")
axes[4].plot(t_ca, r_c, color="#444", linestyle="--", linewidth=1.0, label="commanded")
cmd_drawn = True
t_a, roll_a, pitch_a, yaw_a = head_rpy(rows, "act_")
t_aa, l_a, r_a = antennas(rows, "act_")
axes[0].plot(t_a, roll_a, color=color, alpha=0.55, linewidth=0.9)
axes[1].plot(t_a, pitch_a, color=color, alpha=0.55, linewidth=0.9)
axes[2].plot(t_a, yaw_a, color=color, alpha=0.55, linewidth=0.9)
axes[3].plot(t_aa, l_a, color=color, alpha=0.55, linewidth=0.9)
axes[4].plot(t_aa, r_a, color=color, alpha=0.55, linewidth=0.9)
def main():
python_csvs = sorted(glob.glob(str(CSV_DIR / f"{DANCE}-python_native-run*.csv")))
mariontte_csvs = sorted(glob.glob(str(CSV_DIR / f"{DANCE}-marionette_style-run*.csv")))
print(f"python_native runs: {len(python_csvs)}")
print(f"marionette_style runs: {len(mariontte_csvs)}")
fig, axes = plt.subplots(5, 2, figsize=(13, 12), sharex=True)
fig.suptitle(f"Dance playback comparison — {DANCE}\n"
f"Same motion, two streaming strategies, {len(python_csvs)} runs each",
fontsize=13, fontweight="bold")
plot_method_column(axes[:, 0], python_csvs, "python_native", "#1f77b4")
plot_method_column(axes[:, 1], mariontte_csvs, "marionette_style", "#2ca02c")
titles = ["Head roll (deg)", "Head pitch (deg)", "Head yaw (deg)", "Left antenna (deg)", "Right antenna (deg)"]
for row, t in enumerate(titles):
axes[row, 0].set_ylabel(t, fontsize=10)
for col in (0, 1):
axes[row, col].grid(True, alpha=0.3)
axes[row, col].tick_params(labelsize=8)
axes[0, 0].set_title("Python native (3 calls/tick @ 100 Hz, no lead comp)", fontsize=10)
axes[0, 1].set_title("Marionette style (combined set_target @ 50 Hz, lead 90/205 ms)", fontsize=10)
for col in (0, 1):
axes[-1, col].set_xlabel("t (s)", fontsize=10)
axes[0, col].legend(fontsize=8, loc="upper right")
# Share y-axis per channel for fair visual comparison
for row in range(5):
ymin = min(axes[row, 0].get_ylim()[0], axes[row, 1].get_ylim()[0])
ymax = max(axes[row, 0].get_ylim()[1], axes[row, 1].get_ylim()[1])
axes[row, 0].set_ylim(ymin, ymax)
axes[row, 1].set_ylim(ymin, ymax)
fig.tight_layout(rect=(0, 0, 1, 0.95))
fig.savefig(OUT_PNG, dpi=140)
print(f"Wrote {OUT_PNG}")
if __name__ == "__main__":
main()
|