Download unified/gravity.py from LexHo/tartanimu-a3v21: direct link, hf CLI and curl.
- Browser
- Download file 4.78 kB
-
https://huggingface.co/LexHo/tartanimu-a3v21/resolve/main/unified/gravity.py
- Command line
-
hf download hf://LexHo/tartanimu-a3v21/unified/gravity.py
-
curl -L -o gravity.py https://huggingface.co/LexHo/tartanimu-a3v21/resolve/main/unified/gravity.py
4.78 kB
| #!/usr/bin/env python3 | |
| """Body-frame gravity direction as an auxiliary training target. | |
| The accelerometer channel is dominated by gravity: the mean specific force over | |
| a window has magnitude ~9.8, while the motion component of a 1 s window is | |
| usually an order of magnitude smaller. Any velocity head therefore has to | |
| learn, implicitly, where "down" is before the residual carries information -- | |
| and it has to learn that from the same scalar loss that grades velocity. The | |
| ground-truth attitude in the train/val .npz files makes that sub-problem | |
| supervisable directly: a second head predicting the body-frame gravity | |
| direction gets a dense, well-scaled signal at every window, and forces the | |
| trunk to keep an attitude estimate in its features. | |
| This is a training-time-only signal. The test .npz files carry no quat, so the | |
| gravity head exists purely to shape the trunk and is dropped (or simply | |
| ignored) at inference. | |
| Conventions match unified/segments.py. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import pandas as pd | |
| from unified.segments import quat_to_R | |
| WINDOW_SIZE = 200 | |
| def gravity_dir_body(quat: np.ndarray, g_world=(0.0, 0.0, -1.0)) -> np.ndarray: | |
| """(M,4) quaternion (x, y, z, w) -> (M,3) unit gravity direction in the body frame. | |
| quat_to_R returns the body->world matrix, so world->body is its transpose: | |
| g_body = R.T @ g_world. The returned vector points the way gravity *pulls* | |
| (down), not the way the accelerometer reads. A resting IMU measures | |
| specific force, i.e. the reaction to gravity, so its mean acceleration | |
| vector aligns with MINUS this vector -- verified on | |
| train/human/human_train_0000.npz over its 200 slowest windows (speed | |
| <= 0.03 m/s): median angle to -gravity_dir_body 0.39 deg, median |mean acc| | |
| 9.8055. (Against +gravity_dir_body the same windows give 179.61 deg, which | |
| is the expected | |
| reading of this sign convention, not an inversion.) The default | |
| g_world=(0,0,-1) is the world frame's down axis; the data confirms world +z | |
| is up. | |
| """ | |
| R = quat_to_R(np.asarray(quat, np.float64)) | |
| g = np.asarray(g_world, np.float64) | |
| g = g / np.linalg.norm(g) | |
| # einsum over the first matrix axis = multiplying by R.T | |
| v = np.einsum("mji,j->mi", R, g) | |
| v /= np.linalg.norm(v, axis=1, keepdims=True) | |
| return v.astype(np.float32) | |
| def window_gravity_target(frame: pd.DataFrame, n_per_window: int = 1) -> np.ndarray: | |
| """(len(frame), n_per_window, 3) float32 body-frame gravity directions. | |
| Samples n_per_window frames evenly across each window; n_per_window=1 lands | |
| on the window's middle frame, matching attach_pose's quaternion choice. | |
| """ | |
| n = len(frame) | |
| # sub-block centres: [0.5, 1.5, ...] * WINDOW_SIZE / n, so n=1 -> 100 | |
| offsets = ((np.arange(n_per_window) + 0.5) * (WINDOW_SIZE / n_per_window)).astype(np.int64) | |
| out = np.empty((n, n_per_window, 3), np.float32) | |
| for file_path, rows in frame.groupby("file_path", sort=False): | |
| with np.load(file_path) as d: | |
| quat = d["quat"] | |
| starts = rows["win_idx"].to_numpy() * WINDOW_SIZE | |
| take = np.minimum(starts[:, None] + offsets[None, :], len(quat) - 1) | |
| g = gravity_dir_body(quat[take.ravel()]) | |
| out[rows.index.to_numpy()] = g.reshape(len(starts), n_per_window, 3) | |
| return out | |
| def cosine_loss(pred, target): | |
| """1 - cosine_similarity, averaged. Shapes (..., 3); pred need not be unit.""" | |
| import torch.nn.functional as F | |
| return (1.0 - F.cosine_similarity(pred, target, dim=-1, eps=1e-8)).mean() | |
| if __name__ == "__main__": | |
| # Self-check: on low-dynamics windows the mean measured acceleration must | |
| # sit opposite the predicted gravity direction. | |
| from unified.data import load_index | |
| train = load_index()["train"] | |
| # reset_index because window_gravity_target, like attach_pose, writes rows | |
| # by frame index | |
| rows = train[train["traj_id"] == "human_train_0000"].reset_index(drop=True) | |
| with np.load(rows["file_path"].iloc[0]) as d: | |
| imu = d["imu"] | |
| starts = rows["win_idx"].to_numpy() * WINDOW_SIZE | |
| acc = np.stack([imu[s:s + WINDOW_SIZE, :3].mean(0) for s in starts]) | |
| mag = np.linalg.norm(acc, axis=1) | |
| g = window_gravity_target(rows)[:, 0, :] | |
| speed = np.linalg.norm(rows[["vx", "vy", "vz"]].to_numpy(), axis=1) | |
| k = np.argsort(speed)[:200] | |
| cos = ((acc / mag[:, None]) * -g).sum(1) | |
| ang = np.degrees(np.arccos(np.clip(cos, -1.0, 1.0))) | |
| print(f"windows checked {len(k)} of {len(rows)}") | |
| print(f"speed of those <= {speed[k].max():.4f} m/s") | |
| print(f"median angle to -g {np.median(ang[k]):.3f} deg") | |
| print(f"median angle to +g {180 - np.median(ang[k]):.3f} deg") | |
| print(f"median |mean acc| {np.median(mag[k]):.4f}") | |