Skip to content

Add ndr.reader.video: native video reader (port of NDR-matlab #140) - #33

Merged
stevevanhooser merged 3 commits into
mainfrom
claude/clever-bardeen-hf5jgx
Oct 21, 2026
Merged

stevevanhooser merged 3 commits into
mainfrom
claude/clever-bardeen-hf5jgx

Conversation

@stevevanhooser

@stevevanhooser stevevanhooser commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Ports +ndr/+reader/video.m from VH-Lab/NDR-matlab#140, synced through VH-Lab/NDR-matlab#143 (d2ebe98). Adds cross-language symmetry tests for it, paired with VH-Lab/NDR-matlab#144 (same branch name, claude/clever-bardeen-hf5jgx, so test-symmetry.yml checks it out).

The reader

  • src/ndr/reader/video.py: ndr_reader_video mirrors every method of video.m under the same names, frame API only. Where MATLAB uses VideoReader, Python uses PyAV (av, the FFmpeg binding). It is an optional dependency in [formats] and [dev] and is only imported when a video is read. PyAV wheels bundle FFmpeg.
  • Behaviour matches MATLAB:
    • An epoch is exactly one video file. Companion files are ignored and directories are expanded.
    • Frames come back in YXCZT order.
    • A frame's time is its stored presentation time, measured per decoded frame, so variable frame rate is handled correctly.
    • Those times are cached per file, keyed by path, size and mtime.
    • t0_t1 seeks near the end instead of decoding the whole file.
    • Grey pixel formats and grey-palette pal8 (MATLAB's 'Grayscale AVI') give C=1. Video above 8 bits gives uint16.
  • Registry: adds the aliases video, mp4, avi and mov. The MATLAB-specific videoreader alias was dropped upstream in #143, so it is dropped here too.
  • Bridge YAML: a video entry (regular_port, d2ebe98). Its decision_log covers where Python differs: a PyAV container stands in for VideoReader; readframes seeks by pts read from the demuxed packets (Python-only _framepts); and MATLAB error IDs map to ValueError, IndexError or FileNotFoundError.
  • Tests: tests/test_video_reader.py (32 tests) ports TestVideo.m, plus cases for variable frame rate, a container with no frame count in its header, 16-bit grey, and the checked-in example files.

Symmetry

  • New fixtures: tools/make_example_data.py now also writes example_video.avi (6-frame 12×16 uncompressed 24-bit RGB, 10 fps) and example_video_grey.avi (8-bit, grey palette).
    • The AVI is written by hand, laid out as MATLAB's VideoWriter writes it: BI_RGB frames stored bottom-up.
    • Both are uncompressed, so VideoReader reads them on every platform and the ports must agree pixel for pixel.
    • They are committed byte-identically to NDR-matlab's example_data/, and the README is updated in both repos.
  • New tests: tests/symmetry/{make,read}_artifacts/reader/test_read_video.py, paired with NDR-matlab's +makeArtifacts/+readArtifacts/+reader/readVideo.m. They compare:
    • numframes, framesize, dimensionorder, datatype, frametimes, t0_t1, epochclock, channels and image metadata;
    • exact pixels of the first and last RGB frame per channel, a SelectC read, and an out-of-order greyscale read.
  • Time tolerance: frame times are compared to 1e-6 s, since each port measures them through its own decoder.

Bug fixed by the new fixture

FFmpeg decodes a bottom-up 8-bit DIB with a negative line stride. The pal8 path read the plane in memory order, so MATLAB-written greyscale AVIs came back upside down. Test files written by FFmpeg are top-down, so they could not show this. A unit test on the checked-in bottom-up file now covers it, and that test fails without the fix.

Verification

  • pytest: 723 passed, 15 skipped (same skips as before), 24 xfailed.
  • NDR_BRIDGE_CHECK_STRICT=1 bridge completeness + conventions, run against a full NDR-matlab clone: 191 passed.
  • black and ruff check are clean.
  • Cross-language symmetry (run 39476490054, attempt 2), against NDR-matlab claude/clever-bardeen-hf5jgx:
    • Every video check ran and passed in both directions: MATLAB make → Python read, and Python make → MATLAB read.
    • MATLAB's VideoReader reads the grey-palette AVI as one channel, matching Python pixel for pixel.
    • Attempt 1 had resolved NDR-matlab to main because the matching branch was pushed seconds later, so its video checks skipped. It was re-run rather than counted.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QjkvVXsdCVw6NNe5SnGNyX

Ports +ndr/+reader/video.m (NDR-matlab 794a0e8). MATLAB reads video with
VideoReader; the Python port uses PyAV (the FFmpeg binding, an optional
dependency in [formats] and [dev], imported lazily).

- One video file per epoch; companion files ignored, directories expanded.
- Frame API only: numframes, framesize, dimensionorder, datatype,
  frametimes, readframes, epochclock, t0_t1, getchannelsepoch, metadata,
  plus the static helpers videoextensions, videonumframes,
  channelsanddatatype, checkframeind, endtimes, scanframetimes.
- Frame times are measured per decoded frame (correct for VFR) and cached
  per file by path/size/mtime; t0_t1 seeks near the end instead of
  scanning. readframes seeks by pts located from demuxed packets.
- Grey pixel formats and grey-palette pal8 (MATLAB 'Grayscale AVI') give
  C=1; >8-bit video gives uint16.
- Registers the video/videoreader/mp4/avi/mov aliases, records the class
  in the reader bridge YAML, and ports TestVideo.m, with extra cases for
  variable frame rate, header-less frame counts and 16-bit grey.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjkvVXsdCVw6NNe5SnGNyX
…'videoreader' alias)

NDR-matlab #143 rewords the video reader's help so the 'video' type is
defined by the file format and each frame's stored presentation time,
not by VideoReader, and removes the MATLAB-specific 'videoreader' alias.
No change to how frames are read. Mirror the wording in the module
docstring, drop the alias from the registry and its tests, and bump the
bridge hash so the drift check passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjkvVXsdCVw6NNe5SnGNyX
… rows

Symmetry:
- tools/make_example_data.py now also writes example_video.avi (6-frame
  12x16 uncompressed 24-bit RGB) and example_video_grey.avi (8-bit with a
  grey palette, MATLAB's 'Grayscale AVI' storage). The AVI is written by
  hand, laid out as MATLAB's VideoWriter writes it: BI_RGB frames stored
  bottom-up. Uncompressed so VideoReader reads it on every platform and the
  two ports must agree pixel for pixel. Committed byte-identically to
  NDR-matlab's example_data.
- tests/symmetry/{make,read}_artifacts/reader/test_read_video.py, mirroring
  NDR-matlab's +makeArtifacts/+readArtifacts/+reader/readVideo.m (same
  branch name, so the symmetry workflow pairs them). Frame times compare to
  1e-6 s, since each port measures them through its own decoder.

Fix found by the new fixture: FFmpeg decodes a bottom-up 8-bit DIB with a
negative line stride, and the pal8 path read the plane in memory order, so
MATLAB-written greyscale AVIs came back upside down. FFmpeg-written test
files are top-down and could not show it; a unit test on the checked-in
bottom-up file now does (it fails without the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjkvVXsdCVw6NNe5SnGNyX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants