Files
icassp2027-experiment/prepare_egospeak_floor_eval.py
T

141 lines
5.0 KiB
Python

"""Convert saved EgoSpeak MiniROADA forecasts for floor-event evaluation.
EgoSpeak MiniROADA saves one array per session as
``[source_time, future_horizon, class]``. The repository floor evaluator uses
the LSTR-compatible target-aligned layout ``[target_time, class, horizon]``
plus an explicit causal-valid mask. This script performs only that alignment;
it does not change scores, targets, or class order.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
NUM_CLASSES = 3
NUM_HORIZONS = 10
def normalize_session_name(value: str | int) -> str:
text = str(value)
suffix = text.removeprefix("Session_")
if not suffix.isdigit():
raise ValueError(f"Invalid EasyCom session: {value}")
return f"Session_{int(suffix)}"
def align_miniroad_prediction(
raw_prediction: np.ndarray,
target_steps: int,
) -> tuple[np.ndarray, np.ndarray]:
"""Align MiniROADA source-indexed forecasts without padding them as valid."""
raw_prediction = np.asarray(raw_prediction)
expected = (target_steps - NUM_HORIZONS, NUM_HORIZONS, NUM_CLASSES)
if raw_prediction.shape != expected:
raise ValueError(
"Expected MiniROADA prediction "
f"{expected} for target length {target_steps}, got "
f"{raw_prediction.shape}"
)
if not np.isfinite(raw_prediction).all():
raise ValueError("MiniROADA prediction contains NaN/Inf")
if (raw_prediction < -1e-6).any() or (raw_prediction > 1.0 + 1e-6).any():
raise ValueError("MiniROADA prediction is outside [0,1]")
raw_prediction = np.asarray(raw_prediction, dtype=np.float32)
aligned = np.zeros(
(target_steps, NUM_CLASSES, NUM_HORIZONS), dtype=np.float32
)
valid = np.zeros((target_steps, NUM_HORIZONS), dtype=bool)
source_rows = np.arange(len(raw_prediction), dtype=np.int64)
for horizon_index in range(NUM_HORIZONS):
target_rows = source_rows + horizon_index + 1
aligned[target_rows, :, horizon_index] = raw_prediction[
:, horizon_index, :
]
valid[target_rows, horizon_index] = True
return aligned, valid
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--prediction-root",
type=Path,
required=True,
help="Directory containing raw MiniROADA Session_X.npy predictions.",
)
parser.add_argument("--target-root", type=Path, required=True)
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument("--sessions", nargs="+", required=True)
return parser.parse_args()
def main() -> None:
args = parse_args()
sessions = [normalize_session_name(value) for value in args.sessions]
if len(sessions) != len(set(sessions)):
raise ValueError("Duplicate sessions")
converted: dict[str, tuple[np.ndarray, np.ndarray]] = {}
sources: dict[str, str] = {}
for session in sessions:
target_path = args.target_root / f"{session}.npy"
prediction_path = args.prediction_root / f"{session}.npy"
if not target_path.is_file():
raise FileNotFoundError(target_path)
if not prediction_path.is_file():
raise FileNotFoundError(prediction_path)
target = np.load(target_path, allow_pickle=False)
if target.ndim != 2 or target.shape[1] != NUM_CLASSES:
raise ValueError(
f"Expected target [T,{NUM_CLASSES}], got {target.shape}: "
f"{target_path}"
)
raw_prediction = np.load(prediction_path, allow_pickle=False)
converted[session] = align_miniroad_prediction(
raw_prediction, len(target)
)
sources[session] = str(prediction_path.expanduser().resolve())
prediction_dir = args.output_dir / "pred"
valid_dir = args.output_dir / "valid"
destinations = [
directory / f"{session}.npy"
for directory in (prediction_dir, valid_dir)
for session in sessions
]
destinations.append(args.output_dir / "conversion.json")
existing = [path for path in destinations if path.exists()]
if existing:
raise FileExistsError(
"Refusing to overwrite converted output: "
+ ", ".join(str(path) for path in existing)
)
prediction_dir.mkdir(parents=True, exist_ok=True)
valid_dir.mkdir(parents=True, exist_ok=True)
for session, (prediction, valid) in converted.items():
np.save(prediction_dir / f"{session}.npy", prediction)
np.save(valid_dir / f"{session}.npy", valid)
metadata = {
"protocol": "egospeak-miniroada-source-to-target-alignment-v1",
"source_layout": "[source_time, future_horizon, class]",
"output_layout": "[target_time, class, future_horizon]",
"sessions": sessions,
"sources": sources,
}
(args.output_dir / "conversion.json").write_text(
json.dumps(metadata, indent=2) + "\n"
)
print(f"Converted {len(sessions)} session(s): {args.output_dir}")
if __name__ == "__main__":
main()