Download src/features.py from duj626/tartanimu-e003: direct link, hf CLI and curl.
- Browser
- Download file 8.5 kB
-
https://huggingface.co/duj626/tartanimu-e003/resolve/main/src/features.py
- Command line
-
hf download hf://duj626/tartanimu-e003/src/features.py
-
curl -L -o features.py https://huggingface.co/duj626/tartanimu-e003/resolve/main/src/features.py
8.5 kB
| #!/usr/bin/env python3 | |
| """Deterministic, leak-free IMU feature extraction for the TartanIMU challenge.""" | |
| from __future__ import annotations | |
| import argparse | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| WINDOW = 200 | |
| def _add(parts: list[np.ndarray], names: list[str], prefix: str, values: np.ndarray) -> None: | |
| values = np.asarray(values, dtype=np.float32) | |
| if values.ndim == 1: | |
| values = values[:, None] | |
| parts.append(values) | |
| names.extend(f"{prefix}_{i}" for i in range(values.shape[1])) | |
| def window_features(windows: np.ndarray) -> tuple[np.ndarray, list[str]]: | |
| """Extract per-window statistics from (windows, 200, 6) raw IMU.""" | |
| w = np.asarray(windows, dtype=np.float32) | |
| parts: list[np.ndarray] = [] | |
| names: list[str] = [] | |
| mean = w.mean(axis=1) | |
| std = w.std(axis=1) | |
| centered = w - mean[:, None, :] | |
| rms = np.sqrt(np.mean(w * w, axis=1)) | |
| quant = np.quantile(w, [0.10, 0.25, 0.50, 0.75, 0.90], axis=1) | |
| slope_t = np.arange(WINDOW, dtype=np.float32) - (WINDOW - 1) / 2 | |
| slope = np.einsum("t,ntc->nc", slope_t, centered) / np.sum(slope_t * slope_t) | |
| diff = np.diff(w, axis=1) | |
| for prefix, values in ( | |
| ("mean", mean), | |
| ("std", std), | |
| ("q10", quant[0]), | |
| ("q25", quant[1]), | |
| ("median", quant[2]), | |
| ("q75", quant[3]), | |
| ("q90", quant[4]), | |
| ("min", w.min(axis=1)), | |
| ("max", w.max(axis=1)), | |
| ("rms", rms), | |
| ("absmean", np.abs(w).mean(axis=1)), | |
| ("first", w[:, 0]), | |
| ("last", w[:, -1]), | |
| ("delta", w[:, -1] - w[:, 0]), | |
| ("slope", slope), | |
| ("diff_absmean", np.abs(diff).mean(axis=1)), | |
| ("diff_std", diff.std(axis=1)), | |
| ("diff_rms", np.sqrt(np.mean(diff * diff, axis=1))), | |
| ): | |
| _add(parts, names, prefix, values) | |
| norms = np.stack( | |
| [np.linalg.norm(w[:, :, :3], axis=2), np.linalg.norm(w[:, :, 3:], axis=2)], | |
| axis=2, | |
| ) | |
| nq = np.quantile(norms, [0.10, 0.50, 0.90], axis=1) | |
| for prefix, values in ( | |
| ("norm_mean", norms.mean(axis=1)), | |
| ("norm_std", norms.std(axis=1)), | |
| ("norm_q10", nq[0]), | |
| ("norm_median", nq[1]), | |
| ("norm_q90", nq[2]), | |
| ("norm_min", norms.min(axis=1)), | |
| ("norm_max", norms.max(axis=1)), | |
| ("norm_rms", np.sqrt(np.mean(norms * norms, axis=1))), | |
| ): | |
| _add(parts, names, prefix, values) | |
| chunk_mean = w.reshape(len(w), 10, 20, 6).mean(axis=2).reshape(len(w), -1) | |
| chunk_std = w.reshape(len(w), 4, 50, 6).std(axis=2).reshape(len(w), -1) | |
| _add(parts, names, "chunk20_mean", chunk_mean) | |
| _add(parts, names, "chunk50_std", chunk_std) | |
| spectrum = np.abs(np.fft.rfft(centered, axis=1)).astype(np.float32) / WINDOW | |
| bands = ((1, 3), (3, 6), (6, 11), (11, 21), (21, 41), (41, 61), (61, 81), (81, 101)) | |
| band_values = [] | |
| for lo, hi in bands: | |
| band_values.append(np.sqrt(np.mean(spectrum[:, lo:hi] ** 2, axis=1))) | |
| _add(parts, names, "fft_band_rms", np.concatenate(band_values, axis=1)) | |
| denom = np.maximum(std[:, :, None] * std[:, None, :], 1e-8) | |
| corr = np.einsum("ntc,ntd->ncd", centered, centered) / WINDOW / denom | |
| pairs = [(i, j) for i in range(6) for j in range(i + 1, 6)] | |
| corr_values = np.stack([corr[:, i, j] for i, j in pairs], axis=1) | |
| _add(parts, names, "corr", corr_values) | |
| x = np.concatenate(parts, axis=1).astype(np.float32) | |
| if not np.isfinite(x).all(): | |
| raise ValueError("non-finite feature generated") | |
| return x, names | |
| def add_trajectory_context(x: np.ndarray, names: list[str]) -> tuple[np.ndarray, list[str]]: | |
| """Add same-trajectory context without platform labels or hidden information.""" | |
| core_idx = [i for i, n in enumerate(names) if n.startswith(("mean_", "std_", "rms_", "norm_mean_", "norm_std_"))] | |
| core = x[:, core_idx] | |
| context = [x] | |
| context_names = list(names) | |
| for shift in (-5, -2, -1, 1, 2, 5): | |
| idx = np.clip(np.arange(len(x)) + shift, 0, len(x) - 1) | |
| context.append(core[idx]) | |
| context_names.extend(f"ctx_s{shift:+d}_{names[i]}" for i in core_idx) | |
| for radius in (2, 5): | |
| padded = np.pad(core, ((radius, radius), (0, 0)), mode="edge") | |
| csum = np.vstack([np.zeros((1, core.shape[1]), dtype=np.float64), np.cumsum(padded, axis=0)]) | |
| rolled = (csum[2 * radius + 1:] - csum[: -(2 * radius + 1)]) / (2 * radius + 1) | |
| context.append(rolled.astype(np.float32)) | |
| context_names.extend(f"ctx_r{radius}_{names[i]}" for i in core_idx) | |
| global_stats = np.concatenate( | |
| [np.median(core, axis=0), np.std(core, axis=0)], axis=0 | |
| ).astype(np.float32) | |
| context.append(np.repeat(global_stats[None, :], len(x), axis=0)) | |
| context_names.extend([f"traj_median_{names[i]}" for i in core_idx]) | |
| context_names.extend([f"traj_std_{names[i]}" for i in core_idx]) | |
| pos = np.linspace(0.0, 1.0, len(x), dtype=np.float32) | |
| context.append(np.stack([pos, 1.0 - pos], axis=1)) | |
| context_names.extend(["trajectory_fraction", "trajectory_fraction_reverse"]) | |
| out = np.concatenate(context, axis=1).astype(np.float32) | |
| return out, context_names | |
| def _trajectory_path(root: Path, split: str, platform: str | None, traj_id: str) -> Path: | |
| if split == "test": | |
| return root / split / f"{traj_id}.npz" | |
| assert platform is not None | |
| return root / split / platform / f"{traj_id}.npz" | |
| def extract_split(root: Path, split: str, out: Path) -> None: | |
| index = pd.read_csv(root / "index" / f"{split}_windows.csv") | |
| target = None | |
| if split != "test": | |
| target = pd.read_csv(root / "index" / f"{split}_targets.csv").set_index("window_id") | |
| x_parts: list[np.ndarray] = [] | |
| ids: list[np.ndarray] = [] | |
| trajs: list[np.ndarray] = [] | |
| win_idxs: list[np.ndarray] = [] | |
| platforms: list[np.ndarray] = [] | |
| y_parts: list[np.ndarray] = [] | |
| feature_names: list[str] | None = None | |
| grouped = list(index.groupby("traj_id", sort=False)) | |
| for number, (traj_id, rows) in enumerate(grouped, start=1): | |
| rows = rows.sort_values("win_idx") | |
| platform = None if split == "test" else str(rows["platform"].iloc[0]) | |
| path = _trajectory_path(root, split, platform, str(traj_id)) | |
| if not path.exists(): | |
| raise FileNotFoundError(path) | |
| with np.load(path) as loaded: | |
| imu = np.asarray(loaded["imu"], dtype=np.float32) | |
| wi = rows["win_idx"].to_numpy(np.int64) | |
| if wi.min() < 0 or (wi.max() + 1) * WINDOW > len(imu): | |
| raise ValueError(f"window index out of bounds for {path}") | |
| windows = np.stack([imu[k * WINDOW : (k + 1) * WINDOW] for k in wi]) | |
| raw_x, raw_names = window_features(windows) | |
| traj_x, traj_names = add_trajectory_context(raw_x, raw_names) | |
| if feature_names is None: | |
| feature_names = traj_names | |
| elif feature_names != traj_names: | |
| raise ValueError("feature name/order mismatch") | |
| x_parts.append(traj_x) | |
| ids.append(rows["window_id"].to_numpy(np.int64)) | |
| trajs.append(np.repeat(str(traj_id), len(rows))) | |
| win_idxs.append(wi) | |
| platforms.append(np.repeat(platform or "unknown", len(rows))) | |
| if target is not None: | |
| y_parts.append(target.loc[rows["window_id"], ["vx", "vy", "vz"]].to_numpy(np.float32)) | |
| if number % 25 == 0 or number == len(grouped): | |
| print(f"{split}: {number}/{len(grouped)} trajectories") | |
| payload: dict[str, np.ndarray] = { | |
| "X": np.concatenate(x_parts), | |
| "window_id": np.concatenate(ids), | |
| "traj_id": np.concatenate(trajs), | |
| "win_idx": np.concatenate(win_idxs), | |
| "platform": np.concatenate(platforms), | |
| "feature_names": np.asarray(feature_names), | |
| } | |
| if y_parts: | |
| payload["y"] = np.concatenate(y_parts) | |
| out.parent.mkdir(parents=True, exist_ok=True) | |
| np.savez_compressed(out, **payload) | |
| print(f"wrote {out}: X={payload['X'].shape}") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--root", type=Path, default=Path("tartan_imu/data/extracted")) | |
| parser.add_argument("--out-dir", type=Path, default=Path("tartan_imu/artifacts/features_v1")) | |
| parser.add_argument("--splits", nargs="+", choices=("train", "val", "test"), default=("train", "val", "test")) | |
| args = parser.parse_args() | |
| for split in args.splits: | |
| extract_split(args.root, split, args.out_dir / f"{split}.npz") | |
| if __name__ == "__main__": | |
| main() | |