Repository navigation
feat!: multi-ROI motion energy from binary masks, one NPZ per video - #46
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saved traces, keyframe mask and CSV now have length N (frame 0 = 0) instead of N-1 per-diff values, so index i lines up with video frame start + i. compute_motion_energy still returns raw per-diff arrays; the new align_to_frames helper pads at the CLI edge. Also splits cli.main() into _build_parser() and _process_video(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ROIs of any shape and number are consumed as binary masks (the ROI
annotator's *_masks.npz export). Each frame is decoded and differenced
once; the whole-frame trace and every ROI are reduced from that same diff
with one fancy-index + np.add.reduceat call (exact uint32/uint64 sums).
- rois.py: RoiSet (compact flat-index storage, validation, size guards on
total mask area), RoiSet.from_box for --roi, load_roi_masks.
- compute: compute_motion_energy(rois=...) returns MotionEnergyResult with
me (T, 1+K), column 0 = full_frame; full-frame avg map accumulated in
uint32. clean_trace handles (T, K).
- cli: --roi-masks (dir matched by camera key, or one file for one video),
validated before any processing; one {stem}_motion_energy.npz per video
(atomic write), wide CSV, masks copy + sha256 in metadata, --viz-rois.
- viz: QC trace PNG (full-frame trace + ROI x time z-score heatmap), ROI
outlines on the map and in the MP4, multi-trace scrolling plot.
BREAKING CHANGE: the four per-video .npy outputs are replaced by
{stem}_motion_energy.npz; compute_motion_energy's roi= tuple is replaced by
rois=RoiSet and it returns a MotionEnergyResult; --format npy is now npz.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ample notebook README flags/outputs/ROIs sections, design notes (mask pathway, kernel measurements, NPZ rationale and limits), optimization backlog, the example notebook's compute_motion_energy call, and the plan's as-built notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frame 0 has no preceding frame, so its motion energy is undefined, not zero. A 0.0 pad looked like a real still frame, biased means, and put an outlier at t=0 under z-scoring. Saved me/me_clean (NPZ and CSV) now start with NaN; the keyframe mask still starts with False. - align_to_frames: default fill is NaN for float arrays, False/0 otherwise (np.full(nan, dtype=bool) would silently give True). - viz: NaN-aware binned z-score for the ROI heatmap and NaN-safe y-limits. - cli: NaN-aware summary line; metadata first_frame_fill is null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…races Found by running a real session (behavior_800886, flat layout, 1000 fps, 2.6M frames/camera) with ROIs drawn in the annotator: - Mask files are matched to videos ignoring case and separators, on whole '_'-separated name parts, so the annotator's *_BottomCamera_masks.npz matches bottom_camera.mp4. Mask files matching no video are listed. - Plots drop the camera prefix shared by all ROI ids (legend, outlines, heatmap labels); saved ids are unchanged. - compute_motion_energy writes per-frame sums into a preallocated buffer (grown if the probed frame count is short) instead of a list of arrays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on list Saved traces are now frame-aligned (length N, NaN row 0), but 0.3.0 readers pad when n_me_frames == n_frames_decoded - 1. Renaming the key makes that code fail with a KeyError instead of silently padding twice. docs/plans/downstream-migration.md lists the changes needed in kinematics_analysis, the ME capsule, the batch launcher and the ROI annotator before 0.4.0 outputs are used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…frame_index array Row i is video frame first_frame + i. The per-row int64 array cost 21 MB per camera on a 2.6M-frame session and only repeated the row number in whole-video runs. The CSV keeps its frame_index column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NPZ is always written, so --format only ever switched the CSV on, and 'csv' and 'both' did the same thing. --csv says what it does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A negative start, or an end not after the start, used to give silently empty outputs. check_frame_range raises in compute_motion_energy and the CLI reports it before any video is processed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gned traces Removes ~250 lines of library code without losing ROI features: - ROIs are a plain dict of name -> (H, W) bool mask. The RoiSet class (compact index storage, bounding boxes, mask reconstruction) is gone; load_roi_masks returns the dict, box_roi builds --roi's one-entry dict, check_rois validates. The summing setup is a few lines in compute. - Size guards (area warn/refuse, ROI-count warn) and the uint32/uint64 switch are removed; frames over 16.8 megapixels are refused instead. - compute_motion_energy returns frame-aligned traces (row 0 NaN) plus first_frame, so Python results match the saved files. align_to_frames, n_diffs, n_output_frames and first_frame_fill are gone; n_me_frames stays removed so old padding code still fails loudly. - Outlines are contoured from the full-size masks (no crop/pad math). - roi_bboxes and the masks SHA-256 are dropped; the copied masks file is the provenance. Re-checked on behavior_800886_2025-09-11_13-52-51 (both cameras, 10 ROIs): traces match a brute-force reference (max error 1.1e-7), summary numbers match the previous run, outlines land on the drawn anatomy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Motion energy for any number of ROIs of any shape, computed in one decode pass per video.
*_masks.npzexport (masks (H, W, K),roi_ids) is the input. Boxes, polygons and compound shapes all arrive as masks.--roi-masks DIRmatches one file per camera by name, ignoring case and separators, so the annotator's…_BottomCamera_masks.npzmatchesbottom_camera.mp4.--roi X Y W Hstill works as a single rectangular mask. In Python, ROIs are a plaindictof name → boolean mask..npyfiles. It holdsmeandme_clean(frames × (1+K), column 0 =full_frame),keyframe_mask,first_frame,roi_ids,pixel_countsandavg_map. It is written atomically, and the input masks are copied next to it for provenance.compute_motion_energyreturns, have one value per frame (row i = framefirst_frame + i), with NaN at row 0, which has no previous frame.--viz-roischooses which traces scroll in the MP4.--csvreplaces--format. An invalid--start-frame/--end-framerange is refused up front, as are masks that are empty or don't match the video's size.Breaking changes
.npyoutputs are replaced by{stem}_motion_energy.npz. The oldmotion_energy_clean.npyis nownpz["me_clean"][:, 0].n_me_framesis removed, so existing padding logic fails loudly instead of shifting traces by one frame.compute_motion_energy(rois={name: mask})returns a frame-alignedMotionEnergyResult(it wasroi=returning a 4-tuple of N−1 arrays).--format {npy,csv,both}is replaced by--csv.Downstream updates (kinematics_analysis, the ME capsule, the batch launcher, the ROI annotator) are listed in
docs/plans/downstream-migration.md. They are not part of this PR. The version bump to 0.4.0 will follow as a separatechore:PR.Testing
behavior_800886_2025-09-11_13-52-51(flat layout, 1000 fps, 2.6M frames per camera), with 10 ROIs drawn in the annotator. Every ROI trace matches a brute-force per-mask reference (max error 1e-7), and the outlines sit on the intended anatomy. 6 ROIs cost about 8% throughput (1040 → 955 fps).docs/design-notes.md.A simplification pass before merge removed ~250 lines: the
RoiSetclass, bounding boxes, size guards and the separate frame-alignment step. The real-session check was re-run afterwards with identical results.Plan and as-built notes:
docs/plans/multi-roi-motion-energy.md.🤖 Generated with Claude Code