EgoDex: temporal action rates as Daft window expressions#47
Conversation
The feature table becomes one row per frame. frame_features() explodes an episode-level spatial UDF into per-frame rows; temporal_features() computes every action rate (grasping, lifting, reaching, stillness, articulation, twisting) as window expressions over partition_by(task, episode_id) .order_by(frame_index) — lead(1) diffs, euclidean_distance speeds, and a centered rows_between(-2, 2) smoothing mean, with forearm_roll as the one custom UDF fed by the window. calculate_features() composes both stages. Query scenarios are unchanged: _match_episodes regroups frames into per-episode tracks via groupby.agg(list_agg), re-sorted by frame_index inside the match UDF. calibrate() becomes Daft percentile aggregations over the pooled per-frame rows. TemporalFeatureComputer is deleted; a new test pins the window rates to a NumPy reference at 1e-9 on scrambled row order, and the existing query-behavior tests pass unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2a4ddb57cf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # list_agg does not guarantee frame order within an episode; re-sort every | ||
| # track by frame_index so mask position i is frame i. | ||
| order = np.argsort(np.asarray(frame_index)) |
There was a problem hiding this comment.
Preserve actual frame indexes when building segments
When the per-frame feature table has gaps or non-zero-based frame_index values (for example after filtering rows, downsampling, or using an external per-frame parquet), sorting by frame_index does not make mask position i equal frame i. The subsequent np.flatnonzero(mask)/segment stitching returns positional offsets instead of the original frame indexes, so reported segments point at the wrong video frames and combined text+pose queries can miss sampled frames whose index is beyond len(mask). Map matching positions back through the sorted frame_index values or enforce a dense frame index contract before querying.
Useful? React with 👍 / 👎.
What
Follow-up to #46. The pose-feature stage moves to one row per frame, and every temporal action rate moves out of NumPy-in-a-UDF and into the query plan as Daft window expressions.
EgoDexPipeline.frame_features()— an episode-level UDF computes instantaneous hand geometry and explodes it into per-frame rows keyed by(task, episode_id, frame_index).EgoDexPipeline.temporal_features()/ newtemporal.py— every rate is an expression overWindow().partition_by("task", "episode_id").order_by("frame_index"):lead(1)diffs →curl_rate(grasping),wrist_vert_vel(lifting),arm_ext_rate(reaching)euclidean_distancewithlead(1)→wrist_speed(stillness),articulation(in-hand)forearm_roll— the one custom UDF, fed by the window (this frame's rotation + next frame's vialead(1)), smoothed with a centeredrows_between(-2, 2)mean (twisting)calculate_features()composes both stages;TemporalFeatureComputeris deleted.calibrate()becomes Daftpercentileaggregations over the pooled per-frame rows.query.pyscenarios are unchanged —_match_episodesregroups frames into per-episode tracks viagroupby.agg(list_agg), re-sorted byframe_indexinside the match UDF (list_agg does not guarantee order).Why
temporal.pynow.Semantics
Window semantics are pinned to the old NumPy behavior:
fill_null(0.0)reproduces the 0-at-last-frame convention, androws_between(-2, 2)shrinks at episode edges exactly like the old centered mean.Tests
test_temporal_window_rates_match_numpy_reference: all six rates vs an independent NumPy reference at 1e-9 tolerance, on scrambled row order across two episodes (provesorder_byrecovers frame order).make lint+ruff format --checkclean; 5/5 intests/test_egodex.py.Breaking
The features table (and
features_dirparquet fromrun_all) is now per-frame instead of one-row-per-episode;POSE_FEATURES_DTYPEandnum_framesare gone.calibrate/query/pose_searchsignatures are unchanged.🤖 Generated with Claude Code