File size: 6,303 Bytes
b4b2877 | 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 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """Visualize mocap skeleton frames, IMU waveforms, EMG waveforms."""
import os, numpy as np, pandas as pd, matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # noqa
REC = "${PULSE_ROOT}/dataset/v1/s1"
OUT = "${PULSE_ROOT}/paper/figures"
os.makedirs(OUT, exist_ok=True)
# ---- Skeleton bone definition (marker pairs) ----
BONES = [
# torso
("HeadTop","HeadFront"),("HeadL","HeadR"),("HeadFront","SpineTop"),
("SpineTop","Chest"),("Chest","WaistLFront"),("Chest","WaistRFront"),
("WaistLFront","WaistLBack"),("WaistRFront","WaistRBack"),
("WaistLBack","BackL"),("WaistRBack","BackR"),("BackL","BackR"),
("SpineTop","LShoulderTop"),("SpineTop","RShoulderTop"),
("LShoulderTop","LShoulderBack"),("RShoulderTop","RShoulderBack"),
# left arm
("LShoulderTop","LArm"),("LArm","LElbowOut"),("LElbowOut","LElbowBack"),
("LElbowOut","LForearmRoll"),("LForearmRoll","LWristOut"),
("LWristOut","LWristIn"),("LWristOut","LHandOut"),("LWristIn","LHandIn"),
("LHandOut","LIndex2"),("LIndex2","LIndexTip"),
("LHandOut","LMiddle2"),("LMiddle2","LMiddleTip"),
("LHandIn","LRing2"),("LRing2","LRingTip"),
("LHandIn","LPinky2"),("LPinky2","LPinkyTip"),
("LWristIn","LThumb1"),("LThumb1","LThumbTip"),
# right arm
("RShoulderTop","RArm"),("RArm","RElbowOut"),("RElbowOut","RElbowBack"),
("RElbowOut","RForearmRoll"),("RForearmRoll","RWristOut"),
("RWristOut","RWristIn"),("RWristOut","RHandOut"),("RWristIn","RHandIn"),
("RHandOut","RIndex2"),("RIndex2","RIndexTip"),
("RHandOut","RMiddle2"),("RMiddle2","RMiddleTip"),
("RHandIn","RRing2"),("RRing2","RRingTip"),
("RHandIn","RPinky2"),("RPinky2","RPinkyTip"),
("RWristIn","RThumb1"),("RThumb1","RThumbTip"),
]
def load_mocap(path):
df = pd.read_csv(path)
# Extract x,y,z for each marker ignoring Type cols
markers = {}
for col in df.columns:
if col.startswith("Q_") and col.endswith(" X"):
name = col[2:-2]
xs = df[f"Q_{name} X"].to_numpy()
ys = df[f"Q_{name} Y"].to_numpy()
zs = df[f"Q_{name} Z"].to_numpy()
markers[name] = np.stack([xs, ys, zs], axis=-1)
return df["Time"].to_numpy(), markers
def plot_skeletons():
t, mk = load_mocap(os.path.join(REC, "aligned_mocap_100hz.csv"))
N = len(t)
# pick 4 time frames well spread through the recording with valid data
candidate = np.linspace(int(0.1*N), int(0.9*N), 4).astype(int)
fig = plt.figure(figsize=(12, 3.2))
for i, fr in enumerate(candidate):
ax = fig.add_subplot(1, 4, i+1, projection='3d')
# gather all points at this frame
pts = np.array([mk[n][fr] for n in mk])
pts = pts[~np.isnan(pts).any(axis=1)]
if len(pts) == 0:
continue
# draw bones
for a, b in BONES:
if a in mk and b in mk:
pa, pb = mk[a][fr], mk[b][fr]
if np.isnan(pa).any() or np.isnan(pb).any():
continue
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], [pa[2], pb[2]],
color='#2266aa', lw=1.2)
ax.scatter(pts[:, 0], pts[:, 1], pts[:, 2], s=4, c='#cc3333', alpha=0.8)
# equal aspect
c = pts.mean(0)
r = np.ptp(pts, axis=0).max() / 2
ax.set_xlim(c[0]-r, c[0]+r); ax.set_ylim(c[1]-r, c[1]+r); ax.set_zlim(c[2]-r, c[2]+r)
ax.set_xticks([]); ax.set_yticks([]); ax.set_zticks([])
ax.set_title(f"t={t[fr]:.1f}s", fontsize=9)
ax.view_init(elev=12, azim=-75)
fig.suptitle("MoCap skeleton frames (56-marker Qualisys, v1/s1)", fontsize=11)
fig.tight_layout()
out = os.path.join(OUT, "mocap_skeleton.pdf")
fig.savefig(out, bbox_inches='tight'); fig.savefig(out.replace('.pdf', '.png'), dpi=150, bbox_inches='tight')
plt.close(fig)
print("Saved", out)
def plot_imu():
df = pd.read_csv(os.path.join(REC, "aligned_imu_100hz.csv"))
t = df["time"].to_numpy(); t = t - t[0]
# pick 5 body locations (WT0..WT9 order roughly: wrists, forearms, upper arms, shins, thighs, torso)
sites = [("WT0", "Wrist R"), ("WT2", "Forearm R"),
("WT4", "Upper arm R"), ("WT6", "Shin R"), ("WT9", "Torso")]
fig, axes = plt.subplots(len(sites), 1, figsize=(9, 6), sharex=True)
# crop to 20s window mid-recording
mid = len(t)//2
sl = slice(max(0, mid-1000), min(len(t), mid+1000))
for ax, (sid, lbl) in zip(axes, sites):
for comp, col in zip(["x", "y", "z"], ["#d62728", "#2ca02c", "#1f77b4"]):
ax.plot(t[sl], df[f"{sid}_acc_{comp}"].to_numpy()[sl], color=col, lw=0.8, label=f"acc_{comp}")
ax.set_ylabel(lbl, fontsize=9)
ax.grid(alpha=0.3)
axes[0].legend(loc="upper right", ncol=3, fontsize=8)
axes[-1].set_xlabel("Time (s)")
fig.suptitle("IMU 3-axis acceleration across 5 body sites (v1/s1, 20s window)", fontsize=11)
fig.tight_layout()
out = os.path.join(OUT, "imu_waveforms.pdf")
fig.savefig(out, bbox_inches='tight'); fig.savefig(out.replace('.pdf', '.png'), dpi=150, bbox_inches='tight')
plt.close(fig)
print("Saved", out)
def plot_emg():
df = pd.read_csv(os.path.join(REC, "aligned_emg_100hz.csv"))
t = df["time"].to_numpy(); t = t - t[0]
ch = [f"emg_{i}" for i in range(1, 9)]
# 20s window mid-recording
mid = len(t)//2
sl = slice(max(0, mid-1000), min(len(t), mid+1000))
fig, axes = plt.subplots(8, 1, figsize=(9, 7), sharex=True)
for ax, c in zip(axes, ch):
sig = df[c].to_numpy()[sl]
ax.plot(t[sl], sig, color="#555", lw=0.5)
# envelope overlay
env = pd.Series(np.abs(sig)).rolling(20, min_periods=1).mean().to_numpy()
ax.plot(t[sl], env, color="#d62728", lw=0.9)
ax.set_ylabel(c, fontsize=8)
ax.grid(alpha=0.3)
axes[-1].set_xlabel("Time (s)")
fig.suptitle("Surface EMG 8-channel raw (grey) with rectified envelope (red), v1/s1, 20s window",
fontsize=11)
fig.tight_layout()
out = os.path.join(OUT, "emg_waveforms.pdf")
fig.savefig(out, bbox_inches='tight'); fig.savefig(out.replace('.pdf', '.png'), dpi=150, bbox_inches='tight')
plt.close(fig)
print("Saved", out)
if __name__ == "__main__":
plot_skeletons()
plot_imu()
plot_emg()
|